initial footer simulation and placement

This commit is contained in:
PgBiel 2025-05-21 00:42:40 -03:00
parent db2ac385a9
commit b63f6c99df
2 changed files with 234 additions and 99 deletions

View File

@ -166,6 +166,12 @@ pub(super) struct Current {
/// when finding a new header and causing existing repeating headers to /// when finding a new header and causing existing repeating headers to
/// stop. /// stop.
pub(super) repeating_header_heights: Vec<Abs>, 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 simulated footer height for this region.
/// ///
/// The simulation occurs before any rows are laid out for a region. /// The simulation occurs before any rows are laid out for a region.
@ -264,7 +270,7 @@ impl<'a> GridLayouter<'a> {
repeating_headers: vec![], repeating_headers: vec![],
upcoming_headers: &grid.headers, upcoming_headers: &grid.headers,
pending_headers: Default::default(), pending_headers: Default::default(),
// This is calculated on layout // This is updated on layout
repeating_footers: vec![], repeating_footers: vec![],
upcoming_footers: &grid.footers, upcoming_footers: &grid.footers,
upcoming_sorted_footers: &grid.sorted_footers, upcoming_sorted_footers: &grid.sorted_footers,
@ -279,6 +285,7 @@ impl<'a> GridLayouter<'a> {
lrows_orphan_snapshot: None, lrows_orphan_snapshot: None,
repeating_header_height: Abs::zero(), repeating_header_height: Abs::zero(),
repeating_header_heights: vec![], repeating_header_heights: vec![],
repeating_footer_heights: vec![],
footer_height: Abs::zero(), footer_height: Abs::zero(),
}, },
span, span,
@ -289,15 +296,12 @@ impl<'a> GridLayouter<'a> {
pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> { pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> {
self.measure_columns(engine)?; self.measure_columns(engine)?;
if let Some(footer) = &self.grid.footer { self.prepare_next_repeating_footers(true, engine)?;
if footer.repeated {
// Ensure rows in the first region will be aware of the // Ensure rows in the first region will be aware of the possible
// possible presence of the footer. // presence of the footer.
self.prepare_footer(footer, engine, 0)?; self.regions.size.y -= self.current.footer_height;
self.regions.size.y -= self.current.footer_height; self.current.initial_after_repeats = self.regions.size.y;
self.current.initial_after_repeats = self.regions.size.y;
}
}
let mut y = 0; let mut y = 0;
let mut consecutive_header_count = 0; let mut consecutive_header_count = 0;
@ -313,13 +317,14 @@ impl<'a> GridLayouter<'a> {
} }
} }
if let Some(footer) = &self.grid.footer { if let Some(next_footer) = self.upcoming_footers.first() {
if footer.repeated && y >= footer.start { // TODO(subfooters): effective range (consider gutter before
if y == footer.start { // if it was removed)
self.layout_footer(footer, engine, self.finished.len())?; if next_footer.range().contains(&y) {
self.flush_orphans(); self.place_new_footer(engine, next_footer)?;
} self.flush_orphans();
y = footer.end; y = next_footer.end;
continue; continue;
} }
} }
@ -1581,26 +1586,25 @@ impl<'a> GridLayouter<'a> {
// TODO(subfooters): explicitly check for short-lived footers. // TODO(subfooters): explicitly check for short-lived footers.
// TODO(subfooters): widow prevention for non-repeated footers with a // TODO(subfooters): widow prevention for non-repeated footers with a
// similar mechanism / when implementing multiple footers. // 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.lrows.is_empty()
&& self.current.could_progress_at_top; && self.current.could_progress_at_top;
let mut laid_out_footer_start = None; if may_place_footers {
if !footer_would_be_widow { // Don't layout the footer if it would be alone with the header
if let Some(footer) = &self.grid.footer { // in the page (hence the widow check), and don't layout it
// Don't layout the footer if it would be alone with the header // twice (it is removed from repeating_footers once it is
// in the page (hence the widow check), and don't layout it // reached).
// twice (check below). //
// // Use index for iteration to avoid borrow conflict.
// TODO(subfooters): this check can be replaced by a vector of //
// repeating footers in the future, and/or some "pending // TODO(subfooters): "pending footers" vector for footers we're
// footers" vector for footers we're about to place. // about to place. Needed for widow prevention of non-repeated
if footer.repeated // footers.
&& self.current.lrows.iter().all(|row| row.index() < footer.start) let mut i = 0;
{ while let Some(footer) = self.repeating_footers.get(i) {
laid_out_footer_start = Some(footer.start); self.layout_footer(footer, false, engine, self.finished.len())?;
self.layout_footer(footer, engine, self.finished.len())?; i += 1;
}
} }
} }
@ -1699,12 +1703,24 @@ impl<'a> GridLayouter<'a> {
// laid out at the first frame of the row). // laid out at the first frame of the row).
// Any rowspans ending before this row are laid out even // Any rowspans ending before this row are laid out even
// on this row's first frame. // on this row's first frame.
if laid_out_footer_start.is_none_or(|footer_start| { if !may_place_footers
// If this is a footer row, then only lay out this rowspan || self.repeating_footers.iter().all(|footer| {
// if the rowspan is contained within the footer. // If this is a footer row, then only lay out this rowspan
y < footer_start || rowspan.y >= footer_start // if the rowspan is contained within the footer.
}) && (rowspan.y + rowspan.rowspan < y + 1 // Since the footer is a row from "the future", it
|| rowspan.y + rowspan.rowspan == y + 1 && is_last) // 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 // Rowspan ends at this or an earlier row, so we take
// it from the rowspans vector and lay it out. // it from the rowspans vector and lay it out.
@ -1748,10 +1764,15 @@ impl<'a> GridLayouter<'a> {
if !last { if !last {
let disambiguator = self.finished.len(); let disambiguator = self.finished.len();
if let Some(footer) = if !self.repeating_footers.is_empty() {
self.grid.footer.as_ref().and_then(Repeatable::as_repeated) // TODO(subfooters): let's not...
{ let footers = self.repeating_footers.clone();
self.prepare_footer(footer, engine, disambiguator)?; self.prepare_repeating_footers(
footers.iter().map(|f| *f),
true,
engine,
disambiguator,
)?;
} }
// Ensure rows don't try to overrun the footer. // 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.last_repeated_header_end = 0;
self.current.repeating_header_height = Abs::zero(); self.current.repeating_header_height = Abs::zero();
self.current.repeating_header_heights.clear(); self.current.repeating_header_heights.clear();
self.current.footer_height = Abs::zero();
self.current.repeating_footer_heights.clear();
if !self.grid.headers.is_empty() { if !self.grid.headers.is_empty() {
self.finished_header_rows.push(header_row_info); self.finished_header_rows.push(header_row_info);

View File

@ -240,16 +240,17 @@ impl<'a> GridLayouter<'a> {
self.current.initial_after_repeats = self.regions.size.y; self.current.initial_after_repeats = self.regions.size.y;
} }
if let Some(footer) = &self.grid.footer { if !self.repeating_footers.is_empty() && skipped_region {
if footer.repeated && skipped_region { // Simulate the footer again; the region's 'full' might have
// Simulate the footer again; the region's 'full' might have // changed.
// changed. let (footer_height, footer_heights) = self.simulate_footer_heights(
self.regions.size.y += self.current.footer_height; self.repeating_footers.iter().map(|x| *x),
self.current.footer_height = self engine,
.simulate_footer(footer, &self.regions, engine, disambiguator)? disambiguator,
.height; )?;
self.regions.size.y -= self.current.footer_height;
} self.current.footer_height = footer_height;
self.current.repeating_footer_heights.extend(footer_heights);
} }
let repeating_header_rows = 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.
let [next_footer, other_footers @ ..] = self.upcoming_sorted_footers else { pub fn place_new_footer(
return &[]; &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;
if !next_footer.repeated { // Skip to fitting region where only this footer fits.
// TODO(subfooters): grouping and laying them out together? while self.unbreakable_rows_left == 0
self.upcoming_sorted_footers = other_footers; && !self.regions.size.y.fits(footer_height)
return &[]; && 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 other_footers.is_empty() { // If the next group of footers would conflict with other repeating
return std::mem::replace(&mut self.upcoming_sorted_footers, &[]); // 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 {
// No footers to take.
return Ok(());
};
// TODO(subfooters): also ignore short-lived footers.
if !next_footer.repeated {
// 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 Ok(());
} }
let first_conflicting_index = other_footers 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.split_at(first_conflicting_index);
self.upcoming_sorted_footers = new_upcoming_footers; 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. /// Updates `self.current.repeating_footer_height` by simulating repeating
pub fn prepare_footer( /// footers, and skips to fitting region.
pub fn prepare_repeating_footers(
&mut self, &mut self,
footer: &Footer, footers: impl Iterator<Item = &'a Footer> + ExactSizeIterator + Clone,
at_region_top: bool,
engine: &mut Engine, engine: &mut Engine,
disambiguator: usize, disambiguator: usize,
) -> SourceResult<()> { ) -> SourceResult<()> {
let footer_height = self let (mut expected_footer_height, mut expected_footer_heights) =
.simulate_footer(footer, &self.regions, engine, disambiguator)? self.simulate_footer_heights(footers.clone(), engine, disambiguator)?;
.height;
// 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; let mut skipped_region = false;
while self.unbreakable_rows_left == 0 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() && self.regions.may_progress()
{ {
// Advance regions without any output until we can place the // Advance regions without any output until we can place the
// footer. // footer.
self.finish_region_internal( if at_region_top {
Frame::soft(Axes::splat(Abs::zero())), self.finish_region_internal(
vec![], Frame::soft(Axes::splat(Abs::zero())),
Default::default(), vec![],
); Default::default(),
);
} else {
self.finish_region(engine, false)?;
}
skipped_region = true; skipped_region = true;
} }
// TODO(subfooters): Consider resetting header height etc. if we skip if skipped_region {
// 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 // Simulate the footer again; the region's 'full' might have
// changed. // changed, and the vector of heights was cleared.
self.simulate_footer(footer, &self.regions, engine, disambiguator)? (expected_footer_height, expected_footer_heights) =
.height self.simulate_footer_heights(footers, engine, disambiguator)?;
} else { }
footer_height
}; self.current.footer_height += expected_footer_height;
self.current.repeating_footer_heights.extend(expected_footer_heights);
Ok(()) 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. /// Lays out all rows in the footer.
/// They are unbreakable. /// They are unbreakable.
pub fn layout_footer( pub fn layout_footer(
&mut self, &mut self,
footer: &Footer, footer: &Footer,
as_short_lived: bool,
engine: &mut Engine, engine: &mut Engine,
disambiguator: usize, disambiguator: usize,
) -> SourceResult<()> { ) -> SourceResult<()> {
// Ensure footer rows have their own height available. let footer_len = footer.end - footer.start;
// 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;
self.unbreakable_rows_left += footer_len; 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() { for y in footer.start..self.grid.rows.len() {
self.layout_row_with_state( self.layout_row_with_state(
y, y,
engine, engine,
disambiguator, disambiguator,
RowState { RowState {
in_active_repeatable: repeats, in_active_repeatable: !as_short_lived,
..Default::default() ..Default::default()
}, },
)?; )?;
@ -597,3 +701,11 @@ pub fn total_header_row_count<'h>(
) -> usize { ) -> usize {
headers.into_iter().map(|h| h.range.end - h.range.start).sum() 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()
}