count header rows in each region

- Currently also includes non-repeatable header row heights, which will lead to bugs on rowspans; needs fix
This commit is contained in:
PgBiel 2025-04-05 00:54:30 -03:00
parent e586cffa6c
commit 1301b901b7
3 changed files with 95 additions and 58 deletions

View File

@ -46,8 +46,21 @@ pub struct GridLayouter<'a> {
pub(super) rowspans: Vec<Rowspan>,
/// The initial size of the current region before we started subtracting.
pub(super) initial: Size,
/// The amount of header rows at the start of the current region. Note that
/// `repeating_headers` and `pending_headers` can change if we find a new
/// header inside the region, so this has to be stored separately for each
/// new region - we can't just reuse those.
///
/// This is used for orphan prevention checks (if there are no rows other
/// than header rows upon finishing a region, we'd have an orphan). In
/// addition, this information is stored for each finished region so
/// rowspans placed later can know which rows to skip at the top of the
/// region when spanning more than one page.
pub(super) current_header_rows: usize,
/// Frames for finished regions.
pub(super) finished: Vec<Frame>,
/// The height of header rows on each finished region.
pub(super) finished_header_rows: Vec<Abs>,
/// Whether this is an RTL grid.
pub(super) is_rtl: bool,
/// Currently repeating headers, one per level.
@ -131,7 +144,9 @@ impl<'a> GridLayouter<'a> {
unbreakable_rows_left: 0,
rowspans: vec![],
initial: regions.size,
current_header_rows: 0,
finished: vec![],
finished_header_rows: vec![],
is_rtl: TextElem::dir_in(styles) == Dir::RTL,
repeating_headers: vec![],
upcoming_headers: &grid.headers,
@ -979,16 +994,8 @@ impl<'a> GridLayouter<'a> {
let frame = self.layout_single_row(engine, disambiguator, first, y)?;
self.push_row(frame, y, true);
if self
.grid
.header
.as_ref()
.and_then(Repeatable::as_repeated)
.is_some_and(|header| y < header.end)
{
if self.lrows.len() < self.current_header_rows {
// 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;
}
@ -1221,14 +1228,9 @@ impl<'a> GridLayouter<'a> {
let resolved = v.resolve(self.styles).relative_to(self.regions.base().y);
let frame = self.layout_single_row(engine, disambiguator, resolved, y)?;
if self
.grid
.header
.as_ref()
.and_then(Repeatable::as_repeated)
.is_some_and(|header| y < header.end)
{
// Add to header height.
if self.lrows.len() < self.current_header_rows {
// Add to header height (not all headers were laid out yet, so this
// must be a repeated or pending header at the top of the region).
self.header_height += resolved;
}
@ -1389,20 +1391,20 @@ impl<'a> GridLayouter<'a> {
self.lrows.pop().unwrap();
}
let footer_would_be_widow = if let Some(last_header) = self
.pending_headers
.last()
.map(Repeatable::unwrap)
.or_else(|| self.repeating_headers.last().map(|h| *h))
let footer_would_be_widow = if let Some(last_header_row) = self
.current_header_rows
.checked_sub(1)
.and_then(|last_header_index| self.lrows.get(last_header_index))
{
if self.grid.rows.len() > last_header.end
let last_header_end = last_header_row.index();
if self.grid.rows.len() > last_header_end
&& self
.grid
.footer
.as_ref()
.and_then(Repeatable::as_repeated)
.is_none_or(|footer| footer.start != last_header.end)
&& self.lrows.last().is_some_and(|row| row.index() < last_header.end)
.is_none_or(|footer| footer.start != last_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,
@ -1471,9 +1473,10 @@ impl<'a> GridLayouter<'a> {
let mut pos = Point::zero();
let mut rrows = vec![];
let current_region = self.finished.len();
let mut header_row_height = Abs::zero();
// Place finished rows and layout fractional rows.
for row in std::mem::take(&mut self.lrows) {
for (i, row) in std::mem::take(&mut self.lrows).into_iter().enumerate() {
let (frame, y, is_last) = match row {
Row::Frame(frame, y, is_last) => (frame, y, is_last),
Row::Fr(v, y, disambiguator) => {
@ -1484,6 +1487,9 @@ impl<'a> GridLayouter<'a> {
};
let height = frame.height();
if i < self.current_header_rows {
header_row_height += height;
}
// Ensure rowspans which span this row will have enough space to
// be laid out over it later.
@ -1562,7 +1568,11 @@ impl<'a> GridLayouter<'a> {
// we have to check the same index again in the next
// iteration.
let rowspan = self.rowspans.remove(i);
self.layout_rowspan(rowspan, Some((&mut output, &rrows)), engine)?;
self.layout_rowspan(
rowspan,
Some((&mut output, header_row_height)),
engine,
)?;
} else {
i += 1;
}
@ -1573,9 +1583,10 @@ impl<'a> GridLayouter<'a> {
pos.y += height;
}
self.finish_region_internal(output, rrows);
self.finish_region_internal(output, rrows, header_row_height);
if !last {
self.current_header_rows = 0;
let disambiguator = self.finished.len();
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
self.prepare_footer(footer, engine, disambiguator)?;
@ -1602,11 +1613,16 @@ impl<'a> GridLayouter<'a> {
&mut self,
output: Frame,
resolved_rows: Vec<RowPiece>,
header_row_height: Abs,
) {
self.finished.push(output);
self.rrows.push(resolved_rows);
self.regions.next();
self.initial = self.regions.size;
if !self.grid.headers.is_empty() {
self.finished_header_rows.push(header_row_height);
}
}
}

View File

@ -189,7 +189,11 @@ impl<'a> GridLayouter<'a> {
{
// Advance regions without any output until we can place the
// header and the footer.
self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]);
self.finish_region_internal(
Frame::soft(Axes::splat(Abs::zero())),
vec![],
Abs::zero(),
);
// TODO: re-calculate heights of headers and footers on each region
// if 'full'changes? (Assuming height doesn't change for now...)
@ -250,9 +254,18 @@ impl<'a> GridLayouter<'a> {
// TODO: maybe extract this into a function to share code with multiple
// footers.
if matches!(headers, HeadersToLayout::RepeatingAndPending) || skipped_region {
self.unbreakable_rows_left +=
let repeating_header_rows =
total_header_row_count(self.repeating_headers.iter().map(Deref::deref));
let pending_header_rows = total_header_row_count(
self.pending_headers.into_iter().map(Repeatable::unwrap),
);
// Include both repeating and pending header rows as this number is
// used for orphan prevention.
self.current_header_rows = repeating_header_rows + pending_header_rows;
self.unbreakable_rows_left += repeating_header_rows + pending_header_rows;
// Use indices to avoid double borrow. We don't mutate headers in
// 'layout_row' so this is fine.
let mut i = 0;
@ -336,7 +349,11 @@ impl<'a> GridLayouter<'a> {
{
// Advance regions without any output until we can place the
// footer.
self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]);
self.finish_region_internal(
Frame::soft(Axes::splat(Abs::zero())),
vec![],
Abs::zero(),
);
skipped_region = true;
}

View File

@ -5,7 +5,7 @@ use typst_library::layout::grid::resolve::Repeatable;
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
use typst_utils::MaybeReverseIter;
use super::layouter::{in_last_with_offset, points, Row, RowPiece};
use super::layouter::{in_last_with_offset, points, Row};
use super::{layout_cell, Cell, GridLayouter};
/// All information needed to layout a single rowspan.
@ -87,10 +87,10 @@ pub struct CellMeasurementData<'layouter> {
impl GridLayouter<'_> {
/// Layout a rowspan over the already finished regions, plus the current
/// region's frame and resolved rows, if it wasn't finished yet (because
/// we're being called from `finish_region`, but note that this function is
/// also called once after all regions are finished, in which case
/// `current_region_data` is `None`).
/// region's frame and height of resolved header rows, if it wasn't
/// finished yet (because we're being called from `finish_region`, but note
/// that this function is also called once after all regions are finished,
/// in which case `current_region_data` is `None`).
///
/// We need to do this only once we already know the heights of all
/// spanned rows, which is only possible after laying out the last row
@ -98,7 +98,7 @@ impl GridLayouter<'_> {
pub fn layout_rowspan(
&mut self,
rowspan_data: Rowspan,
current_region_data: Option<(&mut Frame, &[RowPiece])>,
current_region_data: Option<(&mut Frame, Abs)>,
engine: &mut Engine,
) -> SourceResult<()> {
let Rowspan {
@ -142,11 +142,31 @@ impl GridLayouter<'_> {
// Push the layouted frames directly into the finished frames.
let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?;
let (current_region, current_rrows) = current_region_data.unzip();
for ((i, finished), frame) in self
let (current_region, current_header_row_height) = current_region_data.unzip();
// Clever trick to process finished header rows:
// - If there are grid headers, the vector will be filled with one
// finished header row height per region, so, chaining with the height
// for the current one, we get the header row height for each region.
//
// - But if there are no grid headers, the vector will be empty, so in
// theory the regions and resolved header row heights wouldn't match.
// But that's fine - 'current_header_row_height' can only be either
// 'Some(zero)' or 'None' in such a case, and for all other rows we
// append infinite zeros. That is, in such a case, the resolved header
// row height is always zero, so that's our fallback.
let finished_header_rows = self
.finished_header_rows
.iter()
.copied()
.chain(current_header_row_height)
.chain(std::iter::repeat(Abs::zero()));
for ((i, (finished, header_dy)), frame) in self
.finished
.iter_mut()
.chain(current_region.into_iter())
.zip(finished_header_rows)
.skip(first_region)
.enumerate()
.zip(fragment)
@ -158,25 +178,9 @@ impl GridLayouter<'_> {
} else {
// The rowspan continuation starts after the header (thus,
// 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)
.map(Vec::as_slice)
.or(current_rrows)
.unwrap_or(&[])
.iter()
.take_while(|row| row.y < header.end);
header_rows.map(|row| row.height).sum()
} else {
// Without a header, start at the very top of the region.
Abs::zero()
}
// rows). Without a header, this is zero, so the rowspan can
// start at the very top of the region as usual.
header_dy
};
finished.push_frame(Point::new(dx, dy), frame);