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
/// 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.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())?;
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 = footer.end;
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 {
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 (check below).
// twice (it is removed from repeating_footers once it is
// reached).
//
// 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())?;
}
// 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,10 +1703,22 @@ 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 !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.
y < footer_start || rowspan.y >= footer_start
// 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)
{
@ -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);

View File

@ -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 {
if !self.repeating_footers.is_empty() && 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;
}
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>] {
let [next_footer, other_footers @ ..] = self.upcoming_sorted_footers else {
return &[];
};
/// 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;
if !next_footer.repeated {
// TODO(subfooters): grouping and laying them out together?
self.upcoming_sorted_footers = other_footers;
return &[];
// 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)?;
}
if other_footers.is_empty() {
return std::mem::replace(&mut self.upcoming_sorted_footers, &[]);
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 {
// 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
@ -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.
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()
}