mark headers as short-lived during resolve

This commit is contained in:
PgBiel 2025-04-19 23:08:10 -03:00
parent af0c27cb98
commit 6e21eae3eb
3 changed files with 81 additions and 106 deletions

View File

@ -236,54 +236,12 @@ impl<'a> GridLayouter<'a> {
let mut y = 0;
let mut consecutive_header_count = 0;
while y < self.grid.rows.len() {
if let Some(first_header) =
self.upcoming_headers.get(consecutive_header_count)
if let Some(next_header) = self.upcoming_headers.get(consecutive_header_count)
{
if first_header.unwrap().range().contains(&y) {
consecutive_header_count += 1;
if next_header.unwrap().range().contains(&y) {
self.place_new_headers(&mut consecutive_header_count, engine)?;
y = next_header.unwrap().end;
// TODO: surely there is a better way to do this
match self.upcoming_headers.get(consecutive_header_count) {
// No more headers, so place the latest headers.
None => {
self.place_new_headers(
consecutive_header_count,
None,
engine,
)?;
consecutive_header_count = 0;
}
// Next header is not consecutive, so place the latest headers.
Some(next_header)
if next_header.unwrap().start > first_header.unwrap().end =>
{
self.place_new_headers(
consecutive_header_count,
None,
engine,
)?;
consecutive_header_count = 0;
}
// Next header is consecutive and conflicts with one or
// more of the latest consecutive headers, so we must
// place them before proceeding.
Some(next_header)
if next_header.unwrap().level
<= first_header.unwrap().level =>
{
self.place_new_headers(
consecutive_header_count,
Some(next_header),
engine,
)?;
consecutive_header_count = 0;
}
// Next header is a non-conflicting consecutive header.
// Keep collecting more headers.
_ => {}
}
y = first_header.unwrap().end;
// Skip header rows during normal layout.
continue;
}

View File

@ -9,71 +9,44 @@ use super::rowspans::UnbreakableRowGroup;
impl<'a> GridLayouter<'a> {
pub fn place_new_headers(
&mut self,
consecutive_header_count: usize,
conflicting_header: Option<&Repeatable<Header>>,
consecutive_header_count: &mut usize,
engine: &mut Engine,
) -> SourceResult<()> {
*consecutive_header_count += 1;
let (consecutive_headers, new_upcoming_headers) =
self.upcoming_headers.split_at(consecutive_header_count);
self.upcoming_headers.split_at(*consecutive_header_count);
if new_upcoming_headers.first().is_some_and(|next_header| {
consecutive_headers.last().is_none_or(|latest_header| {
!latest_header.unwrap().short_lived
&& next_header.unwrap().start == latest_header.unwrap().end
}) && !next_header.unwrap().short_lived
}) {
// More headers coming, so wait until we reach them.
// TODO: refactor
return Ok(());
}
self.upcoming_headers = new_upcoming_headers;
*consecutive_header_count = 0;
let (non_conflicting_headers, conflicting_headers) = match conflicting_header {
// Headers succeeded by end of grid or footer are short lived and
// can be placed in separate regions (no orphan prevention).
// TODO: do this during grid resolving?
// might be needed for multiple footers. Or maybe not if we check
// "upcoming_footers" (O(1) here), however that looks like.
_ if consecutive_headers
.last()
.is_some_and(|x| x.unwrap().end == self.grid.rows.len())
|| self
.grid
.footer
.as_ref()
.zip(consecutive_headers.last())
.is_some_and(|(f, h)| f.unwrap().start == h.unwrap().end) =>
{
(Default::default(), consecutive_headers)
}
Some(conflicting_header) => {
// All immediately conflicting headers will
// be laid out without orphan prevention.
consecutive_headers.split_at(consecutive_headers.partition_point(|h| {
conflicting_header.unwrap().level > h.unwrap().level
}))
}
_ => (consecutive_headers, Default::default()),
};
self.layout_new_pending_headers(non_conflicting_headers, engine)?;
// Layout each conflicting header independently, without orphan
// prevention (as they don't go into 'pending_headers').
// These headers are short-lived as they are immediately followed by 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_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.
conflicting_header,
true,
engine,
)?
}
// No chance of orphans as we're immediately placing conflicting
// headers afterwards, which basically are not headers, for all intents
// and purposes. It is therefore guaranteed that all new headers have
// been placed at least once.
if !conflicting_headers.is_empty() {
// Layout short-lived headers immediately.
if consecutive_headers.last().is_some_and(|h| h.unwrap().short_lived) {
// No chance of orphans as we're immediately placing conflicting
// headers afterwards, which basically are not headers, for all intents
// and purposes. It is therefore guaranteed that all new headers have
// been placed at least once.
self.flush_pending_headers();
}
Ok(())
// Layout each conflicting header independently, without orphan
// prevention (as they don't go into 'pending_headers').
// These headers are short-lived as they are immediately followed by a
// header of the same or lower level, such that they never actually get
// to repeat.
self.layout_new_headers(consecutive_headers, true, engine)
} else {
self.layout_new_pending_headers(consecutive_headers, engine)
}
}
/// Lays out a row while indicating that it should store its persistent

View File

@ -438,6 +438,12 @@ pub struct Header {
/// lower level header stops repeating, all higher level headers do as
/// well.
pub level: u32,
/// Whether this header cannot be repeated nor should have orphan
/// prevention because it would be about to cease repetition, either
/// because it is followed by headers of conflicting levels, or because
/// it is at the end of the table (possibly followed by some footers at the
/// end).
pub short_lived: bool,
}
impl Header {
@ -469,12 +475,15 @@ impl Footer {
}
}
/// A possibly repeatable grid object.
/// A possibly repeatable grid child.
///
/// It still exists even when not repeatable, but must not have additional
/// considerations by grid layout, other than for consistency (such as making
/// a certain group of rows unbreakable).
pub enum Repeatable<T> {
/// The user asked this grid child to repeat.
Repeated(T),
/// The user asked this grid child to not repeat.
NotRepeated(T),
}
@ -490,7 +499,7 @@ impl<T> Repeatable<T> {
}
/// Gets the value inside this repeatable, regardless of whether
/// it repeats.
/// it repeats (mutably).
#[inline]
pub fn unwrap_mut(&mut self) -> &mut T {
match self {
@ -1540,8 +1549,29 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
end: group_range.end,
level: row_group.repeatable_level.get(),
// This can only change at a later iteration, if we
// find a conflicting header or footer right away.
short_lived: false,
};
// Mark consecutive headers right before this one as short
// lived if they would have a higher or equal level, as
// then they would immediately stop repeating during
// layout.
let mut consecutive_header_start = data.start;
for conflicting_header in
headers.iter_mut().rev().take_while(move |h| {
let conflicts = h.unwrap().end == consecutive_header_start
&& h.unwrap().level >= data.level;
consecutive_header_start = h.unwrap().start;
conflicts
})
{
conflicting_header.unwrap_mut().short_lived = true;
}
headers.push(if row_group.repeat {
Repeatable::Repeated(data)
} else {
@ -1826,6 +1856,20 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
}
});
// Mark consecutive headers right before the end of the table, or the
// final footer, as short lived, given that there are no normal rows
// after them, so repeating them is pointless.
let mut consecutive_header_start =
footer.as_ref().map(|f| f.unwrap().start).unwrap_or(row_amount);
for header_at_the_end in headers.iter_mut().rev().take_while(move |h| {
let at_the_end = h.unwrap().end == consecutive_header_start;
consecutive_header_start = h.unwrap().start;
at_the_end
}) {
header_at_the_end.unwrap_mut().short_lived = true;
}
Ok(footer)
}