Merge 6d4e71085d7391610afcfa61c9a5413a716fe478 into 9b09146a6b5e936966ed7ee73bce9dd2df3810ae

This commit is contained in:
PgBiel 2025-05-09 19:12:30 +00:00 committed by GitHub
commit 79177168ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 2547 additions and 427 deletions

View File

@ -3,7 +3,9 @@ use std::fmt::Debug;
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Resolve, StyleChain}; 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::{ use typst_library::layout::{
Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel, Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel,
Size, Sizing, Size, Sizing,
@ -30,10 +32,8 @@ pub struct GridLayouter<'a> {
pub(super) rcols: Vec<Abs>, pub(super) rcols: Vec<Abs>,
/// The sum of `rcols`. /// The sum of `rcols`.
pub(super) width: Abs, pub(super) width: Abs,
/// Resolve row sizes, by region. /// Resolved row sizes, by region.
pub(super) rrows: Vec<Vec<RowPiece>>, 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 /// The amount of unbreakable rows remaining to be laid out in the
/// current unbreakable row group. While this is positive, no region breaks /// current unbreakable row group. While this is positive, no region breaks
/// should occur. /// should occur.
@ -41,24 +41,145 @@ pub struct GridLayouter<'a> {
/// Rowspans not yet laid out because not all of their spanned rows were /// Rowspans not yet laid out because not all of their spanned rows were
/// laid out yet. /// laid out yet.
pub(super) rowspans: Vec<Rowspan>, pub(super) rowspans: Vec<Rowspan>,
/// The initial size of the current region before we started subtracting. /// Grid layout state for the current region.
pub(super) initial: Size, pub(super) current: Current,
/// Frames for finished regions. /// Frames for finished regions.
pub(super) finished: Vec<Frame>, 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. /// Whether this is an RTL grid.
pub(super) is_rtl: bool, pub(super) is_rtl: bool,
/// The simulated header height. /// Currently repeating headers, one per level. Sorted by increasing
/// This field is reset in `layout_header` and properly updated by /// 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,
/// 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 total simulated height for all headers currently in
/// `repeating_headers` and `pending_headers`.
///
/// This field is reset on each new region and properly updated by
/// `layout_auto_row` and `layout_relative_row`, and should not be read /// `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 /// before all header rows are fully laid out. It is usually fine because
/// header rows themselves are unbreakable, and unbreakable rows do not /// header rows themselves are unbreakable, and unbreakable rows do not
/// need to read this field at all. /// need to read this field at all.
///
/// 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) header_height: Abs, pub(super) header_height: Abs,
/// The height of effectively repeating headers, that is, ignoring
/// non-repeating pending headers.
///
/// 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.
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 simulated footer height for this region.
///
/// The simulation occurs before any rows are laid out for a region. /// The simulation occurs before any rows are laid out for a region.
pub(super) footer_height: Abs, 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. /// Details about a resulting row piece.
@ -114,14 +235,26 @@ impl<'a> GridLayouter<'a> {
rcols: vec![Abs::zero(); grid.cols.len()], rcols: vec![Abs::zero(); grid.cols.len()],
width: Abs::zero(), width: Abs::zero(),
rrows: vec![], rrows: vec![],
lrows: vec![],
unbreakable_rows_left: 0, unbreakable_rows_left: 0,
rowspans: vec![], rowspans: vec![],
initial: regions.size,
finished: vec![], finished: vec![],
finished_header_rows: vec![],
is_rtl: TextElem::dir_in(styles) == Dir::RTL, is_rtl: TextElem::dir_in(styles) == Dir::RTL,
header_height: Abs::zero(), repeating_headers: vec![],
footer_height: Abs::zero(), upcoming_headers: &grid.headers,
pending_headers: Default::default(),
row_state: RowState::default(),
current: Current {
initial: regions.size,
lrows: vec![],
repeated_header_rows: 0,
last_repeated_header_end: 0,
lrows_orphan_snapshot: None,
header_height: Abs::zero(),
repeating_header_height: Abs::zero(),
repeating_header_heights: vec![],
footer_height: Abs::zero(),
},
span, span,
} }
} }
@ -134,19 +267,18 @@ impl<'a> GridLayouter<'a> {
// Ensure rows in the first region will be aware of the possible // Ensure rows in the first region will be aware of the possible
// presence of the footer. // presence of the footer.
self.prepare_footer(footer, engine, 0)?; self.prepare_footer(footer, engine, 0)?;
if matches!(self.grid.header, None | Some(Repeatable::NotRepeated(_))) { self.regions.size.y -= self.current.footer_height;
// No repeatable header, so we won't subtract it later.
self.regions.size.y -= self.footer_height;
}
} }
for y in 0..self.grid.rows.len() { let mut y = 0;
if let Some(Repeatable::Repeated(header)) = &self.grid.header { let mut consecutive_header_count = 0;
if y < header.end { while y < self.grid.rows.len() {
if y == 0 { if let Some(next_header) = self.upcoming_headers.get(consecutive_header_count)
self.layout_header(header, engine, 0)?; {
self.regions.size.y -= self.footer_height; if next_header.range().contains(&y) {
} self.place_new_headers(&mut consecutive_header_count, engine)?;
y = next_header.end;
// Skip header rows during normal layout. // Skip header rows during normal layout.
continue; continue;
} }
@ -156,12 +288,29 @@ impl<'a> GridLayouter<'a> {
if y >= footer.start { if y >= footer.start {
if y == footer.start { if y == footer.start {
self.layout_footer(footer, engine, self.finished.len())?; self.layout_footer(footer, engine, self.finished.len())?;
self.flush_orphans();
} }
y = footer.end;
continue; continue;
} }
} }
self.layout_row(y, engine, 0)?; 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)?; self.finish_region(engine, true)?;
@ -184,12 +333,46 @@ impl<'a> GridLayouter<'a> {
self.render_fills_strokes() 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( pub(super) fn layout_row(
&mut self, &mut self,
y: usize, y: usize,
engine: &mut Engine, engine: &mut Engine,
disambiguator: usize, 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<()> { ) -> SourceResult<()> {
// Skip to next region if current one is full, but only for content // 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 // rows, not for gutter rows, and only if we aren't laying out an
@ -206,13 +389,18 @@ impl<'a> GridLayouter<'a> {
} }
// Don't layout gutter rows at the top of a region. // 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] { match self.grid.rows[y] {
Sizing::Auto => self.layout_auto_row(engine, disambiguator, y)?, Sizing::Auto => self.layout_auto_row(engine, disambiguator, y)?,
Sizing::Rel(v) => { Sizing::Rel(v) => {
self.layout_relative_row(engine, disambiguator, v, y)? 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 +413,13 @@ impl<'a> GridLayouter<'a> {
fn render_fills_strokes(mut self) -> SourceResult<Fragment> { fn render_fills_strokes(mut self) -> SourceResult<Fragment> {
let mut finished = std::mem::take(&mut self.finished); let mut finished = std::mem::take(&mut self.finished);
let frame_amount = finished.len(); let frame_amount = finished.len();
for ((frame_index, frame), rows) in for (((frame_index, frame), rows), finished_header_rows) in
finished.iter_mut().enumerate().zip(&self.rrows) 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() { if self.rcols.is_empty() || rows.is_empty() {
continue; continue;
@ -347,7 +540,8 @@ impl<'a> GridLayouter<'a> {
let hline_indices = rows let hline_indices = rows
.iter() .iter()
.map(|piece| piece.y) .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 // Converts a row to the corresponding index in the vector of
// hlines. // hlines.
@ -372,7 +566,7 @@ impl<'a> GridLayouter<'a> {
}; };
let mut prev_y = None; 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. // Position of lines below the row index in the previous iteration.
let expected_prev_line_position = prev_y let expected_prev_line_position = prev_y
.map(|prev_y| { .map(|prev_y| {
@ -383,44 +577,31 @@ impl<'a> GridLayouter<'a> {
}) })
.unwrap_or(LinePosition::Before); .unwrap_or(LinePosition::Before);
// FIXME: In the future, directly specify in 'self.rrows' when // Header's lines at the bottom have priority when repeated.
// we place a repeated header rather than its original rows. // This will store the end bound of the last header if the
// That would let us remove most of those verbose checks, both // current iteration is calculating lines under it.
// in 'lines.rs' and here. Those checks also aren't fully let last_repeated_header_end_above = finished_header_rows
// accurate either, since they will also trigger when some rows .filter(|info| prev_y.is_some() && i == info.repeated_amount)
// have been removed between the header and what's below it. .map(|info| info.last_repeated_header_end);
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
});
// If some grid rows were omitted between the previous resolved // If some grid rows were omitted between the previous resolved
// row and the current one, we ensure lines below the previous // row and the current one, we ensure lines below the previous
// row don't "disappear" and are considered, albeit with less // row don't "disappear" and are considered, albeit with less
// priority. However, don't do this when we're below a header, // priority. However, don't do this when we're below a header,
// as it must have more priority instead of less, so it is // 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 // last row in the header is removed, in which case we append
// both the lines under the row above us and also (later) the // both the lines under the row above us and also (later) the
// lines under the header's (removed) last row. // lines under the header's (removed) last row.
let prev_lines = prev_y let prev_lines = prev_y
.filter(|prev_y| { .filter(|prev_y| {
prev_y + 1 != y prev_y + 1 != y
&& (!is_under_repeated_header && last_repeated_header_end_above.is_none_or(
|| self |last_repeated_header_end| {
.grid prev_y + 1 != last_repeated_header_end
.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)) .map(|prev_y| get_hlines_at(prev_y + 1))
.unwrap_or(&[]); .unwrap_or(&[]);
@ -441,15 +622,14 @@ impl<'a> GridLayouter<'a> {
}; };
let mut expected_header_line_position = LinePosition::Before; let mut expected_header_line_position = LinePosition::Before;
let header_hlines = if let Some((Repeatable::Repeated(header), prev_y)) = let header_hlines = if let Some((header_end_above, prev_y)) =
self.grid.header.as_ref().zip(prev_y) last_repeated_header_end_above.zip(prev_y)
{ {
if is_under_repeated_header if !self.grid.has_gutter
&& (!self.grid.has_gutter || matches!(
|| matches!( self.grid.rows[prev_y],
self.grid.rows[prev_y], Sizing::Rel(length) if length.is_zero()
Sizing::Rel(length) if length.is_zero() )
))
{ {
// For lines below a header, give priority to the // For lines below a header, give priority to the
// lines originally below the header rather than // lines originally below the header rather than
@ -468,10 +648,10 @@ impl<'a> GridLayouter<'a> {
// column-gutter is specified, for example. In that // column-gutter is specified, for example. In that
// case, we still repeat the line under the gutter. // case, we still repeat the line under the gutter.
expected_header_line_position = expected_line_position( expected_header_line_position = expected_line_position(
header.end, header_end_above,
header.end == self.grid.rows.len(), header_end_above == self.grid.rows.len(),
); );
get_hlines_at(header.end) get_hlines_at(header_end_above)
} else { } else {
&[] &[]
} }
@ -529,6 +709,7 @@ impl<'a> GridLayouter<'a> {
grid, grid,
rows, rows,
local_top_y, local_top_y,
last_repeated_header_end_above,
in_last_region, in_last_region,
y, y,
x, x,
@ -941,15 +1122,9 @@ impl<'a> GridLayouter<'a> {
let frame = self.layout_single_row(engine, disambiguator, first, y)?; let frame = self.layout_single_row(engine, disambiguator, first, y)?;
self.push_row(frame, y, true); self.push_row(frame, y, true);
if self if let Some(row_height) = &mut self.row_state.current_row_height {
.grid // Add to header height, as we are in a header row.
.header *row_height += first;
.as_ref()
.and_then(Repeatable::as_repeated)
.is_some_and(|header| y < header.end)
{
// Add to header height.
self.header_height += first;
} }
return Ok(()); return Ok(());
@ -958,19 +1133,21 @@ impl<'a> GridLayouter<'a> {
// Expand all but the last region. // Expand all but the last region.
// Skip the first region if the space is eaten up by an fr row. // Skip the first region if the space is eaten up by an fr row.
let len = resolved.len(); let len = resolved.len();
for ((i, region), target) in self for ((i, region), target) in
.regions self.regions
.iter() .iter()
.enumerate() .enumerate()
.zip(&mut resolved[..len - 1]) .zip(&mut resolved[..len - 1])
.skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize) .skip(self.current.lrows.iter().any(|row| matches!(row, Row::Fr(..)))
as usize)
{ {
// Subtract header and footer heights from the region height when // 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( target.set_max(
region.y region.y
- if i > 0 { - if i > 0 {
self.header_height + self.footer_height self.current.repeating_header_height + self.current.footer_height
} else { } else {
Abs::zero() Abs::zero()
}, },
@ -1181,25 +1358,27 @@ impl<'a> GridLayouter<'a> {
let resolved = v.resolve(self.styles).relative_to(self.regions.base().y); let resolved = v.resolve(self.styles).relative_to(self.regions.base().y);
let frame = self.layout_single_row(engine, disambiguator, resolved, y)?; let frame = self.layout_single_row(engine, disambiguator, resolved, y)?;
if self if let Some(row_height) = &mut self.row_state.current_row_height {
.grid // Add to header height, as we are in a header row.
.header *row_height += resolved;
.as_ref()
.and_then(Repeatable::as_repeated)
.is_some_and(|header| y < header.end)
{
// Add to header height.
self.header_height += resolved;
} }
// Skip to fitting region, but only if we aren't part of an unbreakable // 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 // row group. We use 'may_progress_with_offset' so our 'may_progress'
// properly considers that a header and a footer would be added on each // call properly considers that a header and a footer would be added
// region break. // on each region break, so we only keep skipping regions until we
// reach one with the same height of the 'last' region (which can be
// endlessly repeated) when subtracting header and footer height.
//
// See 'check_for_unbreakable_rows' as for why we're using
// 'repeating_header_height' to predict header height.
let height = frame.height(); let height = frame.height();
while self.unbreakable_rows_left == 0 while self.unbreakable_rows_left == 0
&& !self.regions.size.y.fits(height) && !self.regions.size.y.fits(height)
&& !in_last_with_offset(self.regions, self.header_height + self.footer_height) && may_progress_with_offset(
self.regions,
self.current.repeating_header_height + self.current.footer_height,
)
{ {
self.finish_region(engine, false)?; self.finish_region(engine, false)?;
@ -1323,8 +1502,13 @@ impl<'a> GridLayouter<'a> {
/// will be pushed for this particular row. It can be `false` for rows /// will be pushed for this particular row. It can be `false` for rows
/// spanning multiple regions. /// spanning multiple regions.
fn push_row(&mut self, frame: Frame, y: usize, is_last: bool) { 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.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. /// Finish rows for one region.
@ -1333,68 +1517,71 @@ impl<'a> GridLayouter<'a> {
engine: &mut Engine, engine: &mut Engine,
last: bool, last: bool,
) -> SourceResult<()> { ) -> SourceResult<()> {
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 if self
.current
.lrows .lrows
.last() .last()
.is_some_and(|row| self.grid.is_gutter_track(row.index())) .is_some_and(|row| self.grid.is_gutter_track(row.index()))
{ {
// Remove the last row in the region if it is a gutter row. // 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 // If no rows other than the footer have been laid out so far
// there are rows beside the footer, then don't lay it out at all. // (e.g. due to header orphan prevention), and there are rows
// This check doesn't apply, and is thus overridden, when there is a // beside the footer, then don't lay it out at all.
// header. //
let mut footer_would_be_orphan = self.lrows.is_empty() // It is worth noting that the footer is made non-repeatable at
&& !in_last_with_offset( // the grid resolving stage if it is short-lived, that is, if
self.regions, // it is at the start of the table (or right after headers at
self.header_height + self.footer_height, // the start of the table).
) // TODO(subfooters): explicitly check for short-lived footers.
&& self // TODO(subfooters): widow prevention for non-repeated footers with a
.grid // similar mechanism / when implementing multiple footers.
.footer let footer_would_be_widow =
.as_ref() matches!(self.grid.footer, Some(Repeatable::Repeated(_)))
.and_then(Repeatable::as_repeated) && self.current.lrows.is_empty()
.is_some_and(|footer| footer.start != 0); && may_progress_with_offset(
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.regions,
self.header_height + self.footer_height, // Don't sum header height as we just confirmed that there
) // are no headers in this region.
{ self.current.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;
}
}
let mut laid_out_footer_start = None; let mut laid_out_footer_start = None;
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { if !footer_would_be_widow {
// Don't layout the footer if it would be alone with the header in if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
// the page, and don't layout it twice. // Don't layout the footer if it would be alone with the header
if !footer_would_be_orphan // in the page (hence the widow check), and don't layout it
&& self.lrows.iter().all(|row| row.index() < footer.start) // twice.
{ // TODO: this check can be replaced by a vector of repeating
laid_out_footer_start = Some(footer.start); // footers in the future.
self.layout_footer(footer, engine, self.finished.len())?; if 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. // Determine the height of existing rows in the region.
let mut used = Abs::zero(); let mut used = Abs::zero();
let mut fr = Fr::zero(); let mut fr = Fr::zero();
for row in &self.lrows { for row in &self.current.lrows {
match row { match row {
Row::Frame(frame, _, _) => used += frame.height(), Row::Frame(frame, _, _) => used += frame.height(),
Row::Fr(v, _, _) => fr += *v, Row::Fr(v, _, _) => fr += *v,
@ -1403,9 +1590,9 @@ impl<'a> GridLayouter<'a> {
// Determine the size of the grid in this region, expanding fully if // Determine the size of the grid in this region, expanding fully if
// there are fr rows. // there are fr rows.
let mut size = Size::new(self.width, used).min(self.initial); let mut size = Size::new(self.width, used).min(self.current.initial);
if fr.get() > 0.0 && self.initial.y.is_finite() { if fr.get() > 0.0 && self.current.initial.y.is_finite() {
size.y = self.initial.y; size.y = self.current.initial.y;
} }
// The frame for the region. // The frame for the region.
@ -1413,9 +1600,10 @@ impl<'a> GridLayouter<'a> {
let mut pos = Point::zero(); let mut pos = Point::zero();
let mut rrows = vec![]; let mut rrows = vec![];
let current_region = self.finished.len(); let current_region = self.finished.len();
let mut repeated_header_row_height = Abs::zero();
// Place finished rows and layout fractional rows. // 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 { let (frame, y, is_last) = match row {
Row::Frame(frame, y, is_last) => (frame, y, is_last), Row::Frame(frame, y, is_last) => (frame, y, is_last),
Row::Fr(v, y, disambiguator) => { Row::Fr(v, y, disambiguator) => {
@ -1426,6 +1614,9 @@ impl<'a> GridLayouter<'a> {
}; };
let height = frame.height(); 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 // Ensure rowspans which span this row will have enough space to
// be laid out over it later. // be laid out over it later.
@ -1504,7 +1695,11 @@ impl<'a> GridLayouter<'a> {
// we have to check the same index again in the next // we have to check the same index again in the next
// iteration. // iteration.
let rowspan = self.rowspans.remove(i); 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 { } else {
i += 1; i += 1;
} }
@ -1515,21 +1710,38 @@ impl<'a> GridLayouter<'a> {
pos.y += height; 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 { if !last {
self.current.repeated_header_rows = 0;
self.current.last_repeated_header_end = 0;
self.current.header_height = Abs::zero();
self.current.repeating_header_height = Abs::zero();
self.current.repeating_header_heights.clear();
let disambiguator = self.finished.len(); let disambiguator = self.finished.len();
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
self.prepare_footer(footer, engine, disambiguator)?; 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. // 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;
if !self.repeating_headers.is_empty() || !self.pending_headers.is_empty() {
// Add headers to the new region.
self.layout_active_headers(engine)?;
}
} }
Ok(()) Ok(())
@ -1541,11 +1753,19 @@ impl<'a> GridLayouter<'a> {
&mut self, &mut self,
output: Frame, output: Frame,
resolved_rows: Vec<RowPiece>, resolved_rows: Vec<RowPiece>,
header_row_info: FinishedHeaderRowInfo,
) { ) {
self.finished.push(output); self.finished.push(output);
self.rrows.push(resolved_rows); self.rrows.push(resolved_rows);
self.regions.next(); self.regions.next();
self.initial = self.regions.size; self.current.initial = self.regions.size;
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());
} }
} }
@ -1561,12 +1781,15 @@ pub(super) fn points(
}) })
} }
/// Checks if the first region of a sequence of regions is the last usable /// Checks if the first region of a sequence of regions is not the last usable
/// region, assuming that the last region will always be occupied by some /// region, assuming that the last region will always be occupied by some
/// specific offset height, even after calling `.next()`, due to some /// specific offset height, even after calling `.next()`, due to some
/// additional logic which adds content automatically on each region turn (in /// additional logic which adds content automatically on each region turn (in
/// our case, headers). /// our case, headers).
pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool { pub(super) fn may_progress_with_offset(regions: Regions<'_>, offset: Abs) -> bool {
regions.backlog.is_empty() // Use 'approx_eq' as float addition and subtraction are not associative.
&& regions.last.is_none_or(|height| regions.size.y + offset == height) !regions.backlog.is_empty()
|| regions
.last
.is_some_and(|height| !(regions.size.y + offset).approx_eq(height))
} }

View File

@ -391,10 +391,12 @@ pub fn vline_stroke_at_row(
/// ///
/// This function assumes columns are sorted by increasing `x`, and rows are /// This function assumes columns are sorted by increasing `x`, and rows are
/// sorted by increasing `y`. /// sorted by increasing `y`.
#[allow(clippy::too_many_arguments)]
pub fn hline_stroke_at_column( pub fn hline_stroke_at_column(
grid: &CellGrid, grid: &CellGrid,
rows: &[RowPiece], rows: &[RowPiece],
local_top_y: Option<usize>, local_top_y: Option<usize>,
header_end_above: Option<usize>,
in_last_region: bool, in_last_region: bool,
y: usize, y: usize,
x: usize, x: usize,
@ -499,17 +501,11 @@ pub fn hline_stroke_at_column(
// Top border stroke and header stroke are generally prioritized, unless // Top border stroke and header stroke are generally prioritized, unless
// they don't have explicit hline overrides and one or more user-provided // they don't have explicit hline overrides and one or more user-provided
// hlines would appear at the same position, which then are prioritized. // hlines would appear at the same position, which then are prioritized.
let top_stroke_comes_from_header = grid let top_stroke_comes_from_header = header_end_above.zip(local_top_y).is_some_and(
.header |(last_repeated_header_end, local_top_y)| {
.as_ref() local_top_y < last_repeated_header_end && y > last_repeated_header_end
.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
});
// Prioritize the footer's top stroke as well where applicable. // Prioritize the footer's top stroke as well where applicable.
let bottom_stroke_comes_from_footer = grid let bottom_stroke_comes_from_footer = grid
@ -637,7 +633,7 @@ mod test {
}, },
vec![], vec![],
vec![], vec![],
None, vec![],
None, None,
entries, entries,
) )
@ -1175,7 +1171,7 @@ mod test {
}, },
vec![], vec![],
vec![], vec![],
None, vec![],
None, None,
entries, entries,
) )
@ -1268,6 +1264,7 @@ mod test {
grid, grid,
&rows, &rows,
y.checked_sub(1), y.checked_sub(1),
None,
true, true,
y, y,
x, x,
@ -1461,6 +1458,7 @@ mod test {
grid, grid,
&rows, &rows,
y.checked_sub(1), y.checked_sub(1),
None,
true, true,
y, y,
x, x,
@ -1506,6 +1504,7 @@ mod test {
grid, grid,
&rows, &rows,
if y == 4 { Some(2) } else { y.checked_sub(1) }, if y == 4 { Some(2) } else { y.checked_sub(1) },
None,
true, true,
y, y,
x, x,

View File

@ -3,55 +3,448 @@ use typst_library::engine::Engine;
use typst_library::layout::grid::resolve::{Footer, Header, Repeatable}; use typst_library::layout::grid::resolve::{Footer, Header, Repeatable};
use typst_library::layout::{Abs, Axes, Frame, Regions}; use typst_library::layout::{Abs, Axes, Frame, Regions};
use super::layouter::GridLayouter; use super::layouter::{may_progress_with_offset, GridLayouter, RowState};
use super::rowspans::UnbreakableRowGroup; use super::rowspans::UnbreakableRowGroup;
impl GridLayouter<'_> { impl<'a> GridLayouter<'a> {
/// Layouts the header's rows. pub fn place_new_headers(
/// Skips regions as necessary. &mut self,
pub fn layout_header( 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.start == latest_header.end
}) && !next_header.short_lived
}) {
// More headers coming, so wait until we reach them.
// TODO: refactor
return Ok(());
}
self.upcoming_headers = new_upcoming_headers;
*consecutive_header_count = 0;
// 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)?;
Ok(())
} else {
self.layout_new_pending_headers(consecutive_headers, engine)
}
}
/// 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, &mut self,
header: &Header, header: &Header,
engine: &mut Engine, engine: &mut Engine,
disambiguator: usize, disambiguator: usize,
as_short_lived: bool,
) -> SourceResult<Abs> {
let mut header_height = Abs::zero();
for y in header.range() {
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)
}
/// Queues new pending headers for layout. Headers remain pending until
/// they are successfully laid out in some page once. Then, they will be
/// moved to `repeating_headers`, at which point it is safe to stop them
/// from repeating at any time.
fn layout_new_pending_headers(
&mut self,
headers: &'a [Repeatable<Header>],
engine: &mut Engine,
) -> SourceResult<()> { ) -> SourceResult<()> {
let header_rows = let [first_header, ..] = headers else {
self.simulate_header(header, &self.regions, engine, disambiguator)?; return Ok(());
};
// Should be impossible to have two consecutive chunks of pending
// headers since they are always as long as possible, only being
// interrupted by direct conflict between consecutive headers, in which
// case we flush pending headers immediately.
assert!(self.pending_headers.is_empty());
// 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.
// If we go to a new region before the pending headers fit alongside
// their children, 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;
}
// Non-repeating headers stop at the pending stage for orphan
// prevention only. Flushing pending headers, so those will no longer
// appear in a future region.
self.current.header_height = self.current.repeating_header_height;
// Let's try to place them 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(headers, false, engine)?;
// 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 = 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(())
}
/// 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 let Repeatable::Repeated(header) = header {
// 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::unwrap)),
&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; let mut skipped_region = false;
while self.unbreakable_rows_left == 0 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.regions.may_progress() && may_progress_with_offset(
self.regions,
// - Assume footer height already starts subtracted from the
// first region's size;
// - On each iteration, we subtract footer height from the
// available size for consistency with the first region, so we
// need to consider the footer when evaluating if skipping yet
// another region would make a difference.
self.current.footer_height,
)
{ {
// Advance regions without any output until we can place the // Advance regions without any output until we can place the
// header and the footer. // header and the footer.
self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]); self.finish_region_internal(
skipped_region = true; Frame::soft(Axes::splat(Abs::zero())),
} vec![],
Default::default(),
);
// Reset the header height for this region. // TODO: re-calculate heights of headers and footers on each region
// It will be re-calculated when laying out each header row. // if 'full' changes? (Assuming height doesn't change for now...)
self.header_height = Abs::zero(); skipped_region = true;
self.regions.size.y -= self.current.footer_height;
}
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
if skipped_region { if skipped_region {
// Simulate the footer again; the region's 'full' might have // Simulate the footer again; the region's 'full' might have
// changed. // changed.
self.footer_height = self // TODO: maybe this should go in the loop, a bit hacky as is...
self.regions.size.y += self.current.footer_height;
self.current.footer_height = self
.simulate_footer(footer, &self.regions, engine, disambiguator)? .simulate_footer(footer, &self.regions, engine, disambiguator)?
.height; .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::unwrap));
// Group of headers is unbreakable.
// Thus, no risk of 'finish_region' being recursively called from // Thus, no risk of 'finish_region' being recursively called from
// within 'layout_row'. // within 'layout_row'.
self.unbreakable_rows_left += header.end; self.unbreakable_rows_left += repeating_header_rows + pending_header_rows;
for y in 0..header.end {
self.layout_row(y, engine, disambiguator)?; self.current.last_repeated_header_end =
self.repeating_headers.last().map(|h| h.end).unwrap_or_default();
// Reset the header height for this region.
// It will be re-calculated when laying out each header row.
self.current.header_height = Abs::zero();
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 =
may_progress_with_offset(self.regions, self.current.footer_height);
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.header_height += header_height;
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();
for header in self.pending_headers {
let header_height =
self.layout_header_rows(header.unwrap(), engine, disambiguator, false)?;
self.current.header_height += header_height;
if matches!(header, Repeatable::Repeated(_)) {
self.current.repeating_header_height += header_height;
self.current.repeating_header_heights.push(header_height);
}
}
if !may_progress {
// Flush pending headers immediately, as placing them again later
// won't help.
self.flush_orphans();
}
Ok(()) 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::unwrap),
&self.regions,
engine,
0,
)?;
// TODO: remove this 'unbreakable rows left check',
// consider if we can already be in an unbreakable row group?
while self.unbreakable_rows_left == 0
&& !self.regions.size.y.fits(header_height)
&& may_progress_with_offset(
self.regions,
// 'finish_region' will place currently active headers and
// footers again. We assume previous pending headers have
// already been flushed, so in principle
// 'header_height == repeating_header_height' here
// (there won't be any pending headers at this point, other
// than the ones we are about to place).
self.current.repeating_header_height + self.current.footer_height,
)
{
// 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()
&& may_progress_with_offset(
self.regions,
self.current.repeating_header_height + self.current.footer_height,
);
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());
}
self.unbreakable_rows_left +=
total_header_row_count(headers.iter().map(Repeatable::unwrap));
for header in headers {
let header_height =
self.layout_header_rows(header.unwrap(), 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 {
self.current.header_height += header_height;
if matches!(header, Repeatable::Repeated(_)) {
self.current.repeating_header_height += header_height;
self.current.repeating_header_heights.push(header_height);
}
}
}
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. /// Simulate the header's group of rows.
pub fn simulate_header( pub fn simulate_header(
&self, &self,
@ -66,8 +459,8 @@ impl GridLayouter<'_> {
// assume that the amount of unbreakable rows following the first row // assume that the amount of unbreakable rows following the first row
// in the header will be precisely the rows in the header. // in the header will be precisely the rows in the header.
self.simulate_unbreakable_row_group( self.simulate_unbreakable_row_group(
0, header.start,
Some(header.end), Some(header.end - header.start),
regions, regions,
engine, engine,
disambiguator, disambiguator,
@ -91,11 +484,19 @@ impl GridLayouter<'_> {
{ {
// Advance regions without any output until we can place the // Advance regions without any output until we can place the
// footer. // 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; skipped_region = true;
} }
self.footer_height = if skipped_region { // TODO: Consider resetting header height etc. if we skip region.
// That is unnecessary at the moment as 'prepare_footers' is only
// called at the start of the region, 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 // Simulate the footer again; the region's 'full' might have
// changed. // changed.
self.simulate_footer(footer, &self.regions, engine, disambiguator)? self.simulate_footer(footer, &self.regions, engine, disambiguator)?
@ -118,12 +519,26 @@ impl GridLayouter<'_> {
// Ensure footer rows have their own height available. // Ensure footer rows have their own height available.
// Won't change much as we're creating an unbreakable row group // Won't change much as we're creating an unbreakable row group
// anyway, so this is mostly for correctness. // 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| matches!(f, Repeatable::Repeated(_)));
let footer_len = self.grid.rows.len() - footer.start; let footer_len = self.grid.rows.len() - footer.start;
self.unbreakable_rows_left += footer_len; self.unbreakable_rows_left += footer_len;
for y in footer.start..self.grid.rows.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(()) Ok(())
@ -144,10 +559,18 @@ impl GridLayouter<'_> {
// in the footer will be precisely the rows in the footer. // in the footer will be precisely the rows in the footer.
self.simulate_unbreakable_row_group( self.simulate_unbreakable_row_group(
footer.start, footer.start,
Some(self.grid.rows.len() - footer.start), Some(footer.end - footer.start),
regions, regions,
engine, engine,
disambiguator, 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.end - h.start).sum()
}

View File

@ -4,7 +4,7 @@ use typst_library::foundations::Resolve;
use typst_library::layout::grid::resolve::Repeatable; use typst_library::layout::grid::resolve::Repeatable;
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; 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::{may_progress_with_offset, points, Row};
use super::{layout_cell, Cell, GridLayouter}; use super::{layout_cell, Cell, GridLayouter};
/// All information needed to layout a single rowspan. /// All information needed to layout a single rowspan.
@ -90,10 +90,10 @@ pub struct CellMeasurementData<'layouter> {
impl GridLayouter<'_> { impl GridLayouter<'_> {
/// Layout a rowspan over the already finished regions, plus the current /// Layout a rowspan over the already finished regions, plus the current
/// region's frame and resolved rows, if it wasn't finished yet (because /// region's frame and height of resolved header rows, if it wasn't
/// we're being called from `finish_region`, but note that this function is /// finished yet (because we're being called from `finish_region`, but note
/// also called once after all regions are finished, in which case /// that this function is also called once after all regions are finished,
/// `current_region_data` is `None`). /// in which case `current_region_data` is `None`).
/// ///
/// We need to do this only once we already know the heights of all /// 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 /// spanned rows, which is only possible after laying out the last row
@ -101,7 +101,7 @@ impl GridLayouter<'_> {
pub fn layout_rowspan( pub fn layout_rowspan(
&mut self, &mut self,
rowspan_data: Rowspan, rowspan_data: Rowspan,
current_region_data: Option<(&mut Frame, &[RowPiece])>, current_region_data: Option<(&mut Frame, Abs)>,
engine: &mut Engine, engine: &mut Engine,
) -> SourceResult<()> { ) -> SourceResult<()> {
let Rowspan { let Rowspan {
@ -146,11 +146,31 @@ impl GridLayouter<'_> {
// Push the layouted frames directly into the finished frames. // Push the layouted frames directly into the finished frames.
let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?; let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?;
let (current_region, current_rrows) = current_region_data.unzip(); let (current_region, current_header_row_height) = current_region_data.unzip();
for ((i, finished), frame) in self
// 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 .finished
.iter_mut() .iter_mut()
.chain(current_region.into_iter()) .chain(current_region.into_iter())
.zip(finished_header_rows)
.skip(first_region) .skip(first_region)
.enumerate() .enumerate()
.zip(fragment) .zip(fragment)
@ -162,22 +182,9 @@ impl GridLayouter<'_> {
} else { } else {
// The rowspan continuation starts after the header (thus, // The rowspan continuation starts after the header (thus,
// at a position after the sum of the laid out header // at a position after the sum of the laid out header
// rows). // rows). Without a header, this is zero, so the rowspan can
if let Some(Repeatable::Repeated(header)) = &self.grid.header { // start at the very top of the region as usual.
let header_rows = self header_dy
.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()
}
}; };
finished.push_frame(Point::new(dx, dy), frame); finished.push_frame(Point::new(dx, dy), frame);
@ -231,15 +238,12 @@ impl GridLayouter<'_> {
// current row is dynamic and depends on the amount of upcoming // current row is dynamic and depends on the amount of upcoming
// unbreakable cells (with or without a rowspan setting). // unbreakable cells (with or without a rowspan setting).
let mut amount_unbreakable_rows = None; 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 let Some(Repeatable::NotRepeated(footer)) = &self.grid.footer {
if current_row >= footer.start { if current_row >= footer.start {
// Non-repeated footer, so keep it unbreakable. // Non-repeated footer, so keep it unbreakable.
// TODO: 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); amount_unbreakable_rows = Some(self.grid.rows.len() - footer.start);
} }
} }
@ -254,9 +258,13 @@ impl GridLayouter<'_> {
// Skip to fitting region. // Skip to fitting region.
while !self.regions.size.y.fits(row_group.height) while !self.regions.size.y.fits(row_group.height)
&& !in_last_with_offset( && may_progress_with_offset(
self.regions, self.regions,
self.header_height + self.footer_height, // Use 'repeating_header_height' (ignoring the height of
// non-repeated headers) to allow skipping if the
// non-repeated header is too large. It will become an
// orphan, but when there is no space left, anything goes.
self.current.repeating_header_height + self.current.footer_height,
) )
{ {
self.finish_region(engine, false)?; self.finish_region(engine, false)?;
@ -396,7 +404,8 @@ impl GridLayouter<'_> {
// auto rows don't depend on the backlog, as they only span one // auto rows don't depend on the backlog, as they only span one
// region. // region.
if breakable if breakable
&& (matches!(self.grid.header, Some(Repeatable::Repeated(_))) && (!self.repeating_headers.is_empty()
|| !self.pending_headers.is_empty()
|| matches!(self.grid.footer, Some(Repeatable::Repeated(_)))) || matches!(self.grid.footer, Some(Repeatable::Repeated(_))))
{ {
// Subtract header and footer height from all upcoming regions // Subtract header and footer height from all upcoming regions
@ -404,8 +413,20 @@ impl GridLayouter<'_> {
// //
// This will update the 'custom_backlog' vector with the // This will update the 'custom_backlog' vector with the
// updated heights of the upcoming regions. // 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| { 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 // Callees must use the custom backlog instead of the current
@ -459,6 +480,7 @@ impl GridLayouter<'_> {
// Height of the rowspan covered by spanned rows in the current // Height of the rowspan covered by spanned rows in the current
// region. // region.
let laid_out_height: Abs = self let laid_out_height: Abs = self
.current
.lrows .lrows
.iter() .iter()
.filter_map(|row| match row { .filter_map(|row| match row {
@ -506,7 +528,28 @@ impl GridLayouter<'_> {
.iter() .iter()
.copied() .copied()
.chain(std::iter::once(if breakable { .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).
// TODO: maybe cache this
// NOTE: it is safe to access 'lrows' here since
// 'breakable' can only be true outside of headers
// and unbreakable rows in general, so there is no risk
// of accessing an incomplete list of rows.
let initial_header_height = self.current.lrows
[..self.current.repeated_header_rows]
.iter()
.map(|row| match row {
Row::Frame(frame, _, _) => frame.height(),
Row::Fr(_, _, _) => Abs::zero(),
})
.sum();
self.current.initial.y
- initial_header_height
- self.current.footer_height
} else { } else {
// When measuring unbreakable auto rows, infinite // When measuring unbreakable auto rows, infinite
// height is available for content to expand. // height is available for content to expand.
@ -518,11 +561,13 @@ impl GridLayouter<'_> {
// rowspan's already laid out heights with the current // rowspan's already laid out heights with the current
// region's height and current backlog to ensure a good // region's height and current backlog to ensure a good
// level of accuracy in the measurements. // level of accuracy in the measurements.
let backlog = self //
.regions // Assume only repeating headers will survive starting at
.backlog // the next region.
.iter() let backlog = self.regions.backlog.iter().map(|&size| {
.map(|&size| size - self.header_height - self.footer_height); size - self.current.repeating_header_height
- self.current.footer_height
});
heights_up_to_current_region.chain(backlog).collect::<Vec<_>>() heights_up_to_current_region.chain(backlog).collect::<Vec<_>>()
} else { } else {
@ -536,10 +581,10 @@ impl GridLayouter<'_> {
height = *rowspan_height; height = *rowspan_height;
backlog = None; backlog = None;
full = rowspan_full; full = rowspan_full;
last = self last = self.regions.last.map(|size| {
.regions size - self.current.repeating_header_height
.last - self.current.footer_height
.map(|size| size - self.header_height - self.footer_height); });
} else { } else {
// The rowspan started in the current region, as its vector // The rowspan started in the current region, as its vector
// of heights in regions is currently empty. // of heights in regions is currently empty.
@ -741,10 +786,11 @@ impl GridLayouter<'_> {
simulated_regions.next(); simulated_regions.next();
disambiguator += 1; disambiguator += 1;
// Subtract the initial header and footer height, since that's the // Subtract the repeating header and footer height, since that's
// height we used when subtracting from the region backlog's // the height we used when subtracting from the region backlog's
// heights while measuring cells. // 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 { if let Some(original_last_resolved_size) = last_resolved_size {
@ -879,8 +925,12 @@ impl GridLayouter<'_> {
let rowspan_simulator = RowspanSimulator::new( let rowspan_simulator = RowspanSimulator::new(
disambiguator, disambiguator,
simulated_regions, simulated_regions,
self.header_height, // There can be no new headers or footers within a multi-page
self.footer_height, // rowspan, since headers and footers are unbreakable, so
// assuming the repeating header height and footer height
// won't change is safe.
self.current.repeating_header_height,
self.current.footer_height,
); );
let total_spanned_height = rowspan_simulator.simulate_rowspan_layout( let total_spanned_height = rowspan_simulator.simulate_rowspan_layout(
@ -963,7 +1013,8 @@ impl GridLayouter<'_> {
{ {
extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero()); extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero());
simulated_regions.next(); 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; disambiguator += 1;
} }
simulated_regions.size.y -= extra_amount_to_grow; simulated_regions.size.y -= extra_amount_to_grow;
@ -1053,7 +1104,7 @@ impl<'a> RowspanSimulator<'a> {
0, 0,
)?; )?;
while !self.regions.size.y.fits(row_group.height) while !self.regions.size.y.fits(row_group.height)
&& !in_last_with_offset( && may_progress_with_offset(
self.regions, self.regions,
self.header_height + self.footer_height, self.header_height + self.footer_height,
) )
@ -1078,7 +1129,7 @@ impl<'a> RowspanSimulator<'a> {
let mut skipped_region = false; let mut skipped_region = false;
while unbreakable_rows_left == 0 while unbreakable_rows_left == 0
&& !self.regions.size.y.fits(height) && !self.regions.size.y.fits(height)
&& !in_last_with_offset( && may_progress_with_offset(
self.regions, self.regions,
self.header_height + self.footer_height, self.header_height + self.footer_height,
) )
@ -1127,14 +1178,27 @@ impl<'a> RowspanSimulator<'a> {
// our simulation checks what happens AFTER the auto row, so we can // our simulation checks what happens AFTER the auto row, so we can
// just use the original backlog from `self.regions`. // just use the original backlog from `self.regions`.
let disambiguator = self.finished; let disambiguator = self.finished;
let header_height =
if let Some(Repeatable::Repeated(header)) = &layouter.grid.header { let (repeating_headers, header_height) = if !layouter.repeating_headers.is_empty()
layouter || !layouter.pending_headers.is_empty()
.simulate_header(header, &self.regions, engine, disambiguator)? {
.height // Only repeating headers have survived after the first region
} else { // break.
Abs::zero() 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 = let footer_height =
if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer { if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer {
@ -1156,13 +1220,16 @@ impl<'a> RowspanSimulator<'a> {
skipped_region = true; 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 { self.header_height = if skipped_region {
// Simulate headers again, at the new region, as // Simulate headers again, at the new region, as
// the full region height may change. // the full region height may change.
layouter layouter.simulate_header_height(
.simulate_header(header, &self.regions, engine, disambiguator)? repeating_headers,
.height &self.regions,
engine,
disambiguator,
)?
} else { } else {
header_height header_height
}; };

View File

@ -1,4 +1,6 @@
use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError}; use std::num::{
NonZeroI64, NonZeroIsize, NonZeroU32, NonZeroU64, NonZeroUsize, ParseIntError,
};
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use smallvec::SmallVec; use smallvec::SmallVec;
@ -482,3 +484,16 @@ cast! {
"number too large" "number too large"
})?, })?,
} }
cast! {
NonZeroU32,
self => Value::Int(self.get() as _),
v: i64 => v
.try_into()
.and_then(|v: u32| v.try_into())
.map_err(|_| if v <= 0 {
"number must be positive"
} else {
"number too large"
})?,
}

View File

@ -1,6 +1,6 @@
pub mod resolve; pub mod resolve;
use std::num::NonZeroUsize; use std::num::{NonZeroU32, NonZeroUsize};
use std::sync::Arc; use std::sync::Arc;
use comemo::Track; use comemo::Track;
@ -468,6 +468,14 @@ pub struct GridHeader {
#[default(true)] #[default(true)]
pub repeat: bool, pub repeat: bool,
/// The level of the header. Must not be zero.
///
/// This is used during repetition multiple headers at once. When a header
/// with a lower level starts repeating, all headers with a lower level stop
/// repeating.
#[default(NonZeroU32::ONE)]
pub level: NonZeroU32,
/// The cells and lines within the header. /// The cells and lines within the header.
#[variadic] #[variadic]
pub children: Vec<GridItem>, pub children: Vec<GridItem>,

View File

@ -1,5 +1,5 @@
use std::num::NonZeroUsize; use std::num::{NonZeroU32, NonZeroUsize};
use std::ops::Range; use std::ops::{Deref, Range};
use std::sync::Arc; use std::sync::Arc;
use ecow::eco_format; use ecow::eco_format;
@ -48,6 +48,7 @@ pub fn grid_to_cellgrid<'a>(
let children = elem.children.iter().map(|child| match child { let children = elem.children.iter().map(|child| match child {
GridChild::Header(header) => ResolvableGridChild::Header { GridChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles), repeat: header.repeat(styles),
level: header.level(styles),
span: header.span(), span: header.span(),
items: header.children.iter().map(resolve_item), items: header.children.iter().map(resolve_item),
}, },
@ -101,6 +102,7 @@ pub fn table_to_cellgrid<'a>(
let children = elem.children.iter().map(|child| match child { let children = elem.children.iter().map(|child| match child {
TableChild::Header(header) => ResolvableGridChild::Header { TableChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles), repeat: header.repeat(styles),
level: header.level(styles),
span: header.span(), span: header.span(),
items: header.children.iter().map(resolve_item), items: header.children.iter().map(resolve_item),
}, },
@ -426,8 +428,30 @@ pub struct Line {
/// A repeatable grid header. Starts at the first row. /// A repeatable grid header. Starts at the first row.
#[derive(Debug)] #[derive(Debug)]
pub struct Header { pub struct Header {
/// The first row included in this header.
pub start: usize,
/// The index after the last row included in this header. /// The index after the last row included in this header.
pub end: usize, pub end: usize,
/// The header's level.
///
/// Higher level headers repeat together with lower level headers. If a
/// lower level header stops repeating, all higher level headers do as
/// well.
pub level: u32,
/// Whether this header cannot be repeated nor should have orphan
/// prevention because it would be about to cease repetition, either
/// because it is followed by headers of conflicting levels, or because
/// it is at the end of the table (possibly followed by some footers at the
/// end).
pub short_lived: bool,
}
impl Header {
/// The header's range of included rows.
#[inline]
pub fn range(&self) -> Range<usize> {
self.start..self.end
}
} }
/// A repeatable grid footer. Stops at the last row. /// A repeatable grid footer. Stops at the last row.
@ -435,20 +459,46 @@ pub struct Header {
pub struct Footer { pub struct Footer {
/// The first row included in this footer. /// The first row included in this footer.
pub start: usize, pub start: usize,
/// The index after the last row included in this footer.
pub end: usize,
/// The footer's level.
///
/// Used similarly to header level.
pub level: u32,
} }
/// A possibly repeatable grid object. impl Footer {
/// The footer's range of included rows.
#[inline]
pub fn range(&self) -> Range<usize> {
self.start..self.end
}
}
/// A possibly repeatable grid child.
///
/// It still exists even when not repeatable, but must not have additional /// It still exists even when not repeatable, but must not have additional
/// considerations by grid layout, other than for consistency (such as making /// considerations by grid layout, other than for consistency (such as making
/// a certain group of rows unbreakable). /// a certain group of rows unbreakable).
pub enum Repeatable<T> { pub enum Repeatable<T> {
/// The user asked this grid child to repeat.
Repeated(T), Repeated(T),
/// The user asked this grid child to not repeat.
NotRepeated(T), NotRepeated(T),
} }
impl<T> Deref for Repeatable<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.unwrap()
}
}
impl<T> Repeatable<T> { impl<T> Repeatable<T> {
/// Gets the value inside this repeatable, regardless of whether /// Gets the value inside this repeatable, regardless of whether
/// it repeats. /// it repeats.
#[inline]
pub fn unwrap(&self) -> &T { pub fn unwrap(&self) -> &T {
match self { match self {
Self::Repeated(repeated) => repeated, Self::Repeated(repeated) => repeated,
@ -456,7 +506,18 @@ impl<T> Repeatable<T> {
} }
} }
/// Gets the value inside this repeatable, regardless of whether
/// it repeats (mutably).
#[inline]
pub fn unwrap_mut(&mut self) -> &mut T {
match self {
Self::Repeated(repeated) => repeated,
Self::NotRepeated(not_repeated) => not_repeated,
}
}
/// Returns `Some` if the value is repeated, `None` otherwise. /// Returns `Some` if the value is repeated, `None` otherwise.
#[inline]
pub fn as_repeated(&self) -> Option<&T> { pub fn as_repeated(&self) -> Option<&T> {
match self { match self {
Self::Repeated(repeated) => Some(repeated), Self::Repeated(repeated) => Some(repeated),
@ -617,7 +678,7 @@ impl<'a> Entry<'a> {
/// Any grid child, which can be either a header or an item. /// Any grid child, which can be either a header or an item.
pub enum ResolvableGridChild<T: ResolvableCell, I> { pub enum ResolvableGridChild<T: ResolvableCell, I> {
Header { repeat: bool, span: Span, items: I }, Header { repeat: bool, level: NonZeroU32, span: Span, items: I },
Footer { repeat: bool, span: Span, items: I }, Footer { repeat: bool, span: Span, items: I },
Item(ResolvableGridItem<T>), Item(ResolvableGridItem<T>),
} }
@ -638,8 +699,8 @@ pub struct CellGrid<'a> {
/// Gutter rows are not included. /// Gutter rows are not included.
/// Contains up to 'rows_without_gutter.len() + 1' vectors of lines. /// Contains up to 'rows_without_gutter.len() + 1' vectors of lines.
pub hlines: Vec<Vec<Line>>, pub hlines: Vec<Vec<Line>>,
/// The repeatable header of this grid. /// The repeatable headers of this grid.
pub header: Option<Repeatable<Header>>, pub headers: Vec<Repeatable<Header>>,
/// The repeatable footer of this grid. /// The repeatable footer of this grid.
pub footer: Option<Repeatable<Footer>>, pub footer: Option<Repeatable<Footer>>,
/// Whether this grid has gutters. /// Whether this grid has gutters.
@ -654,7 +715,7 @@ impl<'a> CellGrid<'a> {
cells: impl IntoIterator<Item = Cell<'a>>, cells: impl IntoIterator<Item = Cell<'a>>,
) -> Self { ) -> Self {
let entries = cells.into_iter().map(Entry::Cell).collect(); let entries = cells.into_iter().map(Entry::Cell).collect();
Self::new_internal(tracks, gutter, vec![], vec![], None, None, entries) Self::new_internal(tracks, gutter, vec![], vec![], vec![], None, entries)
} }
/// Generates the cell grid, given the tracks and resolved entries. /// Generates the cell grid, given the tracks and resolved entries.
@ -663,7 +724,7 @@ impl<'a> CellGrid<'a> {
gutter: Axes<&[Sizing]>, gutter: Axes<&[Sizing]>,
vlines: Vec<Vec<Line>>, vlines: Vec<Vec<Line>>,
hlines: Vec<Vec<Line>>, hlines: Vec<Vec<Line>>,
header: Option<Repeatable<Header>>, headers: Vec<Repeatable<Header>>,
footer: Option<Repeatable<Footer>>, footer: Option<Repeatable<Footer>>,
entries: Vec<Entry<'a>>, entries: Vec<Entry<'a>>,
) -> Self { ) -> Self {
@ -717,7 +778,7 @@ impl<'a> CellGrid<'a> {
entries, entries,
vlines, vlines,
hlines, hlines,
header, headers,
footer, footer,
has_gutter, has_gutter,
} }
@ -852,6 +913,11 @@ impl<'a> CellGrid<'a> {
self.cols.len() self.cols.len()
} }
} }
#[inline]
pub fn has_repeated_headers(&self) -> bool {
self.headers.iter().any(|h| matches!(h, Repeatable::Repeated(_)))
}
} }
/// Resolves and positions all cells in the grid before creating it. /// Resolves and positions all cells in the grid before creating it.
@ -937,6 +1003,12 @@ struct RowGroupData {
span: Span, span: Span,
kind: RowGroupKind, kind: RowGroupKind,
/// Whether this header or footer may repeat.
repeat: bool,
/// Level of this header or footer.
repeatable_level: NonZeroU32,
/// Start of the range of indices of hlines at the top of the row group. /// Start of the range of indices of hlines at the top of the row group.
/// This is always the first index after the last hline before we started /// This is always the first index after the last hline before we started
/// building the row group - any upcoming hlines would appear at least at /// building the row group - any upcoming hlines would appear at least at
@ -984,14 +1056,17 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
let mut pending_vlines: Vec<(Span, Line)> = vec![]; let mut pending_vlines: Vec<(Span, Line)> = vec![];
let has_gutter = self.gutter.any(|tracks| !tracks.is_empty()); let has_gutter = self.gutter.any(|tracks| !tracks.is_empty());
let mut header: Option<Header> = None; let mut headers: Vec<Repeatable<Header>> = vec![];
let mut repeat_header = false;
// Stores where the footer is supposed to end, its span, and the // Stores where the footer is supposed to end, its span, and the
// actual footer structure. // actual footer structure.
let mut footer: Option<(usize, Span, Footer)> = None; let mut footer: Option<(usize, Span, Footer)> = None;
let mut repeat_footer = false; let mut repeat_footer = false;
// If true, there has been at least one cell besides headers and
// footers. When false, footers at the end are forced to not repeat.
let mut at_least_one_cell = false;
// We can't just use the cell's index in the 'cells' vector to // We can't just use the cell's index in the 'cells' vector to
// determine its automatic position, since cells could have arbitrary // determine its automatic position, since cells could have arbitrary
// positions, so the position of a cell in 'cells' can differ from its // positions, so the position of a cell in 'cells' can differ from its
@ -1008,6 +1083,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// automatically-positioned cell. // automatically-positioned cell.
let mut auto_index: usize = 0; let mut auto_index: usize = 0;
// The next header after the latest auto-positioned cell. This is used
// to avoid checking for collision with headers that were already
// skipped.
let mut next_header = 0;
// We have to rebuild the grid to account for fixed cell positions. // We have to rebuild the grid to account for fixed cell positions.
// //
// Create at least 'children.len()' positions, since there could be at // Create at least 'children.len()' positions, since there could be at
@ -1028,12 +1108,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
columns, columns,
&mut pending_hlines, &mut pending_hlines,
&mut pending_vlines, &mut pending_vlines,
&mut header, &mut headers,
&mut repeat_header,
&mut footer, &mut footer,
&mut repeat_footer, &mut repeat_footer,
&mut auto_index, &mut auto_index,
&mut next_header,
&mut resolved_cells, &mut resolved_cells,
&mut at_least_one_cell,
child, child,
)?; )?;
} }
@ -1049,13 +1130,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
row_amount, row_amount,
)?; )?;
let (header, footer) = self.finalize_headers_and_footers( let footer = self.finalize_headers_and_footers(
has_gutter, has_gutter,
header, &mut headers,
repeat_header,
footer, footer,
repeat_footer, repeat_footer,
row_amount, row_amount,
at_least_one_cell,
)?; )?;
Ok(CellGrid::new_internal( Ok(CellGrid::new_internal(
@ -1063,7 +1144,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
self.gutter, self.gutter,
vlines, vlines,
hlines, hlines,
header, headers,
footer, footer,
resolved_cells, resolved_cells,
)) ))
@ -1083,12 +1164,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
columns: usize, columns: usize,
pending_hlines: &mut Vec<(Span, Line, bool)>, pending_hlines: &mut Vec<(Span, Line, bool)>,
pending_vlines: &mut Vec<(Span, Line)>, pending_vlines: &mut Vec<(Span, Line)>,
header: &mut Option<Header>, headers: &mut Vec<Repeatable<Header>>,
repeat_header: &mut bool,
footer: &mut Option<(usize, Span, Footer)>, footer: &mut Option<(usize, Span, Footer)>,
repeat_footer: &mut bool, repeat_footer: &mut bool,
auto_index: &mut usize, auto_index: &mut usize,
next_header: &mut usize,
resolved_cells: &mut Vec<Option<Entry<'x>>>, resolved_cells: &mut Vec<Option<Entry<'x>>>,
at_least_one_cell: &mut bool,
child: ResolvableGridChild<T, I>, child: ResolvableGridChild<T, I>,
) -> SourceResult<()> ) -> SourceResult<()>
where where
@ -1112,7 +1194,32 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// position than it would usually be if it would be in a non-empty // position than it would usually be if it would be in a non-empty
// row, so we must step a local index inside headers and footers // row, so we must step a local index inside headers and footers
// instead, and use a separate counter outside them. // instead, and use a separate counter outside them.
let mut local_auto_index = *auto_index; let local_auto_index = if matches!(child, ResolvableGridChild::Item(_)) {
auto_index
} else {
// Although 'usize' is Copy, we need to be explicit here that we
// aren't reborrowing the original auto index but rather making a
// mutable copy of it using 'clone'.
&mut (*auto_index).clone()
};
// NOTE: usually, if 'next_header' were to be updated inside a row
// group (indicating a header was skipped by a cell), that would
// indicate a collision between the row group and that header, which
// is an error. However, the exception is for the first auto cell of
// the row group, which may skip headers while searching for a position
// where to begin the row group in the first place.
//
// Therefore, we cannot safely share the counter in the row group with
// the counter used by auto cells outside, as it might update it in a
// valid situation, whereas it must not, since its auto cells use a
// different auto index counter and will have seen different headers,
// so we copy the next header counter while inside a row group.
let local_next_header = if matches!(child, ResolvableGridChild::Item(_)) {
next_header
} else {
&mut (*next_header).clone()
};
// The first row in which this table group can fit. // The first row in which this table group can fit.
// //
@ -1123,23 +1230,19 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
let mut first_available_row = 0; let mut first_available_row = 0;
let (header_footer_items, simple_item) = match child { let (header_footer_items, simple_item) = match child {
ResolvableGridChild::Header { repeat, span, items, .. } => { ResolvableGridChild::Header { repeat, level, span, items, .. } => {
if header.is_some() {
bail!(span, "cannot have more than one header");
}
row_group_data = Some(RowGroupData { row_group_data = Some(RowGroupData {
range: None, range: None,
span, span,
kind: RowGroupKind::Header, kind: RowGroupKind::Header,
repeat,
repeatable_level: level,
top_hlines_start: pending_hlines.len(), top_hlines_start: pending_hlines.len(),
top_hlines_end: None, top_hlines_end: None,
}); });
*repeat_header = repeat;
first_available_row = first_available_row =
find_next_empty_row(resolved_cells, local_auto_index, columns); find_next_empty_row(resolved_cells, *local_auto_index, columns);
// If any cell in the header is automatically positioned, // If any cell in the header is automatically positioned,
// have it skip to the next empty row. This is to avoid // have it skip to the next empty row. This is to avoid
@ -1150,7 +1253,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// latest auto-position cell, since each auto-position cell // latest auto-position cell, since each auto-position cell
// always occupies the first available position after the // always occupies the first available position after the
// previous one. Therefore, this will be >= auto_index. // previous one. Therefore, this will be >= auto_index.
local_auto_index = first_available_row * columns; *local_auto_index = first_available_row * columns;
(Some(items), None) (Some(items), None)
} }
@ -1162,21 +1265,27 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
row_group_data = Some(RowGroupData { row_group_data = Some(RowGroupData {
range: None, range: None,
span, span,
repeat,
kind: RowGroupKind::Footer, kind: RowGroupKind::Footer,
repeatable_level: NonZeroU32::ONE,
top_hlines_start: pending_hlines.len(), top_hlines_start: pending_hlines.len(),
top_hlines_end: None, top_hlines_end: None,
}); });
*repeat_footer = repeat;
first_available_row = first_available_row =
find_next_empty_row(resolved_cells, local_auto_index, columns); find_next_empty_row(resolved_cells, *local_auto_index, columns);
local_auto_index = first_available_row * columns; *local_auto_index = first_available_row * columns;
(Some(items), None) (Some(items), None)
} }
ResolvableGridChild::Item(item) => (None, Some(item)), ResolvableGridChild::Item(item) => {
if matches!(item, ResolvableGridItem::Cell(_)) {
*at_least_one_cell = true;
}
(None, Some(item))
}
}; };
let items = header_footer_items.into_iter().flatten().chain(simple_item); let items = header_footer_items.into_iter().flatten().chain(simple_item);
@ -1191,7 +1300,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// gutter. // gutter.
skip_auto_index_through_fully_merged_rows( skip_auto_index_through_fully_merged_rows(
resolved_cells, resolved_cells,
&mut local_auto_index, local_auto_index,
columns, columns,
); );
@ -1266,7 +1375,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// automatically positioned cell. Same for footers. // automatically positioned cell. Same for footers.
local_auto_index local_auto_index
.checked_sub(1) .checked_sub(1)
.filter(|_| local_auto_index > first_available_row * columns) .filter(|_| *local_auto_index > first_available_row * columns)
.map_or(0, |last_auto_index| last_auto_index % columns + 1) .map_or(0, |last_auto_index| last_auto_index % columns + 1)
}); });
if end.is_some_and(|end| end.get() < start) { if end.is_some_and(|end| end.get() < start) {
@ -1295,10 +1404,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
cell_y, cell_y,
colspan, colspan,
rowspan, rowspan,
header.as_ref(), headers,
footer.as_ref(), footer.as_ref(),
resolved_cells, resolved_cells,
&mut local_auto_index, local_auto_index,
local_next_header,
first_available_row, first_available_row,
columns, columns,
row_group_data.is_some(), row_group_data.is_some(),
@ -1350,7 +1460,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
); );
if top_hlines_end.is_none() if top_hlines_end.is_none()
&& local_auto_index > first_available_row * columns && *local_auto_index > first_available_row * columns
{ {
// Auto index was moved, so upcoming auto-pos hlines should // Auto index was moved, so upcoming auto-pos hlines should
// no longer appear at the top. // no longer appear at the top.
@ -1437,7 +1547,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
None => { None => {
// Empty header/footer: consider the header/footer to be // Empty header/footer: consider the header/footer to be
// at the next empty row after the latest auto index. // at the next empty row after the latest auto index.
local_auto_index = first_available_row * columns; *local_auto_index = first_available_row * columns;
let group_start = first_available_row; let group_start = first_available_row;
let group_end = group_start + 1; let group_end = group_start + 1;
@ -1454,8 +1564,8 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// 'find_next_empty_row' will skip through any existing headers // 'find_next_empty_row' will skip through any existing headers
// and footers without having to loop through them each time. // and footers without having to loop through them each time.
// Cells themselves, unfortunately, still have to. // Cells themselves, unfortunately, still have to.
assert!(resolved_cells[local_auto_index].is_none()); assert!(resolved_cells[*local_auto_index].is_none());
resolved_cells[local_auto_index] = resolved_cells[*local_auto_index] =
Some(Entry::Cell(self.resolve_cell( Some(Entry::Cell(self.resolve_cell(
T::default(), T::default(),
0, 0,
@ -1483,20 +1593,43 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
match row_group.kind { match row_group.kind {
RowGroupKind::Header => { RowGroupKind::Header => {
if group_range.start != 0 { let data = Header {
bail!( start: group_range.start,
row_group.span,
"header must start at the first row";
hint: "remove any rows before the header"
);
}
*header = Some(Header {
// Later on, we have to correct this number in case there // Later on, we have to correct this number in case there
// is gutter. But only once all cells have been analyzed // is gutter. But only once all cells have been analyzed
// and the header has fully expanded in the fixup loop // and the header has fully expanded in the fixup loop
// below. // below.
end: group_range.end, end: group_range.end,
level: row_group.repeatable_level.get(),
// This can only change at a later iteration, if we
// find a conflicting header or footer right away.
short_lived: false,
};
// Mark consecutive headers right before this one as short
// lived if they would have a higher or equal level, as
// then they would immediately stop repeating during
// layout.
let mut consecutive_header_start = data.start;
for conflicting_header in
headers.iter_mut().rev().take_while(move |h| {
let conflicts = h.end == consecutive_header_start
&& h.level >= data.level;
consecutive_header_start = h.start;
conflicts
})
{
conflicting_header.unwrap_mut().short_lived = true;
}
headers.push(if row_group.repeat {
Repeatable::Repeated(data)
} else {
Repeatable::NotRepeated(data)
}); });
} }
@ -1514,15 +1647,14 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// before the footer might not be included as part of // before the footer might not be included as part of
// the footer if it is contained within the header. // the footer if it is contained within the header.
start: group_range.start, start: group_range.start,
end: group_range.end,
level: 1,
}, },
)); ));
*repeat_footer = row_group.repeat;
} }
} }
} else {
// The child was a single cell outside headers or footers.
// Therefore, 'local_auto_index' for this table child was
// simply an alias for 'auto_index', so we update it as needed.
*auto_index = local_auto_index;
} }
Ok(()) Ok(())
@ -1689,47 +1821,44 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
fn finalize_headers_and_footers( fn finalize_headers_and_footers(
&self, &self,
has_gutter: bool, has_gutter: bool,
header: Option<Header>, headers: &mut [Repeatable<Header>],
repeat_header: bool,
footer: Option<(usize, Span, Footer)>, footer: Option<(usize, Span, Footer)>,
repeat_footer: bool, repeat_footer: bool,
row_amount: usize, row_amount: usize,
) -> SourceResult<(Option<Repeatable<Header>>, Option<Repeatable<Footer>>)> { at_least_one_cell: bool,
let header = header ) -> SourceResult<Option<Repeatable<Footer>>> {
.map(|mut header| { // Repeat the gutter below a header (hence why we don't
// Repeat the gutter below a header (hence why we don't // subtract 1 from the gutter case).
// subtract 1 from the gutter case). // Don't do this if there are no rows under the header.
// Don't do this if there are no rows under the header. if has_gutter {
if has_gutter { for header in &mut *headers {
// - 'header.end' is always 'last y + 1'. The header stops let header = header.unwrap_mut();
// before that row.
// - Therefore, '2 * header.end' will be 2 * (last y + 1),
// which is the adjusted index of the row before which the
// header stops, meaning it will still stop right before it
// even with gutter thanks to the multiplication below.
// - This means that it will span all rows up to
// '2 * (last y + 1) - 1 = 2 * last y + 1', which equates
// to the index of the gutter row right below the header,
// which is what we want (that gutter spacing should be
// repeated across pages to maintain uniformity).
header.end *= 2;
// If the header occupies the entire grid, ensure we don't // Index of first y is doubled, as each row before it
// include an extra gutter row when it doesn't exist, since // receives a gutter row below.
// the last row of the header is at the very bottom, header.start *= 2;
// therefore '2 * last y + 1' is not a valid index.
let row_amount = (2 * row_amount).saturating_sub(1); // - 'header.end' is always 'last y + 1'. The header stops
header.end = header.end.min(row_amount); // before that row.
} // - Therefore, '2 * header.end' will be 2 * (last y + 1),
header // which is the adjusted index of the row before which the
}) // header stops, meaning it will still stop right before it
.map(|header| { // even with gutter thanks to the multiplication below.
if repeat_header { // - This means that it will span all rows up to
Repeatable::Repeated(header) // '2 * (last y + 1) - 1 = 2 * last y + 1', which equates
} else { // to the index of the gutter row right below the header,
Repeatable::NotRepeated(header) // which is what we want (that gutter spacing should be
} // repeated across pages to maintain uniformity).
}); header.end *= 2;
// If the header occupies the entire grid, ensure we don't
// include an extra gutter row when it doesn't exist, since
// the last row of the header is at the very bottom,
// therefore '2 * last y + 1' is not a valid index.
let row_amount = (2 * row_amount).saturating_sub(1);
header.end = header.end.min(row_amount);
}
}
let footer = footer let footer = footer
.map(|(footer_end, footer_span, mut footer)| { .map(|(footer_end, footer_span, mut footer)| {
@ -1737,8 +1866,17 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
bail!(footer_span, "footer must end at the last row"); bail!(footer_span, "footer must end at the last row");
} }
let header_end = // TODO(subfooters): will need a global slice of headers and
header.as_ref().map(Repeatable::unwrap).map(|header| header.end); // footers for when we have multiple footers
// Alternatively, never include the gutter in the footer's
// range and manually add it later on layout. This would allow
// laying out the gutter as part of both the header and footer,
// and, if the page only has headers, the gutter row below the
// header is automatically removed (as it becomes the last), so
// only the gutter above the footer is kept, ensuring the same
// gutter row isn't laid out two times in a row. When laying
// out the footer for real, the mechanism can be disabled.
let last_header_end = headers.last().map(|header| header.end);
if has_gutter { if has_gutter {
// Convert the footer's start index to post-gutter coordinates. // Convert the footer's start index to post-gutter coordinates.
@ -1747,23 +1885,58 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// Include the gutter right before the footer, unless there is // Include the gutter right before the footer, unless there is
// none, or the gutter is already included in the header (no // none, or the gutter is already included in the header (no
// rows between the header and the footer). // rows between the header and the footer).
if header_end != Some(footer.start) { if last_header_end != Some(footer.start) {
footer.start = footer.start.saturating_sub(1); footer.start = footer.start.saturating_sub(1);
} }
// Adapt footer end but DO NOT include the gutter below it,
// if it exists. Calculation:
// - Starts as 'last y + 1'.
// - The result will be
// 2 * (last_y + 1) - 1 = 2 * last_y + 1,
// which is the new index of the last footer row plus one,
// meaning we do exclude any gutter below this way.
//
// It also keeps us within the total amount of rows, so we
// don't need to '.min()' later.
footer.end = (2 * footer.end).saturating_sub(1);
} }
Ok(footer) Ok(footer)
}) })
.transpose()? .transpose()?
.map(|footer| { .map(|footer| {
if repeat_footer { // Don't repeat footers when the table only has headers and
// footers.
// TODO(subfooters): Switch this to marking the last N
// consecutive footers as short lived.
if repeat_footer && at_least_one_cell {
Repeatable::Repeated(footer) Repeatable::Repeated(footer)
} else { } else {
Repeatable::NotRepeated(footer) Repeatable::NotRepeated(footer)
} }
}); });
Ok((header, footer)) // Mark consecutive headers right before the end of the table, or the
// final footer, as short lived, given that there are no normal rows
// after them, so repeating them is pointless.
//
// TODO(subfooters): take the last footer if it is at the end and
// backtrack through consecutive footers until the first one in the
// sequence is found. If there is no footer at the end, there are no
// haeders to turn short-lived.
let mut consecutive_header_start =
footer.as_ref().map(|f| f.start).unwrap_or(row_amount);
for header_at_the_end in headers.iter_mut().rev().take_while(move |h| {
let at_the_end = h.end == consecutive_header_start;
consecutive_header_start = h.start;
at_the_end
}) {
header_at_the_end.unwrap_mut().short_lived = true;
}
Ok(footer)
} }
/// Resolves the cell's fields based on grid-wide properties. /// Resolves the cell's fields based on grid-wide properties.
@ -1934,28 +2107,28 @@ fn expand_row_group(
/// Check if a cell's fixed row would conflict with a header or footer. /// Check if a cell's fixed row would conflict with a header or footer.
fn check_for_conflicting_cell_row( fn check_for_conflicting_cell_row(
header: Option<&Header>, headers: &[Repeatable<Header>],
footer: Option<&(usize, Span, Footer)>, footer: Option<&(usize, Span, Footer)>,
cell_y: usize, cell_y: usize,
rowspan: usize, rowspan: usize,
) -> HintedStrResult<()> { ) -> HintedStrResult<()> {
if let Some(header) = header { // NOTE: y + rowspan >, not >=, header.start, to check if the rowspan
// TODO: check start (right now zero, always satisfied) // enters the header. For example, consider a rowspan of 1: if
if cell_y < header.end { // `y + 1 = header.start` holds, that means `y < header.start`, and it
bail!( // only occupies one row (`y`), so the cell is actually not in
"cell would conflict with header spanning the same position"; // conflict.
hint: "try moving the cell or the header" if headers
); .iter()
} .any(|header| cell_y < header.end && cell_y + rowspan > header.start)
{
bail!(
"cell would conflict with header spanning the same position";
hint: "try moving the cell or the header"
);
} }
if let Some((footer_end, _, footer)) = footer { if let Some((_, _, footer)) = footer {
// NOTE: y + rowspan >, not >=, footer.start, to check if the rowspan if cell_y < footer.end && cell_y + rowspan > footer.start {
// enters the footer. For example, consider a rowspan of 1: if
// `y + 1 = footer.start` holds, that means `y < footer.start`, and it
// only occupies one row (`y`), so the cell is actually not in
// conflict.
if cell_y < *footer_end && cell_y + rowspan > footer.start {
bail!( bail!(
"cell would conflict with footer spanning the same position"; "cell would conflict with footer spanning the same position";
hint: "try reducing the cell's rowspan or moving the footer" hint: "try reducing the cell's rowspan or moving the footer"
@ -1981,10 +2154,11 @@ fn resolve_cell_position(
cell_y: Smart<usize>, cell_y: Smart<usize>,
colspan: usize, colspan: usize,
rowspan: usize, rowspan: usize,
header: Option<&Header>, headers: &[Repeatable<Header>],
footer: Option<&(usize, Span, Footer)>, footer: Option<&(usize, Span, Footer)>,
resolved_cells: &[Option<Entry>], resolved_cells: &[Option<Entry>],
auto_index: &mut usize, auto_index: &mut usize,
next_header: &mut usize,
first_available_row: usize, first_available_row: usize,
columns: usize, columns: usize,
in_row_group: bool, in_row_group: bool,
@ -2006,11 +2180,12 @@ fn resolve_cell_position(
// but automatically-positioned cells will avoid conflicts by // but automatically-positioned cells will avoid conflicts by
// simply skipping existing cells, headers and footers. // simply skipping existing cells, headers and footers.
let resolved_index = find_next_available_position::<false>( let resolved_index = find_next_available_position::<false>(
header, headers,
footer, footer,
resolved_cells, resolved_cells,
columns, columns,
*auto_index, *auto_index,
next_header,
)?; )?;
// Ensure the next cell with automatic position will be // Ensure the next cell with automatic position will be
@ -2046,7 +2221,7 @@ fn resolve_cell_position(
// footer (but only if it isn't already in one, otherwise there // footer (but only if it isn't already in one, otherwise there
// will already be a separate check). // will already be a separate check).
if !in_row_group { if !in_row_group {
check_for_conflicting_cell_row(header, footer, cell_y, rowspan)?; check_for_conflicting_cell_row(headers, footer, cell_y, rowspan)?;
} }
cell_index(cell_x, cell_y) cell_index(cell_x, cell_y)
@ -2064,11 +2239,26 @@ fn resolve_cell_position(
// ('None'), in which case we'd create a new row to place this // ('None'), in which case we'd create a new row to place this
// cell in. // cell in.
find_next_available_position::<true>( find_next_available_position::<true>(
header, headers,
footer, footer,
resolved_cells, resolved_cells,
columns, columns,
initial_index, initial_index,
// Make our own copy of the 'next_header' counter, since it
// should only be updated by auto cells. However, we cannot
// start with the same value as we are searching from the
// start, and not from 'auto_index', so auto cells might
// have skipped some headers already which this cell will
// also need to skip.
//
// We could, in theory, keep a separate 'next_header'
// counter for cells with fixed columns. But then we would
// need one for every column, and much like how there isn't
// an index counter for each column either, the potential
// speed gain seems less relevant for a less used feature.
// Still, it is something to consider for the future if
// this turns out to be a bottleneck in important cases.
&mut 0,
) )
} }
} }
@ -2078,7 +2268,7 @@ fn resolve_cell_position(
// footer (but only if it isn't already in one, otherwise there // footer (but only if it isn't already in one, otherwise there
// will already be a separate check). // will already be a separate check).
if !in_row_group { if !in_row_group {
check_for_conflicting_cell_row(header, footer, cell_y, rowspan)?; check_for_conflicting_cell_row(headers, footer, cell_y, rowspan)?;
} }
// Let's find the first column which has that row available. // Let's find the first column which has that row available.
@ -2112,11 +2302,12 @@ fn resolve_cell_position(
/// have cells specified by the user) as well as any headers and footers. /// have cells specified by the user) as well as any headers and footers.
#[inline] #[inline]
fn find_next_available_position<const SKIP_ROWS: bool>( fn find_next_available_position<const SKIP_ROWS: bool>(
header: Option<&Header>, headers: &[Repeatable<Header>],
footer: Option<&(usize, Span, Footer)>, footer: Option<&(usize, Span, Footer)>,
resolved_cells: &[Option<Entry<'_>>], resolved_cells: &[Option<Entry<'_>>],
columns: usize, columns: usize,
initial_index: usize, initial_index: usize,
next_header: &mut usize,
) -> HintedStrResult<usize> { ) -> HintedStrResult<usize> {
let mut resolved_index = initial_index; let mut resolved_index = initial_index;
@ -2139,17 +2330,26 @@ fn find_next_available_position<const SKIP_ROWS: bool>(
// would become impractically large before this overflows. // would become impractically large before this overflows.
resolved_index += 1; resolved_index += 1;
} }
} else if let Some(header) = } else if let Some(header) = headers
header.filter(|header| resolved_index < header.end * columns) .get(*next_header)
.filter(|header| resolved_index >= header.start * columns)
{ {
// Skip header (can't place a cell inside it from outside it). // Skip header (can't place a cell inside it from outside it).
resolved_index = header.end * columns; // No changes needed if we already passed this header (which
// also triggers this branch) - in that case, we only update the
// counter.
if resolved_index < header.end * columns {
resolved_index = header.end * columns;
if SKIP_ROWS { if SKIP_ROWS {
// Ensure the cell's chosen column is kept after the // Ensure the cell's chosen column is kept after the
// header. // header.
resolved_index += initial_index % columns; resolved_index += initial_index % columns;
}
} }
// From now on, only check the headers afterwards.
*next_header += 1;
} else if let Some((footer_end, _, _)) = footer.filter(|(end, _, footer)| { } else if let Some((footer_end, _, _)) = footer.filter(|(end, _, footer)| {
resolved_index >= footer.start * columns && resolved_index < *end * columns resolved_index >= footer.start * columns && resolved_index < *end * columns
}) { }) {

View File

@ -1,4 +1,4 @@
use std::num::NonZeroUsize; use std::num::{NonZeroU32, NonZeroUsize};
use std::sync::Arc; use std::sync::Arc;
use typst_utils::NonZeroExt; use typst_utils::NonZeroExt;
@ -292,16 +292,61 @@ fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
elem(tag::tr, Content::sequence(row)) elem(tag::tr, Content::sequence(row))
}; };
// TODO(subfooters): similarly to headers, take consecutive footers from
// the end for 'tfoot'.
let footer = grid.footer.map(|ft| { let footer = grid.footer.map(|ft| {
let rows = rows.drain(ft.unwrap().start..); let rows = rows.drain(ft.start..);
elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row)))) elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row))))
}); });
let header = grid.header.map(|hd| {
let rows = rows.drain(..hd.unwrap().end);
elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))
});
let mut body = Content::sequence(rows.into_iter().map(|row| tr(tag::td, row))); // Store all consecutive headers at the start in 'thead'. All remaining
// headers are just 'th' rows across the table body.
let mut consecutive_header_end = 0;
let first_mid_table_header = grid
.headers
.iter()
.take_while(|hd| {
let is_consecutive = hd.start == consecutive_header_end;
consecutive_header_end = hd.end;
is_consecutive
})
.count();
let (y_offset, header) = if first_mid_table_header > 0 {
let removed_header_rows =
grid.headers.get(first_mid_table_header - 1).unwrap().end;
let rows = rows.drain(..removed_header_rows);
(
removed_header_rows,
Some(elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))),
)
} else {
(0, None)
};
// TODO: Consider improving accessibility properties of multi-level headers
// inside tables in the future, e.g. indicating which columns they are
// relative to and so on. See also:
// https://www.w3.org/WAI/tutorials/tables/multi-level/
let mut next_header = first_mid_table_header;
let mut body =
Content::sequence(rows.into_iter().enumerate().map(|(relative_y, row)| {
let y = relative_y + y_offset;
if let Some(current_header) =
grid.headers.get(next_header).filter(|h| h.range().contains(&y))
{
if y + 1 == current_header.end {
next_header += 1;
}
tr(tag::th, row)
} else {
tr(tag::td, row)
}
}));
if header.is_some() || footer.is_some() { if header.is_some() || footer.is_some() {
body = elem(tag::tbody, body); body = elem(tag::tbody, body);
} }
@ -492,6 +537,14 @@ pub struct TableHeader {
#[default(true)] #[default(true)]
pub repeat: bool, pub repeat: bool,
/// The level of the header. Must not be zero.
///
/// This is used during repetition multiple headers at once. When a header
/// with a lower level starts repeating, all headers with a lower level stop
/// repeating.
#[default(NonZeroU32::ONE)]
pub level: NonZeroU32,
/// The cells and lines within the header. /// The cells and lines within the header.
#[variadic] #[variadic]
pub children: Vec<TableItem>, pub children: Vec<TableItem>,

View File

@ -71,10 +71,7 @@ impl Span {
/// Create a span that does not point into any file. /// Create a span that does not point into any file.
pub const fn detached() -> Self { pub const fn detached() -> Self {
match NonZeroU64::new(Self::DETACHED) { Self(NonZeroU64::new(Self::DETACHED).unwrap())
Some(v) => Self(v),
None => unreachable!(),
}
} }
/// Create a new span from a file id and a number. /// Create a new span from a file id and a number.
@ -111,11 +108,9 @@ impl Span {
/// Pack a file ID and the low bits into a span. /// Pack a file ID and the low bits into a span.
const fn pack(id: FileId, low: u64) -> Self { const fn pack(id: FileId, low: u64) -> Self {
let bits = ((id.into_raw().get() as u64) << Self::FILE_ID_SHIFT) | low; let bits = ((id.into_raw().get() as u64) << Self::FILE_ID_SHIFT) | low;
match NonZeroU64::new(bits) {
Some(v) => Self(v), // The file ID is non-zero.
// The file ID is non-zero. Self(NonZeroU64::new(bits).unwrap())
None => unreachable!(),
}
} }
/// Whether the span is detached. /// Whether the span is detached.

View File

@ -26,7 +26,7 @@ pub use once_cell;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use std::hash::Hash; use std::hash::Hash;
use std::iter::{Chain, Flatten, Rev}; use std::iter::{Chain, Flatten, Rev};
use std::num::NonZeroUsize; use std::num::{NonZeroU32, NonZeroUsize};
use std::ops::{Add, Deref, Div, Mul, Neg, Sub}; use std::ops::{Add, Deref, Div, Mul, Neg, Sub};
use std::sync::Arc; use std::sync::Arc;
@ -66,10 +66,11 @@ pub trait NonZeroExt {
} }
impl NonZeroExt for NonZeroUsize { impl NonZeroExt for NonZeroUsize {
const ONE: Self = match Self::new(1) { const ONE: Self = Self::new(1).unwrap();
Some(v) => v, }
None => unreachable!(),
}; impl NonZeroExt for NonZeroU32 {
const ONE: Self = Self::new(1).unwrap();
} }
/// Extra methods for [`Arc`]. /// Extra methods for [`Arc`].

View File

@ -95,10 +95,7 @@ impl PicoStr {
} }
}; };
match NonZeroU64::new(value) { Ok(Self(NonZeroU64::new(value).unwrap()))
Some(value) => Ok(Self(value)),
None => unreachable!(),
}
} }
/// Resolve to a decoded string. /// Resolve to a decoded string.

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

View File

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<table>
<thead>
<tr>
<th>First</th>
<th>Header</th>
</tr>
<tr>
<th>Second</th>
<th>Header</th>
</tr>
<tr>
<th>Level 2</th>
<th>Header</th>
</tr>
<tr>
<th>Level 3</th>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr>
<td>Body</td>
<td>Cells</td>
</tr>
<tr>
<td>Yet</td>
<td>More</td>
</tr>
<tr>
<th>Level 2</th>
<th>Header Inside</th>
</tr>
<tr>
<th>Level 3</th>
<th></th>
</tr>
<tr>
<td>Even</td>
<td>More</td>
</tr>
<tr>
<td>Body</td>
<td>Cells</td>
</tr>
<tr>
<th>One Last Header</th>
<th>For Good Measure</th>
</tr>
</tbody>
<tfoot>
<tr>
<td>Footer</td>
<td>Row</td>
</tr>
<tr>
<td>Ending</td>
<td>Table</td>
</tr>
</tfoot>
</table>
</body>
</html>

View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<table>
<thead>
<tr>
<th>First</th>
<th>Header</th>
</tr>
<tr>
<th>Second</th>
<th>Header</th>
</tr>
<tr>
<th>Level 2</th>
<th>Header</th>
</tr>
<tr>
<th>Level 3</th>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr>
<td>Body</td>
<td>Cells</td>
</tr>
<tr>
<td>Yet</td>
<td>More</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>Footer</td>
<td>Row</td>
</tr>
<tr>
<td>Ending</td>
<td>Table</td>
</tr>
</tfoot>
</table>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

View File

@ -118,30 +118,46 @@
) )
--- grid-header-not-at-first-row --- --- grid-header-not-at-first-row ---
// Error: 3:3-3:19 header must start at the first row
// Hint: 3:3-3:19 remove any rows before the header
#grid( #grid(
[a], [a],
grid.header([b]) grid.header([b])
) )
--- grid-header-not-at-first-row-two-columns --- --- grid-header-not-at-first-row-two-columns ---
// Error: 4:3-4:19 header must start at the first row
// Hint: 4:3-4:19 remove any rows before the header
#grid( #grid(
columns: 2, columns: 2,
[a], [a],
grid.header([b]) grid.header([b])
) )
--- grow-header-multiple --- --- grid-header-multiple ---
// Error: 3:3-3:19 cannot have more than one header
#grid( #grid(
grid.header([a]), grid.header([a]),
grid.header([b]), grid.header([b]),
[a], [a],
) )
--- grid-header-skip ---
#grid(
columns: 2,
[x], [y],
grid.header([a]),
grid.header([b]),
grid.cell(x: 1)[c], [d],
grid.header([e]),
[f], grid.cell(x: 1)[g]
)
--- grid-header-too-large-non-repeating-orphan ---
#set page(height: 8em)
#grid(
grid.header(
[a\ ] * 5,
repeat: false,
),
[b]
)
--- table-header-in-grid --- --- table-header-in-grid ---
// Error: 2:3-2:20 cannot use `table.header` as a grid header // Error: 2:3-2:20 cannot use `table.header` as a grid header
// Hint: 2:3-2:20 use `grid.header` instead // Hint: 2:3-2:20 use `grid.header` instead
@ -255,6 +271,17 @@
..([Test], [Test], [Test]) * 20 ..([Test], [Test], [Test]) * 20
) )
--- grid-header-non-repeating-orphan-prevention ---
#set page(height: 5em)
#v(2em)
#grid(
grid.header(repeat: false)[*Abc*],
[a],
[b],
[c],
[d]
)
--- grid-header-empty --- --- grid-header-empty ---
// Empty header should just be a repeated blank row // Empty header should just be a repeated blank row
#set page(height: 12em) #set page(height: 12em)
@ -339,6 +366,56 @@
[a\ b] [a\ b]
) )
--- grid-header-not-at-the-top ---
#set page(height: 5em)
#v(2em)
#grid(
[a],
[b],
grid.header[*Abc*],
[d],
[e],
[f],
)
--- grid-header-replace ---
#set page(height: 5em)
#v(1.5em)
#grid(
grid.header[*Abc*],
[a],
[b],
grid.header[*Def*],
[d],
[e],
[f],
)
--- grid-header-replace-orphan ---
#set page(height: 5em)
#grid(
grid.header[*Abc*],
[a],
[b],
grid.header[*Def*],
[d],
[e],
[f],
)
--- grid-header-replace-doesnt-fit ---
#set page(height: 5em)
#v(0.8em)
#grid(
grid.header[*Abc*],
[a],
[b],
grid.header[*Def*],
[d],
[e],
[f],
)
--- grid-header-stroke-edge-cases --- --- grid-header-stroke-edge-cases ---
// Test header stroke priority edge case (last header row removed) // Test header stroke priority edge case (last header row removed)
#set page(height: 8em) #set page(height: 8em)
@ -463,8 +540,6 @@
#table( #table(
columns: 3, columns: 3,
[Outside], [Outside],
// Error: 1:3-4:4 header must start at the first row
// Hint: 1:3-4:4 remove any rows before the header
table.header( table.header(
[A], table.cell(x: 1)[B], [C], [A], table.cell(x: 1)[B], [C],
table.cell(x: 1)[D], table.cell(x: 1)[D],

View File

@ -57,3 +57,78 @@
[d], [e], [f], [d], [e], [f],
[g], [h], [i] [g], [h], [i]
) )
--- multi-header-table html ---
#table(
columns: 2,
table.header(
[First], [Header]
),
table.header(
[Second], [Header]
),
table.header(
[Level 2], [Header],
level: 2,
),
table.header(
[Level 3], [Header],
level: 3,
),
[Body], [Cells],
[Yet], [More],
table.footer(
[Footer], [Row],
[Ending], [Table],
),
)
--- multi-header-inside-table html ---
#table(
columns: 2,
table.header(
[First], [Header]
),
table.header(
[Second], [Header]
),
table.header(
[Level 2], [Header],
level: 2,
),
table.header(
[Level 3], [Header],
level: 3,
),
[Body], [Cells],
[Yet], [More],
table.header(
[Level 2], [Header Inside],
level: 2,
),
table.header(
[Level 3],
level: 3,
),
[Even], [More],
[Body], [Cells],
table.header(
[One Last Header],
[For Good Measure],
repeat: false,
level: 4,
),
table.footer(
[Footer], [Row],
[Ending], [Table],
),
)

View File

@ -0,0 +1,871 @@
--- grid-subheaders-demo ---
#set page(height: 15.2em)
#table(
columns: 2,
align: center,
table.header(
table.cell(colspan: 2)[*Regional User Data*],
),
table.header(
level: 2,
table.cell(colspan: 2)[*Germany*],
[*Username*], [*Joined*]
),
[john123], [2024],
[rob8], [2025],
[joe1], [2025],
[joe2], [2025],
[martha], [2025],
[pear], [2025],
table.header(
level: 2,
table.cell(colspan: 2)[*United States*],
[*Username*], [*Joined*]
),
[cool4], [2023],
[roger], [2023],
[bigfan55], [2022]
)
--- grid-subheaders-colorful ---
#set page(width: auto, height: 12em)
#let rows(n) = {
range(n).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
}
#table(
columns: 5,
align: center + horizon,
table.header(
table.cell(colspan: 5)[*Cool Zone*],
),
table.header(
level: 2,
table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
table.hline(start: 2, end: 3, stroke: yellow)
),
..rows(2),
table.header(
level: 2,
table.cell(stroke: red)[*New Name*], table.cell(stroke: aqua, colspan: 4)[*Other Data*],
table.hline(start: 2, end: 3, stroke: yellow)
),
..rows(3)
)
--- grid-subheaders-basic ---
#grid(
grid.header(
[a]
),
grid.header(
level: 2,
[b]
),
[c]
)
--- grid-subheaders-basic-non-consecutive ---
#grid(
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
[y],
)
--- grid-subheaders-basic-replace ---
#grid(
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
[y],
grid.header(
level: 2,
[c]
),
[z],
)
--- grid-subheaders-basic-with-footer ---
#grid(
grid.header(
[a]
),
grid.header(
level: 2,
[b]
),
[c],
grid.footer(
[d]
)
)
--- grid-subheaders-basic-non-consecutive-with-footer ---
#grid(
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
[y],
grid.footer(
[f]
)
)
--- grid-subheaders-repeat ---
#set page(height: 8em)
#grid(
grid.header(
[a]
),
grid.header(
level: 2,
[b]
),
..([c],) * 10,
)
--- grid-subheaders-repeat-non-consecutive ---
#set page(height: 8em)
#grid(
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
..([y],) * 10,
)
--- grid-subheaders-repeat-with-footer ---
#set page(height: 8em)
#grid(
grid.header(
[a]
),
[m],
grid.header(
level: 2,
[b]
),
..([c],) * 10,
grid.footer(
[f]
)
)
--- grid-subheaders-repeat-gutter ---
// Gutter below the header is also repeated
#set page(height: 8em)
#grid(
inset: (bottom: 0.5pt),
stroke: (bottom: 1pt),
gutter: (1pt, 6pt, 1pt),
grid.header(
[a]
),
grid.header(
level: 2,
[b]
),
..([c],) * 10,
)
--- grid-subheaders-repeat-replace ---
#set page(height: 8em)
#grid(
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
..([y],) * 10,
grid.header(
level: 2,
[c]
),
..([z],) * 10,
)
--- grid-subheaders-repeat-replace-multiple-levels ---
#set page(height: 8em)
#grid(
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
grid.header(
level: 3,
[c]
),
..([y],) * 10,
grid.header(
level: 2,
[d]
),
..([z],) * 6,
)
--- grid-subheaders-repeat-replace-orphan ---
#set page(height: 8em)
#grid(
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
..([y],) * 12,
grid.header(
level: 2,
[c]
),
..([z],) * 10,
)
--- grid-subheaders-repeat-replace-double-orphan ---
#set page(height: 8em)
#grid(
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
..([y],) * 11,
grid.header(
level: 2,
[c]
),
grid.header(
level: 3,
[d]
),
..([z],) * 10,
)
--- grid-subheaders-repeat-replace-didnt-fit-once ---
#set page(height: 8em)
#grid(
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
..([y],) * 10,
grid.header(
level: 2,
[c\ c\ c]
),
..([z],) * 4,
)
--- grid-subheaders-repeat-replace-with-footer ---
#set page(height: 8em)
#grid(
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
grid.header(
level: 3,
[c]
),
..([y],) * 10,
grid.header(
level: 2,
[d]
),
..([z],) * 6,
grid.footer(
[f]
)
)
--- grid-subheaders-repeat-replace-with-footer-orphan ---
#set page(height: 8em)
#grid(
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
..([y],) * 10,
grid.header(
level: 2,
[c]
),
..([z],) * 10,
grid.footer(
[f]
)
)
--- grid-subheaders-repeat-replace-short-lived ---
// No orphan prevention for short-lived headers
// (followed by replacing headers).
#set page(height: 8em)
#grid(
grid.header(
[a]
),
grid.header(
level: 2,
[b]
),
grid.header(
level: 2,
[c]
),
grid.header(
level: 2,
[d]
),
grid.header(
level: 2,
[e]
),
grid.header(
level: 2,
[f]
),
grid.header(
level: 2,
[g]
),
grid.header(
level: 2,
[h]
),
grid.header(
level: 2,
[i]
),
grid.header(
level: 2,
[j]
),
grid.header(
level: 3,
[k]
),
..([z],) * 10,
)
--- grid-subheaders-multi-page-row ---
#set page(height: 8em)
#grid(
columns: 2,
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
[y],
grid.header(
level: 3,
[c]
),
[a], [b],
grid.cell(
block(fill: red, width: 1.5em, height: 6.4em)
),
[y],
..([z],) * 10,
)
--- grid-subheaders-non-repeat ---
#set page(height: 8em)
#grid(
grid.header(
[a],
repeat: false,
),
[x],
grid.header(
level: 2,
repeat: false,
[b]
),
..([y],) * 10,
)
--- grid-subheaders-non-repeat-replace ---
#set page(height: 8em)
#grid(
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
grid.header(
level: 3,
[c]
),
..([y],) * 9,
grid.header(
level: 2,
[d],
repeat: false,
),
..([z],) * 6,
)
--- grid-subheaders-non-repeating-replace-orphan ---
#set page(height: 8em)
#grid(
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
..([y],) * 12,
grid.header(
level: 2,
repeat: false,
[c]
),
..([z],) * 10,
)
--- grid-subheaders-non-repeating-replace-didnt-fit-once ---
#set page(height: 8em)
#grid(
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
..([y],) * 10,
grid.header(
level: 2,
repeat: false,
[c\ c\ c]
),
..([z],) * 4,
)
--- grid-subheaders-multi-page-rowspan ---
#set page(height: 8em)
#grid(
columns: 2,
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
[y],
grid.header(
level: 3,
[c]
),
[z], [z],
grid.cell(
rowspan: 5,
block(fill: red, width: 1.5em, height: 6.4em)
),
[cell],
[cell]
)
--- grid-subheaders-multi-page-row-right-after ---
#set page(height: 8em)
#grid(
columns: 1,
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
grid.header(
level: 3,
[c]
),
grid.cell(
block(fill: red, width: 1.5em, height: 6.4em)
),
[done.],
[done.]
)
--- grid-subheaders-multi-page-rowspan-right-after ---
#set page(height: 8em)
#grid(
columns: 2,
grid.header(
[a]
),
[x], [y],
grid.header(
level: 2,
[b]
),
grid.header(
level: 3,
[c]
),
grid.cell(
rowspan: 5,
block(fill: red, width: 1.5em, height: 6.4em)
),
[cell],
[cell],
grid.cell(x: 0)[done.],
grid.cell(x: 0)[done.]
)
--- grid-subheaders-multi-page-row-with-footer ---
#set page(height: 8em)
#grid(
columns: 2,
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
[y],
grid.header(
level: 3,
[c]
),
[a], [b],
grid.cell(
block(fill: red, width: 1.5em, height: 6.4em)
),
[y],
..([z],) * 10,
grid.footer(
[f]
)
)
--- grid-subheaders-multi-page-rowspan-with-footer ---
#set page(height: 8em)
#grid(
columns: 2,
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
[y],
grid.header(
level: 3,
[c]
),
[z], [z],
grid.cell(
rowspan: 5,
block(fill: red, width: 1.5em, height: 6.4em)
),
[cell],
[cell],
grid.footer(
[f]
)
)
--- grid-subheaders-multi-page-row-right-after-with-footer ---
#set page(height: 8em)
#grid(
columns: 1,
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
grid.header(
level: 3,
[c]
),
grid.cell(
block(fill: red, width: 1.5em, height: 6.4em)
),
[done.],
[done.],
grid.footer(
[f]
)
)
--- grid-subheaders-multi-page-rowspan-gutter ---
#set page(height: 9em)
#grid(
columns: 2,
column-gutter: 4pt,
row-gutter: (0pt, 4pt, 8pt, 4pt),
inset: (bottom: 0.5pt),
stroke: (bottom: 1pt),
grid.header(
[a]
),
[x],
grid.header(
level: 2,
[b]
),
[y],
grid.header(
level: 3,
[c]
),
[z], [z],
grid.cell(
rowspan: 5,
block(fill: red, width: 1.5em, height: 6.4em)
),
[cell],
[cell],
[a\ b],
grid.cell(x: 0)[end],
)
--- grid-subheaders-non-repeating-header-before-multi-page-row ---
#set page(height: 6em)
#grid(
grid.header(
repeat: false,
[h]
),
[row #colbreak() row]
)
--- grid-subheaders-short-lived-no-orphan-prevention ---
// No orphan prevention for short-lived headers.
#set page(height: 8em)
#v(5em)
#grid(
grid.header(
level: 2,
[b]
),
grid.header(
level: 2,
[c]
),
[d]
)
--- grid-subheaders-repeating-orphan-prevention ---
#set page(height: 8em)
#v(4.5em)
#grid(
grid.header(
repeat: true,
level: 2,
[L2]
),
grid.header(
repeat: true,
level: 4,
[L4]
),
[a]
)
--- grid-subheaders-non-repeating-orphan-prevention ---
#set page(height: 8em)
#v(4.5em)
#grid(
grid.header(
repeat: false,
level: 2,
[L2]
),
grid.header(
repeat: false,
level: 4,
[L4]
),
[a]
)
--- grid-subheaders-alone ---
#table(
table.header(
[a]
),
table.header(
level: 2,
[b]
),
)
--- grid-subheaders-alone-no-orphan-prevention ---
#set page(height: 5.3em)
#v(2em)
#grid(
grid.header(
// (
[L1]
),
grid.header(
// (
level: 2,
[L2]
),
)
--- grid-subheaders-alone-with-footer ---
#table(
table.header(
[a]
),
table.header(
level: 2,
[b]
),
table.footer(
[c],
)
)
--- grid-subheaders-alone-with-footer-no-orphan-prevention ---
#set page(height: 5.3em)
#table(
table.header(
[L1]
),
table.header(
level: 2,
[L2]
),
table.footer(
[a],
)
)
--- grid-subheaders-alone-with-gutter-and-footer-no-orphan-prevention ---
#set page(height: 5.5em)
#table(
gutter: 4pt,
table.header(
[L1]
),
table.header(
level: 2,
[L2]
),
table.footer(
[a],
)
)
--- grid-subheaders-too-large-non-repeating-orphan-before-auto ---
#set page(height: 8em)
#grid(
grid.header([1]),
grid.header(
[a\ ] * 2,
level: 2,
repeat: false,
),
grid.header([2], level: 3),
[b\ b\ b],
)
--- grid-subheaders-too-large-repeating-orphan-before-auto ---
#set page(height: 8em)
#grid(
grid.header([1]),
grid.header(
[a\ ] * 2,
level: 2,
repeat: true,
),
grid.header([2], level: 3),
rect(width: 10pt, height: 3em, fill: red),
)
--- grid-subheaders-too-large-repeating-orphan-before-relative ---
#set page(height: 8em)
#grid(
rows: (auto, auto, auto, 3em),
grid.header([1]),
grid.header(
[a\ ] * 2,
level: 2,
repeat: true,
),
grid.header([2], level: 3),
rect(width: 10pt, height: 3em, fill: red),
)
--- grid-subheaders-too-large-non-repeating-orphan-before-relative ---
#set page(height: 8em)
#grid(
rows: (auto, auto, auto, 3em),
grid.header([1]),
grid.header(
[a\ ] * 2,
level: 2,
repeat: false,
),
grid.header([2], level: 3),
rect(width: 10pt, height: 3em, fill: red),
)