separate layout_active_headers and new_headers

- Active headers will push empty regions when skipping regions, whereas new_headers may be called from a non-empty region, so we need to finish the region in full. The code separation also makes it all clearer.
This commit is contained in:
PgBiel 2025-04-09 14:00:58 -03:00 committed by GitHub
parent fe957e804a
commit cefa7fc72c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -6,21 +6,6 @@ use typst_library::layout::{Abs, Axes, Frame, Regions};
use super::layouter::{may_progress_with_offset, GridLayouter};
use super::rowspans::UnbreakableRowGroup;
pub enum HeadersToLayout<'a> {
RepeatingAndPending,
NewHeaders {
headers: &'a [Repeatable<Header>],
/// Whether this new header will become a pending header. If false, we
/// assume it simply won't repeat and so its header height is ignored.
/// Later on, cells can assume that this header won't occupy any height
/// in a future region, and indeed, since it won't be pending, it won't
/// have orphan prevention, so it will be placed immediately and stay
/// where it is.
short_lived: bool,
},
}
impl<'a> GridLayouter<'a> {
pub fn place_new_headers(
&mut self,
@ -51,15 +36,13 @@ impl<'a> GridLayouter<'a> {
// header of the same or lower level, such that they never actually get
// to repeat.
for conflicting_header in conflicting_headers.chunks_exact(1) {
self.layout_headers(
self.layout_new_headers(
// Using 'chunks_exact", we pass a slice of length one instead
// of a reference for type consistency.
// In addition, this is the only place where we layout
// short-lived headers.
HeadersToLayout::NewHeaders {
headers: conflicting_header,
short_lived: true,
},
conflicting_header,
true,
engine,
)?
}
@ -155,10 +138,7 @@ impl<'a> GridLayouter<'a> {
// This might be a waste as we could generate an orphan and thus have
// to try to place old and new headers all over again, but that happens
// for every new region anyway, so it's rather unavoidable.
self.layout_headers(
HeadersToLayout::NewHeaders { headers, short_lived: false },
engine,
)?;
self.layout_new_headers(headers, false, engine)?;
// After the first subsequent row is laid out, move to repeating, as
// it's then confirmed the headers won't be moved due to orphan
@ -190,25 +170,18 @@ impl<'a> GridLayouter<'a> {
self.pending_headers = Default::default();
}
/// Layouts the headers' rows.
/// Lays out the rows of repeating and pending headers at the top of the
/// region.
///
/// Assumes the footer height for the current region has already been
/// calculated. Skips regions as necessary to fit all headers and all
/// footers.
pub fn layout_headers(
&mut self,
headers: HeadersToLayout<'a>,
engine: &mut Engine,
) -> SourceResult<()> {
pub fn layout_active_headers(&mut self, engine: &mut Engine) -> SourceResult<()> {
// Generate different locations for content in headers across its
// repetitions by assigning a unique number for each one.
let mut disambiguator = self.finished.len();
let disambiguator = self.finished.len();
// At first, only consider the height of the given headers. However,
// for upcoming regions, we will have to consider repeating headers as
// well.
let mut header_height = match headers {
HeadersToLayout::RepeatingAndPending => self.simulate_header_height(
let header_height = self.simulate_header_height(
self.repeating_headers
.iter()
.copied()
@ -216,14 +189,7 @@ impl<'a> GridLayouter<'a> {
&self.regions,
engine,
disambiguator,
)?,
HeadersToLayout::NewHeaders { headers, .. } => self.simulate_header_height(
headers.iter().map(Repeatable::unwrap),
&self.regions,
engine,
disambiguator,
)?,
};
)?;
// We already take the footer into account below.
// While skipping regions, footer height won't be automatically
@ -252,26 +218,6 @@ impl<'a> GridLayouter<'a> {
// TODO: re-calculate heights of headers and footers on each region
// if 'full' changes? (Assuming height doesn't change for now...)
if !skipped_region {
if let HeadersToLayout::NewHeaders { headers, .. } = headers {
// Update disambiguator as we are re-measuring headers
// which were already laid out.
disambiguator = self.finished.len();
header_height =
// Laying out new headers, so we have to consider the
// combined height of already repeating headers as well
// when beginning a new region.
self.simulate_header_height(
self.repeating_headers
.iter().copied()
.chain(self.pending_headers.iter().chain(headers).map(Repeatable::unwrap)),
&self.regions,
engine,
disambiguator,
)?;
}
}
skipped_region = true;
self.regions.size.y -= self.footer_height;
@ -290,29 +236,15 @@ impl<'a> GridLayouter<'a> {
}
}
// Group of headers is unbreakable.
// Thus, no risk of 'finish_region' being recursively called from
// within 'layout_row'.
if let HeadersToLayout::NewHeaders { headers, .. } = headers {
// Do this before laying out repeating and pending headers from a
// new region to make sure row code is aware that all of those
// headers should stay together!
self.unbreakable_rows_left +=
total_header_row_count(headers.iter().map(Repeatable::unwrap));
}
// Need to relayout ALL headers if we skip a region, not only the
// provided headers.
// TODO: maybe extract this into a function to share code with multiple
// footers.
if matches!(headers, HeadersToLayout::RepeatingAndPending) || skipped_region {
let repeating_header_rows =
total_header_row_count(self.repeating_headers.iter().copied());
let pending_header_rows = total_header_row_count(
self.pending_headers.iter().map(Repeatable::unwrap),
);
let pending_header_rows =
total_header_row_count(self.pending_headers.iter().map(Repeatable::unwrap));
// Group of headers is unbreakable.
// Thus, no risk of 'finish_region' being recursively called from
// within 'layout_row'.
self.unbreakable_rows_left += repeating_header_rows + pending_header_rows;
self.current_last_repeated_header_end =
@ -328,8 +260,7 @@ impl<'a> GridLayouter<'a> {
// 'layout_row' so this is fine.
let mut i = 0;
while let Some(&header) = self.repeating_headers.get(i) {
let header_height =
self.layout_header_rows(header, engine, disambiguator)?;
let header_height = self.layout_header_rows(header, engine, disambiguator)?;
self.header_height += header_height;
self.repeating_header_height += header_height;
@ -371,13 +302,65 @@ impl<'a> GridLayouter<'a> {
// Include both repeating and pending header rows as this number is
// used for orphan prevention.
self.current_header_rows = self.lrows.len();
Ok(())
}
if let HeadersToLayout::NewHeaders { headers, short_lived } = headers {
let placing_at_the_start = skipped_region || self.lrows.is_empty();
/// Lays out headers found for the first time during row layout.
///
/// If 'short_lived' is true, these headers are immediately followed by
/// a conflicting header, so it is assumed they will not be pushed to
/// pending headers.
pub fn layout_new_headers(
&mut self,
headers: &'a [Repeatable<Header>],
short_lived: bool,
engine: &mut Engine,
) -> SourceResult<()> {
// At first, only consider the height of the given headers. However,
// for upcoming regions, we will have to consider repeating headers as
// well.
let header_height = self.simulate_header_height(
headers.iter().map(Repeatable::unwrap),
&self.regions,
engine,
0,
)?;
// We already take the footer into account below.
// While skipping regions, footer height won't be automatically
// re-calculated until the end.
let mut skipped_region = false;
// TODO: remove this 'unbreakable rows left check',
// consider if we can already be in an unbreakable row group?
while self.unbreakable_rows_left == 0
&& !self.regions.size.y.fits(header_height)
&& may_progress_with_offset(
self.regions,
// 'finish_region' will place currently active headers and
// footers again. We assume previous pending headers have
// already been flushed, so in principle
// 'header_height == repeating_header_height' here
// (there won't be any pending headers at this point, other
// than the ones we are about to place).
self.header_height + self.footer_height,
)
{
// Note that, after the first region skip, the new headers will go
// at the top of the region, but after the repeating headers that
// remained (which will be automatically placed in 'finish_region').
self.finish_region(engine, true)?;
skipped_region = true;
}
self.unbreakable_rows_left +=
total_header_row_count(headers.iter().map(Repeatable::unwrap));
let placing_at_the_start =
skipped_region || self.lrows.len() == self.current_header_rows;
for header in headers {
let header_height =
self.layout_header_rows(header.unwrap(), engine, disambiguator)?;
let header_height = self.layout_header_rows(header.unwrap(), engine, 0)?;
// Only store this header height if it is actually going to
// become a pending header. Otherwise, pretend it's not a
@ -398,7 +381,6 @@ impl<'a> GridLayouter<'a> {
// Track header rows at the start of the region.
self.current_header_rows = self.lrows.len();
}
}
Ok(())
}