diff options
| author | PgBiel <9021226+PgBiel@users.noreply.github.com> | 2025-06-10 11:41:16 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-10 14:41:16 +0000 |
| commit | 7c7b962b98a09c1baabdd03ff4ccad8f6d817b37 (patch) | |
| tree | c44c9b82e3c7ec7bf8feb44cedeae9fa71a107cb /crates/typst-layout/src | |
| parent | a18ca3481da17a4de1cc7f9890f0c61efb480655 (diff) | |
Table multiple headers and subheaders (#6168)
Diffstat (limited to 'crates/typst-layout/src')
| -rw-r--r-- | crates/typst-layout/src/grid/layouter.rs | 598 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/lines.rs | 29 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/repeated.rs | 476 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/rowspans.rs | 245 |
4 files changed, 1028 insertions, 320 deletions
diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index 99b85edd..42fe38db 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -3,7 +3,9 @@ use std::fmt::Debug; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{Resolve, StyleChain}; -use typst_library::layout::grid::resolve::{Cell, CellGrid, LinePosition, Repeatable}; +use typst_library::layout::grid::resolve::{ + Cell, CellGrid, Header, LinePosition, Repeatable, +}; use typst_library::layout::{ Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel, Size, Sizing, @@ -30,10 +32,8 @@ pub struct GridLayouter<'a> { pub(super) rcols: Vec<Abs>, /// The sum of `rcols`. pub(super) width: Abs, - /// Resolve row sizes, by region. + /// Resolved row sizes, by region. pub(super) rrows: Vec<Vec<RowPiece>>, - /// Rows in the current region. - pub(super) lrows: Vec<Row>, /// The amount of unbreakable rows remaining to be laid out in the /// current unbreakable row group. While this is positive, no region breaks /// should occur. @@ -41,24 +41,155 @@ pub struct GridLayouter<'a> { /// Rowspans not yet laid out because not all of their spanned rows were /// laid out yet. pub(super) rowspans: Vec<Rowspan>, - /// The initial size of the current region before we started subtracting. - pub(super) initial: Size, + /// Grid layout state for the current region. + pub(super) current: Current, /// Frames for finished regions. pub(super) finished: Vec<Frame>, + /// The amount and height of header rows on each finished region. + pub(super) finished_header_rows: Vec<FinishedHeaderRowInfo>, /// Whether this is an RTL grid. pub(super) is_rtl: bool, - /// The simulated header height. - /// This field is reset in `layout_header` and properly updated by + /// Currently repeating headers, one per level. Sorted by increasing + /// levels. + /// + /// Note that some levels may be absent, in particular level 0, which does + /// not exist (so all levels are >= 1). + pub(super) repeating_headers: Vec<&'a Header>, + /// Headers, repeating or not, awaiting their first successful layout. + /// Sorted by increasing levels. + pub(super) pending_headers: &'a [Repeatable<Header>], + /// Next headers to be processed. + pub(super) upcoming_headers: &'a [Repeatable<Header>], + /// State of the row being currently laid out. + /// + /// This is kept as a field to avoid passing down too many parameters from + /// `layout_row` into called functions, which would then have to pass them + /// down to `push_row`, which reads these values. + pub(super) row_state: RowState, + /// The span of the grid element. + pub(super) span: Span, +} + +/// Grid layout state for the current region. This should be reset or updated +/// on each region break. +pub(super) struct Current { + /// The initial size of the current region before we started subtracting. + pub(super) initial: Size, + /// The height of the region after repeated headers were placed and footers + /// prepared. This also includes pending repeating headers from the start, + /// even if they were not repeated yet, since they will be repeated in the + /// next region anyway (bar orphan prevention). + /// + /// This is used to quickly tell if any additional space in the region has + /// been occupied since then, meaning that additional space will become + /// available after a region break (see + /// [`GridLayouter::may_progress_with_repeats`]). + pub(super) initial_after_repeats: Abs, + /// Whether `layouter.regions.may_progress()` was `true` at the top of the + /// region. + pub(super) could_progress_at_top: bool, + /// Rows in the current region. + pub(super) lrows: Vec<Row>, + /// The amount of repeated header rows at the start of the current region. + /// Thus, excludes rows from pending headers (which were placed for the + /// first time). + /// + /// Note that `repeating_headers` and `pending_headers` can change if we + /// find a new header inside the region (not at the top), so this field + /// is required to access information from the top of the region. + /// + /// This information is used on finish region to calculate the total height + /// of resolved header rows at the top of the region, which is used by + /// multi-page rowspans so they can properly skip the header rows at the + /// top of each region during layout. + pub(super) repeated_header_rows: usize, + /// The end bound of the row range of the last repeating header at the + /// start of the region. + /// + /// The last row might have disappeared from layout due to being empty, so + /// this is how we can become aware of where the last header ends without + /// having to check the vector of rows. Line layout uses this to determine + /// when to prioritize the last lines under a header. + /// + /// A value of zero indicates no repeated headers were placed. + pub(super) last_repeated_header_end: usize, + /// Stores the length of `lrows` before a sequence of rows equipped with + /// orphan prevention was laid out. In this case, if no more rows without + /// orphan prevention are laid out after those rows before the region ends, + /// the rows will be removed, and there may be an attempt to place them + /// again in the new region. Effectively, this is the mechanism used for + /// orphan prevention of rows. + /// + /// At the moment, this is only used by repeated headers (they aren't laid + /// out if alone in the region) and by new headers, which are moved to the + /// `pending_headers` vector and so will automatically be placed again + /// until they fit and are not orphans in at least one region (or exactly + /// one, for non-repeated headers). + pub(super) lrows_orphan_snapshot: Option<usize>, + /// The height of effectively repeating headers, that is, ignoring + /// non-repeating pending headers, in the current region. + /// + /// This is used by multi-page auto rows so they can inform cell layout on + /// how much space should be taken by headers if they break across regions. + /// In particular, non-repeating headers only occupy the initial region, + /// but disappear on new regions, so they can be ignored. + /// + /// This field is reset on each new region and properly updated by /// `layout_auto_row` and `layout_relative_row`, and should not be read /// before all header rows are fully laid out. It is usually fine because /// header rows themselves are unbreakable, and unbreakable rows do not /// need to read this field at all. - pub(super) header_height: Abs, + /// + /// This height is not only computed at the beginning of the region. It is + /// updated whenever a new header is found, subtracting the height of + /// headers which stopped repeating and adding the height of all new + /// headers. + pub(super) repeating_header_height: Abs, + /// The height for each repeating header that was placed in this region. + /// Note that this includes headers not at the top of the region, before + /// their first repetition (pending headers), and excludes headers removed + /// by virtue of a new, conflicting header being found (short-lived + /// headers). + /// + /// This is used to know how much to update `repeating_header_height` by + /// when finding a new header and causing existing repeating headers to + /// stop. + pub(super) repeating_header_heights: Vec<Abs>, /// The simulated footer height for this region. + /// /// The simulation occurs before any rows are laid out for a region. pub(super) footer_height: Abs, - /// The span of the grid element. - pub(super) span: Span, +} + +/// Data about the row being laid out right now. +#[derive(Debug, Default)] +pub(super) struct RowState { + /// If this is `Some`, this will be updated by the currently laid out row's + /// height if it is auto or relative. This is used for header height + /// calculation. + pub(super) current_row_height: Option<Abs>, + /// This is `true` when laying out non-short lived headers and footers. + /// That is, headers and footers which are not immediately followed or + /// preceded (respectively) by conflicting headers and footers of same or + /// lower level, or the end or start of the table (respectively), which + /// would cause them to never repeat, even once. + /// + /// If this is `false`, the next row to be laid out will remove an active + /// orphan snapshot and will flush pending headers, as there is no risk + /// that they will be orphans anymore. + pub(super) in_active_repeatable: bool, +} + +/// Data about laid out repeated header rows for a specific finished region. +#[derive(Debug, Default)] +pub(super) struct FinishedHeaderRowInfo { + /// The amount of repeated headers at the top of the region. + pub(super) repeated_amount: usize, + /// The end bound of the row range of the last repeated header at the top + /// of the region. + pub(super) last_repeated_header_end: usize, + /// The total height of repeated headers at the top of the region. + pub(super) repeated_height: Abs, } /// Details about a resulting row piece. @@ -114,14 +245,27 @@ impl<'a> GridLayouter<'a> { rcols: vec![Abs::zero(); grid.cols.len()], width: Abs::zero(), rrows: vec![], - lrows: vec![], unbreakable_rows_left: 0, rowspans: vec![], - initial: regions.size, finished: vec![], + finished_header_rows: vec![], is_rtl: TextElem::dir_in(styles) == Dir::RTL, - header_height: Abs::zero(), - footer_height: Abs::zero(), + repeating_headers: vec![], + upcoming_headers: &grid.headers, + pending_headers: Default::default(), + row_state: RowState::default(), + current: Current { + initial: regions.size, + initial_after_repeats: regions.size.y, + could_progress_at_top: regions.may_progress(), + lrows: vec![], + repeated_header_rows: 0, + last_repeated_header_end: 0, + lrows_orphan_snapshot: None, + repeating_header_height: Abs::zero(), + repeating_header_heights: vec![], + footer_height: Abs::zero(), + }, span, } } @@ -130,38 +274,57 @@ impl<'a> GridLayouter<'a> { pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> { self.measure_columns(engine)?; - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { - // Ensure rows in the first region will be aware of the possible - // presence of the footer. - self.prepare_footer(footer, engine, 0)?; - if matches!(self.grid.header, None | Some(Repeatable::NotRepeated(_))) { - // No repeatable header, so we won't subtract it later. - self.regions.size.y -= self.footer_height; + if let Some(footer) = &self.grid.footer { + if footer.repeated { + // Ensure rows in the first region will be aware of the + // possible presence of the footer. + self.prepare_footer(footer, engine, 0)?; + self.regions.size.y -= self.current.footer_height; + self.current.initial_after_repeats = self.regions.size.y; } } - for y in 0..self.grid.rows.len() { - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - if y < header.end { - if y == 0 { - self.layout_header(header, engine, 0)?; - self.regions.size.y -= self.footer_height; - } + let mut y = 0; + let mut consecutive_header_count = 0; + while y < self.grid.rows.len() { + if let Some(next_header) = self.upcoming_headers.get(consecutive_header_count) + { + if next_header.range.contains(&y) { + self.place_new_headers(&mut consecutive_header_count, engine)?; + y = next_header.range.end; + // Skip header rows during normal layout. continue; } } - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { - if y >= footer.start { + if let Some(footer) = &self.grid.footer { + if footer.repeated && y >= footer.start { if y == footer.start { self.layout_footer(footer, engine, self.finished.len())?; + self.flush_orphans(); } + y = footer.end; continue; } } self.layout_row(y, engine, 0)?; + + // After the first non-header row is placed, pending headers are no + // longer orphans and can repeat, so we move them to repeating + // headers. + // + // Note that this is usually done in `push_row`, since the call to + // `layout_row` above might trigger region breaks (for multi-page + // auto rows), whereas this needs to be called as soon as any part + // of a row is laid out. However, it's possible a row has no + // visible output and thus does not push any rows even though it + // was successfully laid out, in which case we additionally flush + // here just in case. + self.flush_orphans(); + + y += 1; } self.finish_region(engine, true)?; @@ -184,13 +347,47 @@ impl<'a> GridLayouter<'a> { self.render_fills_strokes() } - /// Layout the given row. + /// Layout a row with a certain initial state, returning the final state. + #[inline] + pub(super) fn layout_row_with_state( + &mut self, + y: usize, + engine: &mut Engine, + disambiguator: usize, + initial_state: RowState, + ) -> SourceResult<RowState> { + // Keep a copy of the previous value in the stack, as this function can + // call itself recursively (e.g. if a region break is triggered and a + // header is placed), so we shouldn't outright overwrite it, but rather + // save and later restore the state when back to this call. + let previous = std::mem::replace(&mut self.row_state, initial_state); + + // Keep it as a separate function to allow inlining the return below, + // as it's usually not needed. + self.layout_row_internal(y, engine, disambiguator)?; + + Ok(std::mem::replace(&mut self.row_state, previous)) + } + + /// Layout the given row with the default row state. + #[inline] pub(super) fn layout_row( &mut self, y: usize, engine: &mut Engine, disambiguator: usize, ) -> SourceResult<()> { + self.layout_row_with_state(y, engine, disambiguator, RowState::default())?; + Ok(()) + } + + /// Layout the given row using the current state. + pub(super) fn layout_row_internal( + &mut self, + y: usize, + engine: &mut Engine, + disambiguator: usize, + ) -> SourceResult<()> { // Skip to next region if current one is full, but only for content // rows, not for gutter rows, and only if we aren't laying out an // unbreakable group of rows. @@ -206,13 +403,18 @@ impl<'a> GridLayouter<'a> { } // Don't layout gutter rows at the top of a region. - if is_content_row || !self.lrows.is_empty() { + if is_content_row || !self.current.lrows.is_empty() { match self.grid.rows[y] { Sizing::Auto => self.layout_auto_row(engine, disambiguator, y)?, Sizing::Rel(v) => { self.layout_relative_row(engine, disambiguator, v, y)? } - Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y, disambiguator)), + Sizing::Fr(v) => { + if !self.row_state.in_active_repeatable { + self.flush_orphans(); + } + self.current.lrows.push(Row::Fr(v, y, disambiguator)) + } } } @@ -225,8 +427,13 @@ impl<'a> GridLayouter<'a> { fn render_fills_strokes(mut self) -> SourceResult<Fragment> { let mut finished = std::mem::take(&mut self.finished); let frame_amount = finished.len(); - for ((frame_index, frame), rows) in - finished.iter_mut().enumerate().zip(&self.rrows) + for (((frame_index, frame), rows), finished_header_rows) in + finished.iter_mut().enumerate().zip(&self.rrows).zip( + self.finished_header_rows + .iter() + .map(Some) + .chain(std::iter::repeat(None)), + ) { if self.rcols.is_empty() || rows.is_empty() { continue; @@ -347,7 +554,8 @@ impl<'a> GridLayouter<'a> { let hline_indices = rows .iter() .map(|piece| piece.y) - .chain(std::iter::once(self.grid.rows.len())); + .chain(std::iter::once(self.grid.rows.len())) + .enumerate(); // Converts a row to the corresponding index in the vector of // hlines. @@ -372,7 +580,7 @@ impl<'a> GridLayouter<'a> { }; let mut prev_y = None; - for (y, dy) in hline_indices.zip(hline_offsets) { + for ((i, y), dy) in hline_indices.zip(hline_offsets) { // Position of lines below the row index in the previous iteration. let expected_prev_line_position = prev_y .map(|prev_y| { @@ -383,47 +591,40 @@ impl<'a> GridLayouter<'a> { }) .unwrap_or(LinePosition::Before); - // FIXME: In the future, directly specify in 'self.rrows' when - // we place a repeated header rather than its original rows. - // That would let us remove most of those verbose checks, both - // in 'lines.rs' and here. Those checks also aren't fully - // accurate either, since they will also trigger when some rows - // have been removed between the header and what's below it. - let is_under_repeated_header = self - .grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .zip(prev_y) - .is_some_and(|(header, prev_y)| { - // Note: 'y == header.end' would mean we're right below - // the NON-REPEATED header, so that case should return - // false. - prev_y < header.end && y > header.end - }); + // Header's lines at the bottom have priority when repeated. + // This will store the end bound of the last header if the + // current iteration is calculating lines under it. + let last_repeated_header_end_above = match finished_header_rows { + Some(info) if prev_y.is_some() && i == info.repeated_amount => { + Some(info.last_repeated_header_end) + } + _ => None, + }; // If some grid rows were omitted between the previous resolved // row and the current one, we ensure lines below the previous // row don't "disappear" and are considered, albeit with less // priority. However, don't do this when we're below a header, // as it must have more priority instead of less, so it is - // chained later instead of before. The exception is when the + // chained later instead of before (stored in the + // 'header_hlines' variable below). The exception is when the // last row in the header is removed, in which case we append // both the lines under the row above us and also (later) the // lines under the header's (removed) last row. - let prev_lines = prev_y - .filter(|prev_y| { - prev_y + 1 != y - && (!is_under_repeated_header - || self - .grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .is_some_and(|header| prev_y + 1 != header.end)) - }) - .map(|prev_y| get_hlines_at(prev_y + 1)) - .unwrap_or(&[]); + let prev_lines = match prev_y { + Some(prev_y) + if prev_y + 1 != y + && last_repeated_header_end_above.is_none_or( + |last_repeated_header_end| { + prev_y + 1 != last_repeated_header_end + }, + ) => + { + get_hlines_at(prev_y + 1) + } + + _ => &[], + }; let expected_hline_position = expected_line_position(y, y == self.grid.rows.len()); @@ -441,15 +642,13 @@ impl<'a> GridLayouter<'a> { }; let mut expected_header_line_position = LinePosition::Before; - let header_hlines = if let Some((Repeatable::Repeated(header), prev_y)) = - self.grid.header.as_ref().zip(prev_y) - { - if is_under_repeated_header - && (!self.grid.has_gutter + let header_hlines = match (last_repeated_header_end_above, prev_y) { + (Some(header_end_above), Some(prev_y)) + if !self.grid.has_gutter || matches!( self.grid.rows[prev_y], Sizing::Rel(length) if length.is_zero() - )) + ) => { // For lines below a header, give priority to the // lines originally below the header rather than @@ -468,15 +667,13 @@ impl<'a> GridLayouter<'a> { // column-gutter is specified, for example. In that // case, we still repeat the line under the gutter. expected_header_line_position = expected_line_position( - header.end, - header.end == self.grid.rows.len(), + header_end_above, + header_end_above == self.grid.rows.len(), ); - get_hlines_at(header.end) - } else { - &[] + get_hlines_at(header_end_above) } - } else { - &[] + + _ => &[], }; // The effective hlines to be considered at this row index are @@ -529,6 +726,7 @@ impl<'a> GridLayouter<'a> { grid, rows, local_top_y, + last_repeated_header_end_above, in_last_region, y, x, @@ -941,15 +1139,9 @@ impl<'a> GridLayouter<'a> { let frame = self.layout_single_row(engine, disambiguator, first, y)?; self.push_row(frame, y, true); - if self - .grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .is_some_and(|header| y < header.end) - { - // Add to header height. - self.header_height += first; + if let Some(row_height) = &mut self.row_state.current_row_height { + // Add to header height, as we are in a header row. + *row_height += first; } return Ok(()); @@ -958,19 +1150,21 @@ impl<'a> GridLayouter<'a> { // Expand all but the last region. // Skip the first region if the space is eaten up by an fr row. let len = resolved.len(); - for ((i, region), target) in self - .regions - .iter() - .enumerate() - .zip(&mut resolved[..len - 1]) - .skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize) + for ((i, region), target) in + self.regions + .iter() + .enumerate() + .zip(&mut resolved[..len - 1]) + .skip(self.current.lrows.iter().any(|row| matches!(row, Row::Fr(..))) + as usize) { // Subtract header and footer heights from the region height when - // it's not the first. + // it's not the first. Ignore non-repeating headers as they only + // appear on the first region by definition. target.set_max( region.y - if i > 0 { - self.header_height + self.footer_height + self.current.repeating_header_height + self.current.footer_height } else { Abs::zero() }, @@ -1181,25 +1375,19 @@ impl<'a> GridLayouter<'a> { let resolved = v.resolve(self.styles).relative_to(self.regions.base().y); let frame = self.layout_single_row(engine, disambiguator, resolved, y)?; - if self - .grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .is_some_and(|header| y < header.end) - { - // Add to header height. - self.header_height += resolved; + if let Some(row_height) = &mut self.row_state.current_row_height { + // Add to header height, as we are in a header row. + *row_height += resolved; } // Skip to fitting region, but only if we aren't part of an unbreakable - // row group. We use 'in_last_with_offset' so our 'in_last' call - // properly considers that a header and a footer would be added on each - // region break. + // row group. We use 'may_progress_with_repeats' to stop trying if we + // would skip to a region with the same height and where the same + // headers would be repeated. let height = frame.height(); while self.unbreakable_rows_left == 0 && !self.regions.size.y.fits(height) - && !in_last_with_offset(self.regions, self.header_height + self.footer_height) + && self.may_progress_with_repeats() { self.finish_region(engine, false)?; @@ -1323,8 +1511,13 @@ impl<'a> GridLayouter<'a> { /// will be pushed for this particular row. It can be `false` for rows /// spanning multiple regions. fn push_row(&mut self, frame: Frame, y: usize, is_last: bool) { + if !self.row_state.in_active_repeatable { + // There is now a row after the rows equipped with orphan + // prevention, so no need to keep moving them anymore. + self.flush_orphans(); + } self.regions.size.y -= frame.height(); - self.lrows.push(Row::Frame(frame, y, is_last)); + self.current.lrows.push(Row::Frame(frame, y, is_last)); } /// Finish rows for one region. @@ -1333,68 +1526,73 @@ impl<'a> GridLayouter<'a> { engine: &mut Engine, last: bool, ) -> SourceResult<()> { + // The latest rows have orphan prevention (headers) and no other rows + // were placed, so remove those rows and try again in a new region, + // unless this is the last region. + if let Some(orphan_snapshot) = self.current.lrows_orphan_snapshot.take() { + if !last { + self.current.lrows.truncate(orphan_snapshot); + self.current.repeated_header_rows = + self.current.repeated_header_rows.min(orphan_snapshot); + + if orphan_snapshot == 0 { + // Removed all repeated headers. + self.current.last_repeated_header_end = 0; + } + } + } + if self + .current .lrows .last() .is_some_and(|row| self.grid.is_gutter_track(row.index())) { // Remove the last row in the region if it is a gutter row. - self.lrows.pop().unwrap(); + self.current.lrows.pop().unwrap(); + self.current.repeated_header_rows = + self.current.repeated_header_rows.min(self.current.lrows.len()); } - // If no rows other than the footer have been laid out so far, and - // there are rows beside the footer, then don't lay it out at all. - // This check doesn't apply, and is thus overridden, when there is a - // header. - let mut footer_would_be_orphan = self.lrows.is_empty() - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) - && self - .grid - .footer - .as_ref() - .and_then(Repeatable::as_repeated) - .is_some_and(|footer| footer.start != 0); - - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - if self.grid.rows.len() > header.end - && self - .grid - .footer - .as_ref() - .and_then(Repeatable::as_repeated) - .is_none_or(|footer| footer.start != header.end) - && self.lrows.last().is_some_and(|row| row.index() < header.end) - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) - { - // Header and footer would be alone in this region, but there are more - // rows beyond the header and the footer. Push an empty region. - self.lrows.clear(); - footer_would_be_orphan = true; - } - } + // If no rows other than the footer have been laid out so far + // (e.g. due to header orphan prevention), and there are rows + // beside the footer, then don't lay it out at all. + // + // It is worth noting that the footer is made non-repeatable at + // the grid resolving stage if it is short-lived, that is, if + // it is at the start of the table (or right after headers at + // the start of the table). + // + // TODO(subfooters): explicitly check for short-lived footers. + // TODO(subfooters): widow prevention for non-repeated footers with a + // similar mechanism / when implementing multiple footers. + let footer_would_be_widow = matches!(&self.grid.footer, Some(footer) if footer.repeated) + && self.current.lrows.is_empty() + && self.current.could_progress_at_top; let mut laid_out_footer_start = None; - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { - // Don't layout the footer if it would be alone with the header in - // the page, and don't layout it twice. - if !footer_would_be_orphan - && self.lrows.iter().all(|row| row.index() < footer.start) - { - laid_out_footer_start = Some(footer.start); - self.layout_footer(footer, engine, self.finished.len())?; + if !footer_would_be_widow { + if let Some(footer) = &self.grid.footer { + // Don't layout the footer if it would be alone with the header + // in the page (hence the widow check), and don't layout it + // twice (check below). + // + // TODO(subfooters): this check can be replaced by a vector of + // repeating footers in the future, and/or some "pending + // footers" vector for footers we're about to place. + if footer.repeated + && self.current.lrows.iter().all(|row| row.index() < footer.start) + { + laid_out_footer_start = Some(footer.start); + self.layout_footer(footer, engine, self.finished.len())?; + } } } // Determine the height of existing rows in the region. let mut used = Abs::zero(); let mut fr = Fr::zero(); - for row in &self.lrows { + for row in &self.current.lrows { match row { Row::Frame(frame, _, _) => used += frame.height(), Row::Fr(v, _, _) => fr += *v, @@ -1403,9 +1601,9 @@ impl<'a> GridLayouter<'a> { // Determine the size of the grid in this region, expanding fully if // there are fr rows. - let mut size = Size::new(self.width, used).min(self.initial); - if fr.get() > 0.0 && self.initial.y.is_finite() { - size.y = self.initial.y; + let mut size = Size::new(self.width, used).min(self.current.initial); + if fr.get() > 0.0 && self.current.initial.y.is_finite() { + size.y = self.current.initial.y; } // The frame for the region. @@ -1413,9 +1611,10 @@ impl<'a> GridLayouter<'a> { let mut pos = Point::zero(); let mut rrows = vec![]; let current_region = self.finished.len(); + let mut repeated_header_row_height = Abs::zero(); // Place finished rows and layout fractional rows. - for row in std::mem::take(&mut self.lrows) { + for (i, row) in std::mem::take(&mut self.current.lrows).into_iter().enumerate() { let (frame, y, is_last) = match row { Row::Frame(frame, y, is_last) => (frame, y, is_last), Row::Fr(v, y, disambiguator) => { @@ -1426,6 +1625,9 @@ impl<'a> GridLayouter<'a> { }; let height = frame.height(); + if i < self.current.repeated_header_rows { + repeated_header_row_height += height; + } // Ensure rowspans which span this row will have enough space to // be laid out over it later. @@ -1504,7 +1706,11 @@ impl<'a> GridLayouter<'a> { // we have to check the same index again in the next // iteration. let rowspan = self.rowspans.remove(i); - self.layout_rowspan(rowspan, Some((&mut output, &rrows)), engine)?; + self.layout_rowspan( + rowspan, + Some((&mut output, repeated_header_row_height)), + engine, + )?; } else { i += 1; } @@ -1515,21 +1721,40 @@ impl<'a> GridLayouter<'a> { pos.y += height; } - self.finish_region_internal(output, rrows); + self.finish_region_internal( + output, + rrows, + FinishedHeaderRowInfo { + repeated_amount: self.current.repeated_header_rows, + last_repeated_header_end: self.current.last_repeated_header_end, + repeated_height: repeated_header_row_height, + }, + ); if !last { + self.current.repeated_header_rows = 0; + self.current.last_repeated_header_end = 0; + self.current.repeating_header_height = Abs::zero(); + self.current.repeating_header_heights.clear(); + let disambiguator = self.finished.len(); - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { + if let Some(footer) = + self.grid.footer.as_ref().and_then(Repeatable::as_repeated) + { self.prepare_footer(footer, engine, disambiguator)?; } - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - // Add a header to the new region. - self.layout_header(header, engine, disambiguator)?; - } - // Ensure rows don't try to overrun the footer. - self.regions.size.y -= self.footer_height; + // Note that header layout will only subtract this again if it has + // to skip regions to fit headers, so there is no risk of + // subtracting this twice. + self.regions.size.y -= self.current.footer_height; + self.current.initial_after_repeats = self.regions.size.y; + + if !self.repeating_headers.is_empty() || !self.pending_headers.is_empty() { + // Add headers to the new region. + self.layout_active_headers(engine)?; + } } Ok(()) @@ -1541,11 +1766,26 @@ impl<'a> GridLayouter<'a> { &mut self, output: Frame, resolved_rows: Vec<RowPiece>, + header_row_info: FinishedHeaderRowInfo, ) { self.finished.push(output); self.rrows.push(resolved_rows); self.regions.next(); - self.initial = self.regions.size; + self.current.initial = self.regions.size; + + // Repeats haven't been laid out yet, so in the meantime, this will + // represent the initial height after repeats laid out so far, and will + // be gradually updated when preparing footers and repeating headers. + self.current.initial_after_repeats = self.current.initial.y; + + self.current.could_progress_at_top = self.regions.may_progress(); + + if !self.grid.headers.is_empty() { + self.finished_header_rows.push(header_row_info); + } + + // Ensure orphan prevention is handled before resolving rows. + debug_assert!(self.current.lrows_orphan_snapshot.is_none()); } } @@ -1560,13 +1800,3 @@ pub(super) fn points( offset }) } - -/// Checks if the first region of a sequence of regions is the last usable -/// region, assuming that the last region will always be occupied by some -/// specific offset height, even after calling `.next()`, due to some -/// additional logic which adds content automatically on each region turn (in -/// our case, headers). -pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool { - regions.backlog.is_empty() - && regions.last.is_none_or(|height| regions.size.y + offset == height) -} diff --git a/crates/typst-layout/src/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs index 7549673f..d5da7e26 100644 --- a/crates/typst-layout/src/grid/lines.rs +++ b/crates/typst-layout/src/grid/lines.rs @@ -391,10 +391,12 @@ pub fn vline_stroke_at_row( /// /// This function assumes columns are sorted by increasing `x`, and rows are /// sorted by increasing `y`. +#[allow(clippy::too_many_arguments)] pub fn hline_stroke_at_column( grid: &CellGrid, rows: &[RowPiece], local_top_y: Option<usize>, + header_end_above: Option<usize>, in_last_region: bool, y: usize, x: usize, @@ -499,17 +501,15 @@ pub fn hline_stroke_at_column( // Top border stroke and header stroke are generally prioritized, unless // they don't have explicit hline overrides and one or more user-provided // hlines would appear at the same position, which then are prioritized. - let top_stroke_comes_from_header = grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .zip(local_top_y) - .is_some_and(|(header, local_top_y)| { - // Ensure the row above us is a repeated header. - // FIXME: Make this check more robust when headers at arbitrary - // positions are added. - local_top_y < header.end && y > header.end - }); + let top_stroke_comes_from_header = header_end_above.zip(local_top_y).is_some_and( + |(last_repeated_header_end, local_top_y)| { + // Check if the last repeated header row is above this line. + // + // Note that `y == last_repeated_header_end` is impossible for a + // strictly repeated header (not in its original position). + local_top_y < last_repeated_header_end && y > last_repeated_header_end + }, + ); // Prioritize the footer's top stroke as well where applicable. let bottom_stroke_comes_from_footer = grid @@ -637,7 +637,7 @@ mod test { }, vec![], vec![], - None, + vec![], None, entries, ) @@ -1175,7 +1175,7 @@ mod test { }, vec![], vec![], - None, + vec![], None, entries, ) @@ -1268,6 +1268,7 @@ mod test { grid, &rows, y.checked_sub(1), + None, true, y, x, @@ -1461,6 +1462,7 @@ mod test { grid, &rows, y.checked_sub(1), + None, true, y, x, @@ -1506,6 +1508,7 @@ mod test { grid, &rows, if y == 4 { Some(2) } else { y.checked_sub(1) }, + None, true, y, x, diff --git a/crates/typst-layout/src/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs index 22d2a09e..8db33df5 100644 --- a/crates/typst-layout/src/grid/repeated.rs +++ b/crates/typst-layout/src/grid/repeated.rs @@ -1,57 +1,446 @@ +use std::ops::Deref; + use typst_library::diag::SourceResult; use typst_library::engine::Engine; use typst_library::layout::grid::resolve::{Footer, Header, Repeatable}; use typst_library::layout::{Abs, Axes, Frame, Regions}; -use super::layouter::GridLayouter; +use super::layouter::{GridLayouter, RowState}; use super::rowspans::UnbreakableRowGroup; -impl GridLayouter<'_> { - /// Layouts the header's rows. - /// Skips regions as necessary. - pub fn layout_header( +impl<'a> GridLayouter<'a> { + /// Checks whether a region break could help a situation where we're out of + /// space for the next row. The criteria are: + /// + /// 1. If we could progress at the top of the region, that indicates the + /// region has a backlog, or (if we're at the first region) a region break + /// is at all possible (`regions.last` is `Some()`), so that's sufficient. + /// + /// 2. Otherwise, we may progress if another region break is possible + /// (`regions.last` is still `Some()`) and non-repeating rows have been + /// placed, since that means the space they occupy will be available in the + /// next region. + #[inline] + pub fn may_progress_with_repeats(&self) -> bool { + // TODO(subfooters): check below isn't enough to detect non-repeating + // footers... we can also change 'initial_after_repeats' to stop being + // calculated if there were any non-repeating footers. + self.current.could_progress_at_top + || self.regions.last.is_some() + && self.regions.size.y != self.current.initial_after_repeats + } + + pub fn place_new_headers( + &mut self, + consecutive_header_count: &mut usize, + engine: &mut Engine, + ) -> SourceResult<()> { + *consecutive_header_count += 1; + let (consecutive_headers, new_upcoming_headers) = + self.upcoming_headers.split_at(*consecutive_header_count); + + if new_upcoming_headers.first().is_some_and(|next_header| { + consecutive_headers.last().is_none_or(|latest_header| { + !latest_header.short_lived + && next_header.range.start == latest_header.range.end + }) && !next_header.short_lived + }) { + // More headers coming, so wait until we reach them. + return Ok(()); + } + + self.upcoming_headers = new_upcoming_headers; + *consecutive_header_count = 0; + + let [first_header, ..] = consecutive_headers else { + self.flush_orphans(); + return Ok(()); + }; + + // Assuming non-conflicting headers sorted by increasing y, this must + // be the header with the lowest level (sorted by increasing levels). + let first_level = first_header.level; + + // Stop repeating conflicting headers, even if the new headers are + // short-lived or won't repeat. + // + // If we go to a new region before the new headers fit alongside their + // children (or in general, for short-lived), the old headers should + // not be displayed anymore. + let first_conflicting_pos = + self.repeating_headers.partition_point(|h| h.level < first_level); + self.repeating_headers.truncate(first_conflicting_pos); + + // Ensure upcoming rows won't see that these headers will occupy any + // space in future regions anymore. + for removed_height in + self.current.repeating_header_heights.drain(first_conflicting_pos..) + { + self.current.repeating_header_height -= removed_height; + } + + // Layout short-lived headers immediately. + if consecutive_headers.last().is_some_and(|h| h.short_lived) { + // No chance of orphans as we're immediately placing conflicting + // headers afterwards, which basically are not headers, for all intents + // and purposes. It is therefore guaranteed that all new headers have + // been placed at least once. + self.flush_orphans(); + + // Layout each conflicting header independently, without orphan + // prevention (as they don't go into 'pending_headers'). + // These headers are short-lived as they are immediately followed by a + // header of the same or lower level, such that they never actually get + // to repeat. + self.layout_new_headers(consecutive_headers, true, engine)?; + } else { + // Let's try to place pending headers at least once. + // This might be a waste as we could generate an orphan and thus have + // to try to place old and new headers all over again, but that happens + // for every new region anyway, so it's rather unavoidable. + let snapshot_created = + self.layout_new_headers(consecutive_headers, false, engine)?; + + // Queue the new headers for layout. They will remain in this + // vector due to orphan prevention. + // + // After the first subsequent row is laid out, move to repeating, as + // it's then confirmed the headers won't be moved due to orphan + // prevention anymore. + self.pending_headers = consecutive_headers; + + if !snapshot_created { + // Region probably couldn't progress. + // + // Mark new pending headers as final and ensure there isn't a + // snapshot. + self.flush_orphans(); + } + } + + Ok(()) + } + + /// Lays out rows belonging to a header, returning the calculated header + /// height only for that header. Indicates to the laid out rows that they + /// should inform their laid out heights if appropriate (auto or fixed + /// size rows only). + #[inline] + fn layout_header_rows( &mut self, header: &Header, engine: &mut Engine, disambiguator: usize, - ) -> SourceResult<()> { - let header_rows = - self.simulate_header(header, &self.regions, engine, disambiguator)?; + as_short_lived: bool, + ) -> SourceResult<Abs> { + let mut header_height = Abs::zero(); + for y in header.range.clone() { + header_height += self + .layout_row_with_state( + y, + engine, + disambiguator, + RowState { + current_row_height: Some(Abs::zero()), + in_active_repeatable: !as_short_lived, + }, + )? + .current_row_height + .unwrap_or_default(); + } + Ok(header_height) + } + + /// This function should be called each time an additional row has been + /// laid out in a region to indicate that orphan prevention has succeeded. + /// + /// It removes the current orphan snapshot and flushes pending headers, + /// such that a non-repeating header won't try to be laid out again + /// anymore, and a repeating header will begin to be part of + /// `repeating_headers`. + pub fn flush_orphans(&mut self) { + self.current.lrows_orphan_snapshot = None; + self.flush_pending_headers(); + } + + /// Indicates all currently pending headers have been successfully placed + /// once, since another row has been placed after them, so they are + /// certainly not orphans. + pub fn flush_pending_headers(&mut self) { + if self.pending_headers.is_empty() { + return; + } + + for header in self.pending_headers { + if header.repeated { + // Vector remains sorted by increasing levels: + // - 'pending_headers' themselves are sorted, since we only + // push non-mutually-conflicting headers at a time. + // - Before pushing new pending headers in + // 'layout_new_pending_headers', we truncate repeating headers + // to remove anything with the same or higher levels as the + // first pending header. + // - Assuming it was sorted before, that truncation only keeps + // elements with a lower level. + // - Therefore, by pushing this header to the end, it will have + // a level larger than all the previous headers, and is thus + // in its 'correct' position. + self.repeating_headers.push(header); + } + } + + self.pending_headers = Default::default(); + } + + /// Lays out the rows of repeating and pending headers at the top of the + /// region. + /// + /// Assumes the footer height for the current region has already been + /// calculated. Skips regions as necessary to fit all headers and all + /// footers. + pub fn layout_active_headers(&mut self, engine: &mut Engine) -> SourceResult<()> { + // Generate different locations for content in headers across its + // repetitions by assigning a unique number for each one. + let disambiguator = self.finished.len(); + + let header_height = self.simulate_header_height( + self.repeating_headers + .iter() + .copied() + .chain(self.pending_headers.iter().map(Repeatable::deref)), + &self.regions, + engine, + disambiguator, + )?; + + // We already take the footer into account below. + // While skipping regions, footer height won't be automatically + // re-calculated until the end. let mut skipped_region = false; while self.unbreakable_rows_left == 0 - && !self.regions.size.y.fits(header_rows.height + self.footer_height) - && self.regions.may_progress() + && !self.regions.size.y.fits(header_height) + && self.may_progress_with_repeats() { // Advance regions without any output until we can place the // header and the footer. - self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]); + self.finish_region_internal( + Frame::soft(Axes::splat(Abs::zero())), + vec![], + Default::default(), + ); + + // TODO(layout model): re-calculate heights of headers and footers + // on each region if 'full' changes? (Assuming height doesn't + // change for now...) + // + // Would remove the footer height update below (move it here). skipped_region = true; - } - // Reset the header height for this region. - // It will be re-calculated when laying out each header row. - self.header_height = Abs::zero(); + self.regions.size.y -= self.current.footer_height; + self.current.initial_after_repeats = self.regions.size.y; + } - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { - if skipped_region { + if let Some(footer) = &self.grid.footer { + if footer.repeated && skipped_region { // Simulate the footer again; the region's 'full' might have // changed. - self.footer_height = self + self.regions.size.y += self.current.footer_height; + self.current.footer_height = self .simulate_footer(footer, &self.regions, engine, disambiguator)? .height; + self.regions.size.y -= self.current.footer_height; } } - // Header is unbreakable. + let repeating_header_rows = + total_header_row_count(self.repeating_headers.iter().copied()); + + let pending_header_rows = + total_header_row_count(self.pending_headers.iter().map(Repeatable::deref)); + + // Group of headers is unbreakable. // Thus, no risk of 'finish_region' being recursively called from // within 'layout_row'. - self.unbreakable_rows_left += header.end; - for y in 0..header.end { - self.layout_row(y, engine, disambiguator)?; + self.unbreakable_rows_left += repeating_header_rows + pending_header_rows; + + self.current.last_repeated_header_end = + self.repeating_headers.last().map(|h| h.range.end).unwrap_or_default(); + + // Reset the header height for this region. + // It will be re-calculated when laying out each header row. + self.current.repeating_header_height = Abs::zero(); + self.current.repeating_header_heights.clear(); + + debug_assert!(self.current.lrows.is_empty()); + debug_assert!(self.current.lrows_orphan_snapshot.is_none()); + let may_progress = self.may_progress_with_repeats(); + + if may_progress { + // Enable orphan prevention for headers at the top of the region. + // Otherwise, we will flush pending headers below, after laying + // them out. + // + // It is very rare for this to make a difference as we're usually + // at the 'last' region after the first skip, at which the snapshot + // is handled by 'layout_new_headers'. Either way, we keep this + // here for correctness. + self.current.lrows_orphan_snapshot = Some(self.current.lrows.len()); + } + + // Use indices to avoid double borrow. We don't mutate headers in + // 'layout_row' so this is fine. + let mut i = 0; + while let Some(&header) = self.repeating_headers.get(i) { + let header_height = + self.layout_header_rows(header, engine, disambiguator, false)?; + self.current.repeating_header_height += header_height; + + // We assume that this vector will be sorted according + // to increasing levels like 'repeating_headers' and + // 'pending_headers' - and, in particular, their union, as this + // vector is pushed repeating heights from both. + // + // This is guaranteed by: + // 1. We always push pending headers after repeating headers, + // as we assume they don't conflict because we remove + // conflicting repeating headers when pushing a new pending + // header. + // + // 2. We push in the same order as each. + // + // 3. This vector is also modified when pushing a new pending + // header, where we remove heights for conflicting repeating + // headers which have now stopped repeating. They are always at + // the end and new pending headers respect the existing sort, + // so the vector will remain sorted. + self.current.repeating_header_heights.push(header_height); + + i += 1; + } + + self.current.repeated_header_rows = self.current.lrows.len(); + self.current.initial_after_repeats = self.regions.size.y; + + let mut has_non_repeated_pending_header = false; + for header in self.pending_headers { + if !header.repeated { + self.current.initial_after_repeats = self.regions.size.y; + has_non_repeated_pending_header = true; + } + let header_height = + self.layout_header_rows(header, engine, disambiguator, false)?; + if header.repeated { + self.current.repeating_header_height += header_height; + self.current.repeating_header_heights.push(header_height); + } + } + + if !has_non_repeated_pending_header { + self.current.initial_after_repeats = self.regions.size.y; } + + if !may_progress { + // Flush pending headers immediately, as placing them again later + // won't help. + self.flush_orphans(); + } + Ok(()) } + /// Lays out headers found for the first time during row layout. + /// + /// If 'short_lived' is true, these headers are immediately followed by + /// a conflicting header, so it is assumed they will not be pushed to + /// pending headers. + /// + /// Returns whether orphan prevention was successfully setup, or couldn't + /// due to short-lived headers or the region couldn't progress. + pub fn layout_new_headers( + &mut self, + headers: &'a [Repeatable<Header>], + short_lived: bool, + engine: &mut Engine, + ) -> SourceResult<bool> { + // At first, only consider the height of the given headers. However, + // for upcoming regions, we will have to consider repeating headers as + // well. + let header_height = self.simulate_header_height( + headers.iter().map(Repeatable::deref), + &self.regions, + engine, + 0, + )?; + + while self.unbreakable_rows_left == 0 + && !self.regions.size.y.fits(header_height) + && self.may_progress_with_repeats() + { + // Note that, after the first region skip, the new headers will go + // at the top of the region, but after the repeating headers that + // remained (which will be automatically placed in 'finish_region'). + self.finish_region(engine, false)?; + } + + // Remove new headers at the end of the region if the upcoming row + // doesn't fit. + // TODO(subfooters): what if there is a footer right after it? + let should_snapshot = !short_lived + && self.current.lrows_orphan_snapshot.is_none() + && self.may_progress_with_repeats(); + + if should_snapshot { + // If we don't enter this branch while laying out non-short lived + // headers, that means we will have to immediately flush pending + // headers and mark them as final, since trying to place them in + // the next page won't help get more space. + self.current.lrows_orphan_snapshot = Some(self.current.lrows.len()); + } + + let mut at_top = self.regions.size.y == self.current.initial_after_repeats; + + self.unbreakable_rows_left += + total_header_row_count(headers.iter().map(Repeatable::deref)); + + for header in headers { + let header_height = self.layout_header_rows(header, engine, 0, false)?; + + // Only store this header height if it is actually going to + // become a pending header. Otherwise, pretend it's not a + // header... This is fine for consumers of 'header_height' as + // it is guaranteed this header won't appear in a future + // region, so multi-page rows and cells can effectively ignore + // this header. + if !short_lived && header.repeated { + self.current.repeating_header_height += header_height; + self.current.repeating_header_heights.push(header_height); + if at_top { + self.current.initial_after_repeats = self.regions.size.y; + } + } else { + at_top = false; + } + } + + Ok(should_snapshot) + } + + /// Calculates the total expected height of several headers. + pub fn simulate_header_height<'h: 'a>( + &self, + headers: impl IntoIterator<Item = &'h Header>, + regions: &Regions<'_>, + engine: &mut Engine, + disambiguator: usize, + ) -> SourceResult<Abs> { + let mut height = Abs::zero(); + for header in headers { + height += + self.simulate_header(header, regions, engine, disambiguator)?.height; + } + Ok(height) + } + /// Simulate the header's group of rows. pub fn simulate_header( &self, @@ -66,8 +455,8 @@ impl GridLayouter<'_> { // assume that the amount of unbreakable rows following the first row // in the header will be precisely the rows in the header. self.simulate_unbreakable_row_group( - 0, - Some(header.end), + header.range.start, + Some(header.range.end - header.range.start), regions, engine, disambiguator, @@ -91,11 +480,22 @@ impl GridLayouter<'_> { { // Advance regions without any output until we can place the // footer. - self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]); + self.finish_region_internal( + Frame::soft(Axes::splat(Abs::zero())), + vec![], + Default::default(), + ); skipped_region = true; } - self.footer_height = if skipped_region { + // TODO(subfooters): Consider resetting header height etc. if we skip + // region. (Maybe move that step to `finish_region_internal`.) + // + // That is unnecessary at the moment as 'prepare_footers' is only + // called at the start of the region, so header height is always zero + // and no headers were placed so far, but what about when we can have + // footers in the middle of the region? Let's think about this then. + self.current.footer_height = if skipped_region { // Simulate the footer again; the region's 'full' might have // changed. self.simulate_footer(footer, &self.regions, engine, disambiguator)? @@ -118,12 +518,22 @@ impl GridLayouter<'_> { // Ensure footer rows have their own height available. // Won't change much as we're creating an unbreakable row group // anyway, so this is mostly for correctness. - self.regions.size.y += self.footer_height; + self.regions.size.y += self.current.footer_height; + let repeats = self.grid.footer.as_ref().is_some_and(|f| f.repeated); let footer_len = self.grid.rows.len() - footer.start; self.unbreakable_rows_left += footer_len; + for y in footer.start..self.grid.rows.len() { - self.layout_row(y, engine, disambiguator)?; + self.layout_row_with_state( + y, + engine, + disambiguator, + RowState { + in_active_repeatable: repeats, + ..Default::default() + }, + )?; } Ok(()) @@ -144,10 +554,18 @@ impl GridLayouter<'_> { // in the footer will be precisely the rows in the footer. self.simulate_unbreakable_row_group( footer.start, - Some(self.grid.rows.len() - footer.start), + Some(footer.end - footer.start), regions, engine, disambiguator, ) } } + +/// The total amount of rows in the given list of headers. +#[inline] +pub fn total_header_row_count<'h>( + headers: impl IntoIterator<Item = &'h Header>, +) -> usize { + headers.into_iter().map(|h| h.range.end - h.range.start).sum() +} diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs index 5ab0417d..02ea1481 100644 --- a/crates/typst-layout/src/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -4,7 +4,7 @@ use typst_library::foundations::Resolve; use typst_library::layout::grid::resolve::Repeatable; use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; -use super::layouter::{in_last_with_offset, points, Row, RowPiece}; +use super::layouter::{points, Row}; use super::{layout_cell, Cell, GridLayouter}; /// All information needed to layout a single rowspan. @@ -90,10 +90,10 @@ pub struct CellMeasurementData<'layouter> { impl GridLayouter<'_> { /// Layout a rowspan over the already finished regions, plus the current - /// region's frame and resolved rows, if it wasn't finished yet (because - /// we're being called from `finish_region`, but note that this function is - /// also called once after all regions are finished, in which case - /// `current_region_data` is `None`). + /// region's frame and height of resolved header rows, if it wasn't + /// finished yet (because we're being called from `finish_region`, but note + /// that this function is also called once after all regions are finished, + /// in which case `current_region_data` is `None`). /// /// We need to do this only once we already know the heights of all /// spanned rows, which is only possible after laying out the last row @@ -101,7 +101,7 @@ impl GridLayouter<'_> { pub fn layout_rowspan( &mut self, rowspan_data: Rowspan, - current_region_data: Option<(&mut Frame, &[RowPiece])>, + current_region_data: Option<(&mut Frame, Abs)>, engine: &mut Engine, ) -> SourceResult<()> { let Rowspan { @@ -146,11 +146,31 @@ impl GridLayouter<'_> { // Push the layouted frames directly into the finished frames. let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?; - let (current_region, current_rrows) = current_region_data.unzip(); - for ((i, finished), frame) in self + let (current_region, current_header_row_height) = current_region_data.unzip(); + + // Clever trick to process finished header rows: + // - If there are grid headers, the vector will be filled with one + // finished header row height per region, so, chaining with the height + // for the current one, we get the header row height for each region. + // + // - But if there are no grid headers, the vector will be empty, so in + // theory the regions and resolved header row heights wouldn't match. + // But that's fine - 'current_header_row_height' can only be either + // 'Some(zero)' or 'None' in such a case, and for all other rows we + // append infinite zeros. That is, in such a case, the resolved header + // row height is always zero, so that's our fallback. + let finished_header_rows = self + .finished_header_rows + .iter() + .map(|info| info.repeated_height) + .chain(current_header_row_height) + .chain(std::iter::repeat(Abs::zero())); + + for ((i, (finished, header_dy)), frame) in self .finished .iter_mut() .chain(current_region.into_iter()) + .zip(finished_header_rows) .skip(first_region) .enumerate() .zip(fragment) @@ -162,22 +182,9 @@ impl GridLayouter<'_> { } else { // The rowspan continuation starts after the header (thus, // at a position after the sum of the laid out header - // rows). - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - let header_rows = self - .rrows - .get(i) - .map(Vec::as_slice) - .or(current_rrows) - .unwrap_or(&[]) - .iter() - .take_while(|row| row.y < header.end); - - header_rows.map(|row| row.height).sum() - } else { - // Without a header, start at the very top of the region. - Abs::zero() - } + // rows). Without a header, this is zero, so the rowspan can + // start at the very top of the region as usual. + header_dy }; finished.push_frame(Point::new(dx, dy), frame); @@ -231,15 +238,13 @@ impl GridLayouter<'_> { // current row is dynamic and depends on the amount of upcoming // unbreakable cells (with or without a rowspan setting). let mut amount_unbreakable_rows = None; - if let Some(Repeatable::NotRepeated(header)) = &self.grid.header { - if current_row < header.end { - // Non-repeated header, so keep it unbreakable. - amount_unbreakable_rows = Some(header.end); - } - } - if let Some(Repeatable::NotRepeated(footer)) = &self.grid.footer { - if current_row >= footer.start { + if let Some(footer) = &self.grid.footer { + if !footer.repeated && current_row >= footer.start { // Non-repeated footer, so keep it unbreakable. + // + // TODO(subfooters): This will become unnecessary + // once non-repeated footers are treated differently and + // have widow prevention. amount_unbreakable_rows = Some(self.grid.rows.len() - footer.start); } } @@ -254,10 +259,7 @@ impl GridLayouter<'_> { // Skip to fitting region. while !self.regions.size.y.fits(row_group.height) - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) + && self.may_progress_with_repeats() { self.finish_region(engine, false)?; } @@ -396,16 +398,29 @@ impl GridLayouter<'_> { // auto rows don't depend on the backlog, as they only span one // region. if breakable - && (matches!(self.grid.header, Some(Repeatable::Repeated(_))) - || matches!(self.grid.footer, Some(Repeatable::Repeated(_)))) + && (!self.repeating_headers.is_empty() + || !self.pending_headers.is_empty() + || matches!(&self.grid.footer, Some(footer) if footer.repeated)) { // Subtract header and footer height from all upcoming regions // when measuring the cell, including the last repeated region. // // This will update the 'custom_backlog' vector with the // updated heights of the upcoming regions. + // + // We predict that header height will only include that of + // repeating headers, as we can assume non-repeating headers in + // the first region have been successfully placed, unless + // something didn't fit on the first region of the auto row, + // but we will only find that out after measurement, and if + // that happens, we discard the measurement and try again. let mapped_regions = self.regions.map(&mut custom_backlog, |size| { - Size::new(size.x, size.y - self.header_height - self.footer_height) + Size::new( + size.x, + size.y + - self.current.repeating_header_height + - self.current.footer_height, + ) }); // Callees must use the custom backlog instead of the current @@ -459,6 +474,7 @@ impl GridLayouter<'_> { // Height of the rowspan covered by spanned rows in the current // region. let laid_out_height: Abs = self + .current .lrows .iter() .filter_map(|row| match row { @@ -506,7 +522,12 @@ impl GridLayouter<'_> { .iter() .copied() .chain(std::iter::once(if breakable { - self.initial.y - self.header_height - self.footer_height + // Here we are calculating the available height for a + // rowspan from the top of the current region, so + // we have to use initial header heights (note that + // header height can change in the middle of the + // region). + self.current.initial_after_repeats } else { // When measuring unbreakable auto rows, infinite // height is available for content to expand. @@ -518,11 +539,13 @@ impl GridLayouter<'_> { // rowspan's already laid out heights with the current // region's height and current backlog to ensure a good // level of accuracy in the measurements. - let backlog = self - .regions - .backlog - .iter() - .map(|&size| size - self.header_height - self.footer_height); + // + // Assume only repeating headers will survive starting at + // the next region. + let backlog = self.regions.backlog.iter().map(|&size| { + size - self.current.repeating_header_height + - self.current.footer_height + }); heights_up_to_current_region.chain(backlog).collect::<Vec<_>>() } else { @@ -536,10 +559,10 @@ impl GridLayouter<'_> { height = *rowspan_height; backlog = None; full = rowspan_full; - last = self - .regions - .last - .map(|size| size - self.header_height - self.footer_height); + last = self.regions.last.map(|size| { + size - self.current.repeating_header_height + - self.current.footer_height + }); } else { // The rowspan started in the current region, as its vector // of heights in regions is currently empty. @@ -741,10 +764,11 @@ impl GridLayouter<'_> { simulated_regions.next(); disambiguator += 1; - // Subtract the initial header and footer height, since that's the - // height we used when subtracting from the region backlog's + // Subtract the repeating header and footer height, since that's + // the height we used when subtracting from the region backlog's // heights while measuring cells. - simulated_regions.size.y -= self.header_height + self.footer_height; + simulated_regions.size.y -= + self.current.repeating_header_height + self.current.footer_height; } if let Some(original_last_resolved_size) = last_resolved_size { @@ -876,12 +900,8 @@ impl GridLayouter<'_> { // which, when used and combined with upcoming spanned rows, covers all // of the requested rowspan height, we give up. for _attempt in 0..5 { - let rowspan_simulator = RowspanSimulator::new( - disambiguator, - simulated_regions, - self.header_height, - self.footer_height, - ); + let rowspan_simulator = + RowspanSimulator::new(disambiguator, simulated_regions, &self.current); let total_spanned_height = rowspan_simulator.simulate_rowspan_layout( y, @@ -963,7 +983,8 @@ impl GridLayouter<'_> { { extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero()); simulated_regions.next(); - simulated_regions.size.y -= self.header_height + self.footer_height; + simulated_regions.size.y -= + self.current.repeating_header_height + self.current.footer_height; disambiguator += 1; } simulated_regions.size.y -= extra_amount_to_grow; @@ -980,10 +1001,17 @@ struct RowspanSimulator<'a> { finished: usize, /// The state of regions during the simulation. regions: Regions<'a>, - /// The height of the header in the currently simulated region. + /// The total height of headers in the currently simulated region. header_height: Abs, - /// The height of the footer in the currently simulated region. + /// The total height of footers in the currently simulated region. footer_height: Abs, + /// Whether `self.regions.may_progress()` was `true` at the top of the + /// region, indicating we can progress anywhere in the current region, + /// even right after a repeated header. + could_progress_at_top: bool, + /// Available height after laying out repeated headers at the top of the + /// currently simulated region. + initial_after_repeats: Abs, /// The total spanned height so far in the simulation. total_spanned_height: Abs, /// Height of the latest spanned gutter row in the simulation. @@ -997,14 +1025,19 @@ impl<'a> RowspanSimulator<'a> { fn new( finished: usize, regions: Regions<'a>, - header_height: Abs, - footer_height: Abs, + current: &super::layouter::Current, ) -> Self { Self { finished, regions, - header_height, - footer_height, + // There can be no new headers or footers within a multi-page + // rowspan, since headers and footers are unbreakable, so + // assuming the repeating header height and footer height + // won't change is safe. + header_height: current.repeating_header_height, + footer_height: current.footer_height, + could_progress_at_top: current.could_progress_at_top, + initial_after_repeats: current.initial_after_repeats, total_spanned_height: Abs::zero(), latest_spanned_gutter_height: Abs::zero(), } @@ -1053,10 +1086,7 @@ impl<'a> RowspanSimulator<'a> { 0, )?; while !self.regions.size.y.fits(row_group.height) - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) + && self.may_progress_with_repeats() { self.finish_region(layouter, engine)?; } @@ -1078,10 +1108,7 @@ impl<'a> RowspanSimulator<'a> { let mut skipped_region = false; while unbreakable_rows_left == 0 && !self.regions.size.y.fits(height) - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) + && self.may_progress_with_repeats() { self.finish_region(layouter, engine)?; @@ -1127,23 +1154,37 @@ impl<'a> RowspanSimulator<'a> { // our simulation checks what happens AFTER the auto row, so we can // just use the original backlog from `self.regions`. let disambiguator = self.finished; - let header_height = - if let Some(Repeatable::Repeated(header)) = &layouter.grid.header { - layouter - .simulate_header(header, &self.regions, engine, disambiguator)? - .height - } else { - Abs::zero() - }; - let footer_height = - if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer { - layouter - .simulate_footer(footer, &self.regions, engine, disambiguator)? - .height - } else { - Abs::zero() - }; + let (repeating_headers, header_height) = if !layouter.repeating_headers.is_empty() + || !layouter.pending_headers.is_empty() + { + // Only repeating headers have survived after the first region + // break. + let repeating_headers = layouter.repeating_headers.iter().copied().chain( + layouter.pending_headers.iter().filter_map(Repeatable::as_repeated), + ); + + let header_height = layouter.simulate_header_height( + repeating_headers.clone(), + &self.regions, + engine, + disambiguator, + )?; + + (Some(repeating_headers), header_height) + } else { + (None, Abs::zero()) + }; + + let footer_height = if let Some(footer) = + layouter.grid.footer.as_ref().and_then(Repeatable::as_repeated) + { + layouter + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height + } else { + Abs::zero() + }; let mut skipped_region = false; @@ -1156,19 +1197,24 @@ impl<'a> RowspanSimulator<'a> { skipped_region = true; } - if let Some(Repeatable::Repeated(header)) = &layouter.grid.header { + if let Some(repeating_headers) = repeating_headers { self.header_height = if skipped_region { // Simulate headers again, at the new region, as // the full region height may change. - layouter - .simulate_header(header, &self.regions, engine, disambiguator)? - .height + layouter.simulate_header_height( + repeating_headers, + &self.regions, + engine, + disambiguator, + )? } else { header_height }; } - if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer { + if let Some(footer) = + layouter.grid.footer.as_ref().and_then(Repeatable::as_repeated) + { self.footer_height = if skipped_region { // Simulate footers again, at the new region, as // the full region height may change. @@ -1185,6 +1231,7 @@ impl<'a> RowspanSimulator<'a> { // header or footer (as an invariant, any rowspans spanning any header // or footer rows are fully contained within that header's or footer's rows). self.regions.size.y -= self.header_height + self.footer_height; + self.initial_after_repeats = self.regions.size.y; Ok(()) } @@ -1201,8 +1248,18 @@ impl<'a> RowspanSimulator<'a> { self.regions.next(); self.finished += 1; + self.could_progress_at_top = self.regions.may_progress(); self.simulate_header_footer_layout(layouter, engine) } + + /// Similar to [`GridLayouter::may_progress_with_repeats`] but for rowspan + /// simulation. + #[inline] + fn may_progress_with_repeats(&self) -> bool { + self.could_progress_at_top + || self.regions.last.is_some() + && self.regions.size.y != self.initial_after_repeats + } } /// Subtracts some size from the end of a vector of sizes. |
