diff --git a/crates/typst/src/layout/grid/layout.rs b/crates/typst/src/layout/grid/layout.rs index 645b68b6..46993989 100644 --- a/crates/typst/src/layout/grid/layout.rs +++ b/crates/typst/src/layout/grid/layout.rs @@ -2599,7 +2599,12 @@ impl<'a> GridLayouter<'a> { let width = self.cell_spanned_width(cell, x); let size = Size::new(width, height); let mut pod = Regions::one(size, Axes::splat(true)); - if self.grid.rows[y] == Sizing::Auto { + if self.grid.rows[y] == Sizing::Auto + && self.unbreakable_rows_left == 0 + { + // Cells at breakable auto rows have lengths relative + // to the entire page, unlike cells in unbreakable auto + // rows. pod.full = self.regions.full; } let frame = cell.layout(engine, self.styles, pod)?.into_frame(); diff --git a/crates/typst/src/layout/grid/rowspans.rs b/crates/typst/src/layout/grid/rowspans.rs index 764a2b70..f4df3e4a 100644 --- a/crates/typst/src/layout/grid/rowspans.rs +++ b/crates/typst/src/layout/grid/rowspans.rs @@ -10,12 +10,16 @@ use super::layout::{in_last_with_offset, points, Repeatable, Row, RowPiece}; /// All information needed to layout a single rowspan. pub(super) struct Rowspan { - // First column of this rowspan. + /// First column of this rowspan. pub(super) x: usize, - // First row of this rowspan. + /// First row of this rowspan. pub(super) y: usize, - // Amount of rows spanned by the cell at (x, y). + /// Amount of rows spanned by the cell at (x, y). pub(super) rowspan: usize, + /// Whether all rows of the rowspan are part of an unbreakable row group. + /// This is true e.g. in headers and footers, regardless of what the user + /// specified for the parent cell's `breakable` field. + pub(super) is_effectively_unbreakable: bool, /// The horizontal offset of this rowspan in all regions. pub(super) dx: Abs, /// The vertical offset of this rowspan in the first region. @@ -96,7 +100,16 @@ impl<'a> GridLayouter<'a> { engine: &mut Engine, ) -> SourceResult<()> { let Rowspan { - x, y, dx, dy, first_region, region_full, heights, .. + x, + y, + rowspan, + is_effectively_unbreakable, + dx, + dy, + first_region, + region_full, + heights, + .. } = rowspan_data; let [first_height, backlog @ ..] = heights.as_slice() else { // Nothing to layout. @@ -110,9 +123,20 @@ impl<'a> GridLayouter<'a> { // Prepare regions. let size = Size::new(width, *first_height); let mut pod = Regions::one(size, Axes::splat(true)); - pod.full = region_full; pod.backlog = backlog; + if !is_effectively_unbreakable + && self.grid.rows[y..][..rowspan] + .iter() + .any(|spanned_row| spanned_row == &Sizing::Auto) + { + // If the rowspan spans an auto row and is breakable, it will see + // '100%' as the full page height, at least at its first region. + // This is consistent with how it is measured, and with how + // non-rowspan cells behave in auto rows. + pod.full = region_full; + } + // Push the layouted frames directly into the finished frames. let fragment = cell.layout(engine, self.styles, pod)?; let (current_region, current_rrows) = current_region_data.unzip(); @@ -172,6 +196,9 @@ impl<'a> GridLayouter<'a> { x, y, rowspan, + // The field below will be updated in + // 'check_for_unbreakable_rows'. + is_effectively_unbreakable: !cell.breakable, dx, // The four fields below will be updated in 'finish_region'. dy: Abs::zero(), @@ -227,9 +254,29 @@ impl<'a> GridLayouter<'a> { { self.finish_region(engine)?; } + + // Update unbreakable rows left. self.unbreakable_rows_left = row_group.rows.len(); } + if self.unbreakable_rows_left > 1 { + // Mark rowspans as effectively unbreakable where applicable + // (if all of their spanned rows would be in the same unbreakable + // row group). + // Not needed if only one unbreakable row is left, since, then, + // no rowspan will be effectively unbreakable, at least necessarily. + // Note that this function is called after 'check_for_rowspans' and + // potentially updates the amount of remaining unbreakable rows, so + // it wouldn't be accurate to only check for this condition in that + // function. We need to check here instead. + for rowspan_data in + self.rowspans.iter_mut().filter(|rowspan| rowspan.y == current_row) + { + rowspan_data.is_effectively_unbreakable |= + self.unbreakable_rows_left >= rowspan_data.rowspan; + } + } + Ok(()) } diff --git a/tests/ref/layout/grid-headers-4.png b/tests/ref/layout/grid-headers-4.png index be6080f7..1f3e4b10 100644 Binary files a/tests/ref/layout/grid-headers-4.png and b/tests/ref/layout/grid-headers-4.png differ diff --git a/tests/ref/layout/grid-rowspan-basic.png b/tests/ref/layout/grid-rowspan-basic.png index 783991b3..b464d8b4 100644 Binary files a/tests/ref/layout/grid-rowspan-basic.png and b/tests/ref/layout/grid-rowspan-basic.png differ diff --git a/tests/ref/layout/grid-rowspan-split-3.png b/tests/ref/layout/grid-rowspan-split-3.png index 3d809123..c3ff4bd1 100644 Binary files a/tests/ref/layout/grid-rowspan-split-3.png and b/tests/ref/layout/grid-rowspan-split-3.png differ diff --git a/tests/typ/layout/grid-headers-4.typ b/tests/typ/layout/grid-headers-4.typ index 6ede601c..6ab0d612 100644 --- a/tests/typ/layout/grid-headers-4.typ +++ b/tests/typ/layout/grid-headers-4.typ @@ -97,3 +97,16 @@ [a], table.cell(stroke: aqua)[b] ) + +--- +#set page(height: 7em) +#set text(6pt) +#let full-block = block(width: 2em, height: 100%, fill: red) +#table( + columns: 3, + inset: 1.5pt, + table.header( + [a], full-block, table.cell(rowspan: 2, full-block), + [b] + ) +) diff --git a/tests/typ/layout/grid-rowspan-basic.typ b/tests/typ/layout/grid-rowspan-basic.typ index 1cc7289b..bbd1c047 100644 --- a/tests/typ/layout/grid-rowspan-basic.typ +++ b/tests/typ/layout/grid-rowspan-basic.typ @@ -230,3 +230,23 @@ [f], [g] ) + +--- +// Block below shouldn't expand to the end of the page, but stay within its +// rows' boundaries. +#set page(height: 9em) +#table( + rows: (1em, 1em, 1fr, 1fr, auto), + table.cell(rowspan: 2, block(width: 2em, height: 100%, fill: red)), + table.cell(rowspan: 2, block(width: 2em, height: 100%, fill: red)), + [a] +) + +--- +#set page(height: 7em) +#table( + columns: 3, + [], [], table.cell(breakable: true, rowspan: 2, block(width: 2em, height: 100%, fill: red)), + table.cell(breakable: false, block(width: 2em, height: 100%, fill: red)), + table.cell(breakable: false, rowspan: 2, block(width: 2em, height: 100%, fill: red)), +)