store height of each repeating header

So we can update in the middle of the region
This commit is contained in:
PgBiel 2025-04-06 00:17:52 -03:00
parent ecc93297f8
commit d172eccfd9
2 changed files with 139 additions and 32 deletions

View File

@ -73,6 +73,16 @@ pub struct GridLayouter<'a> {
/// Sorted by increasing levels. /// Sorted by increasing levels.
pub(super) pending_headers: &'a [Repeatable<Header>], pub(super) pending_headers: &'a [Repeatable<Header>],
pub(super) upcoming_headers: &'a [Repeatable<Header>], pub(super) upcoming_headers: &'a [Repeatable<Header>],
/// The height for each repeating header that was placed in this region.
/// Note that this includes headers not at the top of the region (pending
/// headers), and excludes headers removed by virtue of a new, conflicting
/// header being found.
pub(super) repeating_header_heights: Vec<Abs>,
/// If this is `Some`, this will receive the currently laid out row's
/// height if it is auto or relative. This is used for header height
/// calculation.
/// TODO: consider refactoring this into something nicer.
pub(super) current_row_height: Option<Abs>,
/// The simulated header height. /// The simulated header height.
/// This field is reset in `layout_header` and properly updated by /// This field is reset in `layout_header` and properly updated by
/// `layout_auto_row` and `layout_relative_row`, and should not be read /// `layout_auto_row` and `layout_relative_row`, and should not be read
@ -158,7 +168,9 @@ impl<'a> GridLayouter<'a> {
is_rtl: TextElem::dir_in(styles) == Dir::RTL, is_rtl: TextElem::dir_in(styles) == Dir::RTL,
repeating_headers: vec![], repeating_headers: vec![],
upcoming_headers: &grid.headers, upcoming_headers: &grid.headers,
repeating_header_heights: vec![],
pending_headers: Default::default(), pending_headers: Default::default(),
current_row_height: None,
header_height: Abs::zero(), header_height: Abs::zero(),
repeating_header_height: Abs::zero(), repeating_header_height: Abs::zero(),
footer_height: Abs::zero(), footer_height: Abs::zero(),
@ -1003,9 +1015,9 @@ impl<'a> GridLayouter<'a> {
let frame = self.layout_single_row(engine, disambiguator, first, y)?; let frame = self.layout_single_row(engine, disambiguator, first, y)?;
self.push_row(frame, y, true); self.push_row(frame, y, true);
if self.lrows.len() < self.current_header_rows { if let Some(row_height) = &mut self.current_row_height {
// Add to header height, as we are in a header row. // Add to header height, as we are in a header row.
self.header_height += first; *row_height += first;
} }
return Ok(()); return Ok(());
@ -1237,10 +1249,9 @@ impl<'a> GridLayouter<'a> {
let resolved = v.resolve(self.styles).relative_to(self.regions.base().y); let resolved = v.resolve(self.styles).relative_to(self.regions.base().y);
let frame = self.layout_single_row(engine, disambiguator, resolved, y)?; let frame = self.layout_single_row(engine, disambiguator, resolved, y)?;
if self.lrows.len() < self.current_header_rows { if let Some(row_height) = &mut self.current_row_height {
// Add to header height (not all headers were laid out yet, so this // Add to header height, as we are in a header row.
// must be a repeated or pending header at the top of the region). *row_height += resolved;
self.header_height += resolved;
} }
// Skip to fitting region, but only if we aren't part of an unbreakable // Skip to fitting region, but only if we aren't part of an unbreakable
@ -1598,6 +1609,7 @@ impl<'a> GridLayouter<'a> {
self.current_header_rows = 0; self.current_header_rows = 0;
self.header_height = Abs::zero(); self.header_height = Abs::zero();
self.repeating_header_height = Abs::zero(); self.repeating_header_height = Abs::zero();
self.repeating_header_heights.clear();
let disambiguator = self.finished.len(); let disambiguator = self.finished.len();
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {

View File

@ -10,7 +10,17 @@ use super::rowspans::UnbreakableRowGroup;
pub enum HeadersToLayout<'a> { pub enum HeadersToLayout<'a> {
RepeatingAndPending, RepeatingAndPending,
NewHeaders(&'a [Repeatable<Header>]), 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> { impl<'a> GridLayouter<'a> {
@ -53,7 +63,12 @@ impl<'a> GridLayouter<'a> {
self.layout_headers( self.layout_headers(
// Using 'chunks_exact", we pass a slice of length one instead // Using 'chunks_exact", we pass a slice of length one instead
// of a reference for type consistency. // of a reference for type consistency.
HeadersToLayout::NewHeaders(conflicting_header), // In addition, this is the only place where we layout
// short-lived headers.
HeadersToLayout::NewHeaders {
headers: conflicting_header,
short_lived: true,
},
engine, engine,
)? )?
} }
@ -61,6 +76,40 @@ impl<'a> GridLayouter<'a> {
Ok(()) Ok(())
} }
/// Lays out a row while indicating that it should store its persistent
/// height as a header row, which will be its height if relative or auto,
/// or zero otherwise (fractional).
#[inline]
fn layout_header_row(
&mut self,
y: usize,
engine: &mut Engine,
disambiguator: usize,
) -> SourceResult<Option<Abs>> {
let previous_row_height =
std::mem::replace(&mut self.current_row_height, Some(Abs::zero()));
self.layout_row(y, engine, disambiguator)?;
Ok(std::mem::replace(&mut self.current_row_height, previous_row_height))
}
/// Lays out rows belonging to a header, returning the calculated header
/// height only for that header.
#[inline]
fn layout_header_rows(
&mut self,
header: &Header,
engine: &mut Engine,
disambiguator: usize,
) -> SourceResult<Abs> {
let mut header_height = Abs::zero();
for y in header.range() {
header_height +=
self.layout_header_row(y, engine, disambiguator)?.unwrap_or_default();
}
Ok(header_height)
}
/// Queues new pending headers for layout. Headers remain pending until /// Queues new pending headers for layout. Headers remain pending until
/// they are successfully laid out in some page once. Then, they will be /// they are successfully laid out in some page once. Then, they will be
/// moved to `repeating_headers`, at which point it is safe to stop them /// moved to `repeating_headers`, at which point it is safe to stop them
@ -80,14 +129,26 @@ impl<'a> GridLayouter<'a> {
// Stop repeating conflicting headers. // Stop repeating conflicting headers.
// If we go to a new region before the pending headers fit alongside // If we go to a new region before the pending headers fit alongside
// their children, the old headers should not be displayed anymore. // their children, the old headers should not be displayed anymore.
self.repeating_headers let first_conflicting_pos =
.truncate(self.repeating_headers.partition_point(|h| h.level < first_level)); self.repeating_headers.partition_point(|h| h.level < first_level);
self.repeating_headers.truncate(first_conflicting_pos);
// Ensure upcoming rows won't see that these headers will occupy any
// space in future regions anymore.
for removed_height in self.repeating_header_heights.drain(first_conflicting_pos..)
{
self.header_height -= removed_height;
self.repeating_header_height -= removed_height;
}
// Let's try to place them at least once. // Let's try to place them at least once.
// This might be a waste as we could generate an orphan and thus have // 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 // to try to place old and new headers all over again, but that happens
// for every new region anyway, so it's rather unavoidable. // for every new region anyway, so it's rather unavoidable.
self.layout_headers(HeadersToLayout::NewHeaders(headers), engine); self.layout_headers(
HeadersToLayout::NewHeaders { headers, short_lived: false },
engine,
);
// After the first subsequent row is laid out, move to repeating, as // 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 // it's then confirmed the headers won't be moved due to orphan
@ -171,7 +232,7 @@ impl<'a> GridLayouter<'a> {
engine, engine,
disambiguator, disambiguator,
)?, )?,
HeadersToLayout::NewHeaders(headers) => self.simulate_header_height( HeadersToLayout::NewHeaders { headers, .. } => self.simulate_header_height(
headers.into_iter().map(Repeatable::unwrap), headers.into_iter().map(Repeatable::unwrap),
&self.regions, &self.regions,
engine, engine,
@ -198,7 +259,7 @@ impl<'a> GridLayouter<'a> {
// TODO: re-calculate heights of headers and footers on each region // TODO: re-calculate heights of headers and footers on each region
// if 'full'changes? (Assuming height doesn't change for now...) // if 'full'changes? (Assuming height doesn't change for now...)
if !skipped_region { if !skipped_region {
if let HeadersToLayout::NewHeaders(headers) = headers { if let HeadersToLayout::NewHeaders { headers, .. } = headers {
header_height = header_height =
// Laying out new headers, so we have to consider the // Laying out new headers, so we have to consider the
// combined height of already repeating headers as well // combined height of already repeating headers as well
@ -221,11 +282,6 @@ impl<'a> GridLayouter<'a> {
self.regions.size.y -= self.footer_height; self.regions.size.y -= self.footer_height;
} }
// Reset the header height for this region.
// It will be re-calculated when laying out each header row.
self.header_height = Abs::zero();
self.repeating_header_height = Abs::zero();
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
if skipped_region { if skipped_region {
// Simulate the footer again; the region's 'full' might have // Simulate the footer again; the region's 'full' might have
@ -242,7 +298,7 @@ impl<'a> GridLayouter<'a> {
// Group of headers is unbreakable. // Group of headers is unbreakable.
// Thus, no risk of 'finish_region' being recursively called from // Thus, no risk of 'finish_region' being recursively called from
// within 'layout_row'. // within 'layout_row'.
if let HeadersToLayout::NewHeaders(headers) = headers { if let HeadersToLayout::NewHeaders { headers, .. } = headers {
// Do this before laying out repeating and pending headers from a // Do this before laying out repeating and pending headers from a
// new region to make sure row code is aware that all of those // new region to make sure row code is aware that all of those
// headers should stay together! // headers should stay together!
@ -267,33 +323,72 @@ impl<'a> GridLayouter<'a> {
self.current_header_rows = repeating_header_rows + pending_header_rows; self.current_header_rows = repeating_header_rows + pending_header_rows;
self.unbreakable_rows_left += repeating_header_rows + pending_header_rows; self.unbreakable_rows_left += repeating_header_rows + pending_header_rows;
// Reset the header height for this region.
// It will be re-calculated when laying out each header row.
self.header_height = Abs::zero();
self.repeating_header_height = Abs::zero();
self.repeating_header_heights.clear();
// Use indices to avoid double borrow. We don't mutate headers in // Use indices to avoid double borrow. We don't mutate headers in
// 'layout_row' so this is fine. // 'layout_row' so this is fine.
let mut i = 0; let mut i = 0;
while let Some(&header) = self.repeating_headers.get(i) { while let Some(&header) = self.repeating_headers.get(i) {
for y in header.range() { let header_height =
self.layout_row(y, engine, disambiguator)?; self.layout_header_rows(header, engine, disambiguator)?;
} self.header_height += header_height;
self.repeating_header_height += header_height;
// We assume that this vector will be sorted according
// to increasing levels like 'repeating_headers' and
// 'pending_headers' - and, in particular, their union, as this
// vector is pushed repeating heights from both.
//
// This is guaranteed by:
// 1. We always push pending headers after repeating headers,
// as we assume they don't conflict because we remove
// conflicting repeating headers when pushing a new pending
// header.
//
// 2. We push in the same order as each.
//
// 3. This vector is also modified when pushing a new pending
// header, where we remove heights for conflicting repeating
// headers which have now stopped repeating. They are always at
// the end and new pending headers respect the existing sort,
// so the vector will remain sorted.
self.repeating_header_heights.push(header_height);
i += 1; i += 1;
} }
// All rows so far were repeating headers at the top of the region.
self.repeating_header_height = self.header_height;
for header in self.pending_headers { for header in self.pending_headers {
let header_height = self.header_height; let header_height =
for y in header.unwrap().range() { self.layout_header_rows(header.unwrap(), engine, disambiguator)?;
self.layout_row(y, engine, disambiguator)?; self.header_height += header_height;
}
if matches!(header, Repeatable::Repeated(_)) { if matches!(header, Repeatable::Repeated(_)) {
self.repeating_header_height += self.header_height - header_height; self.repeating_header_height += header_height;
self.repeating_header_heights.push(header_height);
} }
} }
} }
if let HeadersToLayout::NewHeaders(headers) = headers { if let HeadersToLayout::NewHeaders { headers, short_lived } = headers {
for header in headers { for header in headers {
for y in header.unwrap().range() { let header_height =
self.layout_row(y, engine, disambiguator)?; self.layout_header_rows(header.unwrap(), engine, disambiguator)?;
// Only store this header height if it is actually going to
// become a pending header. Otherwise, pretend it's not a
// header... This is fine for consumers of 'header_height' as
// it is guaranteed this header won't appear in a future
// region, so multi-page rows and cells can effectively ignore
// this header.
if !short_lived {
self.header_height += header_height;
if matches!(header, Repeatable::Repeated(_)) {
self.repeating_header_height = header_height;
self.repeating_header_heights.push(header_height);
}
} }
} }
} }