From 4bd3abf44d3bab8c92c0125d35d2ae92ed277a7e Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Mon, 3 Mar 2025 19:59:41 -0300 Subject: [PATCH] initial multi heading progress pending headers change --- crates/typst-layout/src/grid/layouter.rs | 61 ++++++++--- crates/typst-layout/src/grid/repeated.rs | 132 ++++++++++++++++++++--- crates/typst-layout/src/grid/rowspans.rs | 3 + 3 files changed, 170 insertions(+), 26 deletions(-) diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index dc9e2238d..1832c54fa 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -3,7 +3,9 @@ use std::fmt::Debug; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{Resolve, StyleChain}; -use typst_library::layout::grid::resolve::{Cell, CellGrid, LinePosition, Repeatable}; +use typst_library::layout::grid::resolve::{ + Cell, CellGrid, Header, LinePosition, Repeatable, +}; use typst_library::layout::{ Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel, Size, Sizing, @@ -47,6 +49,18 @@ pub struct GridLayouter<'a> { pub(super) finished: Vec, /// Whether this is an RTL grid. pub(super) is_rtl: bool, + /// Currently repeating headers, one per level. + /// Sorted by increasing levels. + /// + /// Note that some levels may be absent, in particular level 0, which does + /// not exist (so the first level is >= 1). + pub(super) repeating_headers: Vec<&'a Header>, + /// End of sequence of consecutive compatible headers found so far. + /// This is one position after the last index in `upcoming_headers`, so `0` + /// indicates no pending headers. + /// Sorted by increasing levels. + pub(super) pending_header_end: usize, + pub(super) upcoming_headers: &'a [Repeatable
], /// The simulated header height. /// This field is reset in `layout_header` and properly updated by /// `layout_auto_row` and `layout_relative_row`, and should not be read @@ -120,6 +134,7 @@ impl<'a> GridLayouter<'a> { initial: regions.size, finished: vec![], is_rtl: TextElem::dir_in(styles) == Dir::RTL, + upcoming_headers: &grid.headers, header_height: Abs::zero(), footer_height: Abs::zero(), span, @@ -140,15 +155,31 @@ impl<'a> GridLayouter<'a> { } } - for y in 0..self.grid.rows.len() { - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - if y < header.end { - if y == 0 { - self.layout_header(header, engine, 0)?; - self.regions.size.y -= self.footer_height; + let mut y = 0; + while y < self.grid.rows.len() { + if let Some(first_header) = self.upcoming_headers.first() { + if first_header.unwrap().range().contains(&y) { + self.bump_pending_headers(); + + if self.peek_upcoming_header().is_none_or(|h| { + h.unwrap().start > y + 1 + || h.unwrap().level <= first_header.unwrap().level + }) { + // Next row either isn't a header. or is in a + // conflicting one, which is the sign that we need to go. + self.layout_headers(next_header, engine, 0)?; } + y = first_header.end; // Skip header rows during normal layout. continue; + + self.bump_repeating_headers(); + if let Repeatable::Repeated(next_header) = first_header { + if y == next_header.start { + self.layout_headers(next_header, engine, 0)?; + self.regions.size.y -= self.footer_height; + } + } } } @@ -162,6 +193,8 @@ impl<'a> GridLayouter<'a> { } self.layout_row(y, engine, 0)?; + + y += 1; } self.finish_region(engine, true)?; @@ -953,7 +986,9 @@ impl<'a> GridLayouter<'a> { .and_then(Repeatable::as_repeated) .is_some_and(|header| y < header.end) { - // Add to header height. + // Add to header height, as we are in a header row. + // TODO: Should we only bump from upcoming_headers to + // repeating_headers AFTER the header height calculation? self.header_height += first; } @@ -1370,15 +1405,15 @@ impl<'a> GridLayouter<'a> { .and_then(Repeatable::as_repeated) .is_some_and(|footer| footer.start != 0); - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - if self.grid.rows.len() > header.end + if let Some(last_header) = self.repeating_headers.last() { + if self.grid.rows.len() > last_header.end && self .grid .footer .as_ref() .and_then(Repeatable::as_repeated) .is_none_or(|footer| footer.start != header.end) - && self.lrows.last().is_some_and(|row| row.index() < header.end) + && self.lrows.last().is_some_and(|row| row.index() < last_header.end) && !in_last_with_offset( self.regions, self.header_height + self.footer_height, @@ -1535,9 +1570,9 @@ impl<'a> GridLayouter<'a> { self.prepare_footer(footer, engine, disambiguator)?; } - if let Some(Repeatable::Repeated(header)) = &self.grid.header { + if !self.repeating_headers.is_empty() { // Add a header to the new region. - self.layout_header(header, engine, disambiguator)?; + self.layout_headers(engine, disambiguator)?; } // Ensure rows don't try to overrun the footer. diff --git a/crates/typst-layout/src/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs index 22d2a09ef..fc364aad0 100644 --- a/crates/typst-layout/src/grid/repeated.rs +++ b/crates/typst-layout/src/grid/repeated.rs @@ -1,3 +1,5 @@ +use std::ops::ControlFlow; + use typst_library::diag::SourceResult; use typst_library::engine::Engine; use typst_library::layout::grid::resolve::{Footer, Header, Repeatable}; @@ -6,20 +8,100 @@ use typst_library::layout::{Abs, Axes, Frame, Regions}; use super::layouter::GridLayouter; use super::rowspans::UnbreakableRowGroup; -impl GridLayouter<'_> { - /// Layouts the header's rows. - /// Skips regions as necessary. - pub fn layout_header( +impl<'a> GridLayouter<'a> { + #[inline] + fn pending_headers(&self) -> &'a [Repeatable
] { + &self.upcoming_headers[..self.pending_header_end] + } + + #[inline] + pub fn bump_pending_headers(&mut self) { + debug_assert!(!self.upcoming_headers.is_empty()); + self.pending_header_end += 1; + } + + #[inline] + pub fn peek_upcoming_header(&self) -> Option<&'a Repeatable
> { + self.upcoming_headers.get(self.pending_header_end) + } + + pub fn flush_pending_headers(&mut self) { + debug_assert!(!self.upcoming_headers.is_empty()); + debug_assert!(self.pending_header_end > 0); + let headers = self.pending_headers(); + + let [first_header, ..] = headers else { + return; + }; + + self.repeating_headers.truncate( + self.repeating_headers + .partition_point(|h| h.level < first_header.unwrap().level), + ); + + for header in self.pending_headers() { + if let Repeatable::Repeated(header) = header { + // Vector remains sorted by increasing levels: + // - It was sorted before, so the truncation above only keeps + // elements with a lower level. + // - Therefore, by pushing this header to the end, it will have + // a level larger than all the previous headers, and is thus + // in its 'correct' position. + self.repeating_headers.push(header); + } + } + + self.upcoming_headers = self + .upcoming_headers + .get(self.pending_header_end..) + .unwrap_or_default(); + + self.pending_header_end = 0; + } + + pub fn bump_repeating_headers(&mut self) { + debug_assert!(!self.upcoming_headers.is_empty()); + + let [next_header, ..] = self.upcoming_headers else { + return; + }; + + // Keep only lower level headers. Assume sorted by increasing levels. + self.repeating_headers.truncate( + self.repeating_headers + .partition_point(|h| h.level < next_header.unwrap().level), + ); + + if let Repeatable::Repeated(next_header) = next_header { + // Vector remains sorted by increasing levels: + // - It was sorted before, so the truncation above only keeps + // elements with a lower level. + // - Therefore, by pushing this header to the end, it will have + // a level larger than all the previous headers, and is thus + // in its 'correct' position. + self.repeating_headers.push(next_header); + } + + // Laying out the next header now. + self.upcoming_headers = self.upcoming_headers.get(1..).unwrap_or_default(); + } + + /// Layouts the headers' rows. + /// + /// 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, - header: &Header, + headers: &[&Header], engine: &mut Engine, disambiguator: usize, ) -> SourceResult<()> { - let header_rows = - self.simulate_header(header, &self.regions, engine, disambiguator)?; + let header_height = + self.simulate_header_height(&self.regions, engine, disambiguator)?; let mut skipped_region = false; while self.unbreakable_rows_left == 0 - && !self.regions.size.y.fits(header_rows.height + self.footer_height) + && !self.regions.size.y.fits(header_height + self.footer_height) && self.regions.may_progress() { // Advance regions without any output until we can place the @@ -42,16 +124,34 @@ impl GridLayouter<'_> { } } - // Header is unbreakable. + // Group of headers is unbreakable. // Thus, no risk of 'finish_region' being recursively called from // within 'layout_row'. - self.unbreakable_rows_left += header.end; - for y in 0..header.end { - self.layout_row(y, engine, disambiguator)?; + self.unbreakable_rows_left += total_header_row_count(headers); + for header in headers { + for y in header.range() { + self.layout_row(y, engine, disambiguator)?; + } } Ok(()) } + /// Calculates the total expected height of several headers. + pub fn simulate_header_height( + &self, + headers: &[&Header], + regions: &Regions<'_>, + engine: &mut Engine, + disambiguator: usize, + ) -> SourceResult { + let mut height = Abs::zero(); + for header in headers { + height += + self.simulate_header(header, regions, engine, disambiguator)?.height; + } + Ok(height) + } + /// Simulate the header's group of rows. pub fn simulate_header( &self, @@ -66,7 +166,7 @@ impl GridLayouter<'_> { // assume that the amount of unbreakable rows following the first row // in the header will be precisely the rows in the header. self.simulate_unbreakable_row_group( - 0, + header.start, Some(header.end), regions, engine, @@ -151,3 +251,9 @@ impl GridLayouter<'_> { ) } } + +/// The total amount of rows in the given list of headers. +#[inline] +pub fn total_header_row_count(headers: &[&Header]) -> usize { + headers.iter().map(|h| h.end - h.start).sum() +} diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs index 21992ed02..a99d38fa8 100644 --- a/crates/typst-layout/src/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -160,6 +160,9 @@ impl GridLayouter<'_> { // at a position after the sum of the laid out header // rows). if let Some(Repeatable::Repeated(header)) = &self.grid.header { + // TODO: Need a way to distinguish header 'rrows' for each + // region, as this calculation - i.e., header height at + // each region - will change depending on'i'. let header_rows = self .rrows .get(i)