diff options
| -rw-r--r-- | crates/typst-layout/src/grid/layouter.rs | 111 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/repeated.rs | 218 |
2 files changed, 232 insertions, 97 deletions
diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index d4682a9d..2c7f957f 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<Abs>, + /// 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<Abs>, /// 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<Fragment> { 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 7ca937e7..89e785ef 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<Footer>] { + /// Place a footer we have reached through normal row layout. + pub fn place_new_footer( + &mut self, + engine: &mut Engine, + footer: &Repeatable<Footer>, + ) -> SourceResult<()> { + // TODO(subfooters): short-lived check + if !footer.repeated { + // TODO(subfooters): widow prevention for this. + // Will need some lookahead. For now, act as short-lived. + let footer_height = + self.simulate_footer(footer, &self.regions, engine, 0)?.height; + + // Skip to fitting region where only this footer fits. + while self.unbreakable_rows_left == 0 + && !self.regions.size.y.fits(footer_height) + && self.may_progress_with_repeats() + { + // Advance regions until we can place the footer. + // Treat as a normal row group. + self.finish_region(engine, false)?; + } + + self.layout_footer(footer, true, engine, 0)?; + } else { + // Placing a non-short-lived repeating footer, so it must be + // the latest one in the repeating footers vector. + let latest_repeating_footer = self.repeating_footers.pop().unwrap(); + assert_eq!(latest_repeating_footer.start, footer.start); + + let expected_footer_height = + self.current.repeating_footer_heights.pop().unwrap(); + + // Ensure upcoming rows won't see that this footer will occupy + // any space in future regions anymore. + self.current.footer_height -= expected_footer_height; + + // Ensure footer rows have their own expected height + // available. While not that relevant for them, as they will be + // laid out as an unbreakable row group, it's relevant for any + // further rows in the same region. + self.regions.size.y += expected_footer_height; + + self.layout_footer(footer, false, engine, self.finished.len())?; + } + + // If the next group of footers would conflict with other repeating + // footers, wait for them to finish repeating before adding more to + // repeat. + if self.repeating_footers.is_empty() + || self + .upcoming_sorted_footers + .first() + .is_none_or(|f| f.level >= footer.level) + { + self.prepare_next_repeating_footers(false, engine); + return Ok(()); + } + + Ok(()) + } + + /// Takes all non-conflicting consecutive footers which are about to start + /// repeating, skips to the first region where they all fit, and pushes + /// them to `repeating_footers`, sorted by ascending levels. + pub fn prepare_next_repeating_footers( + &mut self, + first_footers: bool, + engine: &mut Engine, + ) -> SourceResult<()> { let [next_footer, other_footers @ ..] = self.upcoming_sorted_footers else { - return &[]; + // No footers to take. + return Ok(()); }; + // TODO(subfooters): also ignore short-lived footers. if !next_footer.repeated { - // TODO(subfooters): grouping and laying them out together? + // Skip this footer and don't do anything until we get to it. + // + // TODO(subfooters): grouping and laying out non-repeated with + // repeated, with widow prevention. self.upcoming_sorted_footers = other_footers; - return &[]; - } - - if other_footers.is_empty() { - return std::mem::replace(&mut self.upcoming_sorted_footers, &[]); + return Ok(()); } let first_conflicting_index = other_footers @@ -488,77 +559,110 @@ impl<'a> GridLayouter<'a> { self.upcoming_sorted_footers.split_at(first_conflicting_index); self.upcoming_sorted_footers = new_upcoming_footers; - return next_repeating_footers; + self.prepare_repeating_footers( + next_repeating_footers.iter().map(Repeatable::deref), + first_footers, + engine, + 0, + ); + + Ok(()) } - /// Updates `self.footer_height` by simulating the footer, and skips to fitting region. - pub fn prepare_footer( + /// Updates `self.current.repeating_footer_height` by simulating repeating + /// footers, and skips to fitting region. + pub fn prepare_repeating_footers( &mut self, - footer: &Footer, + footers: impl Iterator<Item = &'a Footer> + ExactSizeIterator + Clone, + at_region_top: bool, engine: &mut Engine, disambiguator: usize, ) -> SourceResult<()> { - let footer_height = self - .simulate_footer(footer, &self.regions, engine, disambiguator)? - .height; + let (mut expected_footer_height, mut expected_footer_heights) = + self.simulate_footer_heights(footers.clone(), engine, disambiguator)?; + + // Skip to fitting region where all of them fit at once. + // + // Can't be widows: they are assumed to not be short-lived, so + // there is at least one non-footer before them, and this + // function is called right after placing a new footer, but + // before the next non-footer, or at the top of the region, + // at which point we haven't reached the row before the highest + // level footer yet since the footer itself won't cause a + // region break. let mut skipped_region = false; while self.unbreakable_rows_left == 0 - && !self.regions.size.y.fits(footer_height) + && !self.regions.size.y.fits(expected_footer_height) && self.regions.may_progress() { // Advance regions without any output until we can place the // footer. - self.finish_region_internal( - Frame::soft(Axes::splat(Abs::zero())), - vec![], - Default::default(), - ); + if at_region_top { + self.finish_region_internal( + Frame::soft(Axes::splat(Abs::zero())), + vec![], + Default::default(), + ); + } else { + self.finish_region(engine, false)?; + } skipped_region = true; } - // 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 { + if skipped_region { // Simulate the footer again; the region's 'full' might have - // changed. - self.simulate_footer(footer, &self.regions, engine, disambiguator)? - .height - } else { - footer_height - }; + // changed, and the vector of heights was cleared. + (expected_footer_height, expected_footer_heights) = + self.simulate_footer_heights(footers, engine, disambiguator)?; + } + + self.current.footer_height += expected_footer_height; + self.current.repeating_footer_heights.extend(expected_footer_heights); Ok(()) } + fn simulate_footer_heights( + &self, + footers: impl Iterator<Item = &'a Footer> + ExactSizeIterator, + engine: &mut Engine, + disambiguator: usize, + ) -> SourceResult<(Abs, Vec<Abs>)> { + let mut total_footer_height = Abs::zero(); + let mut footer_heights = Vec::with_capacity(footers.len()); + for footer in footers { + let footer_height = self + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height; + + total_footer_height += footer_height; + footer_heights.push(footer_height); + } + + Ok((total_footer_height, footer_heights)) + } + /// Lays out all rows in the footer. /// They are unbreakable. pub fn layout_footer( &mut self, footer: &Footer, + as_short_lived: bool, engine: &mut Engine, disambiguator: usize, ) -> SourceResult<()> { - // 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.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; + let footer_len = footer.end - footer.start; self.unbreakable_rows_left += footer_len; + // TODO(subfooters): also consider omitted gutter before the footer + // when there is a header right before it taking it. for y in footer.start..self.grid.rows.len() { self.layout_row_with_state( y, engine, disambiguator, RowState { - in_active_repeatable: repeats, + in_active_repeatable: !as_short_lived, ..Default::default() }, )?; @@ -597,3 +701,11 @@ pub fn total_header_row_count<'h>( ) -> usize { headers.into_iter().map(|h| h.range.end - h.range.start).sum() } + +/// The total amount of rows in the given list of headers. +#[inline] +pub fn total_footer_row_count<'f>( + footers: impl IntoIterator<Item = &'f Footer>, +) -> usize { + footers.into_iter().map(|f| f.end - f.start).sum() +} |
