Merge 6d4e71085d7391610afcfa61c9a5413a716fe478 into 9b09146a6b5e936966ed7ee73bce9dd2df3810ae
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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"
|
||||||
|
})?,
|
||||||
|
}
|
||||||
|
@ -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>,
|
||||||
|
@ -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
|
||||||
}) {
|
}) {
|
||||||
|
@ -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>,
|
||||||
|
@ -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.
|
||||||
|
@ -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`].
|
||||||
|
@ -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.
|
||||||
|
BIN
tests/ref/grid-header-multiple.png
Normal file
After Width: | Height: | Size: 214 B |
BIN
tests/ref/grid-header-non-repeating-orphan-prevention.png
Normal file
After Width: | Height: | Size: 453 B |
BIN
tests/ref/grid-header-not-at-first-row-two-columns.png
Normal file
After Width: | Height: | Size: 176 B |
BIN
tests/ref/grid-header-not-at-first-row.png
Normal file
After Width: | Height: | Size: 176 B |
BIN
tests/ref/grid-header-not-at-the-top.png
Normal file
After Width: | Height: | Size: 605 B |
BIN
tests/ref/grid-header-replace-doesnt-fit.png
Normal file
After Width: | Height: | Size: 559 B |
BIN
tests/ref/grid-header-replace-orphan.png
Normal file
After Width: | Height: | Size: 559 B |
BIN
tests/ref/grid-header-replace.png
Normal file
After Width: | Height: | Size: 692 B |
BIN
tests/ref/grid-header-skip.png
Normal file
After Width: | Height: | Size: 432 B |
BIN
tests/ref/grid-header-too-large-non-repeating-orphan.png
Normal file
After Width: | Height: | Size: 372 B |
BIN
tests/ref/grid-subheaders-alone-no-orphan-prevention.png
Normal file
After Width: | Height: | Size: 254 B |
After Width: | Height: | Size: 378 B |
BIN
tests/ref/grid-subheaders-alone-with-footer.png
Normal file
After Width: | Height: | Size: 319 B |
After Width: | Height: | Size: 382 B |
BIN
tests/ref/grid-subheaders-alone.png
Normal file
After Width: | Height: | Size: 256 B |
BIN
tests/ref/grid-subheaders-basic-non-consecutive-with-footer.png
Normal file
After Width: | Height: | Size: 279 B |
BIN
tests/ref/grid-subheaders-basic-non-consecutive.png
Normal file
After Width: | Height: | Size: 256 B |
BIN
tests/ref/grid-subheaders-basic-replace.png
Normal file
After Width: | Height: | Size: 321 B |
BIN
tests/ref/grid-subheaders-basic-with-footer.png
Normal file
After Width: | Height: | Size: 256 B |
BIN
tests/ref/grid-subheaders-basic.png
Normal file
After Width: | Height: | Size: 210 B |
BIN
tests/ref/grid-subheaders-colorful.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
tests/ref/grid-subheaders-demo.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 1.2 KiB |
BIN
tests/ref/grid-subheaders-multi-page-row-right-after.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
tests/ref/grid-subheaders-multi-page-row-with-footer.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
tests/ref/grid-subheaders-multi-page-row.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
tests/ref/grid-subheaders-multi-page-rowspan-gutter.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
tests/ref/grid-subheaders-multi-page-rowspan-right-after.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
tests/ref/grid-subheaders-multi-page-rowspan-with-footer.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
tests/ref/grid-subheaders-multi-page-rowspan.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
tests/ref/grid-subheaders-non-repeat-replace.png
Normal file
After Width: | Height: | Size: 878 B |
BIN
tests/ref/grid-subheaders-non-repeat.png
Normal file
After Width: | Height: | Size: 614 B |
After Width: | Height: | Size: 410 B |
BIN
tests/ref/grid-subheaders-non-repeating-orphan-prevention.png
Normal file
After Width: | Height: | Size: 347 B |
After Width: | Height: | Size: 895 B |
BIN
tests/ref/grid-subheaders-non-repeating-replace-orphan.png
Normal file
After Width: | Height: | Size: 964 B |
BIN
tests/ref/grid-subheaders-repeat-gutter.png
Normal file
After Width: | Height: | Size: 503 B |
BIN
tests/ref/grid-subheaders-repeat-non-consecutive.png
Normal file
After Width: | Height: | Size: 599 B |
BIN
tests/ref/grid-subheaders-repeat-replace-didnt-fit-once.png
Normal file
After Width: | Height: | Size: 877 B |
BIN
tests/ref/grid-subheaders-repeat-replace-double-orphan.png
Normal file
After Width: | Height: | Size: 950 B |
BIN
tests/ref/grid-subheaders-repeat-replace-multiple-levels.png
Normal file
After Width: | Height: | Size: 877 B |
BIN
tests/ref/grid-subheaders-repeat-replace-orphan.png
Normal file
After Width: | Height: | Size: 939 B |
BIN
tests/ref/grid-subheaders-repeat-replace-short-lived.png
Normal file
After Width: | Height: | Size: 795 B |
BIN
tests/ref/grid-subheaders-repeat-replace-with-footer-orphan.png
Normal file
After Width: | Height: | Size: 961 B |
BIN
tests/ref/grid-subheaders-repeat-replace-with-footer.png
Normal file
After Width: | Height: | Size: 992 B |
BIN
tests/ref/grid-subheaders-repeat-replace.png
Normal file
After Width: | Height: | Size: 953 B |
BIN
tests/ref/grid-subheaders-repeat-with-footer.png
Normal file
After Width: | Height: | Size: 584 B |
BIN
tests/ref/grid-subheaders-repeat.png
Normal file
After Width: | Height: | Size: 472 B |
BIN
tests/ref/grid-subheaders-repeating-orphan-prevention.png
Normal file
After Width: | Height: | Size: 347 B |
BIN
tests/ref/grid-subheaders-short-lived-no-orphan-prevention.png
Normal file
After Width: | Height: | Size: 287 B |
After Width: | Height: | Size: 460 B |
After Width: | Height: | Size: 542 B |
After Width: | Height: | Size: 525 B |
After Width: | Height: | Size: 437 B |
69
tests/ref/html/multi-header-inside-table.html
Normal 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>
|
49
tests/ref/html/multi-header-table.html
Normal 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>
|
BIN
tests/ref/issue-5359-column-override-stays-inside-header.png
Normal file
After Width: | Height: | Size: 674 B |
@ -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],
|
||||||
|
@ -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],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
871
tests/suite/layout/grid/subheaders.typ
Normal 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),
|
||||||
|
)
|