summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst-layout/src/grid/layouter.rs111
-rw-r--r--crates/typst-layout/src/grid/repeated.rs218
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()
+}