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)