From 6e21eae3ebfaaa10a877b92e9ba7bfd88a5fe80d Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sat, 19 Apr 2025 23:08:10 -0300 Subject: [PATCH] mark headers as short-lived during resolve --- crates/typst-layout/src/grid/layouter.rs | 50 +---------- crates/typst-layout/src/grid/repeated.rs | 89 +++++++------------ .../typst-library/src/layout/grid/resolve.rs | 48 +++++++++- 3 files changed, 81 insertions(+), 106 deletions(-) diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index 0a4462d5b..2f3b7df84 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -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; } diff --git a/crates/typst-layout/src/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs index c877d038d..01a087330 100644 --- a/crates/typst-layout/src/grid/repeated.rs +++ b/crates/typst-layout/src/grid/repeated.rs @@ -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
>, + 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 diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs index c6a6265e6..9c872155a 100644 --- a/crates/typst-library/src/layout/grid/resolve.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -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 { + /// 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 Repeatable { } /// 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) }