summaryrefslogtreecommitdiff
path: root/crates/typst-layout/src
diff options
context:
space:
mode:
authorPgBiel <9021226+PgBiel@users.noreply.github.com>2025-06-10 11:41:16 -0300
committerGitHub <noreply@github.com>2025-06-10 14:41:16 +0000
commit7c7b962b98a09c1baabdd03ff4ccad8f6d817b37 (patch)
treec44c9b82e3c7ec7bf8feb44cedeae9fa71a107cb /crates/typst-layout/src
parenta18ca3481da17a4de1cc7f9890f0c61efb480655 (diff)
Table multiple headers and subheaders (#6168)
Diffstat (limited to 'crates/typst-layout/src')
-rw-r--r--crates/typst-layout/src/grid/layouter.rs598
-rw-r--r--crates/typst-layout/src/grid/lines.rs29
-rw-r--r--crates/typst-layout/src/grid/repeated.rs476
-rw-r--r--crates/typst-layout/src/grid/rowspans.rs245
4 files changed, 1028 insertions, 320 deletions
diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs
index 99b85edd..42fe38db 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,
@@ -30,10 +32,8 @@ pub struct GridLayouter<'a> {
pub(super) rcols: Vec<Abs>,
/// The sum of `rcols`.
pub(super) width: Abs,
- /// Resolve row sizes, by region.
+ /// Resolved row sizes, by region.
pub(super) rrows: Vec<Vec<RowPiece>>,
- /// Rows in the current region.
- pub(super) lrows: Vec<Row>,
/// The amount of unbreakable rows remaining to be laid out in the
/// current unbreakable row group. While this is positive, no region breaks
/// should occur.
@@ -41,24 +41,155 @@ pub struct GridLayouter<'a> {
/// Rowspans not yet laid out because not all of their spanned rows were
/// laid out yet.
pub(super) rowspans: Vec<Rowspan>,
- /// The initial size of the current region before we started subtracting.
- pub(super) initial: Size,
+ /// Grid layout state for the current region.
+ pub(super) current: Current,
/// Frames for finished regions.
pub(super) finished: Vec<Frame>,
+ /// The amount and height of header rows on each finished region.
+ pub(super) finished_header_rows: Vec<FinishedHeaderRowInfo>,
/// Whether this is an RTL grid.
pub(super) is_rtl: bool,
- /// The simulated header height.
- /// This field is reset in `layout_header` and properly updated by
+ /// 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 all levels are >= 1).
+ pub(super) repeating_headers: Vec<&'a Header>,
+ /// Headers, repeating or not, awaiting their first successful layout.
+ /// Sorted by increasing levels.
+ pub(super) pending_headers: &'a [Repeatable<Header>],
+ /// Next headers to be processed.
+ pub(super) upcoming_headers: &'a [Repeatable<Header>],
+ /// State of the row being currently laid out.
+ ///
+ /// This is kept as a field to avoid passing down too many parameters from
+ /// `layout_row` into called functions, which would then have to pass them
+ /// down to `push_row`, which reads these values.
+ pub(super) row_state: RowState,
+ /// The span of the grid element.
+ pub(super) span: Span,
+}
+
+/// Grid layout state for the current region. This should be reset or updated
+/// on each region break.
+pub(super) struct Current {
+ /// The initial size of the current region before we started subtracting.
+ pub(super) initial: Size,
+ /// The height of the region after repeated headers were placed and footers
+ /// prepared. This also includes pending repeating headers from the start,
+ /// even if they were not repeated yet, since they will be repeated in the
+ /// next region anyway (bar orphan prevention).
+ ///
+ /// This is used to quickly tell if any additional space in the region has
+ /// been occupied since then, meaning that additional space will become
+ /// available after a region break (see
+ /// [`GridLayouter::may_progress_with_repeats`]).
+ pub(super) initial_after_repeats: Abs,
+ /// Whether `layouter.regions.may_progress()` was `true` at the top of the
+ /// region.
+ pub(super) could_progress_at_top: bool,
+ /// Rows in the current region.
+ pub(super) lrows: Vec<Row>,
+ /// The amount of repeated header rows at the start of the current region.
+ /// Thus, excludes rows from pending headers (which were placed for the
+ /// first time).
+ ///
+ /// Note that `repeating_headers` and `pending_headers` can change if we
+ /// find a new header inside the region (not at the top), so this field
+ /// is required to access information from the top of the region.
+ ///
+ /// This information is used on finish region to calculate the total height
+ /// of resolved header rows at the top of the region, which is used by
+ /// multi-page rowspans so they can properly skip the header rows at the
+ /// top of each region during layout.
+ pub(super) repeated_header_rows: usize,
+ /// The end bound of the row range of the last repeating header at the
+ /// start of the region.
+ ///
+ /// The last row might have disappeared from layout due to being empty, so
+ /// this is how we can become aware of where the last header ends without
+ /// having to check the vector of rows. Line layout uses this to determine
+ /// when to prioritize the last lines under a header.
+ ///
+ /// A value of zero indicates no repeated headers were placed.
+ pub(super) last_repeated_header_end: usize,
+ /// Stores the length of `lrows` before a sequence of rows equipped with
+ /// orphan prevention was laid out. In this case, if no more rows without
+ /// orphan prevention are laid out after those rows before the region ends,
+ /// the rows will be removed, and there may be an attempt to place them
+ /// again in the new region. Effectively, this is the mechanism used for
+ /// orphan prevention of rows.
+ ///
+ /// At the moment, this is only used by repeated headers (they aren't laid
+ /// out if alone in the region) and by new headers, which are moved to the
+ /// `pending_headers` vector and so will automatically be placed again
+ /// until they fit and are not orphans in at least one region (or exactly
+ /// one, for non-repeated headers).
+ pub(super) lrows_orphan_snapshot: Option<usize>,
+ /// The height of effectively repeating headers, that is, ignoring
+ /// non-repeating pending headers, in the current region.
+ ///
+ /// This is used by multi-page auto rows so they can inform cell layout on
+ /// how much space should be taken by headers if they break across regions.
+ /// In particular, non-repeating headers only occupy the initial region,
+ /// but disappear on new regions, so they can be ignored.
+ ///
+ /// This field is reset on each new region and properly updated by
/// `layout_auto_row` and `layout_relative_row`, and should not be read
/// before all header rows are fully laid out. It is usually fine because
/// header rows themselves are unbreakable, and unbreakable rows do not
/// need to read this field at all.
- pub(super) header_height: Abs,
+ ///
+ /// This height is not only computed at the beginning of the region. It is
+ /// updated whenever a new header is found, subtracting the height of
+ /// headers which stopped repeating and adding the height of all new
+ /// headers.
+ pub(super) repeating_header_height: Abs,
+ /// The height for each repeating header that was placed in this region.
+ /// Note that this includes headers not at the top of the region, before
+ /// their first repetition (pending headers), and excludes headers removed
+ /// by virtue of a new, conflicting header being found (short-lived
+ /// headers).
+ ///
+ /// This is used to know how much to update `repeating_header_height` by
+ /// when finding a new header and causing existing repeating headers to
+ /// stop.
+ pub(super) repeating_header_heights: Vec<Abs>,
/// The simulated footer height for this region.
+ ///
/// The simulation occurs before any rows are laid out for a region.
pub(super) footer_height: Abs,
- /// The span of the grid element.
- pub(super) span: Span,
+}
+
+/// Data about the row being laid out right now.
+#[derive(Debug, Default)]
+pub(super) struct RowState {
+ /// If this is `Some`, this will be updated by the currently laid out row's
+ /// height if it is auto or relative. This is used for header height
+ /// calculation.
+ pub(super) current_row_height: Option<Abs>,
+ /// This is `true` when laying out non-short lived headers and footers.
+ /// That is, headers and footers which are not immediately followed or
+ /// preceded (respectively) by conflicting headers and footers of same or
+ /// lower level, or the end or start of the table (respectively), which
+ /// would cause them to never repeat, even once.
+ ///
+ /// If this is `false`, the next row to be laid out will remove an active
+ /// orphan snapshot and will flush pending headers, as there is no risk
+ /// that they will be orphans anymore.
+ pub(super) in_active_repeatable: bool,
+}
+
+/// Data about laid out repeated header rows for a specific finished region.
+#[derive(Debug, Default)]
+pub(super) struct FinishedHeaderRowInfo {
+ /// The amount of repeated headers at the top of the region.
+ pub(super) repeated_amount: usize,
+ /// The end bound of the row range of the last repeated header at the top
+ /// of the region.
+ pub(super) last_repeated_header_end: usize,
+ /// The total height of repeated headers at the top of the region.
+ pub(super) repeated_height: Abs,
}
/// Details about a resulting row piece.
@@ -114,14 +245,27 @@ impl<'a> GridLayouter<'a> {
rcols: vec![Abs::zero(); grid.cols.len()],
width: Abs::zero(),
rrows: vec![],
- lrows: vec![],
unbreakable_rows_left: 0,
rowspans: vec![],
- initial: regions.size,
finished: vec![],
+ finished_header_rows: vec![],
is_rtl: TextElem::dir_in(styles) == Dir::RTL,
- header_height: Abs::zero(),
- footer_height: Abs::zero(),
+ repeating_headers: vec![],
+ upcoming_headers: &grid.headers,
+ pending_headers: Default::default(),
+ row_state: RowState::default(),
+ current: Current {
+ initial: regions.size,
+ initial_after_repeats: regions.size.y,
+ could_progress_at_top: regions.may_progress(),
+ lrows: vec![],
+ repeated_header_rows: 0,
+ last_repeated_header_end: 0,
+ lrows_orphan_snapshot: None,
+ repeating_header_height: Abs::zero(),
+ repeating_header_heights: vec![],
+ footer_height: Abs::zero(),
+ },
span,
}
}
@@ -130,38 +274,57 @@ impl<'a> GridLayouter<'a> {
pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> {
self.measure_columns(engine)?;
- if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
- // Ensure rows in the first region will be aware of the possible
- // presence of the footer.
- self.prepare_footer(footer, engine, 0)?;
- if matches!(self.grid.header, None | Some(Repeatable::NotRepeated(_))) {
- // No repeatable header, so we won't subtract it later.
- self.regions.size.y -= self.footer_height;
+ if let Some(footer) = &self.grid.footer {
+ if footer.repeated {
+ // Ensure rows in the first region will be aware of the
+ // possible presence of the footer.
+ self.prepare_footer(footer, engine, 0)?;
+ self.regions.size.y -= self.current.footer_height;
+ self.current.initial_after_repeats = self.regions.size.y;
}
}
- 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;
+ let mut consecutive_header_count = 0;
+ while y < self.grid.rows.len() {
+ if let Some(next_header) = self.upcoming_headers.get(consecutive_header_count)
+ {
+ if next_header.range.contains(&y) {
+ self.place_new_headers(&mut consecutive_header_count, engine)?;
+ y = next_header.range.end;
+
// Skip header rows during normal layout.
continue;
}
}
- if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
- if y >= footer.start {
+ if let Some(footer) = &self.grid.footer {
+ if footer.repeated && y >= footer.start {
if y == footer.start {
self.layout_footer(footer, engine, self.finished.len())?;
+ self.flush_orphans();
}
+ y = footer.end;
continue;
}
}
self.layout_row(y, engine, 0)?;
+
+ // After the first non-header row is placed, pending headers are no
+ // longer orphans and can repeat, so we move them to repeating
+ // headers.
+ //
+ // Note that this is usually done in `push_row`, since the call to
+ // `layout_row` above might trigger region breaks (for multi-page
+ // auto rows), whereas this needs to be called as soon as any part
+ // of a row is laid out. However, it's possible a row has no
+ // visible output and thus does not push any rows even though it
+ // was successfully laid out, in which case we additionally flush
+ // here just in case.
+ self.flush_orphans();
+
+ y += 1;
}
self.finish_region(engine, true)?;
@@ -184,13 +347,47 @@ impl<'a> GridLayouter<'a> {
self.render_fills_strokes()
}
- /// Layout the given row.
+ /// Layout a row with a certain initial state, returning the final state.
+ #[inline]
+ pub(super) fn layout_row_with_state(
+ &mut self,
+ y: usize,
+ engine: &mut Engine,
+ disambiguator: usize,
+ initial_state: RowState,
+ ) -> SourceResult<RowState> {
+ // Keep a copy of the previous value in the stack, as this function can
+ // call itself recursively (e.g. if a region break is triggered and a
+ // header is placed), so we shouldn't outright overwrite it, but rather
+ // save and later restore the state when back to this call.
+ let previous = std::mem::replace(&mut self.row_state, initial_state);
+
+ // Keep it as a separate function to allow inlining the return below,
+ // as it's usually not needed.
+ self.layout_row_internal(y, engine, disambiguator)?;
+
+ Ok(std::mem::replace(&mut self.row_state, previous))
+ }
+
+ /// Layout the given row with the default row state.
+ #[inline]
pub(super) fn layout_row(
&mut self,
y: usize,
engine: &mut Engine,
disambiguator: usize,
) -> SourceResult<()> {
+ self.layout_row_with_state(y, engine, disambiguator, RowState::default())?;
+ Ok(())
+ }
+
+ /// Layout the given row using the current state.
+ pub(super) fn layout_row_internal(
+ &mut self,
+ y: usize,
+ engine: &mut Engine,
+ disambiguator: usize,
+ ) -> SourceResult<()> {
// Skip to next region if current one is full, but only for content
// rows, not for gutter rows, and only if we aren't laying out an
// unbreakable group of rows.
@@ -206,13 +403,18 @@ impl<'a> GridLayouter<'a> {
}
// Don't layout gutter rows at the top of a region.
- if is_content_row || !self.lrows.is_empty() {
+ if is_content_row || !self.current.lrows.is_empty() {
match self.grid.rows[y] {
Sizing::Auto => self.layout_auto_row(engine, disambiguator, y)?,
Sizing::Rel(v) => {
self.layout_relative_row(engine, disambiguator, v, y)?
}
- Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y, disambiguator)),
+ Sizing::Fr(v) => {
+ if !self.row_state.in_active_repeatable {
+ self.flush_orphans();
+ }
+ self.current.lrows.push(Row::Fr(v, y, disambiguator))
+ }
}
}
@@ -225,8 +427,13 @@ impl<'a> GridLayouter<'a> {
fn render_fills_strokes(mut self) -> SourceResult<Fragment> {
let mut finished = std::mem::take(&mut self.finished);
let frame_amount = finished.len();
- for ((frame_index, frame), rows) in
- finished.iter_mut().enumerate().zip(&self.rrows)
+ for (((frame_index, frame), rows), finished_header_rows) in
+ finished.iter_mut().enumerate().zip(&self.rrows).zip(
+ self.finished_header_rows
+ .iter()
+ .map(Some)
+ .chain(std::iter::repeat(None)),
+ )
{
if self.rcols.is_empty() || rows.is_empty() {
continue;
@@ -347,7 +554,8 @@ impl<'a> GridLayouter<'a> {
let hline_indices = rows
.iter()
.map(|piece| piece.y)
- .chain(std::iter::once(self.grid.rows.len()));
+ .chain(std::iter::once(self.grid.rows.len()))
+ .enumerate();
// Converts a row to the corresponding index in the vector of
// hlines.
@@ -372,7 +580,7 @@ impl<'a> GridLayouter<'a> {
};
let mut prev_y = None;
- for (y, dy) in hline_indices.zip(hline_offsets) {
+ for ((i, y), dy) in hline_indices.zip(hline_offsets) {
// Position of lines below the row index in the previous iteration.
let expected_prev_line_position = prev_y
.map(|prev_y| {
@@ -383,47 +591,40 @@ impl<'a> GridLayouter<'a> {
})
.unwrap_or(LinePosition::Before);
- // FIXME: In the future, directly specify in 'self.rrows' when
- // we place a repeated header rather than its original rows.
- // That would let us remove most of those verbose checks, both
- // in 'lines.rs' and here. Those checks also aren't fully
- // accurate either, since they will also trigger when some rows
- // have been removed between the header and what's below it.
- let is_under_repeated_header = self
- .grid
- .header
- .as_ref()
- .and_then(Repeatable::as_repeated)
- .zip(prev_y)
- .is_some_and(|(header, prev_y)| {
- // Note: 'y == header.end' would mean we're right below
- // the NON-REPEATED header, so that case should return
- // false.
- prev_y < header.end && y > header.end
- });
+ // Header's lines at the bottom have priority when repeated.
+ // This will store the end bound of the last header if the
+ // current iteration is calculating lines under it.
+ let last_repeated_header_end_above = match finished_header_rows {
+ Some(info) if prev_y.is_some() && i == info.repeated_amount => {
+ Some(info.last_repeated_header_end)
+ }
+ _ => None,
+ };
// If some grid rows were omitted between the previous resolved
// row and the current one, we ensure lines below the previous
// row don't "disappear" and are considered, albeit with less
// priority. However, don't do this when we're below a header,
// as it must have more priority instead of less, so it is
- // chained later instead of before. The exception is when the
+ // chained later instead of before (stored in the
+ // 'header_hlines' variable below). The exception is when the
// last row in the header is removed, in which case we append
// both the lines under the row above us and also (later) the
// lines under the header's (removed) last row.
- let prev_lines = prev_y
- .filter(|prev_y| {
- prev_y + 1 != y
- && (!is_under_repeated_header
- || self
- .grid
- .header
- .as_ref()
- .and_then(Repeatable::as_repeated)
- .is_some_and(|header| prev_y + 1 != header.end))
- })
- .map(|prev_y| get_hlines_at(prev_y + 1))
- .unwrap_or(&[]);
+ let prev_lines = match prev_y {
+ Some(prev_y)
+ if prev_y + 1 != y
+ && last_repeated_header_end_above.is_none_or(
+ |last_repeated_header_end| {
+ prev_y + 1 != last_repeated_header_end
+ },
+ ) =>
+ {
+ get_hlines_at(prev_y + 1)
+ }
+
+ _ => &[],
+ };
let expected_hline_position =
expected_line_position(y, y == self.grid.rows.len());
@@ -441,15 +642,13 @@ impl<'a> GridLayouter<'a> {
};
let mut expected_header_line_position = LinePosition::Before;
- let header_hlines = if let Some((Repeatable::Repeated(header), prev_y)) =
- self.grid.header.as_ref().zip(prev_y)
- {
- if is_under_repeated_header
- && (!self.grid.has_gutter
+ let header_hlines = match (last_repeated_header_end_above, prev_y) {
+ (Some(header_end_above), Some(prev_y))
+ if !self.grid.has_gutter
|| matches!(
self.grid.rows[prev_y],
Sizing::Rel(length) if length.is_zero()
- ))
+ ) =>
{
// For lines below a header, give priority to the
// lines originally below the header rather than
@@ -468,15 +667,13 @@ impl<'a> GridLayouter<'a> {
// column-gutter is specified, for example. In that
// case, we still repeat the line under the gutter.
expected_header_line_position = expected_line_position(
- header.end,
- header.end == self.grid.rows.len(),
+ header_end_above,
+ header_end_above == self.grid.rows.len(),
);
- get_hlines_at(header.end)
- } else {
- &[]
+ get_hlines_at(header_end_above)
}
- } else {
- &[]
+
+ _ => &[],
};
// The effective hlines to be considered at this row index are
@@ -529,6 +726,7 @@ impl<'a> GridLayouter<'a> {
grid,
rows,
local_top_y,
+ last_repeated_header_end_above,
in_last_region,
y,
x,
@@ -941,15 +1139,9 @@ 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)
- {
- // Add to header height.
- self.header_height += first;
+ if let Some(row_height) = &mut self.row_state.current_row_height {
+ // Add to header height, as we are in a header row.
+ *row_height += first;
}
return Ok(());
@@ -958,19 +1150,21 @@ impl<'a> GridLayouter<'a> {
// Expand all but the last region.
// Skip the first region if the space is eaten up by an fr row.
let len = resolved.len();
- for ((i, region), target) in self
- .regions
- .iter()
- .enumerate()
- .zip(&mut resolved[..len - 1])
- .skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize)
+ for ((i, region), target) in
+ self.regions
+ .iter()
+ .enumerate()
+ .zip(&mut resolved[..len - 1])
+ .skip(self.current.lrows.iter().any(|row| matches!(row, Row::Fr(..)))
+ as usize)
{
// Subtract header and footer heights from the region height when
- // it's not the first.
+ // it's not the first. Ignore non-repeating headers as they only
+ // appear on the first region by definition.
target.set_max(
region.y
- if i > 0 {
- self.header_height + self.footer_height
+ self.current.repeating_header_height + self.current.footer_height
} else {
Abs::zero()
},
@@ -1181,25 +1375,19 @@ 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.
- self.header_height += resolved;
+ if let Some(row_height) = &mut self.row_state.current_row_height {
+ // Add to header height, as we are in a header row.
+ *row_height += resolved;
}
// Skip to fitting region, but only if we aren't part of an unbreakable
- // row group. We use 'in_last_with_offset' so our 'in_last' call
- // properly considers that a header and a footer would be added on each
- // region break.
+ // row group. We use 'may_progress_with_repeats' to stop trying if we
+ // would skip to a region with the same height and where the same
+ // headers would be repeated.
let height = frame.height();
while self.unbreakable_rows_left == 0
&& !self.regions.size.y.fits(height)
- && !in_last_with_offset(self.regions, self.header_height + self.footer_height)
+ && self.may_progress_with_repeats()
{
self.finish_region(engine, false)?;
@@ -1323,8 +1511,13 @@ impl<'a> GridLayouter<'a> {
/// will be pushed for this particular row. It can be `false` for rows
/// spanning multiple regions.
fn push_row(&mut self, frame: Frame, y: usize, is_last: bool) {
+ if !self.row_state.in_active_repeatable {
+ // There is now a row after the rows equipped with orphan
+ // prevention, so no need to keep moving them anymore.
+ self.flush_orphans();
+ }
self.regions.size.y -= frame.height();
- self.lrows.push(Row::Frame(frame, y, is_last));
+ self.current.lrows.push(Row::Frame(frame, y, is_last));
}
/// Finish rows for one region.
@@ -1333,68 +1526,73 @@ impl<'a> GridLayouter<'a> {
engine: &mut Engine,
last: bool,
) -> SourceResult<()> {
+ // The latest rows have orphan prevention (headers) and no other rows
+ // were placed, so remove those rows and try again in a new region,
+ // unless this is the last region.
+ if let Some(orphan_snapshot) = self.current.lrows_orphan_snapshot.take() {
+ if !last {
+ self.current.lrows.truncate(orphan_snapshot);
+ self.current.repeated_header_rows =
+ self.current.repeated_header_rows.min(orphan_snapshot);
+
+ if orphan_snapshot == 0 {
+ // Removed all repeated headers.
+ self.current.last_repeated_header_end = 0;
+ }
+ }
+ }
+
if self
+ .current
.lrows
.last()
.is_some_and(|row| self.grid.is_gutter_track(row.index()))
{
// Remove the last row in the region if it is a gutter row.
- self.lrows.pop().unwrap();
+ self.current.lrows.pop().unwrap();
+ self.current.repeated_header_rows =
+ self.current.repeated_header_rows.min(self.current.lrows.len());
}
- // If no rows other than the footer have been laid out so far, and
- // there are rows beside the footer, then don't lay it out at all.
- // This check doesn't apply, and is thus overridden, when there is a
- // header.
- let mut footer_would_be_orphan = self.lrows.is_empty()
- && !in_last_with_offset(
- self.regions,
- self.header_height + self.footer_height,
- )
- && self
- .grid
- .footer
- .as_ref()
- .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
- && 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)
- && !in_last_with_offset(
- self.regions,
- self.header_height + self.footer_height,
- )
- {
- // Header and footer would be alone in this region, but there are more
- // rows beyond the header and the footer. Push an empty region.
- self.lrows.clear();
- footer_would_be_orphan = true;
- }
- }
+ // If no rows other than the footer have been laid out so far
+ // (e.g. due to header orphan prevention), and there are rows
+ // beside the footer, then don't lay it out at all.
+ //
+ // It is worth noting that the footer is made non-repeatable at
+ // the grid resolving stage if it is short-lived, that is, if
+ // it is at the start of the table (or right after headers at
+ // the start of the table).
+ //
+ // TODO(subfooters): explicitly check for short-lived footers.
+ // TODO(subfooters): widow prevention for non-repeated footers with a
+ // similar mechanism / when implementing multiple footers.
+ let footer_would_be_widow = matches!(&self.grid.footer, Some(footer) if footer.repeated)
+ && self.current.lrows.is_empty()
+ && self.current.could_progress_at_top;
let mut laid_out_footer_start = None;
- if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
- // Don't layout the footer if it would be alone with the header in
- // the page, and don't layout it twice.
- if !footer_would_be_orphan
- && self.lrows.iter().all(|row| row.index() < footer.start)
- {
- laid_out_footer_start = Some(footer.start);
- self.layout_footer(footer, engine, self.finished.len())?;
+ if !footer_would_be_widow {
+ if let Some(footer) = &self.grid.footer {
+ // Don't layout the footer if it would be alone with the header
+ // in the page (hence the widow check), and don't layout it
+ // twice (check below).
+ //
+ // TODO(subfooters): this check can be replaced by a vector of
+ // repeating footers in the future, and/or some "pending
+ // footers" vector for footers we're about to place.
+ if footer.repeated
+ && self.current.lrows.iter().all(|row| row.index() < footer.start)
+ {
+ laid_out_footer_start = Some(footer.start);
+ self.layout_footer(footer, engine, self.finished.len())?;
+ }
}
}
// Determine the height of existing rows in the region.
let mut used = Abs::zero();
let mut fr = Fr::zero();
- for row in &self.lrows {
+ for row in &self.current.lrows {
match row {
Row::Frame(frame, _, _) => used += frame.height(),
Row::Fr(v, _, _) => fr += *v,
@@ -1403,9 +1601,9 @@ impl<'a> GridLayouter<'a> {
// Determine the size of the grid in this region, expanding fully if
// there are fr rows.
- let mut size = Size::new(self.width, used).min(self.initial);
- if fr.get() > 0.0 && self.initial.y.is_finite() {
- size.y = self.initial.y;
+ let mut size = Size::new(self.width, used).min(self.current.initial);
+ if fr.get() > 0.0 && self.current.initial.y.is_finite() {
+ size.y = self.current.initial.y;
}
// The frame for the region.
@@ -1413,9 +1611,10 @@ impl<'a> GridLayouter<'a> {
let mut pos = Point::zero();
let mut rrows = vec![];
let current_region = self.finished.len();
+ let mut repeated_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.current.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) => {
@@ -1426,6 +1625,9 @@ impl<'a> GridLayouter<'a> {
};
let height = frame.height();
+ if i < self.current.repeated_header_rows {
+ repeated_header_row_height += height;
+ }
// Ensure rowspans which span this row will have enough space to
// be laid out over it later.
@@ -1504,7 +1706,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, repeated_header_row_height)),
+ engine,
+ )?;
} else {
i += 1;
}
@@ -1515,21 +1721,40 @@ impl<'a> GridLayouter<'a> {
pos.y += height;
}
- self.finish_region_internal(output, rrows);
+ self.finish_region_internal(
+ output,
+ rrows,
+ FinishedHeaderRowInfo {
+ repeated_amount: self.current.repeated_header_rows,
+ last_repeated_header_end: self.current.last_repeated_header_end,
+ repeated_height: repeated_header_row_height,
+ },
+ );
if !last {
+ self.current.repeated_header_rows = 0;
+ self.current.last_repeated_header_end = 0;
+ self.current.repeating_header_height = Abs::zero();
+ self.current.repeating_header_heights.clear();
+
let disambiguator = self.finished.len();
- if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
+ if let Some(footer) =
+ self.grid.footer.as_ref().and_then(Repeatable::as_repeated)
+ {
self.prepare_footer(footer, engine, disambiguator)?;
}
- if let Some(Repeatable::Repeated(header)) = &self.grid.header {
- // Add a header to the new region.
- self.layout_header(header, engine, disambiguator)?;
- }
-
// Ensure rows don't try to overrun the footer.
- self.regions.size.y -= self.footer_height;
+ // Note that header layout will only subtract this again if it has
+ // to skip regions to fit headers, so there is no risk of
+ // subtracting this twice.
+ self.regions.size.y -= self.current.footer_height;
+ self.current.initial_after_repeats = self.regions.size.y;
+
+ if !self.repeating_headers.is_empty() || !self.pending_headers.is_empty() {
+ // Add headers to the new region.
+ self.layout_active_headers(engine)?;
+ }
}
Ok(())
@@ -1541,11 +1766,26 @@ impl<'a> GridLayouter<'a> {
&mut self,
output: Frame,
resolved_rows: Vec<RowPiece>,
+ header_row_info: FinishedHeaderRowInfo,
) {
self.finished.push(output);
self.rrows.push(resolved_rows);
self.regions.next();
- self.initial = self.regions.size;
+ self.current.initial = self.regions.size;
+
+ // Repeats haven't been laid out yet, so in the meantime, this will
+ // represent the initial height after repeats laid out so far, and will
+ // be gradually updated when preparing footers and repeating headers.
+ self.current.initial_after_repeats = self.current.initial.y;
+
+ self.current.could_progress_at_top = self.regions.may_progress();
+
+ if !self.grid.headers.is_empty() {
+ self.finished_header_rows.push(header_row_info);
+ }
+
+ // Ensure orphan prevention is handled before resolving rows.
+ debug_assert!(self.current.lrows_orphan_snapshot.is_none());
}
}
@@ -1560,13 +1800,3 @@ pub(super) fn points(
offset
})
}
-
-/// Checks if the first region of a sequence of regions is the last usable
-/// region, assuming that the last region will always be occupied by some
-/// specific offset height, even after calling `.next()`, due to some
-/// additional logic which adds content automatically on each region turn (in
-/// our case, headers).
-pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool {
- regions.backlog.is_empty()
- && regions.last.is_none_or(|height| regions.size.y + offset == height)
-}
diff --git a/crates/typst-layout/src/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs
index 7549673f..d5da7e26 100644
--- a/crates/typst-layout/src/grid/lines.rs
+++ b/crates/typst-layout/src/grid/lines.rs
@@ -391,10 +391,12 @@ pub fn vline_stroke_at_row(
///
/// This function assumes columns are sorted by increasing `x`, and rows are
/// sorted by increasing `y`.
+#[allow(clippy::too_many_arguments)]
pub fn hline_stroke_at_column(
grid: &CellGrid,
rows: &[RowPiece],
local_top_y: Option<usize>,
+ header_end_above: Option<usize>,
in_last_region: bool,
y: usize,
x: usize,
@@ -499,17 +501,15 @@ pub fn hline_stroke_at_column(
// Top border stroke and header stroke are generally prioritized, unless
// they don't have explicit hline overrides and one or more user-provided
// hlines would appear at the same position, which then are prioritized.
- let top_stroke_comes_from_header = grid
- .header
- .as_ref()
- .and_then(Repeatable::as_repeated)
- .zip(local_top_y)
- .is_some_and(|(header, local_top_y)| {
- // Ensure the row above us is a repeated header.
- // FIXME: Make this check more robust when headers at arbitrary
- // positions are added.
- local_top_y < header.end && y > header.end
- });
+ let top_stroke_comes_from_header = header_end_above.zip(local_top_y).is_some_and(
+ |(last_repeated_header_end, local_top_y)| {
+ // Check if the last repeated header row is above this line.
+ //
+ // Note that `y == last_repeated_header_end` is impossible for a
+ // strictly repeated header (not in its original position).
+ local_top_y < last_repeated_header_end && y > last_repeated_header_end
+ },
+ );
// Prioritize the footer's top stroke as well where applicable.
let bottom_stroke_comes_from_footer = grid
@@ -637,7 +637,7 @@ mod test {
},
vec![],
vec![],
- None,
+ vec![],
None,
entries,
)
@@ -1175,7 +1175,7 @@ mod test {
},
vec![],
vec![],
- None,
+ vec![],
None,
entries,
)
@@ -1268,6 +1268,7 @@ mod test {
grid,
&rows,
y.checked_sub(1),
+ None,
true,
y,
x,
@@ -1461,6 +1462,7 @@ mod test {
grid,
&rows,
y.checked_sub(1),
+ None,
true,
y,
x,
@@ -1506,6 +1508,7 @@ mod test {
grid,
&rows,
if y == 4 { Some(2) } else { y.checked_sub(1) },
+ None,
true,
y,
x,
diff --git a/crates/typst-layout/src/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs
index 22d2a09e..8db33df5 100644
--- a/crates/typst-layout/src/grid/repeated.rs
+++ b/crates/typst-layout/src/grid/repeated.rs
@@ -1,57 +1,446 @@
+use std::ops::Deref;
+
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::layout::grid::resolve::{Footer, Header, Repeatable};
use typst_library::layout::{Abs, Axes, Frame, Regions};
-use super::layouter::GridLayouter;
+use super::layouter::{GridLayouter, RowState};
use super::rowspans::UnbreakableRowGroup;
-impl GridLayouter<'_> {
- /// Layouts the header's rows.
- /// Skips regions as necessary.
- pub fn layout_header(
+impl<'a> GridLayouter<'a> {
+ /// Checks whether a region break could help a situation where we're out of
+ /// space for the next row. The criteria are:
+ ///
+ /// 1. If we could progress at the top of the region, that indicates the
+ /// region has a backlog, or (if we're at the first region) a region break
+ /// is at all possible (`regions.last` is `Some()`), so that's sufficient.
+ ///
+ /// 2. Otherwise, we may progress if another region break is possible
+ /// (`regions.last` is still `Some()`) and non-repeating rows have been
+ /// placed, since that means the space they occupy will be available in the
+ /// next region.
+ #[inline]
+ pub fn may_progress_with_repeats(&self) -> bool {
+ // TODO(subfooters): check below isn't enough to detect non-repeating
+ // footers... we can also change 'initial_after_repeats' to stop being
+ // calculated if there were any non-repeating footers.
+ self.current.could_progress_at_top
+ || self.regions.last.is_some()
+ && self.regions.size.y != self.current.initial_after_repeats
+ }
+
+ pub fn place_new_headers(
+ &mut self,
+ 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);
+
+ if new_upcoming_headers.first().is_some_and(|next_header| {
+ consecutive_headers.last().is_none_or(|latest_header| {
+ !latest_header.short_lived
+ && next_header.range.start == latest_header.range.end
+ }) && !next_header.short_lived
+ }) {
+ // More headers coming, so wait until we reach them.
+ return Ok(());
+ }
+
+ self.upcoming_headers = new_upcoming_headers;
+ *consecutive_header_count = 0;
+
+ let [first_header, ..] = consecutive_headers else {
+ self.flush_orphans();
+ return Ok(());
+ };
+
+ // Assuming non-conflicting headers sorted by increasing y, this must
+ // be the header with the lowest level (sorted by increasing levels).
+ let first_level = first_header.level;
+
+ // Stop repeating conflicting headers, even if the new headers are
+ // short-lived or won't repeat.
+ //
+ // If we go to a new region before the new headers fit alongside their
+ // children (or in general, for short-lived), the old headers should
+ // not be displayed anymore.
+ let first_conflicting_pos =
+ self.repeating_headers.partition_point(|h| h.level < first_level);
+ self.repeating_headers.truncate(first_conflicting_pos);
+
+ // Ensure upcoming rows won't see that these headers will occupy any
+ // space in future regions anymore.
+ for removed_height in
+ self.current.repeating_header_heights.drain(first_conflicting_pos..)
+ {
+ self.current.repeating_header_height -= removed_height;
+ }
+
+ // Layout short-lived headers immediately.
+ if consecutive_headers.last().is_some_and(|h| h.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_orphans();
+
+ // 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 {
+ // Let's try to place pending headers at least once.
+ // This might be a waste as we could generate an orphan and thus have
+ // to try to place old and new headers all over again, but that happens
+ // for every new region anyway, so it's rather unavoidable.
+ let snapshot_created =
+ self.layout_new_headers(consecutive_headers, false, engine)?;
+
+ // Queue the new headers for layout. They will remain in this
+ // vector due to orphan prevention.
+ //
+ // After the first subsequent row is laid out, move to repeating, as
+ // it's then confirmed the headers won't be moved due to orphan
+ // prevention anymore.
+ self.pending_headers = consecutive_headers;
+
+ if !snapshot_created {
+ // Region probably couldn't progress.
+ //
+ // Mark new pending headers as final and ensure there isn't a
+ // snapshot.
+ self.flush_orphans();
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Lays out rows belonging to a header, returning the calculated header
+ /// height only for that header. Indicates to the laid out rows that they
+ /// should inform their laid out heights if appropriate (auto or fixed
+ /// size rows only).
+ #[inline]
+ fn layout_header_rows(
&mut self,
header: &Header,
engine: &mut Engine,
disambiguator: usize,
- ) -> SourceResult<()> {
- let header_rows =
- self.simulate_header(header, &self.regions, engine, disambiguator)?;
+ as_short_lived: bool,
+ ) -> SourceResult<Abs> {
+ let mut header_height = Abs::zero();
+ for y in header.range.clone() {
+ header_height += self
+ .layout_row_with_state(
+ y,
+ engine,
+ disambiguator,
+ RowState {
+ current_row_height: Some(Abs::zero()),
+ in_active_repeatable: !as_short_lived,
+ },
+ )?
+ .current_row_height
+ .unwrap_or_default();
+ }
+ Ok(header_height)
+ }
+
+ /// This function should be called each time an additional row has been
+ /// laid out in a region to indicate that orphan prevention has succeeded.
+ ///
+ /// It removes the current orphan snapshot and flushes pending headers,
+ /// such that a non-repeating header won't try to be laid out again
+ /// anymore, and a repeating header will begin to be part of
+ /// `repeating_headers`.
+ pub fn flush_orphans(&mut self) {
+ self.current.lrows_orphan_snapshot = None;
+ self.flush_pending_headers();
+ }
+
+ /// Indicates all currently pending headers have been successfully placed
+ /// once, since another row has been placed after them, so they are
+ /// certainly not orphans.
+ pub fn flush_pending_headers(&mut self) {
+ if self.pending_headers.is_empty() {
+ return;
+ }
+
+ for header in self.pending_headers {
+ if header.repeated {
+ // Vector remains sorted by increasing levels:
+ // - 'pending_headers' themselves are sorted, since we only
+ // push non-mutually-conflicting headers at a time.
+ // - Before pushing new pending headers in
+ // 'layout_new_pending_headers', we truncate repeating headers
+ // to remove anything with the same or higher levels as the
+ // first pending header.
+ // - Assuming it was sorted before, that truncation 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.pending_headers = Default::default();
+ }
+
+ /// Lays out the rows of repeating and pending headers at the top of the
+ /// region.
+ ///
+ /// 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_active_headers(&mut self, engine: &mut Engine) -> SourceResult<()> {
+ // Generate different locations for content in headers across its
+ // repetitions by assigning a unique number for each one.
+ let disambiguator = self.finished.len();
+
+ let header_height = self.simulate_header_height(
+ self.repeating_headers
+ .iter()
+ .copied()
+ .chain(self.pending_headers.iter().map(Repeatable::deref)),
+ &self.regions,
+ engine,
+ disambiguator,
+ )?;
+
+ // We already take the footer into account below.
+ // While skipping regions, footer height won't be automatically
+ // re-calculated until the end.
let mut skipped_region = false;
while self.unbreakable_rows_left == 0
- && !self.regions.size.y.fits(header_rows.height + self.footer_height)
- && self.regions.may_progress()
+ && !self.regions.size.y.fits(header_height)
+ && self.may_progress_with_repeats()
{
// 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![],
+ Default::default(),
+ );
+
+ // TODO(layout model): re-calculate heights of headers and footers
+ // on each region if 'full' changes? (Assuming height doesn't
+ // change for now...)
+ //
+ // Would remove the footer height update below (move it here).
skipped_region = true;
- }
- // Reset the header height for this region.
- // It will be re-calculated when laying out each header row.
- self.header_height = Abs::zero();
+ self.regions.size.y -= self.current.footer_height;
+ self.current.initial_after_repeats = self.regions.size.y;
+ }
- if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
- if skipped_region {
+ if let Some(footer) = &self.grid.footer {
+ if footer.repeated && skipped_region {
// Simulate the footer again; the region's 'full' might have
// changed.
- self.footer_height = self
+ self.regions.size.y += self.current.footer_height;
+ self.current.footer_height = self
.simulate_footer(footer, &self.regions, engine, disambiguator)?
.height;
+ self.regions.size.y -= self.current.footer_height;
}
}
- // Header is unbreakable.
+ let repeating_header_rows =
+ total_header_row_count(self.repeating_headers.iter().copied());
+
+ let pending_header_rows =
+ total_header_row_count(self.pending_headers.iter().map(Repeatable::deref));
+
+ // 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 += repeating_header_rows + pending_header_rows;
+
+ self.current.last_repeated_header_end =
+ self.repeating_headers.last().map(|h| h.range.end).unwrap_or_default();
+
+ // Reset the header height for this region.
+ // It will be re-calculated when laying out each header row.
+ self.current.repeating_header_height = Abs::zero();
+ self.current.repeating_header_heights.clear();
+
+ debug_assert!(self.current.lrows.is_empty());
+ debug_assert!(self.current.lrows_orphan_snapshot.is_none());
+ let may_progress = self.may_progress_with_repeats();
+
+ if may_progress {
+ // Enable orphan prevention for headers at the top of the region.
+ // Otherwise, we will flush pending headers below, after laying
+ // them out.
+ //
+ // It is very rare for this to make a difference as we're usually
+ // at the 'last' region after the first skip, at which the snapshot
+ // is handled by 'layout_new_headers'. Either way, we keep this
+ // here for correctness.
+ self.current.lrows_orphan_snapshot = Some(self.current.lrows.len());
+ }
+
+ // Use indices to avoid double borrow. We don't mutate headers in
+ // 'layout_row' so this is fine.
+ let mut i = 0;
+ while let Some(&header) = self.repeating_headers.get(i) {
+ let header_height =
+ self.layout_header_rows(header, engine, disambiguator, false)?;
+ self.current.repeating_header_height += header_height;
+
+ // We assume that this vector will be sorted according
+ // to increasing levels like 'repeating_headers' and
+ // 'pending_headers' - and, in particular, their union, as this
+ // vector is pushed repeating heights from both.
+ //
+ // This is guaranteed by:
+ // 1. We always push pending headers after repeating headers,
+ // as we assume they don't conflict because we remove
+ // conflicting repeating headers when pushing a new pending
+ // header.
+ //
+ // 2. We push in the same order as each.
+ //
+ // 3. This vector is also modified when pushing a new pending
+ // header, where we remove heights for conflicting repeating
+ // headers which have now stopped repeating. They are always at
+ // the end and new pending headers respect the existing sort,
+ // so the vector will remain sorted.
+ self.current.repeating_header_heights.push(header_height);
+
+ i += 1;
+ }
+
+ self.current.repeated_header_rows = self.current.lrows.len();
+ self.current.initial_after_repeats = self.regions.size.y;
+
+ let mut has_non_repeated_pending_header = false;
+ for header in self.pending_headers {
+ if !header.repeated {
+ self.current.initial_after_repeats = self.regions.size.y;
+ has_non_repeated_pending_header = true;
+ }
+ let header_height =
+ self.layout_header_rows(header, engine, disambiguator, false)?;
+ if header.repeated {
+ self.current.repeating_header_height += header_height;
+ self.current.repeating_header_heights.push(header_height);
+ }
+ }
+
+ if !has_non_repeated_pending_header {
+ self.current.initial_after_repeats = self.regions.size.y;
}
+
+ if !may_progress {
+ // Flush pending headers immediately, as placing them again later
+ // won't help.
+ self.flush_orphans();
+ }
+
Ok(())
}
+ /// Lays out headers found for the first time during row layout.
+ ///
+ /// If 'short_lived' is true, these headers are immediately followed by
+ /// a conflicting header, so it is assumed they will not be pushed to
+ /// pending headers.
+ ///
+ /// Returns whether orphan prevention was successfully setup, or couldn't
+ /// due to short-lived headers or the region couldn't progress.
+ pub fn layout_new_headers(
+ &mut self,
+ headers: &'a [Repeatable<Header>],
+ short_lived: bool,
+ engine: &mut Engine,
+ ) -> SourceResult<bool> {
+ // At first, only consider the height of the given headers. However,
+ // for upcoming regions, we will have to consider repeating headers as
+ // well.
+ let header_height = self.simulate_header_height(
+ headers.iter().map(Repeatable::deref),
+ &self.regions,
+ engine,
+ 0,
+ )?;
+
+ while self.unbreakable_rows_left == 0
+ && !self.regions.size.y.fits(header_height)
+ && self.may_progress_with_repeats()
+ {
+ // Note that, after the first region skip, the new headers will go
+ // at the top of the region, but after the repeating headers that
+ // remained (which will be automatically placed in 'finish_region').
+ self.finish_region(engine, false)?;
+ }
+
+ // Remove new headers at the end of the region if the upcoming row
+ // doesn't fit.
+ // TODO(subfooters): what if there is a footer right after it?
+ let should_snapshot = !short_lived
+ && self.current.lrows_orphan_snapshot.is_none()
+ && self.may_progress_with_repeats();
+
+ if should_snapshot {
+ // If we don't enter this branch while laying out non-short lived
+ // headers, that means we will have to immediately flush pending
+ // headers and mark them as final, since trying to place them in
+ // the next page won't help get more space.
+ self.current.lrows_orphan_snapshot = Some(self.current.lrows.len());
+ }
+
+ let mut at_top = self.regions.size.y == self.current.initial_after_repeats;
+
+ self.unbreakable_rows_left +=
+ total_header_row_count(headers.iter().map(Repeatable::deref));
+
+ for header in headers {
+ let header_height = self.layout_header_rows(header, engine, 0, false)?;
+
+ // Only store this header height if it is actually going to
+ // become a pending header. Otherwise, pretend it's not a
+ // header... This is fine for consumers of 'header_height' as
+ // it is guaranteed this header won't appear in a future
+ // region, so multi-page rows and cells can effectively ignore
+ // this header.
+ if !short_lived && header.repeated {
+ self.current.repeating_header_height += header_height;
+ self.current.repeating_header_heights.push(header_height);
+ if at_top {
+ self.current.initial_after_repeats = self.regions.size.y;
+ }
+ } else {
+ at_top = false;
+ }
+ }
+
+ Ok(should_snapshot)
+ }
+
+ /// Calculates the total expected height of several headers.
+ pub fn simulate_header_height<'h: 'a>(
+ &self,
+ headers: impl IntoIterator<Item = &'h Header>,
+ regions: &Regions<'_>,
+ engine: &mut Engine,
+ disambiguator: usize,
+ ) -> SourceResult<Abs> {
+ 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,8 +455,8 @@ 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,
- Some(header.end),
+ header.range.start,
+ Some(header.range.end - header.range.start),
regions,
engine,
disambiguator,
@@ -91,11 +480,22 @@ impl GridLayouter<'_> {
{
// 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![],
+ Default::default(),
+ );
skipped_region = true;
}
- self.footer_height = if skipped_region {
+ // TODO(subfooters): Consider resetting header height etc. if we skip
+ // region. (Maybe move that step to `finish_region_internal`.)
+ //
+ // That is unnecessary at the moment as 'prepare_footers' is only
+ // called at the start of the region, so header height is always zero
+ // and no headers were placed so far, but what about when we can have
+ // footers in the middle of the region? Let's think about this then.
+ self.current.footer_height = if skipped_region {
// Simulate the footer again; the region's 'full' might have
// changed.
self.simulate_footer(footer, &self.regions, engine, disambiguator)?
@@ -118,12 +518,22 @@ impl GridLayouter<'_> {
// Ensure footer rows have their own height available.
// Won't change much as we're creating an unbreakable row group
// anyway, so this is mostly for correctness.
- self.regions.size.y += self.footer_height;
+ self.regions.size.y += self.current.footer_height;
+ let repeats = self.grid.footer.as_ref().is_some_and(|f| f.repeated);
let footer_len = self.grid.rows.len() - footer.start;
self.unbreakable_rows_left += footer_len;
+
for y in footer.start..self.grid.rows.len() {
- self.layout_row(y, engine, disambiguator)?;
+ self.layout_row_with_state(
+ y,
+ engine,
+ disambiguator,
+ RowState {
+ in_active_repeatable: repeats,
+ ..Default::default()
+ },
+ )?;
}
Ok(())
@@ -144,10 +554,18 @@ impl GridLayouter<'_> {
// in the footer will be precisely the rows in the footer.
self.simulate_unbreakable_row_group(
footer.start,
- Some(self.grid.rows.len() - footer.start),
+ Some(footer.end - footer.start),
regions,
engine,
disambiguator,
)
}
}
+
+/// The total amount of rows in the given list of headers.
+#[inline]
+pub fn total_header_row_count<'h>(
+ headers: impl IntoIterator<Item = &'h Header>,
+) -> usize {
+ headers.into_iter().map(|h| h.range.end - h.range.start).sum()
+}
diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs
index 5ab0417d..02ea1481 100644
--- a/crates/typst-layout/src/grid/rowspans.rs
+++ b/crates/typst-layout/src/grid/rowspans.rs
@@ -4,7 +4,7 @@ use typst_library::foundations::Resolve;
use typst_library::layout::grid::resolve::Repeatable;
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
-use super::layouter::{in_last_with_offset, points, Row, RowPiece};
+use super::layouter::{points, Row};
use super::{layout_cell, Cell, GridLayouter};
/// All information needed to layout a single rowspan.
@@ -90,10 +90,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
@@ -101,7 +101,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 {
@@ -146,11 +146,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()
+ .map(|info| info.repeated_height)
+ .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)
@@ -162,22 +182,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 {
- 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);
@@ -231,15 +238,13 @@ impl GridLayouter<'_> {
// current row is dynamic and depends on the amount of upcoming
// unbreakable cells (with or without a rowspan setting).
let mut amount_unbreakable_rows = None;
- if let Some(Repeatable::NotRepeated(header)) = &self.grid.header {
- if current_row < header.end {
- // Non-repeated header, so keep it unbreakable.
- amount_unbreakable_rows = Some(header.end);
- }
- }
- if let Some(Repeatable::NotRepeated(footer)) = &self.grid.footer {
- if current_row >= footer.start {
+ if let Some(footer) = &self.grid.footer {
+ if !footer.repeated && current_row >= footer.start {
// Non-repeated footer, so keep it unbreakable.
+ //
+ // TODO(subfooters): This will become unnecessary
+ // once non-repeated footers are treated differently and
+ // have widow prevention.
amount_unbreakable_rows = Some(self.grid.rows.len() - footer.start);
}
}
@@ -254,10 +259,7 @@ impl GridLayouter<'_> {
// Skip to fitting region.
while !self.regions.size.y.fits(row_group.height)
- && !in_last_with_offset(
- self.regions,
- self.header_height + self.footer_height,
- )
+ && self.may_progress_with_repeats()
{
self.finish_region(engine, false)?;
}
@@ -396,16 +398,29 @@ impl GridLayouter<'_> {
// auto rows don't depend on the backlog, as they only span one
// region.
if breakable
- && (matches!(self.grid.header, Some(Repeatable::Repeated(_)))
- || matches!(self.grid.footer, Some(Repeatable::Repeated(_))))
+ && (!self.repeating_headers.is_empty()
+ || !self.pending_headers.is_empty()
+ || matches!(&self.grid.footer, Some(footer) if footer.repeated))
{
// Subtract header and footer height from all upcoming regions
// when measuring the cell, including the last repeated region.
//
// This will update the 'custom_backlog' vector with the
// updated heights of the upcoming regions.
+ //
+ // We predict that header height will only include that of
+ // repeating headers, as we can assume non-repeating headers in
+ // the first region have been successfully placed, unless
+ // something didn't fit on the first region of the auto row,
+ // but we will only find that out after measurement, and if
+ // that happens, we discard the measurement and try again.
let mapped_regions = self.regions.map(&mut custom_backlog, |size| {
- Size::new(size.x, size.y - self.header_height - self.footer_height)
+ Size::new(
+ size.x,
+ size.y
+ - self.current.repeating_header_height
+ - self.current.footer_height,
+ )
});
// Callees must use the custom backlog instead of the current
@@ -459,6 +474,7 @@ impl GridLayouter<'_> {
// Height of the rowspan covered by spanned rows in the current
// region.
let laid_out_height: Abs = self
+ .current
.lrows
.iter()
.filter_map(|row| match row {
@@ -506,7 +522,12 @@ impl GridLayouter<'_> {
.iter()
.copied()
.chain(std::iter::once(if breakable {
- self.initial.y - self.header_height - self.footer_height
+ // Here we are calculating the available height for a
+ // rowspan from the top of the current region, so
+ // we have to use initial header heights (note that
+ // header height can change in the middle of the
+ // region).
+ self.current.initial_after_repeats
} else {
// When measuring unbreakable auto rows, infinite
// height is available for content to expand.
@@ -518,11 +539,13 @@ impl GridLayouter<'_> {
// rowspan's already laid out heights with the current
// region's height and current backlog to ensure a good
// level of accuracy in the measurements.
- let backlog = self
- .regions
- .backlog
- .iter()
- .map(|&size| size - self.header_height - self.footer_height);
+ //
+ // Assume only repeating headers will survive starting at
+ // the next region.
+ let backlog = self.regions.backlog.iter().map(|&size| {
+ size - self.current.repeating_header_height
+ - self.current.footer_height
+ });
heights_up_to_current_region.chain(backlog).collect::<Vec<_>>()
} else {
@@ -536,10 +559,10 @@ impl GridLayouter<'_> {
height = *rowspan_height;
backlog = None;
full = rowspan_full;
- last = self
- .regions
- .last
- .map(|size| size - self.header_height - self.footer_height);
+ last = self.regions.last.map(|size| {
+ size - self.current.repeating_header_height
+ - self.current.footer_height
+ });
} else {
// The rowspan started in the current region, as its vector
// of heights in regions is currently empty.
@@ -741,10 +764,11 @@ impl GridLayouter<'_> {
simulated_regions.next();
disambiguator += 1;
- // Subtract the initial header and footer height, since that's the
- // height we used when subtracting from the region backlog's
+ // Subtract the repeating header and footer height, since that's
+ // the height we used when subtracting from the region backlog's
// heights while measuring cells.
- simulated_regions.size.y -= self.header_height + self.footer_height;
+ simulated_regions.size.y -=
+ self.current.repeating_header_height + self.current.footer_height;
}
if let Some(original_last_resolved_size) = last_resolved_size {
@@ -876,12 +900,8 @@ impl GridLayouter<'_> {
// which, when used and combined with upcoming spanned rows, covers all
// of the requested rowspan height, we give up.
for _attempt in 0..5 {
- let rowspan_simulator = RowspanSimulator::new(
- disambiguator,
- simulated_regions,
- self.header_height,
- self.footer_height,
- );
+ let rowspan_simulator =
+ RowspanSimulator::new(disambiguator, simulated_regions, &self.current);
let total_spanned_height = rowspan_simulator.simulate_rowspan_layout(
y,
@@ -963,7 +983,8 @@ impl GridLayouter<'_> {
{
extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero());
simulated_regions.next();
- simulated_regions.size.y -= self.header_height + self.footer_height;
+ simulated_regions.size.y -=
+ self.current.repeating_header_height + self.current.footer_height;
disambiguator += 1;
}
simulated_regions.size.y -= extra_amount_to_grow;
@@ -980,10 +1001,17 @@ struct RowspanSimulator<'a> {
finished: usize,
/// The state of regions during the simulation.
regions: Regions<'a>,
- /// The height of the header in the currently simulated region.
+ /// The total height of headers in the currently simulated region.
header_height: Abs,
- /// The height of the footer in the currently simulated region.
+ /// The total height of footers in the currently simulated region.
footer_height: Abs,
+ /// Whether `self.regions.may_progress()` was `true` at the top of the
+ /// region, indicating we can progress anywhere in the current region,
+ /// even right after a repeated header.
+ could_progress_at_top: bool,
+ /// Available height after laying out repeated headers at the top of the
+ /// currently simulated region.
+ initial_after_repeats: Abs,
/// The total spanned height so far in the simulation.
total_spanned_height: Abs,
/// Height of the latest spanned gutter row in the simulation.
@@ -997,14 +1025,19 @@ impl<'a> RowspanSimulator<'a> {
fn new(
finished: usize,
regions: Regions<'a>,
- header_height: Abs,
- footer_height: Abs,
+ current: &super::layouter::Current,
) -> Self {
Self {
finished,
regions,
- header_height,
- footer_height,
+ // There can be no new headers or footers within a multi-page
+ // rowspan, since headers and footers are unbreakable, so
+ // assuming the repeating header height and footer height
+ // won't change is safe.
+ header_height: current.repeating_header_height,
+ footer_height: current.footer_height,
+ could_progress_at_top: current.could_progress_at_top,
+ initial_after_repeats: current.initial_after_repeats,
total_spanned_height: Abs::zero(),
latest_spanned_gutter_height: Abs::zero(),
}
@@ -1053,10 +1086,7 @@ impl<'a> RowspanSimulator<'a> {
0,
)?;
while !self.regions.size.y.fits(row_group.height)
- && !in_last_with_offset(
- self.regions,
- self.header_height + self.footer_height,
- )
+ && self.may_progress_with_repeats()
{
self.finish_region(layouter, engine)?;
}
@@ -1078,10 +1108,7 @@ impl<'a> RowspanSimulator<'a> {
let mut skipped_region = false;
while unbreakable_rows_left == 0
&& !self.regions.size.y.fits(height)
- && !in_last_with_offset(
- self.regions,
- self.header_height + self.footer_height,
- )
+ && self.may_progress_with_repeats()
{
self.finish_region(layouter, engine)?;
@@ -1127,23 +1154,37 @@ impl<'a> RowspanSimulator<'a> {
// our simulation checks what happens AFTER the auto row, so we can
// just use the original backlog from `self.regions`.
let disambiguator = self.finished;
- let header_height =
- if let Some(Repeatable::Repeated(header)) = &layouter.grid.header {
- layouter
- .simulate_header(header, &self.regions, engine, disambiguator)?
- .height
- } else {
- Abs::zero()
- };
- let footer_height =
- if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer {
- layouter
- .simulate_footer(footer, &self.regions, engine, disambiguator)?
- .height
- } else {
- Abs::zero()
- };
+ let (repeating_headers, header_height) = if !layouter.repeating_headers.is_empty()
+ || !layouter.pending_headers.is_empty()
+ {
+ // Only repeating headers have survived after the first region
+ // break.
+ let repeating_headers = layouter.repeating_headers.iter().copied().chain(
+ layouter.pending_headers.iter().filter_map(Repeatable::as_repeated),
+ );
+
+ let header_height = layouter.simulate_header_height(
+ repeating_headers.clone(),
+ &self.regions,
+ engine,
+ disambiguator,
+ )?;
+
+ (Some(repeating_headers), header_height)
+ } else {
+ (None, Abs::zero())
+ };
+
+ let footer_height = if let Some(footer) =
+ layouter.grid.footer.as_ref().and_then(Repeatable::as_repeated)
+ {
+ layouter
+ .simulate_footer(footer, &self.regions, engine, disambiguator)?
+ .height
+ } else {
+ Abs::zero()
+ };
let mut skipped_region = false;
@@ -1156,19 +1197,24 @@ impl<'a> RowspanSimulator<'a> {
skipped_region = true;
}
- if let Some(Repeatable::Repeated(header)) = &layouter.grid.header {
+ if let Some(repeating_headers) = repeating_headers {
self.header_height = if skipped_region {
// Simulate headers again, at the new region, as
// the full region height may change.
- layouter
- .simulate_header(header, &self.regions, engine, disambiguator)?
- .height
+ layouter.simulate_header_height(
+ repeating_headers,
+ &self.regions,
+ engine,
+ disambiguator,
+ )?
} else {
header_height
};
}
- if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer {
+ if let Some(footer) =
+ layouter.grid.footer.as_ref().and_then(Repeatable::as_repeated)
+ {
self.footer_height = if skipped_region {
// Simulate footers again, at the new region, as
// the full region height may change.
@@ -1185,6 +1231,7 @@ impl<'a> RowspanSimulator<'a> {
// header or footer (as an invariant, any rowspans spanning any header
// or footer rows are fully contained within that header's or footer's rows).
self.regions.size.y -= self.header_height + self.footer_height;
+ self.initial_after_repeats = self.regions.size.y;
Ok(())
}
@@ -1201,8 +1248,18 @@ impl<'a> RowspanSimulator<'a> {
self.regions.next();
self.finished += 1;
+ self.could_progress_at_top = self.regions.may_progress();
self.simulate_header_footer_layout(layouter, engine)
}
+
+ /// Similar to [`GridLayouter::may_progress_with_repeats`] but for rowspan
+ /// simulation.
+ #[inline]
+ fn may_progress_with_repeats(&self) -> bool {
+ self.could_progress_at_top
+ || self.regions.last.is_some()
+ && self.regions.size.y != self.initial_after_repeats
+ }
}
/// Subtracts some size from the end of a vector of sizes.