mirror of
https://github.com/typst/typst
synced 2025-07-15 16:42:53 +08:00
initial footer simulation and placement
This commit is contained in:
parent
db2ac385a9
commit
b63f6c99df
@ -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);
|
||||
|
@ -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>] {
|
||||
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)?;
|
||||
}
|
||||
|
||||
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() {
|
||||
return std::mem::replace(&mut self.upcoming_sorted_footers, &[]);
|
||||
// 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.
|
||||
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()
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user