From b63f6c99df0f5d0b243e85f4b2982ac6b07b4de7 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Wed, 21 May 2025 00:42:40 -0300 Subject: [PATCH] initial footer simulation and placement --- crates/typst-layout/src/grid/layouter.rs | 111 +++++++----- crates/typst-layout/src/grid/repeated.rs | 222 +++++++++++++++++------ 2 files changed, 234 insertions(+), 99 deletions(-) diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index d4682a9da..2c7f957f2 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -166,6 +166,12 @@ pub(super) struct Current { /// when finding a new header and causing existing repeating headers to /// stop. pub(super) repeating_header_heights: Vec, + /// The height for each repeating footer that will be placed in this region. + /// + /// This is used to know how much to update `repeating_footer_height` by + /// when finding a footer and causing existing repeating footers to + /// stop (and new ones to start). + pub(super) repeating_footer_heights: Vec, /// The simulated footer height for this region. /// /// The simulation occurs before any rows are laid out for a region. @@ -264,7 +270,7 @@ impl<'a> GridLayouter<'a> { repeating_headers: vec![], upcoming_headers: &grid.headers, pending_headers: Default::default(), - // This is calculated on layout + // This is updated on layout repeating_footers: vec![], upcoming_footers: &grid.footers, upcoming_sorted_footers: &grid.sorted_footers, @@ -279,6 +285,7 @@ impl<'a> GridLayouter<'a> { lrows_orphan_snapshot: None, repeating_header_height: Abs::zero(), repeating_header_heights: vec![], + repeating_footer_heights: vec![], footer_height: Abs::zero(), }, span, @@ -289,15 +296,12 @@ impl<'a> GridLayouter<'a> { pub fn layout(mut self, engine: &mut Engine) -> SourceResult { self.measure_columns(engine)?; - 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; - } - } + self.prepare_next_repeating_footers(true, engine)?; + + // Ensure rows in the first region will be aware of the possible + // presence of the footer. + self.regions.size.y -= self.current.footer_height; + self.current.initial_after_repeats = self.regions.size.y; let mut y = 0; let mut consecutive_header_count = 0; @@ -313,13 +317,14 @@ impl<'a> GridLayouter<'a> { } } - 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; + if let Some(next_footer) = self.upcoming_footers.first() { + // TODO(subfooters): effective range (consider gutter before + // if it was removed) + if next_footer.range().contains(&y) { + self.place_new_footer(engine, next_footer)?; + self.flush_orphans(); + y = next_footer.end; + continue; } } @@ -1581,26 +1586,25 @@ impl<'a> GridLayouter<'a> { // 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) + let may_place_footers = !self.repeating_footers.is_empty() && self.current.lrows.is_empty() && self.current.could_progress_at_top; - let mut laid_out_footer_start = None; - 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())?; - } + if may_place_footers { + // 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 (it is removed from repeating_footers once it is + // reached). + // + // Use index for iteration to avoid borrow conflict. + // + // TODO(subfooters): "pending footers" vector for footers we're + // about to place. Needed for widow prevention of non-repeated + // footers. + let mut i = 0; + while let Some(footer) = self.repeating_footers.get(i) { + self.layout_footer(footer, false, engine, self.finished.len())?; + i += 1; } } @@ -1699,12 +1703,24 @@ impl<'a> GridLayouter<'a> { // laid out at the first frame of the row). // Any rowspans ending before this row are laid out even // on this row's first frame. - if laid_out_footer_start.is_none_or(|footer_start| { - // If this is a footer row, then only lay out this rowspan - // if the rowspan is contained within the footer. - y < footer_start || rowspan.y >= footer_start - }) && (rowspan.y + rowspan.rowspan < y + 1 - || rowspan.y + rowspan.rowspan == y + 1 && is_last) + if !may_place_footers + || self.repeating_footers.iter().all(|footer| { + // If this is a footer row, then only lay out this rowspan + // if the rowspan is contained within the footer. + // Since the footer is a row from "the future", it + // always has a larger Y than all active rowspans, + // so we must not interpret a rowspan before it to have + // already ended because we saw a repeated footer. + // + // Of course, not a concern for non-repeated or + // short-lived footers as they only appear once. + // + // TODO(subfooters): use effective range + // (what about the gutter?). + !footer.range().contains(&y) + || footer.range().contains(&rowspan.y) + }) && (rowspan.y + rowspan.rowspan < y + 1 + || rowspan.y + rowspan.rowspan == y + 1 && is_last) { // Rowspan ends at this or an earlier row, so we take // it from the rowspans vector and lay it out. @@ -1748,10 +1764,15 @@ impl<'a> GridLayouter<'a> { if !last { let disambiguator = self.finished.len(); - if let Some(footer) = - self.grid.footer.as_ref().and_then(Repeatable::as_repeated) - { - self.prepare_footer(footer, engine, disambiguator)?; + if !self.repeating_footers.is_empty() { + // TODO(subfooters): let's not... + let footers = self.repeating_footers.clone(); + self.prepare_repeating_footers( + footers.iter().map(|f| *f), + true, + engine, + disambiguator, + )?; } // Ensure rows don't try to overrun the footer. @@ -1794,6 +1815,8 @@ impl<'a> GridLayouter<'a> { self.current.last_repeated_header_end = 0; self.current.repeating_header_height = Abs::zero(); self.current.repeating_header_heights.clear(); + self.current.footer_height = Abs::zero(); + self.current.repeating_footer_heights.clear(); if !self.grid.headers.is_empty() { self.finished_header_rows.push(header_row_info); diff --git a/crates/typst-layout/src/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs index 7ca937e7e..89e785efc 100644 --- a/crates/typst-layout/src/grid/repeated.rs +++ b/crates/typst-layout/src/grid/repeated.rs @@ -240,16 +240,17 @@ impl<'a> GridLayouter<'a> { self.current.initial_after_repeats = self.regions.size.y; } - if let Some(footer) = &self.grid.footer { - if footer.repeated && skipped_region { - // Simulate the footer again; the region's 'full' might have - // changed. - 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; - } + if !self.repeating_footers.is_empty() && skipped_region { + // Simulate the footer again; the region's 'full' might have + // changed. + let (footer_height, footer_heights) = self.simulate_footer_heights( + self.repeating_footers.iter().map(|x| *x), + engine, + disambiguator, + )?; + + self.current.footer_height = footer_height; + self.current.repeating_footer_heights.extend(footer_heights); } let repeating_header_rows = @@ -463,19 +464,89 @@ impl<'a> GridLayouter<'a> { ) } - pub fn bump_repeating_footers(&mut self) -> &'a [Repeatable