diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index 8bdbda6e8..c0ea83727 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -46,8 +46,21 @@ pub struct GridLayouter<'a> { pub(super) rowspans: Vec, /// The initial size of the current region before we started subtracting. pub(super) initial: Size, + /// The amount of header rows at the start of the current region. Note that + /// `repeating_headers` and `pending_headers` can change if we find a new + /// header inside the region, so this has to be stored separately for each + /// new region - we can't just reuse those. + /// + /// This is used for orphan prevention checks (if there are no rows other + /// than header rows upon finishing a region, we'd have an orphan). In + /// addition, this information is stored for each finished region so + /// rowspans placed later can know which rows to skip at the top of the + /// region when spanning more than one page. + pub(super) current_header_rows: usize, /// Frames for finished regions. pub(super) finished: Vec, + /// The height of header rows on each finished region. + pub(super) finished_header_rows: Vec, /// Whether this is an RTL grid. pub(super) is_rtl: bool, /// Currently repeating headers, one per level. @@ -131,7 +144,9 @@ impl<'a> GridLayouter<'a> { unbreakable_rows_left: 0, rowspans: vec![], initial: regions.size, + current_header_rows: 0, finished: vec![], + finished_header_rows: vec![], is_rtl: TextElem::dir_in(styles) == Dir::RTL, repeating_headers: vec![], upcoming_headers: &grid.headers, @@ -979,16 +994,8 @@ 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) - { + if self.lrows.len() < self.current_header_rows { // Add to header height, as we are in a header row. - // TODO: Should we only bump from upcoming_headers to - // repeating_headers AFTER the header height calculation? self.header_height += first; } @@ -1221,14 +1228,9 @@ 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. + if self.lrows.len() < self.current_header_rows { + // Add to header height (not all headers were laid out yet, so this + // must be a repeated or pending header at the top of the region). self.header_height += resolved; } @@ -1389,20 +1391,20 @@ impl<'a> GridLayouter<'a> { self.lrows.pop().unwrap(); } - let footer_would_be_widow = if let Some(last_header) = self - .pending_headers - .last() - .map(Repeatable::unwrap) - .or_else(|| self.repeating_headers.last().map(|h| *h)) + let footer_would_be_widow = if let Some(last_header_row) = self + .current_header_rows + .checked_sub(1) + .and_then(|last_header_index| self.lrows.get(last_header_index)) { - if self.grid.rows.len() > last_header.end + let last_header_end = last_header_row.index(); + if self.grid.rows.len() > last_header_end && self .grid .footer .as_ref() .and_then(Repeatable::as_repeated) - .is_none_or(|footer| footer.start != last_header.end) - && self.lrows.last().is_some_and(|row| row.index() < last_header.end) + .is_none_or(|footer| footer.start != last_header_end) + && self.lrows.last().is_some_and(|row| row.index() < last_header_end) && !in_last_with_offset( self.regions, self.header_height + self.footer_height, @@ -1471,9 +1473,10 @@ impl<'a> GridLayouter<'a> { let mut pos = Point::zero(); let mut rrows = vec![]; let current_region = self.finished.len(); + let mut 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.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) => { @@ -1484,6 +1487,9 @@ impl<'a> GridLayouter<'a> { }; let height = frame.height(); + if i < self.current_header_rows { + header_row_height += height; + } // Ensure rowspans which span this row will have enough space to // be laid out over it later. @@ -1562,7 +1568,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, header_row_height)), + engine, + )?; } else { i += 1; } @@ -1573,9 +1583,10 @@ impl<'a> GridLayouter<'a> { pos.y += height; } - self.finish_region_internal(output, rrows); + self.finish_region_internal(output, rrows, header_row_height); if !last { + self.current_header_rows = 0; let disambiguator = self.finished.len(); if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { self.prepare_footer(footer, engine, disambiguator)?; @@ -1602,11 +1613,16 @@ impl<'a> GridLayouter<'a> { &mut self, output: Frame, resolved_rows: Vec, + header_row_height: Abs, ) { self.finished.push(output); self.rrows.push(resolved_rows); self.regions.next(); self.initial = self.regions.size; + + if !self.grid.headers.is_empty() { + self.finished_header_rows.push(header_row_height); + } } } diff --git a/crates/typst-layout/src/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs index fac2efa31..6ff625c32 100644 --- a/crates/typst-layout/src/grid/repeated.rs +++ b/crates/typst-layout/src/grid/repeated.rs @@ -189,7 +189,11 @@ impl<'a> GridLayouter<'a> { { // 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![], + Abs::zero(), + ); // TODO: re-calculate heights of headers and footers on each region // if 'full'changes? (Assuming height doesn't change for now...) @@ -250,9 +254,18 @@ impl<'a> GridLayouter<'a> { // TODO: maybe extract this into a function to share code with multiple // footers. if matches!(headers, HeadersToLayout::RepeatingAndPending) || skipped_region { - self.unbreakable_rows_left += + let repeating_header_rows = total_header_row_count(self.repeating_headers.iter().map(Deref::deref)); + let pending_header_rows = total_header_row_count( + self.pending_headers.into_iter().map(Repeatable::unwrap), + ); + + // Include both repeating and pending header rows as this number is + // used for orphan prevention. + self.current_header_rows = repeating_header_rows + pending_header_rows; + self.unbreakable_rows_left += repeating_header_rows + pending_header_rows; + // Use indices to avoid double borrow. We don't mutate headers in // 'layout_row' so this is fine. let mut i = 0; @@ -336,7 +349,11 @@ impl<'a> GridLayouter<'a> { { // 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![], + Abs::zero(), + ); skipped_region = true; } diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs index a99d38fa8..2c970784c 100644 --- a/crates/typst-layout/src/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -5,7 +5,7 @@ use typst_library::layout::grid::resolve::Repeatable; use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; use typst_utils::MaybeReverseIter; -use super::layouter::{in_last_with_offset, points, Row, RowPiece}; +use super::layouter::{in_last_with_offset, points, Row}; use super::{layout_cell, Cell, GridLayouter}; /// All information needed to layout a single rowspan. @@ -87,10 +87,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 @@ -98,7 +98,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 { @@ -142,11 +142,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() + .copied() + .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) @@ -158,25 +178,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 { - // TODO: Need a way to distinguish header 'rrows' for each - // region, as this calculation - i.e., header height at - // each region - will change depending on'i'. - 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);