Merge 6d4e71085d7391610afcfa61c9a5413a716fe478 into 9b09146a6b5e936966ed7ee73bce9dd2df3810ae
@ -3,7 +3,9 @@ use std::fmt::Debug;
|
||||
use typst_library::diag::{bail, SourceResult};
|
||||
use typst_library::engine::Engine;
|
||||
use typst_library::foundations::{Resolve, StyleChain};
|
||||
use typst_library::layout::grid::resolve::{Cell, CellGrid, LinePosition, Repeatable};
|
||||
use typst_library::layout::grid::resolve::{
|
||||
Cell, CellGrid, Header, LinePosition, Repeatable,
|
||||
};
|
||||
use typst_library::layout::{
|
||||
Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel,
|
||||
Size, Sizing,
|
||||
@ -30,10 +32,8 @@ pub struct GridLayouter<'a> {
|
||||
pub(super) rcols: Vec<Abs>,
|
||||
/// The sum of `rcols`.
|
||||
pub(super) width: Abs,
|
||||
/// Resolve row sizes, by region.
|
||||
/// Resolved row sizes, by region.
|
||||
pub(super) rrows: Vec<Vec<RowPiece>>,
|
||||
/// Rows in the current region.
|
||||
pub(super) lrows: Vec<Row>,
|
||||
/// The amount of unbreakable rows remaining to be laid out in the
|
||||
/// current unbreakable row group. While this is positive, no region breaks
|
||||
/// should occur.
|
||||
@ -41,24 +41,145 @@ pub struct GridLayouter<'a> {
|
||||
/// Rowspans not yet laid out because not all of their spanned rows were
|
||||
/// laid out yet.
|
||||
pub(super) rowspans: Vec<Rowspan>,
|
||||
/// The initial size of the current region before we started subtracting.
|
||||
pub(super) initial: Size,
|
||||
/// Grid layout state for the current region.
|
||||
pub(super) current: Current,
|
||||
/// Frames for finished regions.
|
||||
pub(super) finished: Vec<Frame>,
|
||||
/// The amount and height of header rows on each finished region.
|
||||
pub(super) finished_header_rows: Vec<FinishedHeaderRowInfo>,
|
||||
/// Whether this is an RTL grid.
|
||||
pub(super) is_rtl: bool,
|
||||
/// The simulated header height.
|
||||
/// This field is reset in `layout_header` and properly updated by
|
||||
/// Currently repeating headers, one per level. Sorted by increasing
|
||||
/// levels.
|
||||
///
|
||||
/// Note that some levels may be absent, in particular level 0, which does
|
||||
/// not exist (so all levels are >= 1).
|
||||
pub(super) repeating_headers: Vec<&'a Header>,
|
||||
/// Headers, repeating or not, awaiting their first successful layout.
|
||||
/// Sorted by increasing levels.
|
||||
pub(super) pending_headers: &'a [Repeatable<Header>],
|
||||
/// Next headers to be processed.
|
||||
pub(super) upcoming_headers: &'a [Repeatable<Header>],
|
||||
/// State of the row being currently laid out.
|
||||
///
|
||||
/// This is kept as a field to avoid passing down too many parameters from
|
||||
/// `layout_row` into called functions, which would then have to pass them
|
||||
/// down to `push_row`, which reads these values.
|
||||
pub(super) row_state: RowState,
|
||||
/// The span of the grid element.
|
||||
pub(super) span: Span,
|
||||
}
|
||||
|
||||
/// Grid layout state for the current region. This should be reset or updated
|
||||
/// on each region break.
|
||||
pub(super) struct Current {
|
||||
/// The initial size of the current region before we started subtracting.
|
||||
pub(super) initial: Size,
|
||||
/// 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
|
||||
/// before all header rows are fully laid out. It is usually fine because
|
||||
/// header rows themselves are unbreakable, and unbreakable rows do not
|
||||
/// need to read this field at all.
|
||||
///
|
||||
/// 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,
|
||||
/// 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 simulation occurs before any rows are laid out for a region.
|
||||
pub(super) footer_height: Abs,
|
||||
/// The span of the grid element.
|
||||
pub(super) span: Span,
|
||||
}
|
||||
|
||||
/// Data about the row being laid out right now.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct RowState {
|
||||
/// If this is `Some`, this will be updated by the currently laid out row's
|
||||
/// height if it is auto or relative. This is used for header height
|
||||
/// calculation.
|
||||
pub(super) current_row_height: Option<Abs>,
|
||||
/// This is `true` when laying out non-short lived headers and footers.
|
||||
/// That is, headers and footers which are not immediately followed or
|
||||
/// preceded (respectively) by conflicting headers and footers of same or
|
||||
/// lower level, or the end or start of the table (respectively), which
|
||||
/// would cause them to never repeat, even once.
|
||||
///
|
||||
/// If this is `false`, the next row to be laid out will remove an active
|
||||
/// orphan snapshot and will flush pending headers, as there is no risk
|
||||
/// that they will be orphans anymore.
|
||||
pub(super) in_active_repeatable: bool,
|
||||
}
|
||||
|
||||
/// Data about laid out repeated header rows for a specific finished region.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct FinishedHeaderRowInfo {
|
||||
/// The amount of repeated headers at the top of the region.
|
||||
pub(super) repeated_amount: usize,
|
||||
/// The end bound of the row range of the last repeated header at the top
|
||||
/// of the region.
|
||||
pub(super) last_repeated_header_end: usize,
|
||||
/// The total height of repeated headers at the top of the region.
|
||||
pub(super) repeated_height: Abs,
|
||||
}
|
||||
|
||||
/// Details about a resulting row piece.
|
||||
@ -114,14 +235,26 @@ impl<'a> GridLayouter<'a> {
|
||||
rcols: vec![Abs::zero(); grid.cols.len()],
|
||||
width: Abs::zero(),
|
||||
rrows: vec![],
|
||||
lrows: vec![],
|
||||
unbreakable_rows_left: 0,
|
||||
rowspans: vec![],
|
||||
initial: regions.size,
|
||||
finished: vec![],
|
||||
finished_header_rows: vec![],
|
||||
is_rtl: TextElem::dir_in(styles) == Dir::RTL,
|
||||
header_height: Abs::zero(),
|
||||
footer_height: Abs::zero(),
|
||||
repeating_headers: vec![],
|
||||
upcoming_headers: &grid.headers,
|
||||
pending_headers: Default::default(),
|
||||
row_state: RowState::default(),
|
||||
current: Current {
|
||||
initial: regions.size,
|
||||
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,
|
||||
}
|
||||
}
|
||||
@ -134,19 +267,18 @@ impl<'a> GridLayouter<'a> {
|
||||
// Ensure rows in the first region will be aware of the possible
|
||||
// presence of the footer.
|
||||
self.prepare_footer(footer, engine, 0)?;
|
||||
if matches!(self.grid.header, None | Some(Repeatable::NotRepeated(_))) {
|
||||
// No repeatable header, so we won't subtract it later.
|
||||
self.regions.size.y -= self.footer_height;
|
||||
}
|
||||
self.regions.size.y -= self.current.footer_height;
|
||||
}
|
||||
|
||||
for y in 0..self.grid.rows.len() {
|
||||
if let Some(Repeatable::Repeated(header)) = &self.grid.header {
|
||||
if y < header.end {
|
||||
if y == 0 {
|
||||
self.layout_header(header, engine, 0)?;
|
||||
self.regions.size.y -= self.footer_height;
|
||||
}
|
||||
let mut y = 0;
|
||||
let mut consecutive_header_count = 0;
|
||||
while y < self.grid.rows.len() {
|
||||
if let Some(next_header) = self.upcoming_headers.get(consecutive_header_count)
|
||||
{
|
||||
if next_header.range().contains(&y) {
|
||||
self.place_new_headers(&mut consecutive_header_count, engine)?;
|
||||
y = next_header.end;
|
||||
|
||||
// Skip header rows during normal layout.
|
||||
continue;
|
||||
}
|
||||
@ -156,12 +288,29 @@ impl<'a> GridLayouter<'a> {
|
||||
if y >= footer.start {
|
||||
if y == footer.start {
|
||||
self.layout_footer(footer, engine, self.finished.len())?;
|
||||
self.flush_orphans();
|
||||
}
|
||||
y = footer.end;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
self.layout_row(y, engine, 0)?;
|
||||
|
||||
// After the first non-header row is placed, pending headers are no
|
||||
// longer orphans and can repeat, so we move them to repeating
|
||||
// headers.
|
||||
//
|
||||
// Note that this is usually done in `push_row`, since the call to
|
||||
// `layout_row` above might trigger region breaks (for multi-page
|
||||
// auto rows), whereas this needs to be called as soon as any part
|
||||
// of a row is laid out. However, it's possible a row has no
|
||||
// visible output and thus does not push any rows even though it
|
||||
// was successfully laid out, in which case we additionally flush
|
||||
// here just in case.
|
||||
self.flush_orphans();
|
||||
|
||||
y += 1;
|
||||
}
|
||||
|
||||
self.finish_region(engine, true)?;
|
||||
@ -184,12 +333,46 @@ impl<'a> GridLayouter<'a> {
|
||||
self.render_fills_strokes()
|
||||
}
|
||||
|
||||
/// Layout the given row.
|
||||
/// Layout a row with a certain initial state, returning the final state.
|
||||
#[inline]
|
||||
pub(super) fn layout_row_with_state(
|
||||
&mut self,
|
||||
y: usize,
|
||||
engine: &mut Engine,
|
||||
disambiguator: usize,
|
||||
initial_state: RowState,
|
||||
) -> SourceResult<RowState> {
|
||||
// Keep a copy of the previous value in the stack, as this function can
|
||||
// call itself recursively (e.g. if a region break is triggered and a
|
||||
// header is placed), so we shouldn't outright overwrite it, but rather
|
||||
// save and later restore the state when back to this call.
|
||||
let previous = std::mem::replace(&mut self.row_state, initial_state);
|
||||
|
||||
// Keep it as a separate function to allow inlining the return below,
|
||||
// as it's usually not needed.
|
||||
self.layout_row_internal(y, engine, disambiguator)?;
|
||||
|
||||
Ok(std::mem::replace(&mut self.row_state, previous))
|
||||
}
|
||||
|
||||
/// Layout the given row with the default row state.
|
||||
#[inline]
|
||||
pub(super) fn layout_row(
|
||||
&mut self,
|
||||
y: usize,
|
||||
engine: &mut Engine,
|
||||
disambiguator: usize,
|
||||
) -> SourceResult<()> {
|
||||
self.layout_row_with_state(y, engine, disambiguator, RowState::default())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Layout the given row using the current state.
|
||||
pub(super) fn layout_row_internal(
|
||||
&mut self,
|
||||
y: usize,
|
||||
engine: &mut Engine,
|
||||
disambiguator: usize,
|
||||
) -> SourceResult<()> {
|
||||
// Skip to next region if current one is full, but only for content
|
||||
// rows, not for gutter rows, and only if we aren't laying out an
|
||||
@ -206,13 +389,18 @@ impl<'a> GridLayouter<'a> {
|
||||
}
|
||||
|
||||
// Don't layout gutter rows at the top of a region.
|
||||
if is_content_row || !self.lrows.is_empty() {
|
||||
if is_content_row || !self.current.lrows.is_empty() {
|
||||
match self.grid.rows[y] {
|
||||
Sizing::Auto => self.layout_auto_row(engine, disambiguator, y)?,
|
||||
Sizing::Rel(v) => {
|
||||
self.layout_relative_row(engine, disambiguator, v, y)?
|
||||
}
|
||||
Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y, disambiguator)),
|
||||
Sizing::Fr(v) => {
|
||||
if !self.row_state.in_active_repeatable {
|
||||
self.flush_orphans();
|
||||
}
|
||||
self.current.lrows.push(Row::Fr(v, y, disambiguator))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,8 +413,13 @@ impl<'a> GridLayouter<'a> {
|
||||
fn render_fills_strokes(mut self) -> SourceResult<Fragment> {
|
||||
let mut finished = std::mem::take(&mut self.finished);
|
||||
let frame_amount = finished.len();
|
||||
for ((frame_index, frame), rows) in
|
||||
finished.iter_mut().enumerate().zip(&self.rrows)
|
||||
for (((frame_index, frame), rows), finished_header_rows) in
|
||||
finished.iter_mut().enumerate().zip(&self.rrows).zip(
|
||||
self.finished_header_rows
|
||||
.iter()
|
||||
.map(Some)
|
||||
.chain(std::iter::repeat(None)),
|
||||
)
|
||||
{
|
||||
if self.rcols.is_empty() || rows.is_empty() {
|
||||
continue;
|
||||
@ -347,7 +540,8 @@ impl<'a> GridLayouter<'a> {
|
||||
let hline_indices = rows
|
||||
.iter()
|
||||
.map(|piece| piece.y)
|
||||
.chain(std::iter::once(self.grid.rows.len()));
|
||||
.chain(std::iter::once(self.grid.rows.len()))
|
||||
.enumerate();
|
||||
|
||||
// Converts a row to the corresponding index in the vector of
|
||||
// hlines.
|
||||
@ -372,7 +566,7 @@ impl<'a> GridLayouter<'a> {
|
||||
};
|
||||
|
||||
let mut prev_y = None;
|
||||
for (y, dy) in hline_indices.zip(hline_offsets) {
|
||||
for ((i, y), dy) in hline_indices.zip(hline_offsets) {
|
||||
// Position of lines below the row index in the previous iteration.
|
||||
let expected_prev_line_position = prev_y
|
||||
.map(|prev_y| {
|
||||
@ -383,44 +577,31 @@ impl<'a> GridLayouter<'a> {
|
||||
})
|
||||
.unwrap_or(LinePosition::Before);
|
||||
|
||||
// FIXME: In the future, directly specify in 'self.rrows' when
|
||||
// we place a repeated header rather than its original rows.
|
||||
// That would let us remove most of those verbose checks, both
|
||||
// in 'lines.rs' and here. Those checks also aren't fully
|
||||
// accurate either, since they will also trigger when some rows
|
||||
// have been removed between the header and what's below it.
|
||||
let is_under_repeated_header = self
|
||||
.grid
|
||||
.header
|
||||
.as_ref()
|
||||
.and_then(Repeatable::as_repeated)
|
||||
.zip(prev_y)
|
||||
.is_some_and(|(header, prev_y)| {
|
||||
// Note: 'y == header.end' would mean we're right below
|
||||
// the NON-REPEATED header, so that case should return
|
||||
// false.
|
||||
prev_y < header.end && y > header.end
|
||||
});
|
||||
// Header's lines at the bottom have priority when repeated.
|
||||
// This will store the end bound of the last header if the
|
||||
// current iteration is calculating lines under it.
|
||||
let last_repeated_header_end_above = finished_header_rows
|
||||
.filter(|info| prev_y.is_some() && i == info.repeated_amount)
|
||||
.map(|info| info.last_repeated_header_end);
|
||||
|
||||
// If some grid rows were omitted between the previous resolved
|
||||
// row and the current one, we ensure lines below the previous
|
||||
// row don't "disappear" and are considered, albeit with less
|
||||
// priority. However, don't do this when we're below a header,
|
||||
// as it must have more priority instead of less, so it is
|
||||
// chained later instead of before. The exception is when the
|
||||
// chained later instead of before (stored in the
|
||||
// 'header_hlines' variable below). The exception is when the
|
||||
// last row in the header is removed, in which case we append
|
||||
// both the lines under the row above us and also (later) the
|
||||
// lines under the header's (removed) last row.
|
||||
let prev_lines = prev_y
|
||||
.filter(|prev_y| {
|
||||
prev_y + 1 != y
|
||||
&& (!is_under_repeated_header
|
||||
|| self
|
||||
.grid
|
||||
.header
|
||||
.as_ref()
|
||||
.and_then(Repeatable::as_repeated)
|
||||
.is_some_and(|header| prev_y + 1 != header.end))
|
||||
&& last_repeated_header_end_above.is_none_or(
|
||||
|last_repeated_header_end| {
|
||||
prev_y + 1 != last_repeated_header_end
|
||||
},
|
||||
)
|
||||
})
|
||||
.map(|prev_y| get_hlines_at(prev_y + 1))
|
||||
.unwrap_or(&[]);
|
||||
@ -441,15 +622,14 @@ impl<'a> GridLayouter<'a> {
|
||||
};
|
||||
|
||||
let mut expected_header_line_position = LinePosition::Before;
|
||||
let header_hlines = if let Some((Repeatable::Repeated(header), prev_y)) =
|
||||
self.grid.header.as_ref().zip(prev_y)
|
||||
let header_hlines = if let Some((header_end_above, prev_y)) =
|
||||
last_repeated_header_end_above.zip(prev_y)
|
||||
{
|
||||
if is_under_repeated_header
|
||||
&& (!self.grid.has_gutter
|
||||
|| matches!(
|
||||
self.grid.rows[prev_y],
|
||||
Sizing::Rel(length) if length.is_zero()
|
||||
))
|
||||
if !self.grid.has_gutter
|
||||
|| matches!(
|
||||
self.grid.rows[prev_y],
|
||||
Sizing::Rel(length) if length.is_zero()
|
||||
)
|
||||
{
|
||||
// For lines below a header, give priority to the
|
||||
// lines originally below the header rather than
|
||||
@ -468,10 +648,10 @@ impl<'a> GridLayouter<'a> {
|
||||
// column-gutter is specified, for example. In that
|
||||
// case, we still repeat the line under the gutter.
|
||||
expected_header_line_position = expected_line_position(
|
||||
header.end,
|
||||
header.end == self.grid.rows.len(),
|
||||
header_end_above,
|
||||
header_end_above == self.grid.rows.len(),
|
||||
);
|
||||
get_hlines_at(header.end)
|
||||
get_hlines_at(header_end_above)
|
||||
} else {
|
||||
&[]
|
||||
}
|
||||
@ -529,6 +709,7 @@ impl<'a> GridLayouter<'a> {
|
||||
grid,
|
||||
rows,
|
||||
local_top_y,
|
||||
last_repeated_header_end_above,
|
||||
in_last_region,
|
||||
y,
|
||||
x,
|
||||
@ -941,15 +1122,9 @@ impl<'a> GridLayouter<'a> {
|
||||
let frame = self.layout_single_row(engine, disambiguator, first, y)?;
|
||||
self.push_row(frame, y, true);
|
||||
|
||||
if self
|
||||
.grid
|
||||
.header
|
||||
.as_ref()
|
||||
.and_then(Repeatable::as_repeated)
|
||||
.is_some_and(|header| y < header.end)
|
||||
{
|
||||
// Add to header height.
|
||||
self.header_height += first;
|
||||
if let Some(row_height) = &mut self.row_state.current_row_height {
|
||||
// Add to header height, as we are in a header row.
|
||||
*row_height += first;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
@ -958,19 +1133,21 @@ impl<'a> GridLayouter<'a> {
|
||||
// Expand all but the last region.
|
||||
// Skip the first region if the space is eaten up by an fr row.
|
||||
let len = resolved.len();
|
||||
for ((i, region), target) in self
|
||||
.regions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.zip(&mut resolved[..len - 1])
|
||||
.skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize)
|
||||
for ((i, region), target) in
|
||||
self.regions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.zip(&mut resolved[..len - 1])
|
||||
.skip(self.current.lrows.iter().any(|row| matches!(row, Row::Fr(..)))
|
||||
as usize)
|
||||
{
|
||||
// Subtract header and footer heights from the region height when
|
||||
// it's not the first.
|
||||
// it's not the first. Ignore non-repeating headers as they only
|
||||
// appear on the first region by definition.
|
||||
target.set_max(
|
||||
region.y
|
||||
- if i > 0 {
|
||||
self.header_height + self.footer_height
|
||||
self.current.repeating_header_height + self.current.footer_height
|
||||
} else {
|
||||
Abs::zero()
|
||||
},
|
||||
@ -1181,25 +1358,27 @@ impl<'a> GridLayouter<'a> {
|
||||
let resolved = v.resolve(self.styles).relative_to(self.regions.base().y);
|
||||
let frame = self.layout_single_row(engine, disambiguator, resolved, y)?;
|
||||
|
||||
if self
|
||||
.grid
|
||||
.header
|
||||
.as_ref()
|
||||
.and_then(Repeatable::as_repeated)
|
||||
.is_some_and(|header| y < header.end)
|
||||
{
|
||||
// Add to header height.
|
||||
self.header_height += resolved;
|
||||
if let Some(row_height) = &mut self.row_state.current_row_height {
|
||||
// Add to header height, as we are in a header row.
|
||||
*row_height += resolved;
|
||||
}
|
||||
|
||||
// Skip to fitting region, but only if we aren't part of an unbreakable
|
||||
// row group. We use 'in_last_with_offset' so our 'in_last' call
|
||||
// properly considers that a header and a footer would be added on each
|
||||
// region break.
|
||||
// row group. We use 'may_progress_with_offset' so our 'may_progress'
|
||||
// call properly considers that a header and a footer would be added
|
||||
// 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();
|
||||
while self.unbreakable_rows_left == 0
|
||||
&& !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)?;
|
||||
|
||||
@ -1323,8 +1502,13 @@ impl<'a> GridLayouter<'a> {
|
||||
/// will be pushed for this particular row. It can be `false` for rows
|
||||
/// spanning multiple regions.
|
||||
fn push_row(&mut self, frame: Frame, y: usize, is_last: bool) {
|
||||
if !self.row_state.in_active_repeatable {
|
||||
// There is now a row after the rows equipped with orphan
|
||||
// prevention, so no need to keep moving them anymore.
|
||||
self.flush_orphans();
|
||||
}
|
||||
self.regions.size.y -= frame.height();
|
||||
self.lrows.push(Row::Frame(frame, y, is_last));
|
||||
self.current.lrows.push(Row::Frame(frame, y, is_last));
|
||||
}
|
||||
|
||||
/// Finish rows for one region.
|
||||
@ -1333,68 +1517,71 @@ impl<'a> GridLayouter<'a> {
|
||||
engine: &mut Engine,
|
||||
last: bool,
|
||||
) -> 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
|
||||
.current
|
||||
.lrows
|
||||
.last()
|
||||
.is_some_and(|row| self.grid.is_gutter_track(row.index()))
|
||||
{
|
||||
// Remove the last row in the region if it is a gutter row.
|
||||
self.lrows.pop().unwrap();
|
||||
self.current.lrows.pop().unwrap();
|
||||
self.current.repeated_header_rows =
|
||||
self.current.repeated_header_rows.min(self.current.lrows.len());
|
||||
}
|
||||
|
||||
// If no rows other than the footer have been laid out so far, and
|
||||
// there are rows beside the footer, then don't lay it out at all.
|
||||
// This check doesn't apply, and is thus overridden, when there is a
|
||||
// header.
|
||||
let mut footer_would_be_orphan = self.lrows.is_empty()
|
||||
&& !in_last_with_offset(
|
||||
self.regions,
|
||||
self.header_height + self.footer_height,
|
||||
)
|
||||
&& self
|
||||
.grid
|
||||
.footer
|
||||
.as_ref()
|
||||
.and_then(Repeatable::as_repeated)
|
||||
.is_some_and(|footer| footer.start != 0);
|
||||
|
||||
if let Some(Repeatable::Repeated(header)) = &self.grid.header {
|
||||
if self.grid.rows.len() > header.end
|
||||
&& self
|
||||
.grid
|
||||
.footer
|
||||
.as_ref()
|
||||
.and_then(Repeatable::as_repeated)
|
||||
.is_none_or(|footer| footer.start != header.end)
|
||||
&& self.lrows.last().is_some_and(|row| row.index() < header.end)
|
||||
&& !in_last_with_offset(
|
||||
// If no rows other than the footer have been laid out so far
|
||||
// (e.g. due to header orphan prevention), and there are rows
|
||||
// beside the footer, then don't lay it out at all.
|
||||
//
|
||||
// It is worth noting that the footer is made non-repeatable at
|
||||
// the grid resolving stage if it is short-lived, that is, if
|
||||
// it is at the start of the table (or right after headers at
|
||||
// the start of the table).
|
||||
// TODO(subfooters): explicitly check for short-lived footers.
|
||||
// TODO(subfooters): widow prevention for non-repeated footers with a
|
||||
// similar mechanism / when implementing multiple footers.
|
||||
let footer_would_be_widow =
|
||||
matches!(self.grid.footer, Some(Repeatable::Repeated(_)))
|
||||
&& self.current.lrows.is_empty()
|
||||
&& may_progress_with_offset(
|
||||
self.regions,
|
||||
self.header_height + self.footer_height,
|
||||
)
|
||||
{
|
||||
// Header and footer would be alone in this region, but there are more
|
||||
// rows beyond the header and the footer. Push an empty region.
|
||||
self.lrows.clear();
|
||||
footer_would_be_orphan = true;
|
||||
}
|
||||
}
|
||||
// Don't sum header height as we just confirmed that there
|
||||
// are no headers in this region.
|
||||
self.current.footer_height,
|
||||
);
|
||||
|
||||
let mut laid_out_footer_start = None;
|
||||
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
|
||||
// Don't layout the footer if it would be alone with the header in
|
||||
// the page, and don't layout it twice.
|
||||
if !footer_would_be_orphan
|
||||
&& self.lrows.iter().all(|row| row.index() < footer.start)
|
||||
{
|
||||
laid_out_footer_start = Some(footer.start);
|
||||
self.layout_footer(footer, engine, self.finished.len())?;
|
||||
if !footer_would_be_widow {
|
||||
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
|
||||
// Don't layout the footer if it would be alone with the header
|
||||
// in the page (hence the widow check), and don't layout it
|
||||
// twice.
|
||||
// TODO: this check can be replaced by a vector of repeating
|
||||
// footers in the future.
|
||||
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.
|
||||
let mut used = Abs::zero();
|
||||
let mut fr = Fr::zero();
|
||||
for row in &self.lrows {
|
||||
for row in &self.current.lrows {
|
||||
match row {
|
||||
Row::Frame(frame, _, _) => used += frame.height(),
|
||||
Row::Fr(v, _, _) => fr += *v,
|
||||
@ -1403,9 +1590,9 @@ impl<'a> GridLayouter<'a> {
|
||||
|
||||
// Determine the size of the grid in this region, expanding fully if
|
||||
// there are fr rows.
|
||||
let mut size = Size::new(self.width, used).min(self.initial);
|
||||
if fr.get() > 0.0 && self.initial.y.is_finite() {
|
||||
size.y = self.initial.y;
|
||||
let mut size = Size::new(self.width, used).min(self.current.initial);
|
||||
if fr.get() > 0.0 && self.current.initial.y.is_finite() {
|
||||
size.y = self.current.initial.y;
|
||||
}
|
||||
|
||||
// The frame for the region.
|
||||
@ -1413,9 +1600,10 @@ impl<'a> GridLayouter<'a> {
|
||||
let mut pos = Point::zero();
|
||||
let mut rrows = vec![];
|
||||
let current_region = self.finished.len();
|
||||
let mut repeated_header_row_height = Abs::zero();
|
||||
|
||||
// Place finished rows and layout fractional rows.
|
||||
for row in std::mem::take(&mut self.lrows) {
|
||||
for (i, row) in std::mem::take(&mut self.current.lrows).into_iter().enumerate() {
|
||||
let (frame, y, is_last) = match row {
|
||||
Row::Frame(frame, y, is_last) => (frame, y, is_last),
|
||||
Row::Fr(v, y, disambiguator) => {
|
||||
@ -1426,6 +1614,9 @@ impl<'a> GridLayouter<'a> {
|
||||
};
|
||||
|
||||
let height = frame.height();
|
||||
if i < self.current.repeated_header_rows {
|
||||
repeated_header_row_height += height;
|
||||
}
|
||||
|
||||
// Ensure rowspans which span this row will have enough space to
|
||||
// be laid out over it later.
|
||||
@ -1504,7 +1695,11 @@ impl<'a> GridLayouter<'a> {
|
||||
// we have to check the same index again in the next
|
||||
// iteration.
|
||||
let rowspan = self.rowspans.remove(i);
|
||||
self.layout_rowspan(rowspan, Some((&mut output, &rrows)), engine)?;
|
||||
self.layout_rowspan(
|
||||
rowspan,
|
||||
Some((&mut output, repeated_header_row_height)),
|
||||
engine,
|
||||
)?;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
@ -1515,21 +1710,38 @@ impl<'a> GridLayouter<'a> {
|
||||
pos.y += height;
|
||||
}
|
||||
|
||||
self.finish_region_internal(output, rrows);
|
||||
self.finish_region_internal(
|
||||
output,
|
||||
rrows,
|
||||
FinishedHeaderRowInfo {
|
||||
repeated_amount: self.current.repeated_header_rows,
|
||||
last_repeated_header_end: self.current.last_repeated_header_end,
|
||||
repeated_height: repeated_header_row_height,
|
||||
},
|
||||
);
|
||||
|
||||
if !last {
|
||||
self.current.repeated_header_rows = 0;
|
||||
self.current.last_repeated_header_end = 0;
|
||||
self.current.header_height = Abs::zero();
|
||||
self.current.repeating_header_height = Abs::zero();
|
||||
self.current.repeating_header_heights.clear();
|
||||
|
||||
let disambiguator = self.finished.len();
|
||||
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
|
||||
self.prepare_footer(footer, engine, disambiguator)?;
|
||||
}
|
||||
|
||||
if let Some(Repeatable::Repeated(header)) = &self.grid.header {
|
||||
// Add a header to the new region.
|
||||
self.layout_header(header, engine, disambiguator)?;
|
||||
}
|
||||
|
||||
// Ensure rows don't try to overrun the footer.
|
||||
self.regions.size.y -= self.footer_height;
|
||||
// Note that header layout will only subtract this again if it has
|
||||
// to skip regions to fit headers, so there is no risk of
|
||||
// subtracting this twice.
|
||||
self.regions.size.y -= self.current.footer_height;
|
||||
|
||||
if !self.repeating_headers.is_empty() || !self.pending_headers.is_empty() {
|
||||
// Add headers to the new region.
|
||||
self.layout_active_headers(engine)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -1541,11 +1753,19 @@ impl<'a> GridLayouter<'a> {
|
||||
&mut self,
|
||||
output: Frame,
|
||||
resolved_rows: Vec<RowPiece>,
|
||||
header_row_info: FinishedHeaderRowInfo,
|
||||
) {
|
||||
self.finished.push(output);
|
||||
self.rrows.push(resolved_rows);
|
||||
self.regions.next();
|
||||
self.initial = self.regions.size;
|
||||
self.current.initial = self.regions.size;
|
||||
|
||||
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
|
||||
/// specific offset height, even after calling `.next()`, due to some
|
||||
/// additional logic which adds content automatically on each region turn (in
|
||||
/// our case, headers).
|
||||
pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool {
|
||||
regions.backlog.is_empty()
|
||||
&& regions.last.is_none_or(|height| regions.size.y + offset == height)
|
||||
pub(super) fn may_progress_with_offset(regions: Regions<'_>, offset: Abs) -> bool {
|
||||
// Use 'approx_eq' as float addition and subtraction are not associative.
|
||||
!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
|
||||
/// sorted by increasing `y`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn hline_stroke_at_column(
|
||||
grid: &CellGrid,
|
||||
rows: &[RowPiece],
|
||||
local_top_y: Option<usize>,
|
||||
header_end_above: Option<usize>,
|
||||
in_last_region: bool,
|
||||
y: usize,
|
||||
x: usize,
|
||||
@ -499,17 +501,11 @@ pub fn hline_stroke_at_column(
|
||||
// Top border stroke and header stroke are generally prioritized, unless
|
||||
// they don't have explicit hline overrides and one or more user-provided
|
||||
// hlines would appear at the same position, which then are prioritized.
|
||||
let top_stroke_comes_from_header = grid
|
||||
.header
|
||||
.as_ref()
|
||||
.and_then(Repeatable::as_repeated)
|
||||
.zip(local_top_y)
|
||||
.is_some_and(|(header, local_top_y)| {
|
||||
// Ensure the row above us is a repeated header.
|
||||
// FIXME: Make this check more robust when headers at arbitrary
|
||||
// positions are added.
|
||||
local_top_y < header.end && y > header.end
|
||||
});
|
||||
let top_stroke_comes_from_header = header_end_above.zip(local_top_y).is_some_and(
|
||||
|(last_repeated_header_end, local_top_y)| {
|
||||
local_top_y < last_repeated_header_end && y > last_repeated_header_end
|
||||
},
|
||||
);
|
||||
|
||||
// Prioritize the footer's top stroke as well where applicable.
|
||||
let bottom_stroke_comes_from_footer = grid
|
||||
@ -637,7 +633,7 @@ mod test {
|
||||
},
|
||||
vec![],
|
||||
vec![],
|
||||
None,
|
||||
vec![],
|
||||
None,
|
||||
entries,
|
||||
)
|
||||
@ -1175,7 +1171,7 @@ mod test {
|
||||
},
|
||||
vec![],
|
||||
vec![],
|
||||
None,
|
||||
vec![],
|
||||
None,
|
||||
entries,
|
||||
)
|
||||
@ -1268,6 +1264,7 @@ mod test {
|
||||
grid,
|
||||
&rows,
|
||||
y.checked_sub(1),
|
||||
None,
|
||||
true,
|
||||
y,
|
||||
x,
|
||||
@ -1461,6 +1458,7 @@ mod test {
|
||||
grid,
|
||||
&rows,
|
||||
y.checked_sub(1),
|
||||
None,
|
||||
true,
|
||||
y,
|
||||
x,
|
||||
@ -1506,6 +1504,7 @@ mod test {
|
||||
grid,
|
||||
&rows,
|
||||
if y == 4 { Some(2) } else { y.checked_sub(1) },
|
||||
None,
|
||||
true,
|
||||
y,
|
||||
x,
|
||||
|
@ -3,55 +3,448 @@ use typst_library::engine::Engine;
|
||||
use typst_library::layout::grid::resolve::{Footer, Header, Repeatable};
|
||||
use typst_library::layout::{Abs, Axes, Frame, Regions};
|
||||
|
||||
use super::layouter::GridLayouter;
|
||||
use super::layouter::{may_progress_with_offset, GridLayouter, RowState};
|
||||
use super::rowspans::UnbreakableRowGroup;
|
||||
|
||||
impl GridLayouter<'_> {
|
||||
/// Layouts the header's rows.
|
||||
/// Skips regions as necessary.
|
||||
pub fn layout_header(
|
||||
impl<'a> GridLayouter<'a> {
|
||||
pub fn place_new_headers(
|
||||
&mut self,
|
||||
consecutive_header_count: &mut usize,
|
||||
engine: &mut Engine,
|
||||
) -> SourceResult<()> {
|
||||
*consecutive_header_count += 1;
|
||||
let (consecutive_headers, new_upcoming_headers) =
|
||||
self.upcoming_headers.split_at(*consecutive_header_count);
|
||||
|
||||
if new_upcoming_headers.first().is_some_and(|next_header| {
|
||||
consecutive_headers.last().is_none_or(|latest_header| {
|
||||
!latest_header.short_lived && next_header.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,
|
||||
header: &Header,
|
||||
engine: &mut Engine,
|
||||
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<()> {
|
||||
let header_rows =
|
||||
self.simulate_header(header, &self.regions, engine, disambiguator)?;
|
||||
let [first_header, ..] = headers else {
|
||||
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;
|
||||
while self.unbreakable_rows_left == 0
|
||||
&& !self.regions.size.y.fits(header_rows.height + self.footer_height)
|
||||
&& self.regions.may_progress()
|
||||
&& !self.regions.size.y.fits(header_height)
|
||||
&& 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
|
||||
// header and the footer.
|
||||
self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]);
|
||||
skipped_region = true;
|
||||
}
|
||||
self.finish_region_internal(
|
||||
Frame::soft(Axes::splat(Abs::zero())),
|
||||
vec![],
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
// Reset the header height for this region.
|
||||
// It will be re-calculated when laying out each header row.
|
||||
self.header_height = Abs::zero();
|
||||
// TODO: re-calculate heights of headers and footers on each region
|
||||
// if 'full' changes? (Assuming height doesn't change for now...)
|
||||
skipped_region = true;
|
||||
|
||||
self.regions.size.y -= self.current.footer_height;
|
||||
}
|
||||
|
||||
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
|
||||
if skipped_region {
|
||||
// Simulate the footer again; the region's 'full' might have
|
||||
// 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)?
|
||||
.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
|
||||
// within 'layout_row'.
|
||||
self.unbreakable_rows_left += header.end;
|
||||
for y in 0..header.end {
|
||||
self.layout_row(y, engine, disambiguator)?;
|
||||
self.unbreakable_rows_left += repeating_header_rows + pending_header_rows;
|
||||
|
||||
self.current.last_repeated_header_end =
|
||||
self.repeating_headers.last().map(|h| h.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(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn simulate_header(
|
||||
&self,
|
||||
@ -66,8 +459,8 @@ impl GridLayouter<'_> {
|
||||
// assume that the amount of unbreakable rows following the first row
|
||||
// in the header will be precisely the rows in the header.
|
||||
self.simulate_unbreakable_row_group(
|
||||
0,
|
||||
Some(header.end),
|
||||
header.start,
|
||||
Some(header.end - header.start),
|
||||
regions,
|
||||
engine,
|
||||
disambiguator,
|
||||
@ -91,11 +484,19 @@ impl GridLayouter<'_> {
|
||||
{
|
||||
// Advance regions without any output until we can place the
|
||||
// footer.
|
||||
self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]);
|
||||
self.finish_region_internal(
|
||||
Frame::soft(Axes::splat(Abs::zero())),
|
||||
vec![],
|
||||
Default::default(),
|
||||
);
|
||||
skipped_region = true;
|
||||
}
|
||||
|
||||
self.footer_height = if skipped_region {
|
||||
// TODO: 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
|
||||
// changed.
|
||||
self.simulate_footer(footer, &self.regions, engine, disambiguator)?
|
||||
@ -118,12 +519,26 @@ impl GridLayouter<'_> {
|
||||
// Ensure footer rows have their own height available.
|
||||
// Won't change much as we're creating an unbreakable row group
|
||||
// anyway, so this is mostly for correctness.
|
||||
self.regions.size.y += self.footer_height;
|
||||
self.regions.size.y += self.current.footer_height;
|
||||
|
||||
let repeats = self
|
||||
.grid
|
||||
.footer
|
||||
.as_ref()
|
||||
.is_some_and(|f| matches!(f, Repeatable::Repeated(_)));
|
||||
let footer_len = self.grid.rows.len() - footer.start;
|
||||
self.unbreakable_rows_left += footer_len;
|
||||
|
||||
for y in footer.start..self.grid.rows.len() {
|
||||
self.layout_row(y, engine, disambiguator)?;
|
||||
self.layout_row_with_state(
|
||||
y,
|
||||
engine,
|
||||
disambiguator,
|
||||
RowState {
|
||||
in_active_repeatable: repeats,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -144,10 +559,18 @@ impl GridLayouter<'_> {
|
||||
// in the footer will be precisely the rows in the footer.
|
||||
self.simulate_unbreakable_row_group(
|
||||
footer.start,
|
||||
Some(self.grid.rows.len() - footer.start),
|
||||
Some(footer.end - footer.start),
|
||||
regions,
|
||||
engine,
|
||||
disambiguator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The total amount of rows in the given list of headers.
|
||||
#[inline]
|
||||
pub fn total_header_row_count<'h>(
|
||||
headers: impl IntoIterator<Item = &'h Header>,
|
||||
) -> usize {
|
||||
headers.into_iter().map(|h| h.end - h.start).sum()
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use typst_library::foundations::Resolve;
|
||||
use typst_library::layout::grid::resolve::Repeatable;
|
||||
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
|
||||
|
||||
use super::layouter::{in_last_with_offset, points, Row, RowPiece};
|
||||
use super::layouter::{may_progress_with_offset, points, Row};
|
||||
use super::{layout_cell, Cell, GridLayouter};
|
||||
|
||||
/// All information needed to layout a single rowspan.
|
||||
@ -90,10 +90,10 @@ pub struct CellMeasurementData<'layouter> {
|
||||
|
||||
impl GridLayouter<'_> {
|
||||
/// Layout a rowspan over the already finished regions, plus the current
|
||||
/// region's frame and resolved rows, if it wasn't finished yet (because
|
||||
/// we're being called from `finish_region`, but note that this function is
|
||||
/// also called once after all regions are finished, in which case
|
||||
/// `current_region_data` is `None`).
|
||||
/// region's frame and height of resolved header rows, if it wasn't
|
||||
/// finished yet (because we're being called from `finish_region`, but note
|
||||
/// that this function is also called once after all regions are finished,
|
||||
/// in which case `current_region_data` is `None`).
|
||||
///
|
||||
/// We need to do this only once we already know the heights of all
|
||||
/// spanned rows, which is only possible after laying out the last row
|
||||
@ -101,7 +101,7 @@ impl GridLayouter<'_> {
|
||||
pub fn layout_rowspan(
|
||||
&mut self,
|
||||
rowspan_data: Rowspan,
|
||||
current_region_data: Option<(&mut Frame, &[RowPiece])>,
|
||||
current_region_data: Option<(&mut Frame, Abs)>,
|
||||
engine: &mut Engine,
|
||||
) -> SourceResult<()> {
|
||||
let Rowspan {
|
||||
@ -146,11 +146,31 @@ impl GridLayouter<'_> {
|
||||
|
||||
// Push the layouted frames directly into the finished frames.
|
||||
let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?;
|
||||
let (current_region, current_rrows) = current_region_data.unzip();
|
||||
for ((i, finished), frame) in self
|
||||
let (current_region, current_header_row_height) = current_region_data.unzip();
|
||||
|
||||
// Clever trick to process finished header rows:
|
||||
// - If there are grid headers, the vector will be filled with one
|
||||
// finished header row height per region, so, chaining with the height
|
||||
// for the current one, we get the header row height for each region.
|
||||
//
|
||||
// - But if there are no grid headers, the vector will be empty, so in
|
||||
// theory the regions and resolved header row heights wouldn't match.
|
||||
// But that's fine - 'current_header_row_height' can only be either
|
||||
// 'Some(zero)' or 'None' in such a case, and for all other rows we
|
||||
// append infinite zeros. That is, in such a case, the resolved header
|
||||
// row height is always zero, so that's our fallback.
|
||||
let finished_header_rows = self
|
||||
.finished_header_rows
|
||||
.iter()
|
||||
.map(|info| info.repeated_height)
|
||||
.chain(current_header_row_height)
|
||||
.chain(std::iter::repeat(Abs::zero()));
|
||||
|
||||
for ((i, (finished, header_dy)), frame) in self
|
||||
.finished
|
||||
.iter_mut()
|
||||
.chain(current_region.into_iter())
|
||||
.zip(finished_header_rows)
|
||||
.skip(first_region)
|
||||
.enumerate()
|
||||
.zip(fragment)
|
||||
@ -162,22 +182,9 @@ impl GridLayouter<'_> {
|
||||
} else {
|
||||
// The rowspan continuation starts after the header (thus,
|
||||
// at a position after the sum of the laid out header
|
||||
// rows).
|
||||
if let Some(Repeatable::Repeated(header)) = &self.grid.header {
|
||||
let header_rows = self
|
||||
.rrows
|
||||
.get(i)
|
||||
.map(Vec::as_slice)
|
||||
.or(current_rrows)
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.take_while(|row| row.y < header.end);
|
||||
|
||||
header_rows.map(|row| row.height).sum()
|
||||
} else {
|
||||
// Without a header, start at the very top of the region.
|
||||
Abs::zero()
|
||||
}
|
||||
// rows). Without a header, this is zero, so the rowspan can
|
||||
// start at the very top of the region as usual.
|
||||
header_dy
|
||||
};
|
||||
|
||||
finished.push_frame(Point::new(dx, dy), frame);
|
||||
@ -231,15 +238,12 @@ impl GridLayouter<'_> {
|
||||
// current row is dynamic and depends on the amount of upcoming
|
||||
// unbreakable cells (with or without a rowspan setting).
|
||||
let mut amount_unbreakable_rows = None;
|
||||
if let Some(Repeatable::NotRepeated(header)) = &self.grid.header {
|
||||
if current_row < header.end {
|
||||
// Non-repeated header, so keep it unbreakable.
|
||||
amount_unbreakable_rows = Some(header.end);
|
||||
}
|
||||
}
|
||||
if let Some(Repeatable::NotRepeated(footer)) = &self.grid.footer {
|
||||
if current_row >= footer.start {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@ -254,9 +258,13 @@ impl GridLayouter<'_> {
|
||||
|
||||
// Skip to fitting region.
|
||||
while !self.regions.size.y.fits(row_group.height)
|
||||
&& !in_last_with_offset(
|
||||
&& may_progress_with_offset(
|
||||
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)?;
|
||||
@ -396,7 +404,8 @@ impl GridLayouter<'_> {
|
||||
// auto rows don't depend on the backlog, as they only span one
|
||||
// region.
|
||||
if breakable
|
||||
&& (matches!(self.grid.header, Some(Repeatable::Repeated(_)))
|
||||
&& (!self.repeating_headers.is_empty()
|
||||
|| !self.pending_headers.is_empty()
|
||||
|| matches!(self.grid.footer, Some(Repeatable::Repeated(_))))
|
||||
{
|
||||
// Subtract header and footer height from all upcoming regions
|
||||
@ -404,8 +413,20 @@ impl GridLayouter<'_> {
|
||||
//
|
||||
// This will update the 'custom_backlog' vector with the
|
||||
// updated heights of the upcoming regions.
|
||||
//
|
||||
// We predict that header height will only include that of
|
||||
// repeating headers, as we can assume non-repeating headers in
|
||||
// the first region have been successfully placed, unless
|
||||
// something didn't fit on the first region of the auto row,
|
||||
// but we will only find that out after measurement, and if
|
||||
// that happens, we discard the measurement and try again.
|
||||
let mapped_regions = self.regions.map(&mut custom_backlog, |size| {
|
||||
Size::new(size.x, size.y - self.header_height - self.footer_height)
|
||||
Size::new(
|
||||
size.x,
|
||||
size.y
|
||||
- self.current.repeating_header_height
|
||||
- self.current.footer_height,
|
||||
)
|
||||
});
|
||||
|
||||
// Callees must use the custom backlog instead of the current
|
||||
@ -459,6 +480,7 @@ impl GridLayouter<'_> {
|
||||
// Height of the rowspan covered by spanned rows in the current
|
||||
// region.
|
||||
let laid_out_height: Abs = self
|
||||
.current
|
||||
.lrows
|
||||
.iter()
|
||||
.filter_map(|row| match row {
|
||||
@ -506,7 +528,28 @@ impl GridLayouter<'_> {
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(std::iter::once(if breakable {
|
||||
self.initial.y - self.header_height - self.footer_height
|
||||
// Here we are calculating the available height for a
|
||||
// rowspan from the top of the current region, so
|
||||
// we have to use initial header heights (note that
|
||||
// header height can change in the middle of the
|
||||
// region).
|
||||
// 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 {
|
||||
// When measuring unbreakable auto rows, infinite
|
||||
// height is available for content to expand.
|
||||
@ -518,11 +561,13 @@ impl GridLayouter<'_> {
|
||||
// rowspan's already laid out heights with the current
|
||||
// region's height and current backlog to ensure a good
|
||||
// level of accuracy in the measurements.
|
||||
let backlog = self
|
||||
.regions
|
||||
.backlog
|
||||
.iter()
|
||||
.map(|&size| size - self.header_height - self.footer_height);
|
||||
//
|
||||
// Assume only repeating headers will survive starting at
|
||||
// the next region.
|
||||
let backlog = self.regions.backlog.iter().map(|&size| {
|
||||
size - self.current.repeating_header_height
|
||||
- self.current.footer_height
|
||||
});
|
||||
|
||||
heights_up_to_current_region.chain(backlog).collect::<Vec<_>>()
|
||||
} else {
|
||||
@ -536,10 +581,10 @@ impl GridLayouter<'_> {
|
||||
height = *rowspan_height;
|
||||
backlog = None;
|
||||
full = rowspan_full;
|
||||
last = self
|
||||
.regions
|
||||
.last
|
||||
.map(|size| size - self.header_height - self.footer_height);
|
||||
last = self.regions.last.map(|size| {
|
||||
size - self.current.repeating_header_height
|
||||
- self.current.footer_height
|
||||
});
|
||||
} else {
|
||||
// The rowspan started in the current region, as its vector
|
||||
// of heights in regions is currently empty.
|
||||
@ -741,10 +786,11 @@ impl GridLayouter<'_> {
|
||||
simulated_regions.next();
|
||||
disambiguator += 1;
|
||||
|
||||
// Subtract the initial header and footer height, since that's the
|
||||
// height we used when subtracting from the region backlog's
|
||||
// Subtract the repeating header and footer height, since that's
|
||||
// the height we used when subtracting from the region backlog's
|
||||
// heights while measuring cells.
|
||||
simulated_regions.size.y -= self.header_height + self.footer_height;
|
||||
simulated_regions.size.y -=
|
||||
self.current.repeating_header_height + self.current.footer_height;
|
||||
}
|
||||
|
||||
if let Some(original_last_resolved_size) = last_resolved_size {
|
||||
@ -879,8 +925,12 @@ impl GridLayouter<'_> {
|
||||
let rowspan_simulator = RowspanSimulator::new(
|
||||
disambiguator,
|
||||
simulated_regions,
|
||||
self.header_height,
|
||||
self.footer_height,
|
||||
// There can be no new headers or footers within a multi-page
|
||||
// rowspan, since headers and footers are unbreakable, so
|
||||
// assuming the repeating header height and footer height
|
||||
// won't change is safe.
|
||||
self.current.repeating_header_height,
|
||||
self.current.footer_height,
|
||||
);
|
||||
|
||||
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());
|
||||
simulated_regions.next();
|
||||
simulated_regions.size.y -= self.header_height + self.footer_height;
|
||||
simulated_regions.size.y -=
|
||||
self.current.repeating_header_height + self.current.footer_height;
|
||||
disambiguator += 1;
|
||||
}
|
||||
simulated_regions.size.y -= extra_amount_to_grow;
|
||||
@ -1053,7 +1104,7 @@ impl<'a> RowspanSimulator<'a> {
|
||||
0,
|
||||
)?;
|
||||
while !self.regions.size.y.fits(row_group.height)
|
||||
&& !in_last_with_offset(
|
||||
&& may_progress_with_offset(
|
||||
self.regions,
|
||||
self.header_height + self.footer_height,
|
||||
)
|
||||
@ -1078,7 +1129,7 @@ impl<'a> RowspanSimulator<'a> {
|
||||
let mut skipped_region = false;
|
||||
while unbreakable_rows_left == 0
|
||||
&& !self.regions.size.y.fits(height)
|
||||
&& !in_last_with_offset(
|
||||
&& may_progress_with_offset(
|
||||
self.regions,
|
||||
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
|
||||
// just use the original backlog from `self.regions`.
|
||||
let disambiguator = self.finished;
|
||||
let header_height =
|
||||
if let Some(Repeatable::Repeated(header)) = &layouter.grid.header {
|
||||
layouter
|
||||
.simulate_header(header, &self.regions, engine, disambiguator)?
|
||||
.height
|
||||
} else {
|
||||
Abs::zero()
|
||||
};
|
||||
|
||||
let (repeating_headers, header_height) = if !layouter.repeating_headers.is_empty()
|
||||
|| !layouter.pending_headers.is_empty()
|
||||
{
|
||||
// Only repeating headers have survived after the first region
|
||||
// break.
|
||||
let repeating_headers = layouter.repeating_headers.iter().copied().chain(
|
||||
layouter.pending_headers.iter().filter_map(Repeatable::as_repeated),
|
||||
);
|
||||
|
||||
let header_height = layouter.simulate_header_height(
|
||||
repeating_headers.clone(),
|
||||
&self.regions,
|
||||
engine,
|
||||
disambiguator,
|
||||
)?;
|
||||
|
||||
(Some(repeating_headers), header_height)
|
||||
} else {
|
||||
(None, Abs::zero())
|
||||
};
|
||||
|
||||
let footer_height =
|
||||
if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer {
|
||||
@ -1156,13 +1220,16 @@ impl<'a> RowspanSimulator<'a> {
|
||||
skipped_region = true;
|
||||
}
|
||||
|
||||
if let Some(Repeatable::Repeated(header)) = &layouter.grid.header {
|
||||
if let Some(repeating_headers) = repeating_headers {
|
||||
self.header_height = if skipped_region {
|
||||
// Simulate headers again, at the new region, as
|
||||
// the full region height may change.
|
||||
layouter
|
||||
.simulate_header(header, &self.regions, engine, disambiguator)?
|
||||
.height
|
||||
layouter.simulate_header_height(
|
||||
repeating_headers,
|
||||
&self.regions,
|
||||
engine,
|
||||
disambiguator,
|
||||
)?
|
||||
} else {
|
||||
header_height
|
||||
};
|
||||
|
@ -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 smallvec::SmallVec;
|
||||
@ -482,3 +484,16 @@ cast! {
|
||||
"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;
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::num::{NonZeroU32, NonZeroUsize};
|
||||
use std::sync::Arc;
|
||||
|
||||
use comemo::Track;
|
||||
@ -468,6 +468,14 @@ pub struct GridHeader {
|
||||
#[default(true)]
|
||||
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.
|
||||
#[variadic]
|
||||
pub children: Vec<GridItem>,
|
||||
|
@ -1,5 +1,5 @@
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::Range;
|
||||
use std::num::{NonZeroU32, NonZeroUsize};
|
||||
use std::ops::{Deref, Range};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::eco_format;
|
||||
@ -48,6 +48,7 @@ pub fn grid_to_cellgrid<'a>(
|
||||
let children = elem.children.iter().map(|child| match child {
|
||||
GridChild::Header(header) => ResolvableGridChild::Header {
|
||||
repeat: header.repeat(styles),
|
||||
level: header.level(styles),
|
||||
span: header.span(),
|
||||
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 {
|
||||
TableChild::Header(header) => ResolvableGridChild::Header {
|
||||
repeat: header.repeat(styles),
|
||||
level: header.level(styles),
|
||||
span: header.span(),
|
||||
items: header.children.iter().map(resolve_item),
|
||||
},
|
||||
@ -426,8 +428,30 @@ pub struct Line {
|
||||
/// A repeatable grid header. Starts at the first row.
|
||||
#[derive(Debug)]
|
||||
pub struct Header {
|
||||
/// The first row included in this header.
|
||||
pub start: usize,
|
||||
/// The index after the last row included in this header.
|
||||
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.
|
||||
@ -435,20 +459,46 @@ pub struct Header {
|
||||
pub struct Footer {
|
||||
/// The first row included in this footer.
|
||||
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
|
||||
/// considerations by grid layout, other than for consistency (such as making
|
||||
/// a certain group of rows unbreakable).
|
||||
pub enum Repeatable<T> {
|
||||
/// The user asked this grid child to repeat.
|
||||
Repeated(T),
|
||||
/// The user asked this grid child to not repeat.
|
||||
NotRepeated(T),
|
||||
}
|
||||
|
||||
impl<T> Deref for Repeatable<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Repeatable<T> {
|
||||
/// Gets the value inside this repeatable, regardless of whether
|
||||
/// it repeats.
|
||||
#[inline]
|
||||
pub fn unwrap(&self) -> &T {
|
||||
match self {
|
||||
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.
|
||||
#[inline]
|
||||
pub fn as_repeated(&self) -> Option<&T> {
|
||||
match self {
|
||||
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.
|
||||
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 },
|
||||
Item(ResolvableGridItem<T>),
|
||||
}
|
||||
@ -638,8 +699,8 @@ pub struct CellGrid<'a> {
|
||||
/// Gutter rows are not included.
|
||||
/// Contains up to 'rows_without_gutter.len() + 1' vectors of lines.
|
||||
pub hlines: Vec<Vec<Line>>,
|
||||
/// The repeatable header of this grid.
|
||||
pub header: Option<Repeatable<Header>>,
|
||||
/// The repeatable headers of this grid.
|
||||
pub headers: Vec<Repeatable<Header>>,
|
||||
/// The repeatable footer of this grid.
|
||||
pub footer: Option<Repeatable<Footer>>,
|
||||
/// Whether this grid has gutters.
|
||||
@ -654,7 +715,7 @@ impl<'a> CellGrid<'a> {
|
||||
cells: impl IntoIterator<Item = Cell<'a>>,
|
||||
) -> Self {
|
||||
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.
|
||||
@ -663,7 +724,7 @@ impl<'a> CellGrid<'a> {
|
||||
gutter: Axes<&[Sizing]>,
|
||||
vlines: Vec<Vec<Line>>,
|
||||
hlines: Vec<Vec<Line>>,
|
||||
header: Option<Repeatable<Header>>,
|
||||
headers: Vec<Repeatable<Header>>,
|
||||
footer: Option<Repeatable<Footer>>,
|
||||
entries: Vec<Entry<'a>>,
|
||||
) -> Self {
|
||||
@ -717,7 +778,7 @@ impl<'a> CellGrid<'a> {
|
||||
entries,
|
||||
vlines,
|
||||
hlines,
|
||||
header,
|
||||
headers,
|
||||
footer,
|
||||
has_gutter,
|
||||
}
|
||||
@ -852,6 +913,11 @@ impl<'a> CellGrid<'a> {
|
||||
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.
|
||||
@ -937,6 +1003,12 @@ struct RowGroupData {
|
||||
span: Span,
|
||||
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.
|
||||
/// 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
|
||||
@ -984,14 +1056,17 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
let mut pending_vlines: Vec<(Span, Line)> = vec![];
|
||||
let has_gutter = self.gutter.any(|tracks| !tracks.is_empty());
|
||||
|
||||
let mut header: Option<Header> = None;
|
||||
let mut repeat_header = false;
|
||||
let mut headers: Vec<Repeatable<Header>> = vec![];
|
||||
|
||||
// Stores where the footer is supposed to end, its span, and the
|
||||
// actual footer structure.
|
||||
let mut footer: Option<(usize, Span, Footer)> = None;
|
||||
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
|
||||
// determine its automatic position, since cells could have arbitrary
|
||||
// positions, so the position of a cell in 'cells' can differ from its
|
||||
@ -1008,6 +1083,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
// automatically-positioned cell.
|
||||
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.
|
||||
//
|
||||
// Create at least 'children.len()' positions, since there could be at
|
||||
@ -1028,12 +1108,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
columns,
|
||||
&mut pending_hlines,
|
||||
&mut pending_vlines,
|
||||
&mut header,
|
||||
&mut repeat_header,
|
||||
&mut headers,
|
||||
&mut footer,
|
||||
&mut repeat_footer,
|
||||
&mut auto_index,
|
||||
&mut next_header,
|
||||
&mut resolved_cells,
|
||||
&mut at_least_one_cell,
|
||||
child,
|
||||
)?;
|
||||
}
|
||||
@ -1049,13 +1130,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
row_amount,
|
||||
)?;
|
||||
|
||||
let (header, footer) = self.finalize_headers_and_footers(
|
||||
let footer = self.finalize_headers_and_footers(
|
||||
has_gutter,
|
||||
header,
|
||||
repeat_header,
|
||||
&mut headers,
|
||||
footer,
|
||||
repeat_footer,
|
||||
row_amount,
|
||||
at_least_one_cell,
|
||||
)?;
|
||||
|
||||
Ok(CellGrid::new_internal(
|
||||
@ -1063,7 +1144,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
self.gutter,
|
||||
vlines,
|
||||
hlines,
|
||||
header,
|
||||
headers,
|
||||
footer,
|
||||
resolved_cells,
|
||||
))
|
||||
@ -1083,12 +1164,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
columns: usize,
|
||||
pending_hlines: &mut Vec<(Span, Line, bool)>,
|
||||
pending_vlines: &mut Vec<(Span, Line)>,
|
||||
header: &mut Option<Header>,
|
||||
repeat_header: &mut bool,
|
||||
headers: &mut Vec<Repeatable<Header>>,
|
||||
footer: &mut Option<(usize, Span, Footer)>,
|
||||
repeat_footer: &mut bool,
|
||||
auto_index: &mut usize,
|
||||
next_header: &mut usize,
|
||||
resolved_cells: &mut Vec<Option<Entry<'x>>>,
|
||||
at_least_one_cell: &mut bool,
|
||||
child: ResolvableGridChild<T, I>,
|
||||
) -> SourceResult<()>
|
||||
where
|
||||
@ -1112,7 +1194,32 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
// 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
|
||||
// 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.
|
||||
//
|
||||
@ -1123,23 +1230,19 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
let mut first_available_row = 0;
|
||||
|
||||
let (header_footer_items, simple_item) = match child {
|
||||
ResolvableGridChild::Header { repeat, span, items, .. } => {
|
||||
if header.is_some() {
|
||||
bail!(span, "cannot have more than one header");
|
||||
}
|
||||
|
||||
ResolvableGridChild::Header { repeat, level, span, items, .. } => {
|
||||
row_group_data = Some(RowGroupData {
|
||||
range: None,
|
||||
span,
|
||||
kind: RowGroupKind::Header,
|
||||
repeat,
|
||||
repeatable_level: level,
|
||||
top_hlines_start: pending_hlines.len(),
|
||||
top_hlines_end: None,
|
||||
});
|
||||
|
||||
*repeat_header = repeat;
|
||||
|
||||
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,
|
||||
// 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
|
||||
// always occupies the first available position after the
|
||||
// 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)
|
||||
}
|
||||
@ -1162,21 +1265,27 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
row_group_data = Some(RowGroupData {
|
||||
range: None,
|
||||
span,
|
||||
repeat,
|
||||
kind: RowGroupKind::Footer,
|
||||
repeatable_level: NonZeroU32::ONE,
|
||||
top_hlines_start: pending_hlines.len(),
|
||||
top_hlines_end: None,
|
||||
});
|
||||
|
||||
*repeat_footer = repeat;
|
||||
|
||||
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)
|
||||
}
|
||||
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);
|
||||
@ -1191,7 +1300,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
// gutter.
|
||||
skip_auto_index_through_fully_merged_rows(
|
||||
resolved_cells,
|
||||
&mut local_auto_index,
|
||||
local_auto_index,
|
||||
columns,
|
||||
);
|
||||
|
||||
@ -1266,7 +1375,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
// automatically positioned cell. Same for footers.
|
||||
local_auto_index
|
||||
.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)
|
||||
});
|
||||
if end.is_some_and(|end| end.get() < start) {
|
||||
@ -1295,10 +1404,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
cell_y,
|
||||
colspan,
|
||||
rowspan,
|
||||
header.as_ref(),
|
||||
headers,
|
||||
footer.as_ref(),
|
||||
resolved_cells,
|
||||
&mut local_auto_index,
|
||||
local_auto_index,
|
||||
local_next_header,
|
||||
first_available_row,
|
||||
columns,
|
||||
row_group_data.is_some(),
|
||||
@ -1350,7 +1460,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
);
|
||||
|
||||
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
|
||||
// no longer appear at the top.
|
||||
@ -1437,7 +1547,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
None => {
|
||||
// Empty header/footer: consider the header/footer to be
|
||||
// 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_end = group_start + 1;
|
||||
|
||||
@ -1454,8 +1564,8 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
// 'find_next_empty_row' will skip through any existing headers
|
||||
// and footers without having to loop through them each time.
|
||||
// Cells themselves, unfortunately, still have to.
|
||||
assert!(resolved_cells[local_auto_index].is_none());
|
||||
resolved_cells[local_auto_index] =
|
||||
assert!(resolved_cells[*local_auto_index].is_none());
|
||||
resolved_cells[*local_auto_index] =
|
||||
Some(Entry::Cell(self.resolve_cell(
|
||||
T::default(),
|
||||
0,
|
||||
@ -1483,20 +1593,43 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
|
||||
match row_group.kind {
|
||||
RowGroupKind::Header => {
|
||||
if group_range.start != 0 {
|
||||
bail!(
|
||||
row_group.span,
|
||||
"header must start at the first row";
|
||||
hint: "remove any rows before the header"
|
||||
);
|
||||
}
|
||||
let data = Header {
|
||||
start: group_range.start,
|
||||
|
||||
*header = Some(Header {
|
||||
// Later on, we have to correct this number in case there
|
||||
// is gutter. But only once all cells have been analyzed
|
||||
// and the header has fully expanded in the fixup loop
|
||||
// below.
|
||||
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
|
||||
// the footer if it is contained within the header.
|
||||
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(())
|
||||
@ -1689,47 +1821,44 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
|
||||
fn finalize_headers_and_footers(
|
||||
&self,
|
||||
has_gutter: bool,
|
||||
header: Option<Header>,
|
||||
repeat_header: bool,
|
||||
headers: &mut [Repeatable<Header>],
|
||||
footer: Option<(usize, Span, Footer)>,
|
||||
repeat_footer: bool,
|
||||
row_amount: usize,
|
||||
) -> SourceResult<(Option<Repeatable<Header>>, Option<Repeatable<Footer>>)> {
|
||||
let header = header
|
||||
.map(|mut header| {
|
||||
// Repeat the gutter below a header (hence why we don't
|
||||
// subtract 1 from the gutter case).
|
||||
// Don't do this if there are no rows under the header.
|
||||
if has_gutter {
|
||||
// - 'header.end' is always 'last y + 1'. The header stops
|
||||
// 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;
|
||||
at_least_one_cell: bool,
|
||||
) -> SourceResult<Option<Repeatable<Footer>>> {
|
||||
// Repeat the gutter below a header (hence why we don't
|
||||
// subtract 1 from the gutter case).
|
||||
// Don't do this if there are no rows under the header.
|
||||
if has_gutter {
|
||||
for header in &mut *headers {
|
||||
let header = header.unwrap_mut();
|
||||
|
||||
// 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);
|
||||
}
|
||||
header
|
||||
})
|
||||
.map(|header| {
|
||||
if repeat_header {
|
||||
Repeatable::Repeated(header)
|
||||
} else {
|
||||
Repeatable::NotRepeated(header)
|
||||
}
|
||||
});
|
||||
// Index of first y is doubled, as each row before it
|
||||
// receives a gutter row below.
|
||||
header.start *= 2;
|
||||
|
||||
// - 'header.end' is always 'last y + 1'. The header stops
|
||||
// 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
|
||||
// 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
|
||||
.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");
|
||||
}
|
||||
|
||||
let header_end =
|
||||
header.as_ref().map(Repeatable::unwrap).map(|header| header.end);
|
||||
// TODO(subfooters): will need a global slice of headers and
|
||||
// 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 {
|
||||
// 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
|
||||
// none, or the gutter is already included in the header (no
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
.transpose()?
|
||||
.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)
|
||||
} else {
|
||||
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.
|
||||
@ -1934,28 +2107,28 @@ fn expand_row_group(
|
||||
|
||||
/// Check if a cell's fixed row would conflict with a header or footer.
|
||||
fn check_for_conflicting_cell_row(
|
||||
header: Option<&Header>,
|
||||
headers: &[Repeatable<Header>],
|
||||
footer: Option<&(usize, Span, Footer)>,
|
||||
cell_y: usize,
|
||||
rowspan: usize,
|
||||
) -> HintedStrResult<()> {
|
||||
if let Some(header) = header {
|
||||
// TODO: check start (right now zero, always satisfied)
|
||||
if cell_y < header.end {
|
||||
bail!(
|
||||
"cell would conflict with header spanning the same position";
|
||||
hint: "try moving the cell or the header"
|
||||
);
|
||||
}
|
||||
// NOTE: y + rowspan >, not >=, header.start, to check if the rowspan
|
||||
// enters the header. For example, consider a rowspan of 1: if
|
||||
// `y + 1 = header.start` holds, that means `y < header.start`, and it
|
||||
// only occupies one row (`y`), so the cell is actually not in
|
||||
// conflict.
|
||||
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 {
|
||||
// NOTE: y + rowspan >, not >=, footer.start, to check if the rowspan
|
||||
// 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 {
|
||||
if let Some((_, _, footer)) = footer {
|
||||
if cell_y < footer.end && cell_y + rowspan > footer.start {
|
||||
bail!(
|
||||
"cell would conflict with footer spanning the same position";
|
||||
hint: "try reducing the cell's rowspan or moving the footer"
|
||||
@ -1981,10 +2154,11 @@ fn resolve_cell_position(
|
||||
cell_y: Smart<usize>,
|
||||
colspan: usize,
|
||||
rowspan: usize,
|
||||
header: Option<&Header>,
|
||||
headers: &[Repeatable<Header>],
|
||||
footer: Option<&(usize, Span, Footer)>,
|
||||
resolved_cells: &[Option<Entry>],
|
||||
auto_index: &mut usize,
|
||||
next_header: &mut usize,
|
||||
first_available_row: usize,
|
||||
columns: usize,
|
||||
in_row_group: bool,
|
||||
@ -2006,11 +2180,12 @@ fn resolve_cell_position(
|
||||
// but automatically-positioned cells will avoid conflicts by
|
||||
// simply skipping existing cells, headers and footers.
|
||||
let resolved_index = find_next_available_position::<false>(
|
||||
header,
|
||||
headers,
|
||||
footer,
|
||||
resolved_cells,
|
||||
columns,
|
||||
*auto_index,
|
||||
next_header,
|
||||
)?;
|
||||
|
||||
// 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
|
||||
// will already be a separate check).
|
||||
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)
|
||||
@ -2064,11 +2239,26 @@ fn resolve_cell_position(
|
||||
// ('None'), in which case we'd create a new row to place this
|
||||
// cell in.
|
||||
find_next_available_position::<true>(
|
||||
header,
|
||||
headers,
|
||||
footer,
|
||||
resolved_cells,
|
||||
columns,
|
||||
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
|
||||
// will already be a separate check).
|
||||
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.
|
||||
@ -2112,11 +2302,12 @@ fn resolve_cell_position(
|
||||
/// have cells specified by the user) as well as any headers and footers.
|
||||
#[inline]
|
||||
fn find_next_available_position<const SKIP_ROWS: bool>(
|
||||
header: Option<&Header>,
|
||||
headers: &[Repeatable<Header>],
|
||||
footer: Option<&(usize, Span, Footer)>,
|
||||
resolved_cells: &[Option<Entry<'_>>],
|
||||
columns: usize,
|
||||
initial_index: usize,
|
||||
next_header: &mut usize,
|
||||
) -> HintedStrResult<usize> {
|
||||
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.
|
||||
resolved_index += 1;
|
||||
}
|
||||
} else if let Some(header) =
|
||||
header.filter(|header| resolved_index < header.end * columns)
|
||||
} else if let Some(header) = headers
|
||||
.get(*next_header)
|
||||
.filter(|header| resolved_index >= header.start * columns)
|
||||
{
|
||||
// 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 {
|
||||
// Ensure the cell's chosen column is kept after the
|
||||
// header.
|
||||
resolved_index += initial_index % columns;
|
||||
if SKIP_ROWS {
|
||||
// Ensure the cell's chosen column is kept after the
|
||||
// header.
|
||||
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)| {
|
||||
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 typst_utils::NonZeroExt;
|
||||
@ -292,16 +292,61 @@ fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
|
||||
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 rows = rows.drain(ft.unwrap().start..);
|
||||
let rows = rows.drain(ft.start..);
|
||||
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() {
|
||||
body = elem(tag::tbody, body);
|
||||
}
|
||||
@ -492,6 +537,14 @@ pub struct TableHeader {
|
||||
#[default(true)]
|
||||
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.
|
||||
#[variadic]
|
||||
pub children: Vec<TableItem>,
|
||||
|
@ -71,10 +71,7 @@ impl Span {
|
||||
|
||||
/// Create a span that does not point into any file.
|
||||
pub const fn detached() -> Self {
|
||||
match NonZeroU64::new(Self::DETACHED) {
|
||||
Some(v) => Self(v),
|
||||
None => unreachable!(),
|
||||
}
|
||||
Self(NonZeroU64::new(Self::DETACHED).unwrap())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
const fn pack(id: FileId, low: u64) -> Self {
|
||||
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.
|
||||
None => unreachable!(),
|
||||
}
|
||||
|
||||
// The file ID is non-zero.
|
||||
Self(NonZeroU64::new(bits).unwrap())
|
||||
}
|
||||
|
||||
/// Whether the span is detached.
|
||||
|
@ -26,7 +26,7 @@ pub use once_cell;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::hash::Hash;
|
||||
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::sync::Arc;
|
||||
|
||||
@ -66,10 +66,11 @@ pub trait NonZeroExt {
|
||||
}
|
||||
|
||||
impl NonZeroExt for NonZeroUsize {
|
||||
const ONE: Self = match Self::new(1) {
|
||||
Some(v) => v,
|
||||
None => unreachable!(),
|
||||
};
|
||||
const ONE: Self = Self::new(1).unwrap();
|
||||
}
|
||||
|
||||
impl NonZeroExt for NonZeroU32 {
|
||||
const ONE: Self = Self::new(1).unwrap();
|
||||
}
|
||||
|
||||
/// Extra methods for [`Arc`].
|
||||
|
@ -95,10 +95,7 @@ impl PicoStr {
|
||||
}
|
||||
};
|
||||
|
||||
match NonZeroU64::new(value) {
|
||||
Some(value) => Ok(Self(value)),
|
||||
None => unreachable!(),
|
||||
}
|
||||
Ok(Self(NonZeroU64::new(value).unwrap()))
|
||||
}
|
||||
|
||||
/// 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 ---
|
||||
// Error: 3:3-3:19 header must start at the first row
|
||||
// Hint: 3:3-3:19 remove any rows before the header
|
||||
#grid(
|
||||
[a],
|
||||
grid.header([b])
|
||||
)
|
||||
|
||||
--- 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(
|
||||
columns: 2,
|
||||
[a],
|
||||
grid.header([b])
|
||||
)
|
||||
|
||||
--- grow-header-multiple ---
|
||||
// Error: 3:3-3:19 cannot have more than one header
|
||||
--- grid-header-multiple ---
|
||||
#grid(
|
||||
grid.header([a]),
|
||||
grid.header([b]),
|
||||
[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 ---
|
||||
// Error: 2:3-2:20 cannot use `table.header` as a grid header
|
||||
// Hint: 2:3-2:20 use `grid.header` instead
|
||||
@ -255,6 +271,17 @@
|
||||
..([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 ---
|
||||
// Empty header should just be a repeated blank row
|
||||
#set page(height: 12em)
|
||||
@ -339,6 +366,56 @@
|
||||
[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 ---
|
||||
// Test header stroke priority edge case (last header row removed)
|
||||
#set page(height: 8em)
|
||||
@ -463,8 +540,6 @@
|
||||
#table(
|
||||
columns: 3,
|
||||
[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(
|
||||
[A], table.cell(x: 1)[B], [C],
|
||||
table.cell(x: 1)[D],
|
||||
|
@ -57,3 +57,78 @@
|
||||
[d], [e], [f],
|
||||
[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),
|
||||
)
|