diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-10-27 19:04:55 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-27 18:04:55 +0000 |
| commit | be7cfc85d08c545abfac08098b7b33b4bd71f37e (patch) | |
| tree | f4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-layout/src/grid | |
| parent | b8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff) | |
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-layout/src/grid')
| -rw-r--r-- | crates/typst-layout/src/grid/cells.rs | 1349 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/layouter.rs | 1582 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/lines.rs | 1548 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/mod.rs | 416 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/repeated.rs | 192 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/rowspans.rs | 1217 |
6 files changed, 6304 insertions, 0 deletions
diff --git a/crates/typst-layout/src/grid/cells.rs b/crates/typst-layout/src/grid/cells.rs new file mode 100644 index 00000000..175e2183 --- /dev/null +++ b/crates/typst-layout/src/grid/cells.rs @@ -0,0 +1,1349 @@ +use std::num::NonZeroUsize; +use std::sync::Arc; + +use ecow::eco_format; +use typst_library::diag::{bail, At, Hint, HintedStrResult, HintedString, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{Content, Smart, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{ + Abs, Alignment, Axes, Celled, Fragment, Length, Regions, Rel, ResolvedCelled, Sides, + Sizing, +}; +use typst_library::visualize::{Paint, Stroke}; +use typst_syntax::Span; +use typst_utils::NonZeroExt; + +use super::{Footer, Header, Line, Repeatable}; + +/// Used for cell-like elements which are aware of their final properties in +/// the table, and may have property overrides. +pub trait ResolvableCell { + /// Resolves the cell's fields, given its coordinates and default grid-wide + /// fill, align, inset and stroke properties, plus the expected value of + /// the `breakable` field. + /// Returns a final Cell. + #[allow(clippy::too_many_arguments)] + fn resolve_cell<'a>( + self, + x: usize, + y: usize, + fill: &Option<Paint>, + align: Smart<Alignment>, + inset: Sides<Option<Rel<Length>>>, + stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>, + breakable: bool, + locator: Locator<'a>, + styles: StyleChain, + ) -> Cell<'a>; + + /// Returns this cell's column override. + fn x(&self, styles: StyleChain) -> Smart<usize>; + + /// Returns this cell's row override. + fn y(&self, styles: StyleChain) -> Smart<usize>; + + /// The amount of columns spanned by this cell. + fn colspan(&self, styles: StyleChain) -> NonZeroUsize; + + /// The amount of rows spanned by this cell. + fn rowspan(&self, styles: StyleChain) -> NonZeroUsize; + + /// The cell's span, for errors. + fn span(&self) -> Span; +} + +/// A grid item, possibly affected by automatic cell positioning. Can be either +/// a line or a cell. +pub enum ResolvableGridItem<T: ResolvableCell> { + /// A horizontal line in the grid. + HLine { + /// The row above which the horizontal line is drawn. + y: Smart<usize>, + start: usize, + end: Option<NonZeroUsize>, + stroke: Option<Arc<Stroke<Abs>>>, + /// The span of the corresponding line element. + span: Span, + /// The line's position. "before" here means on top of row `y`, while + /// "after" means below it. + position: LinePosition, + }, + /// A vertical line in the grid. + VLine { + /// The column before which the vertical line is drawn. + x: Smart<usize>, + start: usize, + end: Option<NonZeroUsize>, + stroke: Option<Arc<Stroke<Abs>>>, + /// The span of the corresponding line element. + span: Span, + /// The line's position. "before" here means to the left of column `x`, + /// while "after" means to its right (both considering LTR). + position: LinePosition, + }, + /// A cell in the grid. + Cell(T), +} + +/// Represents a cell in CellGrid, to be laid out by GridLayouter. +pub struct Cell<'a> { + /// The cell's body. + pub body: Content, + /// The cell's locator. + pub locator: Locator<'a>, + /// The cell's fill. + pub fill: Option<Paint>, + /// The amount of columns spanned by the cell. + pub colspan: NonZeroUsize, + /// The amount of rows spanned by the cell. + pub rowspan: NonZeroUsize, + /// The cell's stroke. + /// + /// We use an Arc to avoid unnecessary space usage when all sides are the + /// same, or when the strokes come from a common source. + pub stroke: Sides<Option<Arc<Stroke<Abs>>>>, + /// Which stroke sides were explicitly overridden by the cell, over the + /// grid's global stroke setting. + /// + /// This is used to define whether or not this cell's stroke sides should + /// have priority over adjacent cells' stroke sides, if those don't + /// override their own stroke properties (and thus have less priority when + /// defining with which stroke to draw grid lines around this cell). + pub stroke_overridden: Sides<bool>, + /// Whether rows spanned by this cell can be placed in different pages. + /// By default, a cell spanning only fixed-size rows is unbreakable, while + /// a cell spanning at least one `auto`-sized row is breakable. + pub breakable: bool, +} + +impl<'a> Cell<'a> { + /// Create a simple cell given its body and its locator. + pub fn new(body: Content, locator: Locator<'a>) -> Self { + Self { + body, + locator, + fill: None, + colspan: NonZeroUsize::ONE, + rowspan: NonZeroUsize::ONE, + stroke: Sides::splat(None), + stroke_overridden: Sides::splat(false), + breakable: true, + } + } + + /// Layout the cell into the given regions. + /// + /// The `disambiguator` indicates which instance of this cell this should be + /// layouted as. For normal cells, it is always `0`, but for headers and + /// footers, it indicates the index of the header/footer among all. See the + /// [`Locator`] docs for more details on the concepts behind this. + pub fn layout( + &self, + engine: &mut Engine, + disambiguator: usize, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let mut locator = self.locator.relayout(); + if disambiguator > 0 { + locator = locator.split().next_inner(disambiguator as u128); + } + crate::layout_fragment(engine, &self.body, locator, styles, regions) + } +} + +/// Indicates whether the line should be drawn before or after the track with +/// its index. This is mostly only relevant when gutter is used, since, then, +/// the position after a track is not the same as before the next +/// non-gutter track. +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum LinePosition { + /// The line should be drawn before its track (e.g. hline on top of a row). + Before, + /// The line should be drawn after its track (e.g. hline below a row). + After, +} + +/// A grid entry. +pub enum Entry<'a> { + /// An entry which holds a cell. + Cell(Cell<'a>), + /// An entry which is merged with another cell. + Merged { + /// The index of the cell this entry is merged with. + parent: usize, + }, +} + +impl<'a> Entry<'a> { + /// Obtains the cell inside this entry, if this is not a merged cell. + fn as_cell(&self) -> Option<&Cell<'a>> { + match self { + Self::Cell(cell) => Some(cell), + Self::Merged { .. } => None, + } + } +} + +/// 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 }, + Footer { repeat: bool, span: Span, items: I }, + Item(ResolvableGridItem<T>), +} + +/// A grid of cells, including the columns, rows, and cell data. +pub struct CellGrid<'a> { + /// The grid cells. + pub entries: Vec<Entry<'a>>, + /// The column tracks including gutter tracks. + pub cols: Vec<Sizing>, + /// The row tracks including gutter tracks. + pub rows: Vec<Sizing>, + /// The vertical lines before each column, or on the end border. + /// Gutter columns are not included. + /// Contains up to 'cols_without_gutter.len() + 1' vectors of lines. + pub vlines: Vec<Vec<Line>>, + /// The horizontal lines on top of each row, or on the bottom border. + /// 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 footer of this grid. + pub footer: Option<Repeatable<Footer>>, + /// Whether this grid has gutters. + pub has_gutter: bool, +} + +impl<'a> CellGrid<'a> { + /// Generates the cell grid, given the tracks and cells. + pub fn new( + tracks: Axes<&[Sizing]>, + gutter: Axes<&[Sizing]>, + 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) + } + + /// Resolves and positions all cells in the grid before creating it. + /// Allows them to keep track of their final properties and positions + /// and adjust their fields accordingly. + /// Cells must implement Clone as they will be owned. Additionally, they + /// must implement Default in order to fill positions in the grid which + /// weren't explicitly specified by the user with empty cells. + #[allow(clippy::too_many_arguments)] + pub fn resolve<T, C, I>( + tracks: Axes<&[Sizing]>, + gutter: Axes<&[Sizing]>, + locator: Locator<'a>, + children: C, + fill: &Celled<Option<Paint>>, + align: &Celled<Smart<Alignment>>, + inset: &Celled<Sides<Option<Rel<Length>>>>, + stroke: &ResolvedCelled<Sides<Option<Option<Arc<Stroke>>>>>, + engine: &mut Engine, + styles: StyleChain, + span: Span, + ) -> SourceResult<Self> + where + T: ResolvableCell + Default, + I: Iterator<Item = ResolvableGridItem<T>>, + C: IntoIterator<Item = ResolvableGridChild<T, I>>, + C::IntoIter: ExactSizeIterator, + { + let mut locator = locator.split(); + + // Number of content columns: Always at least one. + let c = tracks.x.len().max(1); + + // Lists of lines. + // Horizontal lines are only pushed later to be able to check for row + // validity, since the amount of rows isn't known until all items were + // analyzed in the for loop below. + // We keep their spans so we can report errors later. + // The additional boolean indicates whether the hline had an automatic + // 'y' index, and is used to change the index of hlines at the top of a + // header or footer. + let mut pending_hlines: Vec<(Span, Line, bool)> = vec![]; + + // For consistency, only push vertical lines later as well. + let mut pending_vlines: Vec<(Span, Line)> = vec![]; + let has_gutter = gutter.any(|tracks| !tracks.is_empty()); + + let mut header: Option<Header> = None; + let mut repeat_header = false; + + // 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; + + // Resolves the breakability of a cell. Cells that span at least one + // auto-sized row or gutter are considered breakable. + let resolve_breakable = |y, rowspan| { + let auto = Sizing::Auto; + let zero = Sizing::Rel(Rel::zero()); + tracks + .y + .iter() + .chain(std::iter::repeat(tracks.y.last().unwrap_or(&auto))) + .skip(y) + .take(rowspan) + .any(|row| row == &Sizing::Auto) + || gutter + .y + .iter() + .chain(std::iter::repeat(gutter.y.last().unwrap_or(&zero))) + .skip(y) + .take(rowspan - 1) + .any(|row_gutter| row_gutter == &Sizing::Auto) + }; + + // 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 + // final position in 'resolved_cells' (see below). + // Therefore, we use a counter, 'auto_index', to determine the position + // of the next cell with (x: auto, y: auto). It is only stepped when + // a cell with (x: auto, y: auto), usually the vast majority, is found. + let mut auto_index: usize = 0; + + // We have to rebuild the grid to account for arbitrary positions. + // Create at least 'children.len()' positions, since there could be at + // least 'children.len()' cells (if no explicit lines were specified), + // even though some of them might be placed in arbitrary positions and + // thus cause the grid to expand. + // Additionally, make sure we allocate up to the next multiple of 'c', + // since each row will have 'c' cells, even if the last few cells + // weren't explicitly specified by the user. + // We apply '% c' twice so that the amount of cells potentially missing + // is zero when 'children.len()' is already a multiple of 'c' (thus + // 'children.len() % c' would be zero). + let children = children.into_iter(); + let Some(child_count) = children.len().checked_add((c - children.len() % c) % c) + else { + bail!(span, "too many cells or lines were given") + }; + let mut resolved_cells: Vec<Option<Entry>> = Vec::with_capacity(child_count); + for child in children { + let mut is_header = false; + let mut is_footer = false; + let mut child_start = usize::MAX; + let mut child_end = 0; + let mut child_span = Span::detached(); + let mut start_new_row = false; + let mut first_index_of_top_hlines = usize::MAX; + let mut first_index_of_non_top_hlines = usize::MAX; + + 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"); + } + + is_header = true; + child_span = span; + repeat_header = repeat; + + // If any cell in the header is automatically positioned, + // have it skip to the next row. This is to avoid having a + // header after a partially filled row just add cells to + // that row instead of starting a new one. + // FIXME: Revise this approach when headers can start from + // arbitrary rows. + start_new_row = true; + + // Any hlines at the top of the header will start at this + // index. + first_index_of_top_hlines = pending_hlines.len(); + + (Some(items), None) + } + ResolvableGridChild::Footer { repeat, span, items, .. } => { + if footer.is_some() { + bail!(span, "cannot have more than one footer"); + } + + is_footer = true; + child_span = span; + repeat_footer = repeat; + + // If any cell in the footer is automatically positioned, + // have it skip to the next row. This is to avoid having a + // footer after a partially filled row just add cells to + // that row instead of starting a new one. + start_new_row = true; + + // Any hlines at the top of the footer will start at this + // index. + first_index_of_top_hlines = pending_hlines.len(); + + (Some(items), None) + } + ResolvableGridChild::Item(item) => (None, Some(item)), + }; + + let items = header_footer_items + .into_iter() + .flatten() + .chain(simple_item.into_iter()); + for item in items { + let cell = match item { + ResolvableGridItem::HLine { + y, + start, + end, + stroke, + span, + position, + } => { + let has_auto_y = y.is_auto(); + let y = y.unwrap_or_else(|| { + // Avoid placing the hline inside consecutive + // rowspans occupying all columns, as it'd just + // disappear, at least when there's no column + // gutter. + skip_auto_index_through_fully_merged_rows( + &resolved_cells, + &mut auto_index, + c, + ); + + // When no 'y' is specified for the hline, we place + // it under the latest automatically positioned + // cell. + // The current value of the auto index is always + // the index of the latest automatically positioned + // cell placed plus one (that's what we do in + // 'resolve_cell_position'), so we subtract 1 to + // get that cell's index, and place the hline below + // its row. The exception is when the auto_index is + // 0, meaning no automatically positioned cell was + // placed yet. In that case, we place the hline at + // the top of the table. + // + // Exceptionally, the hline will be placed before + // the minimum auto index if the current auto index + // from previous iterations is smaller than the + // minimum it should have for the current grid + // child. Effectively, this means that a hline at + // the start of a header will always appear above + // that header's first row. Similarly for footers. + auto_index + .checked_sub(1) + .map_or(0, |last_auto_index| last_auto_index / c + 1) + }); + if end.is_some_and(|end| end.get() < start) { + bail!(span, "line cannot end before it starts"); + } + let line = Line { index: y, start, end, stroke, position }; + + // Since the amount of rows is dynamic, delay placing + // hlines until after all cells were placed so we can + // properly verify if they are valid. Note that we + // can't place hlines even if we already know they + // would be in a valid row, since it's possible that we + // pushed pending hlines in the same row as this one in + // previous iterations, and we need to ensure that + // hlines from previous iterations are pushed to the + // final vector of hlines first - the order of hlines + // must be kept, as this matters when determining which + // one "wins" in case of conflict. Pushing the current + // hline before we push pending hlines later would + // change their order! + pending_hlines.push((span, line, has_auto_y)); + continue; + } + ResolvableGridItem::VLine { + x, + start, + end, + stroke, + span, + position, + } => { + let x = x.unwrap_or_else(|| { + // When no 'x' is specified for the vline, we place + // it after the latest automatically positioned + // cell. + // The current value of the auto index is always + // the index of the latest automatically positioned + // cell placed plus one (that's what we do in + // 'resolve_cell_position'), so we subtract 1 to + // get that cell's index, and place the vline after + // its column. The exception is when the auto_index + // is 0, meaning no automatically positioned cell + // was placed yet. In that case, we place the vline + // to the left of the table. + // + // Exceptionally, a vline is also placed to the + // left of the table if we should start a new row + // for the next automatically positioned cell. + // For example, this means that a vline at + // the beginning of a header will be placed to its + // left rather than after the previous + // automatically positioned cell. Same for footers. + auto_index + .checked_sub(1) + .filter(|_| !start_new_row) + .map_or(0, |last_auto_index| last_auto_index % c + 1) + }); + if end.is_some_and(|end| end.get() < start) { + bail!(span, "line cannot end before it starts"); + } + let line = Line { index: x, start, end, stroke, position }; + + // For consistency with hlines, we only push vlines to + // the final vector of vlines after processing every + // cell. + pending_vlines.push((span, line)); + continue; + } + ResolvableGridItem::Cell(cell) => cell, + }; + let cell_span = cell.span(); + let colspan = cell.colspan(styles).get(); + let rowspan = cell.rowspan(styles).get(); + // Let's calculate the cell's final position based on its + // requested position. + let resolved_index = { + let cell_x = cell.x(styles); + let cell_y = cell.y(styles); + resolve_cell_position( + cell_x, + cell_y, + colspan, + rowspan, + &resolved_cells, + &mut auto_index, + &mut start_new_row, + c, + ) + .at(cell_span)? + }; + let x = resolved_index % c; + let y = resolved_index / c; + + if colspan > c - x { + bail!( + cell_span, + "cell's colspan would cause it to exceed the available column(s)"; + hint: "try placing the cell in another position or reducing its colspan" + ) + } + + let Some(largest_index) = c + .checked_mul(rowspan - 1) + .and_then(|full_rowspan_offset| { + resolved_index.checked_add(full_rowspan_offset) + }) + .and_then(|last_row_pos| last_row_pos.checked_add(colspan - 1)) + else { + bail!( + cell_span, + "cell would span an exceedingly large position"; + hint: "try reducing the cell's rowspan or colspan" + ) + }; + + // Let's resolve the cell so it can determine its own fields + // based on its final position. + let cell = cell.resolve_cell( + x, + y, + &fill.resolve(engine, styles, x, y)?, + align.resolve(engine, styles, x, y)?, + inset.resolve(engine, styles, x, y)?, + stroke.resolve(engine, styles, x, y)?, + resolve_breakable(y, rowspan), + locator.next(&cell_span), + styles, + ); + + if largest_index >= resolved_cells.len() { + // Ensure the length of the vector of resolved cells is + // always a multiple of 'c' by pushing full rows every + // time. Here, we add enough absent positions (later + // converted to empty cells) to ensure the last row in the + // new vector length is completely filled. This is + // necessary so that those positions, even if not + // explicitly used at the end, are eventually susceptible + // to show rules and receive grid styling, as they will be + // resolved as empty cells in a second loop below. + let Some(new_len) = largest_index + .checked_add(1) + .and_then(|new_len| new_len.checked_add((c - new_len % c) % c)) + else { + bail!(cell_span, "cell position too large") + }; + + // Here, the cell needs to be placed in a position which + // doesn't exist yet in the grid (out of bounds). We will + // add enough absent positions for this to be possible. + // They must be absent as no cells actually occupy them + // (they can be overridden later); however, if no cells + // occupy them as we finish building the grid, then such + // positions will be replaced by empty cells. + resolved_cells.resize_with(new_len, || None); + } + + // The vector is large enough to contain the cell, so we can + // just index it directly to access the position it will be + // placed in. However, we still need to ensure we won't try to + // place a cell where there already is one. + let slot = &mut resolved_cells[resolved_index]; + if slot.is_some() { + bail!( + cell_span, + "attempted to place a second cell at column {x}, row {y}"; + hint: "try specifying your cells in a different order" + ); + } + + *slot = Some(Entry::Cell(cell)); + + // Now, if the cell spans more than one row or column, we fill + // the spanned positions in the grid with Entry::Merged + // pointing to the original cell as its parent. + for rowspan_offset in 0..rowspan { + let spanned_y = y + rowspan_offset; + let first_row_index = resolved_index + c * rowspan_offset; + for (colspan_offset, slot) in resolved_cells[first_row_index..] + [..colspan] + .iter_mut() + .enumerate() + { + let spanned_x = x + colspan_offset; + if spanned_x == x && spanned_y == y { + // This is the parent cell. + continue; + } + if slot.is_some() { + bail!( + cell_span, + "cell would span a previously placed cell at column {spanned_x}, row {spanned_y}"; + hint: "try specifying your cells in a different order or reducing the cell's rowspan or colspan" + ) + } + *slot = Some(Entry::Merged { parent: resolved_index }); + } + } + + if is_header || is_footer { + // Ensure each cell in a header or footer is fully + // contained within it. + child_start = child_start.min(y); + child_end = child_end.max(y + rowspan); + + if start_new_row && child_start <= auto_index.div_ceil(c) { + // No need to start a new row as we already include + // the row of the next automatically positioned cell in + // the header or footer. + start_new_row = false; + } + + if !start_new_row { + // From now on, upcoming hlines won't be at the top of + // the child, as the first automatically positioned + // cell was placed. + first_index_of_non_top_hlines = + first_index_of_non_top_hlines.min(pending_hlines.len()); + } + } + } + + if (is_header || is_footer) && child_start == usize::MAX { + // Empty header/footer: consider the header/footer to be + // at the next empty row after the latest auto index. + auto_index = find_next_empty_row(&resolved_cells, auto_index, c); + child_start = auto_index.div_ceil(c); + child_end = child_start + 1; + + if resolved_cells.len() <= c * child_start { + // Ensure the automatically chosen row actually exists. + resolved_cells.resize_with(c * (child_start + 1), || None); + } + } + + if is_header { + if child_start != 0 { + bail!( + child_span, + "header must start at the first row"; + hint: "remove any rows before the header" + ); + } + + header = Some(Header { + // Later on, we have to correct this number in case there + // is gutter. But only once all cells have been analyzed + // and the header has fully expanded in the fixup loop + // below. + end: child_end, + }); + } + + if is_footer { + // Only check if the footer is at the end later, once we know + // the final amount of rows. + footer = Some(( + child_end, + child_span, + Footer { + // Later on, we have to correct this number in case there + // is gutter, but only once all cells have been analyzed + // and the header's and footer's exact boundaries are + // known. That is because the gutter row immediately + // before the footer might not be included as part of + // the footer if it is contained within the header. + start: child_start, + }, + )); + } + + if is_header || is_footer { + let amount_hlines = pending_hlines.len(); + for (_, top_hline, has_auto_y) in pending_hlines + .get_mut( + first_index_of_top_hlines + ..first_index_of_non_top_hlines.min(amount_hlines), + ) + .unwrap_or(&mut []) + { + if *has_auto_y { + // Move this hline to the top of the child, as it was + // placed before the first automatically positioned cell + // and had an automatic index. + top_hline.index = child_start; + } + } + + // Next automatically positioned cell goes under this header. + // FIXME: Consider only doing this if the header has any fully + // automatically positioned cells. Otherwise, + // `resolve_cell_position` should be smart enough to skip + // upcoming headers. + // Additionally, consider that cells with just an 'x' override + // could end up going too far back and making previous + // non-header rows into header rows (maybe they should be + // placed at the first row that is fully empty or something). + // Nothing we can do when both 'x' and 'y' were overridden, of + // course. + // None of the above are concerns for now, as headers must + // start at the first row. + auto_index = auto_index.max(c * child_end); + } + } + + // If the user specified cells occupying less rows than the given rows, + // we shall expand the grid so that it has at least the given amount of + // rows. + let Some(expected_total_cells) = c.checked_mul(tracks.y.len()) else { + bail!(span, "too many rows were specified"); + }; + let missing_cells = expected_total_cells.saturating_sub(resolved_cells.len()); + + // Fixup phase (final step in cell grid generation): + // 1. Replace absent entries by resolved empty cells, and produce a + // vector of 'Entry' from 'Option<Entry>'. + // 2. Add enough empty cells to the end of the grid such that it has at + // least the given amount of rows. + // 3. If any cells were added to the header's rows after the header's + // creation, ensure the header expands enough to accommodate them + // across all of their spanned rows. Same for the footer. + // 4. If any cells before the footer try to span it, error. + let resolved_cells = resolved_cells + .into_iter() + .chain(std::iter::repeat_with(|| None).take(missing_cells)) + .enumerate() + .map(|(i, cell)| { + if let Some(cell) = cell { + if let Some(parent_cell) = cell.as_cell() { + if let Some(header) = &mut header + { + let y = i / c; + if y < header.end { + // Ensure the header expands enough such that + // all cells inside it, even those added later, + // are fully contained within the header. + // FIXME: check if start < y < end when start can + // be != 0. + // FIXME: when start can be != 0, decide what + // happens when a cell after the header placed + // above it tries to span the header (either + // error or expand upwards). + header.end = header.end.max(y + parent_cell.rowspan.get()); + } + } + + if let Some((end, footer_span, footer)) = &mut footer { + let x = i % c; + let y = i / c; + let cell_end = y + parent_cell.rowspan.get(); + if y < footer.start && cell_end > footer.start { + // Don't allow a cell before the footer to span + // it. Surely, we could move the footer to + // start at where this cell starts, so this is + // more of a design choice, as it's unlikely + // for the user to intentionally include a cell + // before the footer spanning it but not + // being repeated with it. + bail!( + *footer_span, + "footer would conflict with a cell placed before it at column {x} row {y}"; + hint: "try reducing that cell's rowspan or moving the footer" + ); + } + if y >= footer.start && y < *end { + // Expand the footer to include all rows + // spanned by this cell, as it is inside the + // footer. + *end = (*end).max(cell_end); + } + } + } + + Ok(cell) + } else { + let x = i % c; + let y = i / c; + + // Ensure all absent entries are affected by show rules and + // grid styling by turning them into resolved empty cells. + let new_cell = T::default().resolve_cell( + x, + y, + &fill.resolve(engine, styles, x, y)?, + align.resolve(engine, styles, x, y)?, + inset.resolve(engine, styles, x, y)?, + stroke.resolve(engine, styles, x, y)?, + resolve_breakable(y, 1), + locator.next(&()), + styles, + ); + Ok(Entry::Cell(new_cell)) + } + }) + .collect::<SourceResult<Vec<Entry>>>()?; + + // Populate the final lists of lines. + // For each line type (horizontal or vertical), we keep a vector for + // every group of lines with the same index. + let mut vlines: Vec<Vec<Line>> = vec![]; + let mut hlines: Vec<Vec<Line>> = vec![]; + let row_amount = resolved_cells.len().div_ceil(c); + + for (line_span, line, _) in pending_hlines { + let y = line.index; + if y > row_amount { + bail!(line_span, "cannot place horizontal line at invalid row {y}"); + } + if y == row_amount && line.position == LinePosition::After { + bail!( + line_span, + "cannot place horizontal line at the 'bottom' position of the bottom border (y = {y})"; + hint: "set the line's position to 'top' or place it at a smaller 'y' index" + ); + } + let line = if line.position == LinePosition::After + && (!has_gutter || y + 1 == row_amount) + { + // Just place the line on top of the next row if + // there's no gutter and the line should be placed + // after the one with given index. + // + // Note that placing after the last row is also the same as + // just placing on the grid's bottom border, even with + // gutter. + Line { + index: y + 1, + position: LinePosition::Before, + ..line + } + } else { + line + }; + let y = line.index; + + if hlines.len() <= y { + hlines.resize_with(y + 1, Vec::new); + } + hlines[y].push(line); + } + + for (line_span, line) in pending_vlines { + let x = line.index; + if x > c { + bail!(line_span, "cannot place vertical line at invalid column {x}"); + } + if x == c && line.position == LinePosition::After { + bail!( + line_span, + "cannot place vertical line at the 'end' position of the end border (x = {c})"; + hint: "set the line's position to 'start' or place it at a smaller 'x' index" + ); + } + let line = + if line.position == LinePosition::After && (!has_gutter || x + 1 == c) { + // Just place the line before the next column if + // there's no gutter and the line should be placed + // after the one with given index. + // + // Note that placing after the last column is also the + // same as just placing on the grid's end border, even + // with gutter. + Line { + index: x + 1, + position: LinePosition::Before, + ..line + } + } else { + line + }; + let x = line.index; + + if vlines.len() <= x { + vlines.resize_with(x + 1, Vec::new); + } + vlines[x].push(line); + } + + 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; + + // 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) + } + }); + + let footer = footer + .map(|(footer_end, footer_span, mut footer)| { + if footer_end != row_amount { + bail!(footer_span, "footer must end at the last row"); + } + + let header_end = + header.as_ref().map(Repeatable::unwrap).map(|header| header.end); + + if has_gutter { + // Convert the footer's start index to post-gutter coordinates. + footer.start *= 2; + + // 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.map_or(true, |header_end| header_end != footer.start) { + footer.start = footer.start.saturating_sub(1); + } + } + + if header_end.is_some_and(|header_end| header_end > footer.start) { + bail!(footer_span, "header and footer must not have common rows"); + } + + Ok(footer) + }) + .transpose()? + .map(|footer| { + if repeat_footer { + Repeatable::Repeated(footer) + } else { + Repeatable::NotRepeated(footer) + } + }); + + Ok(Self::new_internal( + tracks, + gutter, + vlines, + hlines, + header, + footer, + resolved_cells, + )) + } + + /// Generates the cell grid, given the tracks and resolved entries. + pub fn new_internal( + tracks: Axes<&[Sizing]>, + gutter: Axes<&[Sizing]>, + vlines: Vec<Vec<Line>>, + hlines: Vec<Vec<Line>>, + header: Option<Repeatable<Header>>, + footer: Option<Repeatable<Footer>>, + entries: Vec<Entry<'a>>, + ) -> Self { + let mut cols = vec![]; + let mut rows = vec![]; + + // Number of content columns: Always at least one. + let c = tracks.x.len().max(1); + + // Number of content rows: At least as many as given, but also at least + // as many as needed to place each item. + let r = { + let len = entries.len(); + let given = tracks.y.len(); + let needed = len / c + (len % c).clamp(0, 1); + given.max(needed) + }; + + let has_gutter = gutter.any(|tracks| !tracks.is_empty()); + let auto = Sizing::Auto; + let zero = Sizing::Rel(Rel::zero()); + let get_or = |tracks: &[_], idx, default| { + tracks.get(idx).or(tracks.last()).copied().unwrap_or(default) + }; + + // Collect content and gutter columns. + for x in 0..c { + cols.push(get_or(tracks.x, x, auto)); + if has_gutter { + cols.push(get_or(gutter.x, x, zero)); + } + } + + // Collect content and gutter rows. + for y in 0..r { + rows.push(get_or(tracks.y, y, auto)); + if has_gutter { + rows.push(get_or(gutter.y, y, zero)); + } + } + + // Remove superfluous gutter tracks. + if has_gutter { + cols.pop(); + rows.pop(); + } + + Self { + cols, + rows, + entries, + vlines, + hlines, + header, + footer, + has_gutter, + } + } + + /// Get the grid entry in column `x` and row `y`. + /// + /// Returns `None` if it's a gutter cell. + #[track_caller] + pub fn entry(&self, x: usize, y: usize) -> Option<&Entry<'a>> { + assert!(x < self.cols.len()); + assert!(y < self.rows.len()); + + if self.has_gutter { + // Even columns and rows are children, odd ones are gutter. + if x % 2 == 0 && y % 2 == 0 { + let c = 1 + self.cols.len() / 2; + self.entries.get((y / 2) * c + x / 2) + } else { + None + } + } else { + let c = self.cols.len(); + self.entries.get(y * c + x) + } + } + + /// Get the content of the cell in column `x` and row `y`. + /// + /// Returns `None` if it's a gutter cell or merged position. + #[track_caller] + pub fn cell(&self, x: usize, y: usize) -> Option<&Cell<'a>> { + self.entry(x, y).and_then(Entry::as_cell) + } + + /// Returns the position of the parent cell of the grid entry at the given + /// position. It is guaranteed to have a non-gutter, non-merged cell at + /// the returned position, due to how the grid is built. + /// - If the entry at the given position is a cell, returns the given + /// position. + /// - If it is a merged cell, returns the parent cell's position. + /// - If it is a gutter cell, returns None. + #[track_caller] + pub fn parent_cell_position(&self, x: usize, y: usize) -> Option<Axes<usize>> { + self.entry(x, y).map(|entry| match entry { + Entry::Cell(_) => Axes::new(x, y), + Entry::Merged { parent } => { + let c = if self.has_gutter { + 1 + self.cols.len() / 2 + } else { + self.cols.len() + }; + let factor = if self.has_gutter { 2 } else { 1 }; + Axes::new(factor * (*parent % c), factor * (*parent / c)) + } + }) + } + + /// Returns the position of the actual parent cell of a merged position, + /// even if the given position is gutter, in which case we return the + /// parent of the nearest adjacent content cell which could possibly span + /// the given gutter position. If the given position is not a gutter cell, + /// then this function will return the same as `parent_cell_position` would. + /// If the given position is a gutter cell, but no cell spans it, returns + /// `None`. + /// + /// This is useful for lines. A line needs to check if a cell next to it + /// has a stroke override - even at a gutter position there could be a + /// stroke override, since a cell could be merged with two cells at both + /// ends of the gutter cell (e.g. to its left and to its right), and thus + /// that cell would impose a stroke under the gutter. This function allows + /// getting the position of that cell (which spans the given gutter + /// position, if it is gutter), if it exists; otherwise returns None (it's + /// gutter and no cell spans it). + #[track_caller] + pub fn effective_parent_cell_position( + &self, + x: usize, + y: usize, + ) -> Option<Axes<usize>> { + if self.has_gutter { + // If (x, y) is a gutter cell, we skip it (skip a gutter column and + // row) to the nearest adjacent content cell, in the direction + // which merged cells grow toward (increasing x and increasing y), + // such that we can verify if that adjacent cell is merged with the + // gutter cell by checking if its parent would come before (x, y). + // Otherwise, no cell is merged with this gutter cell, and we + // return None. + self.parent_cell_position(x + x % 2, y + y % 2) + .filter(|&parent| parent.x <= x && parent.y <= y) + } else { + self.parent_cell_position(x, y) + } + } + + /// Checks if the track with the given index is gutter. + /// Does not check if the index is a valid track. + #[inline] + pub fn is_gutter_track(&self, index: usize) -> bool { + self.has_gutter && index % 2 == 1 + } + + /// Returns the effective colspan of a cell, considering the gutters it + /// might span if the grid has gutters. + #[inline] + pub fn effective_colspan_of_cell(&self, cell: &Cell) -> usize { + if self.has_gutter { + 2 * cell.colspan.get() - 1 + } else { + cell.colspan.get() + } + } + + /// Returns the effective rowspan of a cell, considering the gutters it + /// might span if the grid has gutters. + #[inline] + pub fn effective_rowspan_of_cell(&self, cell: &Cell) -> usize { + if self.has_gutter { + 2 * cell.rowspan.get() - 1 + } else { + cell.rowspan.get() + } + } +} + +/// Given a cell's requested x and y, the vector with the resolved cell +/// positions, the `auto_index` counter (determines the position of the next +/// `(auto, auto)` cell) and the amount of columns in the grid, returns the +/// final index of this cell in the vector of resolved cells. +/// +/// The `start_new_row` parameter is used to ensure that, if this cell is +/// fully automatically positioned, it should start a new, empty row. This is +/// useful for headers and footers, which must start at their own rows, without +/// interference from previous cells. +#[allow(clippy::too_many_arguments)] +fn resolve_cell_position( + cell_x: Smart<usize>, + cell_y: Smart<usize>, + colspan: usize, + rowspan: usize, + resolved_cells: &[Option<Entry>], + auto_index: &mut usize, + start_new_row: &mut bool, + columns: usize, +) -> HintedStrResult<usize> { + // Translates a (x, y) position to the equivalent index in the final cell vector. + // Errors if the position would be too large. + let cell_index = |x, y: usize| { + y.checked_mul(columns) + .and_then(|row_index| row_index.checked_add(x)) + .ok_or_else(|| HintedString::from(eco_format!("cell position too large"))) + }; + match (cell_x, cell_y) { + // Fully automatic cell positioning. The cell did not + // request a coordinate. + (Smart::Auto, Smart::Auto) => { + // Let's find the first available position starting from the + // automatic position counter, searching in row-major order. + let mut resolved_index = *auto_index; + if *start_new_row { + resolved_index = + find_next_empty_row(resolved_cells, resolved_index, columns); + + // Next cell won't have to start a new row if we just did that, + // in principle. + *start_new_row = false; + } else { + while let Some(Some(_)) = resolved_cells.get(resolved_index) { + // Skip any non-absent cell positions (`Some(None)`) to + // determine where this cell will be placed. An out of + // bounds position (thus `None`) is also a valid new + // position (only requires expanding the vector). + resolved_index += 1; + } + } + + // Ensure the next cell with automatic position will be + // placed after this one (maybe not immediately after). + // + // The calculation below also affects the position of the upcoming + // automatically-positioned lines. + *auto_index = if colspan == columns { + // The cell occupies all columns, so no cells can be placed + // after it until all of its rows have been spanned. + resolved_index + colspan * rowspan + } else { + // The next cell will have to be placed at least after its + // spanned columns. + resolved_index + colspan + }; + + Ok(resolved_index) + } + // Cell has chosen at least its column. + (Smart::Custom(cell_x), cell_y) => { + if cell_x >= columns { + return Err(HintedString::from(eco_format!( + "cell could not be placed at invalid column {cell_x}" + ))); + } + if let Smart::Custom(cell_y) = cell_y { + // Cell has chosen its exact position. + cell_index(cell_x, cell_y) + } else { + // Cell has only chosen its column. + // Let's find the first row which has that column available. + let mut resolved_y = 0; + while let Some(Some(_)) = + resolved_cells.get(cell_index(cell_x, resolved_y)?) + { + // Try each row until either we reach an absent position + // (`Some(None)`) or an out of bounds position (`None`), + // in which case we'd create a new row to place this cell in. + resolved_y += 1; + } + cell_index(cell_x, resolved_y) + } + } + // Cell has only chosen its row, not its column. + (Smart::Auto, Smart::Custom(cell_y)) => { + // Let's find the first column which has that row available. + let first_row_pos = cell_index(0, cell_y)?; + let last_row_pos = first_row_pos + .checked_add(columns) + .ok_or_else(|| eco_format!("cell position too large"))?; + + (first_row_pos..last_row_pos) + .find(|possible_index| { + // Much like in the previous cases, we skip any occupied + // positions until we either reach an absent position + // (`Some(None)`) or an out of bounds position (`None`), + // in which case we can just expand the vector enough to + // place this cell. In either case, we found an available + // position. + !matches!(resolved_cells.get(*possible_index), Some(Some(_))) + }) + .ok_or_else(|| { + eco_format!( + "cell could not be placed in row {cell_y} because it was full" + ) + }) + .hint("try specifying your cells in a different order") + } + } +} + +/// Computes the index of the first cell in the next empty row in the grid, +/// starting with the given initial index. +fn find_next_empty_row( + resolved_cells: &[Option<Entry>], + initial_index: usize, + columns: usize, +) -> usize { + let mut resolved_index = initial_index.next_multiple_of(columns); + while resolved_cells + .get(resolved_index..resolved_index + columns) + .is_some_and(|row| row.iter().any(Option::is_some)) + { + // Skip non-empty rows. + resolved_index += columns; + } + + resolved_index +} + +/// Fully merged rows under the cell of latest auto index indicate rowspans +/// occupying all columns, so we skip the auto index until the shortest rowspan +/// ends, such that, in the resulting row, we will be able to place an +/// automatically positioned cell - and, in particular, hlines under it. The +/// idea is that an auto hline will be placed after the shortest such rowspan. +/// Otherwise, the hline would just be placed under the first row of those +/// rowspans and disappear (except at the presence of column gutter). +fn skip_auto_index_through_fully_merged_rows( + resolved_cells: &[Option<Entry>], + auto_index: &mut usize, + columns: usize, +) { + // If the auto index isn't currently at the start of a row, that means + // there's still at least one auto position left in the row, ignoring + // cells with manual positions, so we wouldn't have a problem in placing + // further cells or, in this case, hlines here. + if *auto_index % columns == 0 { + while resolved_cells + .get(*auto_index..*auto_index + columns) + .is_some_and(|row| { + row.iter().all(|entry| matches!(entry, Some(Entry::Merged { .. }))) + }) + { + *auto_index += columns; + } + } +} diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs new file mode 100644 index 00000000..7c94617d --- /dev/null +++ b/crates/typst-layout/src/grid/layouter.rs @@ -0,0 +1,1582 @@ +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::{ + Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel, + Size, Sizing, +}; +use typst_library::text::TextElem; +use typst_library::visualize::Geometry; +use typst_syntax::Span; +use typst_utils::{MaybeReverseIter, Numeric}; + +use super::{ + generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Cell, CellGrid, + LinePosition, LineSegment, Repeatable, Rowspan, UnbreakableRowGroup, +}; + +/// Performs grid layout. +pub struct GridLayouter<'a> { + /// The grid of cells. + pub(super) grid: &'a CellGrid<'a>, + /// The regions to layout children into. + pub(super) regions: Regions<'a>, + /// The inherited styles. + pub(super) styles: StyleChain<'a>, + /// Resolved column sizes. + pub(super) rcols: Vec<Abs>, + /// The sum of `rcols`. + pub(super) width: Abs, + /// Resolve 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. + pub(super) unbreakable_rows_left: usize, + /// 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, + /// Frames for finished regions. + pub(super) finished: Vec<Frame>, + /// 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 + /// `layout_auto_row` and `layout_relative_row`, and should not be read + /// before all header rows are fully laid out. It is usually fine because + /// header rows themselves are unbreakable, and unbreakable rows do not + /// need to read this field at all. + pub(super) header_height: Abs, + /// 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, +} + +/// Details about a resulting row piece. +#[derive(Debug)] +pub struct RowPiece { + /// The height of the segment. + pub height: Abs, + /// The index of the row. + pub y: usize, +} + +/// Produced by initial row layout, auto and relative rows are already finished, +/// fractional rows not yet. +pub(super) enum Row { + /// Finished row frame of auto or relative row with y index. + /// The last parameter indicates whether or not this is the last region + /// where this row is laid out, and it can only be false when a row uses + /// `layout_multi_row`, which in turn is only used by breakable auto rows. + Frame(Frame, usize, bool), + /// Fractional row with y index and disambiguator. + Fr(Fr, usize, usize), +} + +impl Row { + /// Returns the `y` index of this row. + fn index(&self) -> usize { + match self { + Self::Frame(_, y, _) => *y, + Self::Fr(_, y, _) => *y, + } + } +} + +impl<'a> GridLayouter<'a> { + /// Create a new grid layouter. + /// + /// This prepares grid layout by unifying content and gutter tracks. + pub fn new( + grid: &'a CellGrid<'a>, + regions: Regions<'a>, + styles: StyleChain<'a>, + span: Span, + ) -> Self { + // We use these regions for auto row measurement. Since at that moment, + // columns are already sized, we can enable horizontal expansion. + let mut regions = regions; + regions.expand = Axes::new(true, false); + + Self { + grid, + regions, + styles, + 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![], + is_rtl: TextElem::dir_in(styles) == Dir::RTL, + header_height: Abs::zero(), + footer_height: Abs::zero(), + span, + } + } + + /// Determines the columns sizes and then layouts the grid row-by-row. + pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> { + self.measure_columns(engine)?; + + if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { + // Ensure rows in the first region will be aware of the possible + // presence of the footer. + self.prepare_footer(footer, engine, 0)?; + if matches!(self.grid.header, None | Some(Repeatable::NotRepeated(_))) { + // No repeatable header, so we won't subtract it later. + self.regions.size.y -= self.footer_height; + } + } + + 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; + } + // Skip header rows during normal layout. + continue; + } + } + + if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { + if y >= footer.start { + if y == footer.start { + self.layout_footer(footer, engine, self.finished.len())?; + } + continue; + } + } + + self.layout_row(y, engine, 0)?; + } + + self.finish_region(engine, true)?; + + // Layout any missing rowspans. + // There are only two possibilities for rowspans not yet laid out + // (usually, a rowspan is laid out as soon as its last row, or any row + // after it, is laid out): + // 1. The rowspan was fully empty and only spanned fully empty auto + // rows, which were all prevented from being laid out. Those rowspans + // are ignored by 'layout_rowspan', and are not of any concern. + // + // 2. The rowspan's last row was an auto row at the last region which + // was not laid out, and no other rows were laid out after it. Those + // might still need to be laid out, so we check for them. + for rowspan in std::mem::take(&mut self.rowspans) { + self.layout_rowspan(rowspan, None, engine)?; + } + + self.render_fills_strokes() + } + + /// Layout the given row. + pub(super) fn layout_row( + &mut self, + y: usize, + engine: &mut Engine, + disambiguator: usize, + ) -> SourceResult<()> { + // Skip to next region if current one is full, but only for content + // rows, not for gutter rows, and only if we aren't laying out an + // unbreakable group of rows. + let is_content_row = !self.grid.is_gutter_track(y); + if self.unbreakable_rows_left == 0 && self.regions.is_full() && is_content_row { + self.finish_region(engine, false)?; + } + + if is_content_row { + // Gutter rows have no rowspans or possibly unbreakable cells. + self.check_for_rowspans(disambiguator, y); + self.check_for_unbreakable_rows(y, engine)?; + } + + // Don't layout gutter rows at the top of a region. + if is_content_row || !self.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)), + } + } + + self.unbreakable_rows_left = self.unbreakable_rows_left.saturating_sub(1); + + Ok(()) + } + + /// Add lines and backgrounds. + 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) + { + if self.rcols.is_empty() || rows.is_empty() { + continue; + } + + // Render grid lines. + // We collect lines into a vector before rendering so we can sort + // them based on thickness, such that the lines with largest + // thickness are drawn on top; and also so we can prepend all of + // them at once in the frame, as calling prepend() for each line, + // and thus pushing all frame items forward each time, would result + // in quadratic complexity. + let mut lines = vec![]; + + // Which line position to look for in the list of lines for a + // track, such that placing lines with those positions will + // correspond to placing them before the given track index. + // + // If the index represents a gutter track, this means the list of + // lines will actually correspond to the list of lines in the + // previous index, so we must look for lines positioned after the + // previous index, and not before, to determine which lines should + // be placed before gutter. + // + // Note that the maximum index is always an odd number when + // there's gutter, so we must check for it to ensure we don't give + // it the same treatment as a line before a gutter track. + let expected_line_position = |index, is_max_index: bool| { + if self.grid.is_gutter_track(index) && !is_max_index { + LinePosition::After + } else { + LinePosition::Before + } + }; + + // Render vertical lines. + // Render them first so horizontal lines have priority later. + for (x, dx) in points(self.rcols.iter().copied()).enumerate() { + let dx = if self.is_rtl { self.width - dx } else { dx }; + let is_end_border = x == self.grid.cols.len(); + let expected_vline_position = expected_line_position(x, is_end_border); + + let vlines_at_column = self + .grid + .vlines + .get(if !self.grid.has_gutter { + x + } else if is_end_border { + // The end border has its own vector of lines, but + // dividing it by 2 and flooring would give us the + // vector of lines with the index of the last column. + // Add 1 so we get the border's lines. + x / 2 + 1 + } else { + // If x is a gutter column, this will round down to the + // index of the previous content column, which is + // intentional - the only lines which can appear before + // a gutter column are lines for the previous column + // marked with "LinePosition::After". Therefore, we get + // the previous column's lines. Worry not, as + // 'generate_line_segments' will correctly filter lines + // based on their LinePosition for us. + // + // If x is a content column, this will correctly return + // its index before applying gutters, so nothing + // special here (lines with "LinePosition::After" would + // then be ignored for this column, as we are drawing + // lines before it, not after). + x / 2 + }) + .into_iter() + .flatten() + .filter(|line| line.position == expected_vline_position); + + let tracks = rows.iter().map(|row| (row.y, row.height)); + + // Determine all different line segments we have to draw in + // this column, and convert them to points and shapes. + // + // Even a single, uniform line might generate more than one + // segment, if it happens to cross a colspan (over which it + // must not be drawn). + let segments = generate_line_segments( + self.grid, + tracks, + x, + vlines_at_column, + vline_stroke_at_row, + ) + .map(|segment| { + let LineSegment { stroke, offset: dy, length, priority } = segment; + let stroke = (*stroke).clone().unwrap_or_default(); + let thickness = stroke.thickness; + let half = thickness / 2.0; + let target = Point::with_y(length + thickness); + let vline = Geometry::Line(target).stroked(stroke); + ( + thickness, + priority, + Point::new(dx, dy - half), + FrameItem::Shape(vline, self.span), + ) + }); + + lines.extend(segments); + } + + // Render horizontal lines. + // They are rendered second as they default to appearing on top. + // First, calculate their offsets from the top of the frame. + let hline_offsets = points(rows.iter().map(|piece| piece.height)); + + // Additionally, determine their indices (the indices of the + // rows they are drawn on top of). In principle, this will + // correspond to the rows' indices directly, except for the + // last hline index, which must be (amount of rows) in order to + // draw the table's bottom border. + let hline_indices = rows + .iter() + .map(|piece| piece.y) + .chain(std::iter::once(self.grid.rows.len())); + + // Converts a row to the corresponding index in the vector of + // hlines. + let hline_index_of_row = |y: usize| { + if !self.grid.has_gutter { + y + } else if y == self.grid.rows.len() { + y / 2 + 1 + } else { + // Check the vlines loop for an explanation regarding + // these index operations. + y / 2 + } + }; + + let get_hlines_at = |y| { + self.grid + .hlines + .get(hline_index_of_row(y)) + .map(Vec::as_slice) + .unwrap_or(&[]) + }; + + let mut prev_y = None; + for (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| { + expected_line_position( + prev_y + 1, + prev_y + 1 == self.grid.rows.len(), + ) + }) + .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 + }); + + // 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 + // last row in the header is removed, in which case we append + // both the lines under the row above us and also (later) the + // lines under the header's (removed) last row. + let prev_lines = prev_y + .filter(|prev_y| { + prev_y + 1 != y + && (!is_under_repeated_header + || self + .grid + .header + .as_ref() + .and_then(Repeatable::as_repeated) + .is_some_and(|header| prev_y + 1 != header.end)) + }) + .map(|prev_y| get_hlines_at(prev_y + 1)) + .unwrap_or(&[]); + + let expected_hline_position = + expected_line_position(y, y == self.grid.rows.len()); + + let hlines_at_y = get_hlines_at(y) + .iter() + .filter(|line| line.position == expected_hline_position); + + let top_border_hlines = if prev_y.is_none() && y != 0 { + // For lines at the top of the region, give priority to + // the lines at the top border. + get_hlines_at(0) + } else { + &[] + }; + + let mut expected_header_line_position = LinePosition::Before; + let header_hlines = if let Some((Repeatable::Repeated(header), prev_y)) = + self.grid.header.as_ref().zip(prev_y) + { + if is_under_repeated_header + && (!self.grid.has_gutter + || 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 + // the lines of what's below the repeated header. + // However, no need to do that when we're laying + // out the header for the first time, since the + // lines being normally laid out then will be + // precisely the lines below the header. + // + // Additionally, we don't repeat lines above the row + // below the header when gutter is enabled, since, in + // that case, there will be a gutter row between header + // and content, so no lines should overlap. The + // exception is when the gutter at the end of the + // header has a size of zero, which happens when only + // 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(), + ); + get_hlines_at(header.end) + } else { + &[] + } + } else { + &[] + }; + + // The effective hlines to be considered at this row index are + // chained in order of increasing priority: + // 1. Lines from the row right above us, if needed; + // 2. Lines from the current row (usually, only those are + // present); + // 3. Lines from the top border (above the top cells, hence + // 'before' position only); + // 4. Lines from the header above us, if present. + let hlines_at_row = + prev_lines + .iter() + .filter(|line| line.position == expected_prev_line_position) + .chain(hlines_at_y) + .chain( + top_border_hlines + .iter() + .filter(|line| line.position == LinePosition::Before), + ) + .chain(header_hlines.iter().filter(|line| { + line.position == expected_header_line_position + })); + + let tracks = self.rcols.iter().copied().enumerate(); + + // Normally, given an hline above row y, the row above it is + // 'y - 1' (if y > 0). However, sometimes that's not true, for + // example if 'y - 1' is in a previous region, or if 'y - 1' + // was an empty auto row which was removed. Therefore, we tell + // the hlines at this index which row is actually above them in + // the laid out region so they can include that row's bottom + // strokes in the folding process. + let local_top_y = prev_y; + + // When we're in the last region, the bottom border stroke + // doesn't necessarily gain priority like it does in previous + // regions. + let in_last_region = frame_index + 1 == frame_amount; + + // Determine all different line segments we have to draw in + // this row, and convert them to points and shapes. + let segments = generate_line_segments( + self.grid, + tracks, + y, + hlines_at_row, + |grid, y, x, stroke| { + hline_stroke_at_column( + grid, + rows, + local_top_y, + in_last_region, + y, + x, + stroke, + ) + }, + ) + .map(|segment| { + let LineSegment { stroke, offset: dx, length, priority } = segment; + let stroke = (*stroke).clone().unwrap_or_default(); + let thickness = stroke.thickness; + let half = thickness / 2.0; + let dx = if self.is_rtl { self.width - dx - length } else { dx }; + let target = Point::with_x(length + thickness); + let hline = Geometry::Line(target).stroked(stroke); + ( + thickness, + priority, + Point::new(dx - half, dy), + FrameItem::Shape(hline, self.span), + ) + }); + + // Draw later (after we sort all lines below.) + lines.extend(segments); + + prev_y = Some(y); + } + + // Sort by increasing thickness, so that we draw larger strokes + // on top. When the thickness is the same, sort by priority. + // + // Sorting by thickness avoids layering problems where a smaller + // hline appears "inside" a larger vline. When both have the same + // size, hlines are drawn on top (since the sort is stable, and + // they are pushed later). + lines.sort_by_key(|(thickness, priority, ..)| (*thickness, *priority)); + + // Render cell backgrounds. + // We collect them into a vector so they can all be prepended at + // once to the frame, together with lines. + let mut fills = vec![]; + + // Reverse with RTL so that later columns start first. + let mut dx = Abs::zero(); + for (x, &col) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { + let mut dy = Abs::zero(); + for row in rows { + // We want to only draw the fill starting at the parent + // positions of cells. However, sometimes the parent + // position is absent from the current region, either + // because the first few rows of a rowspan were empty auto + // rows and thus removed from layout, or because the parent + // cell was in a previous region (in which case we'd want + // to draw its fill again, in the current region). + // Therefore, we first analyze the parent position to see + // if the current row would be the first row spanned by the + // parent cell in this region. If so, this means we have to + // start drawing the cell's fill here. If not, we ignore + // the position `(x, row.y)`, as its fill will already have + // been rendered before. + // + // Note: In the case of gutter rows, we have to check the + // row below before discarding them fully, because a + // gutter row might be the first row spanned by a rowspan + // in this region (e.g. if the first row was empty and + // therefore removed), so its fill could start in that + // gutter row. That's why we use + // 'effective_parent_cell_position'. + let parent = self + .grid + .effective_parent_cell_position(x, row.y) + .filter(|parent| { + // Ensure this is the first column spanned by the + // cell before drawing its fill, otherwise we + // already rendered its fill in a previous + // iteration of the outer loop (and/or this is a + // gutter column, which we ignore). + // + // Additionally, we should only draw the fill when + // this row is the local parent Y for this cell, + // that is, the first row spanned by the cell's + // parent in this region, because if the parent + // cell's fill was already drawn in a previous + // region, we must render it again in later regions + // spanned by that cell. Note that said condition + // always holds when the current cell has a rowspan + // of 1 and we're not currently at a gutter row. + parent.x == x + && (parent.y == row.y + || rows + .iter() + .find(|row| row.y >= parent.y) + .is_some_and(|first_spanned_row| { + first_spanned_row.y == row.y + })) + }); + + if let Some(parent) = parent { + let cell = self.grid.cell(parent.x, parent.y).unwrap(); + let fill = cell.fill.clone(); + if let Some(fill) = fill { + let rowspan = self.grid.effective_rowspan_of_cell(cell); + let height = if rowspan == 1 { + row.height + } else { + rows.iter() + .filter(|row| { + (parent.y..parent.y + rowspan).contains(&row.y) + }) + .map(|row| row.height) + .sum() + }; + let width = self.cell_spanned_width(cell, x); + // In the grid, cell colspans expand to the right, + // so we're at the leftmost (lowest 'x') column + // spanned by the cell. However, in RTL, cells + // expand to the left. Therefore, without the + // offset below, cell fills would start at the + // rightmost visual position of a cell and extend + // over to unrelated columns to the right in RTL. + // We avoid this by ensuring the fill starts at the + // very left of the cell, even with colspan > 1. + let offset = + if self.is_rtl { -width + col } else { Abs::zero() }; + let pos = Point::new(dx + offset, dy); + let size = Size::new(width, height); + let rect = Geometry::Rect(size).filled(fill); + fills.push((pos, FrameItem::Shape(rect, self.span))); + } + } + dy += row.height; + } + dx += col; + } + + // Now we render each fill and stroke by prepending to the frame, + // such that both appear below cell contents. Fills come first so + // that they appear below lines. + frame.prepend_multiple( + fills + .into_iter() + .chain(lines.into_iter().map(|(_, _, point, shape)| (point, shape))), + ); + } + + Ok(Fragment::frames(finished)) + } + + /// Determine all column sizes. + fn measure_columns(&mut self, engine: &mut Engine) -> SourceResult<()> { + // Sum of sizes of resolved relative tracks. + let mut rel = Abs::zero(); + + // Sum of fractions of all fractional tracks. + let mut fr = Fr::zero(); + + // Resolve the size of all relative columns and compute the sum of all + // fractional tracks. + for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) { + match col { + Sizing::Auto => {} + Sizing::Rel(v) => { + let resolved = + v.resolve(self.styles).relative_to(self.regions.base().x); + *rcol = resolved; + rel += resolved; + } + Sizing::Fr(v) => fr += v, + } + } + + // Size that is not used by fixed-size columns. + let available = self.regions.size.x - rel; + if available >= Abs::zero() { + // Determine size of auto columns. + let (auto, count) = self.measure_auto_columns(engine, available)?; + + // If there is remaining space, distribute it to fractional columns, + // otherwise shrink auto columns. + let remaining = available - auto; + if remaining >= Abs::zero() { + self.grow_fractional_columns(remaining, fr); + } else { + self.shrink_auto_columns(available, count); + } + } + + // Sum up the resolved column sizes once here. + self.width = self.rcols.iter().sum(); + + Ok(()) + } + + /// Total width spanned by the cell (among resolved columns). + /// Includes spanned gutter columns. + pub(super) fn cell_spanned_width(&self, cell: &Cell, x: usize) -> Abs { + let colspan = self.grid.effective_colspan_of_cell(cell); + self.rcols.iter().skip(x).take(colspan).sum() + } + + /// Measure the size that is available to auto columns. + fn measure_auto_columns( + &mut self, + engine: &mut Engine, + available: Abs, + ) -> SourceResult<(Abs, usize)> { + let mut auto = Abs::zero(); + let mut count = 0; + let all_frac_cols = self + .grid + .cols + .iter() + .enumerate() + .filter(|(_, col)| col.is_fractional()) + .map(|(x, _)| x) + .collect::<Vec<_>>(); + + // Determine size of auto columns by laying out all cells in those + // columns, measuring them and finding the largest one. + for (x, &col) in self.grid.cols.iter().enumerate() { + if col != Sizing::Auto { + continue; + } + + let mut resolved = Abs::zero(); + for y in 0..self.grid.rows.len() { + // We get the parent cell in case this is a merged position. + let Some(parent) = self.grid.parent_cell_position(x, y) else { + continue; + }; + if parent.y != y { + // Don't check the width of rowspans more than once. + continue; + } + let cell = self.grid.cell(parent.x, parent.y).unwrap(); + let colspan = self.grid.effective_colspan_of_cell(cell); + if colspan > 1 { + let last_spanned_auto_col = self + .grid + .cols + .iter() + .enumerate() + .skip(parent.x) + .take(colspan) + .rev() + .find(|(_, col)| **col == Sizing::Auto) + .map(|(x, _)| x); + + if last_spanned_auto_col != Some(x) { + // A colspan only affects the size of the last spanned + // auto column. + continue; + } + } + + if colspan > 1 + && self.regions.size.x.is_finite() + && !all_frac_cols.is_empty() + && all_frac_cols + .iter() + .all(|x| (parent.x..parent.x + colspan).contains(x)) + { + // Additionally, as a heuristic, a colspan won't affect the + // size of auto columns if it already spans all fractional + // columns, since those would already expand to provide all + // remaining available after auto column sizing to that + // cell. However, this heuristic is only valid in finite + // regions (pages without 'auto' width), since otherwise + // the fractional columns don't expand at all. + continue; + } + + // Sum the heights of spanned rows to find the expected + // available height for the cell, unless it spans a fractional + // or auto column. + let rowspan = self.grid.effective_rowspan_of_cell(cell); + let height = self + .grid + .rows + .iter() + .skip(y) + .take(rowspan) + .try_fold(Abs::zero(), |acc, col| { + // For relative rows, we can already resolve the correct + // base and for auto and fr we could only guess anyway. + match col { + Sizing::Rel(v) => Some( + acc + v + .resolve(self.styles) + .relative_to(self.regions.base().y), + ), + _ => None, + } + }) + .unwrap_or_else(|| self.regions.base().y); + + // Don't expand this auto column more than the cell actually + // needs. To do this, we check how much the other, previously + // resolved columns provide to the cell in terms of width + // (if it is a colspan), and subtract this from its expected + // width when comparing with other cells in this column. Note + // that, since this is the last auto column spanned by this + // cell, all other auto columns will already have been resolved + // and will be considered. + // Only fractional columns will be excluded from this + // calculation, which can lead to auto columns being expanded + // unnecessarily when cells span both a fractional column and + // an auto column. One mitigation for this is the heuristic + // used above to not expand the last auto column spanned by a + // cell if it spans all fractional columns in a finite region. + let already_covered_width = self.cell_spanned_width(cell, parent.x); + + let size = Size::new(available, height); + let pod = Region::new(size, Axes::splat(false)); + let frame = cell.layout(engine, 0, self.styles, pod.into())?.into_frame(); + resolved.set_max(frame.width() - already_covered_width); + } + + self.rcols[x] = resolved; + auto += resolved; + count += 1; + } + + Ok((auto, count)) + } + + /// Distribute remaining space to fractional columns. + fn grow_fractional_columns(&mut self, remaining: Abs, fr: Fr) { + if fr.is_zero() { + return; + } + + for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) { + if let Sizing::Fr(v) = col { + *rcol = v.share(fr, remaining); + } + } + } + + /// Redistribute space to auto columns so that each gets a fair share. + fn shrink_auto_columns(&mut self, available: Abs, count: usize) { + let mut last; + let mut fair = -Abs::inf(); + let mut redistribute = available; + let mut overlarge = count; + let mut changed = true; + + // Iteratively remove columns that don't need to be shrunk. + while changed && overlarge > 0 { + changed = false; + last = fair; + fair = redistribute / (overlarge as f64); + + for (&col, &rcol) in self.grid.cols.iter().zip(&self.rcols) { + // Remove an auto column if it is not overlarge (rcol <= fair), + // but also hasn't already been removed (rcol > last). + if col == Sizing::Auto && rcol <= fair && rcol > last { + redistribute -= rcol; + overlarge -= 1; + changed = true; + } + } + } + + // Redistribute space fairly among overlarge columns. + for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) { + if col == Sizing::Auto && *rcol > fair { + *rcol = fair; + } + } + } + + /// Layout a row with automatic height. Such a row may break across multiple + /// regions. + fn layout_auto_row( + &mut self, + engine: &mut Engine, + disambiguator: usize, + y: usize, + ) -> SourceResult<()> { + // Determine the size for each region of the row. If the first region + // ends up empty for some column, skip the region and remeasure. + let mut resolved = match self.measure_auto_row( + engine, + disambiguator, + y, + true, + self.unbreakable_rows_left, + None, + )? { + Some(resolved) => resolved, + None => { + self.finish_region(engine, false)?; + self.measure_auto_row( + engine, + disambiguator, + y, + false, + self.unbreakable_rows_left, + None, + )? + .unwrap() + } + }; + + // Nothing to layout. + if resolved.is_empty() { + return Ok(()); + } + + // Layout into a single region. + if let &[first] = resolved.as_slice() { + 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; + } + + return Ok(()); + } + + // 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) + { + // Subtract header and footer heights from the region height when + // it's not the first. + target.set_max( + region.y + - if i > 0 { + self.header_height + self.footer_height + } else { + Abs::zero() + }, + ); + } + + // Layout into multiple regions. + let fragment = self.layout_multi_row(engine, disambiguator, &resolved, y)?; + let len = fragment.len(); + for (i, frame) in fragment.into_iter().enumerate() { + self.push_row(frame, y, i + 1 == len); + if i + 1 < len { + self.finish_region(engine, false)?; + } + } + + Ok(()) + } + + /// Measure the regions sizes of an auto row. The option is always `Some(_)` + /// if `can_skip` is false. + /// If `unbreakable_rows_left` is positive, this function shall only return + /// a single frame. Useful when an unbreakable rowspan crosses this auto + /// row. + /// The `row_group_data` option is used within the unbreakable row group + /// simulator to predict the height of the auto row if previous rows in the + /// group were placed in the same region. + pub(super) fn measure_auto_row( + &self, + engine: &mut Engine, + disambiguator: usize, + y: usize, + can_skip: bool, + unbreakable_rows_left: usize, + row_group_data: Option<&UnbreakableRowGroup>, + ) -> SourceResult<Option<Vec<Abs>>> { + let breakable = unbreakable_rows_left == 0; + let mut resolved: Vec<Abs> = vec![]; + let mut pending_rowspans: Vec<(usize, usize, Vec<Abs>)> = vec![]; + + for x in 0..self.rcols.len() { + // Get the parent cell in case this is a merged position. + let Some(parent) = self.grid.parent_cell_position(x, y) else { + // Skip gutter columns. + continue; + }; + if parent.x != x { + // Only check the height of a colspan once. + continue; + } + // The parent cell is never a gutter or merged position. + let cell = self.grid.cell(parent.x, parent.y).unwrap(); + let rowspan = self.grid.effective_rowspan_of_cell(cell); + + if rowspan > 1 { + let last_spanned_auto_row = self + .grid + .rows + .iter() + .enumerate() + .skip(parent.y) + .take(rowspan) + .rev() + .find(|(_, &row)| row == Sizing::Auto) + .map(|(y, _)| y); + + if last_spanned_auto_row != Some(y) { + // A rowspan should only affect the height of its last + // spanned auto row. + continue; + } + } + + let measurement_data = self.prepare_auto_row_cell_measurement( + parent, + cell, + breakable, + row_group_data, + ); + let size = Axes::new(measurement_data.width, measurement_data.height); + let backlog = + measurement_data.backlog.unwrap_or(&measurement_data.custom_backlog); + + let pod = if !breakable { + // Force cell to fit into a single region when the row is + // unbreakable, even when it is a breakable rowspan, as a best + // effort. + let mut pod: Regions = Region::new(size, self.regions.expand).into(); + pod.full = measurement_data.full; + + if measurement_data.frames_in_previous_regions > 0 { + // Best effort to conciliate a breakable rowspan which + // started at a previous region going through an + // unbreakable auto row. Ensure it goes through previously + // laid out regions, but stops at this one when measuring. + pod.backlog = backlog; + } + + pod + } else { + // This row is breakable, so measure the cell normally, with + // the initial height and backlog determined previously. + let mut pod = self.regions; + pod.size = size; + pod.backlog = backlog; + pod.full = measurement_data.full; + pod.last = measurement_data.last; + + pod + }; + + let frames = + cell.layout(engine, disambiguator, self.styles, pod)?.into_frames(); + + // Skip the first region if one cell in it is empty. Then, + // remeasure. + if let Some([first, rest @ ..]) = + frames.get(measurement_data.frames_in_previous_regions..) + { + if can_skip + && breakable + && first.is_empty() + && rest.iter().any(|frame| !frame.is_empty()) + { + return Ok(None); + } + } + + // Skip frames from previous regions if applicable. + let mut sizes = frames + .iter() + .skip(measurement_data.frames_in_previous_regions) + .map(|frame| frame.height()) + .collect::<Vec<_>>(); + + // Don't expand this row more than the cell needs. + // To figure out how much height the cell needs, we must first + // subtract, from the cell's expected height, the already resolved + // heights of its spanned rows. Note that this is the last spanned + // auto row, so all previous auto rows were already resolved, as + // well as fractional rows in previous regions. + // Additionally, we subtract the heights of fixed-size rows which + // weren't laid out yet, since those heights won't change in + // principle. + // Upcoming fractional rows are ignored. + // Upcoming gutter rows might be removed, so we need to simulate + // them. + if rowspan > 1 { + let should_simulate = self.prepare_rowspan_sizes( + y, + &mut sizes, + cell, + parent.y, + rowspan, + unbreakable_rows_left, + &measurement_data, + ); + + if should_simulate { + // Rowspan spans gutter and is breakable. We'll need to + // run a simulation to predict how much this auto row needs + // to expand so that the rowspan's contents fit into the + // table. + pending_rowspans.push((parent.y, rowspan, sizes)); + continue; + } + } + + let mut sizes = sizes.into_iter(); + + for (target, size) in resolved.iter_mut().zip(&mut sizes) { + target.set_max(size); + } + + // New heights are maximal by virtue of being new. Note that + // this extend only uses the rest of the sizes iterator. + resolved.extend(sizes); + } + + // Simulate the upcoming regions in order to predict how much we need + // to expand this auto row for rowspans which span gutter. + if !pending_rowspans.is_empty() { + self.simulate_and_measure_rowspans_in_auto_row( + y, + &mut resolved, + &pending_rowspans, + unbreakable_rows_left, + row_group_data, + disambiguator, + engine, + )?; + } + + debug_assert!(breakable || resolved.len() <= 1); + + Ok(Some(resolved)) + } + + /// Layout a row with relative height. Such a row cannot break across + /// multiple regions, but it may force a region break. + fn layout_relative_row( + &mut self, + engine: &mut Engine, + disambiguator: usize, + v: Rel<Length>, + y: usize, + ) -> SourceResult<()> { + 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; + } + + // 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. + let height = frame.height(); + while self.unbreakable_rows_left == 0 + && !self.regions.size.y.fits(height) + && !in_last_with_offset(self.regions, self.header_height + self.footer_height) + { + self.finish_region(engine, false)?; + + // Don't skip multiple regions for gutter and don't push a row. + if self.grid.is_gutter_track(y) { + return Ok(()); + } + } + + self.push_row(frame, y, true); + + Ok(()) + } + + /// Layout a row with fixed height and return its frame. + fn layout_single_row( + &mut self, + engine: &mut Engine, + disambiguator: usize, + height: Abs, + y: usize, + ) -> SourceResult<Frame> { + if !self.width.is_finite() { + bail!(self.span, "cannot create grid with infinite width"); + } + + if !height.is_finite() { + bail!(self.span, "cannot create grid with infinite height"); + } + + let mut output = Frame::soft(Size::new(self.width, height)); + let mut pos = Point::zero(); + + // Reverse the column order when using RTL. + for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { + if let Some(cell) = self.grid.cell(x, y) { + // Rowspans have a separate layout step + if cell.rowspan.get() == 1 { + let width = self.cell_spanned_width(cell, x); + let size = Size::new(width, height); + let mut pod: Regions = Region::new(size, Axes::splat(true)).into(); + if self.grid.rows[y] == Sizing::Auto + && self.unbreakable_rows_left == 0 + { + // Cells at breakable auto rows have lengths relative + // to the entire page, unlike cells in unbreakable auto + // rows. + pod.full = self.regions.full; + } + let frame = cell + .layout(engine, disambiguator, self.styles, pod)? + .into_frame(); + let mut pos = pos; + if self.is_rtl { + // In the grid, cell colspans expand to the right, + // so we're at the leftmost (lowest 'x') column + // spanned by the cell. However, in RTL, cells + // expand to the left. Therefore, without the + // offset below, the cell's contents would be laid out + // starting at its rightmost visual position and extend + // over to unrelated cells to its right in RTL. + // We avoid this by ensuring the rendered cell starts at + // the very left of the cell, even with colspan > 1. + let offset = -width + rcol; + pos.x += offset; + } + output.push_frame(pos, frame); + } + } + + pos.x += rcol; + } + + Ok(output) + } + + /// Layout a row spanning multiple regions. + fn layout_multi_row( + &mut self, + engine: &mut Engine, + disambiguator: usize, + heights: &[Abs], + y: usize, + ) -> SourceResult<Fragment> { + // Prepare frames. + let mut outputs: Vec<_> = heights + .iter() + .map(|&h| Frame::soft(Size::new(self.width, h))) + .collect(); + + // Prepare regions. + let size = Size::new(self.width, heights[0]); + let mut pod: Regions = Region::new(size, Axes::splat(true)).into(); + pod.full = self.regions.full; + pod.backlog = &heights[1..]; + + // Layout the row. + let mut pos = Point::zero(); + for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { + if let Some(cell) = self.grid.cell(x, y) { + // Rowspans have a separate layout step + if cell.rowspan.get() == 1 { + let width = self.cell_spanned_width(cell, x); + pod.size.x = width; + + // Push the layouted frames into the individual output frames. + let fragment = + cell.layout(engine, disambiguator, self.styles, pod)?; + for (output, frame) in outputs.iter_mut().zip(fragment) { + let mut pos = pos; + if self.is_rtl { + let offset = -width + rcol; + pos.x += offset; + } + output.push_frame(pos, frame); + } + } + } + + pos.x += rcol; + } + + Ok(Fragment::frames(outputs)) + } + + /// Push a row frame into the current region. + /// The `is_last` parameter must be `true` if this is the last frame which + /// 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) { + self.regions.size.y -= frame.height(); + self.lrows.push(Row::Frame(frame, y, is_last)); + } + + /// Finish rows for one region. + pub(super) fn finish_region( + &mut self, + engine: &mut Engine, + last: bool, + ) -> SourceResult<()> { + if self + .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(); + } + + // 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) + .map_or(true, |footer| footer.start != header.end) + && self.lrows.last().is_some_and(|row| row.index() < header.end) + && !in_last_with_offset( + self.regions, + self.header_height + self.footer_height, + ) + { + // Header and footer would be alone in this region, but there are more + // rows beyond the header and the footer. Push an empty region. + self.lrows.clear(); + footer_would_be_orphan = true; + } + } + + 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())?; + } + } + + // Determine the height of existing rows in the region. + let mut used = Abs::zero(); + let mut fr = Fr::zero(); + for row in &self.lrows { + match row { + Row::Frame(frame, _, _) => used += frame.height(), + Row::Fr(v, _, _) => fr += *v, + } + } + + // 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; + } + + // The frame for the region. + let mut output = Frame::soft(size); + let mut pos = Point::zero(); + let mut rrows = vec![]; + let current_region = self.finished.len(); + + // Place finished rows and layout fractional rows. + for row in std::mem::take(&mut self.lrows) { + let (frame, y, is_last) = match row { + Row::Frame(frame, y, is_last) => (frame, y, is_last), + Row::Fr(v, y, disambiguator) => { + let remaining = self.regions.full - used; + let height = v.share(fr, remaining); + (self.layout_single_row(engine, disambiguator, height, y)?, y, true) + } + }; + + let height = frame.height(); + + // Ensure rowspans which span this row will have enough space to + // be laid out over it later. + for rowspan in self + .rowspans + .iter_mut() + .filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y)) + .filter(|rowspan| { + rowspan.max_resolved_row.map_or(true, |max_row| y > max_row) + }) + { + // If the first region wasn't defined yet, it will have the + // initial value of usize::MAX, so we can set it to the current + // region's index. + if rowspan.first_region > current_region { + rowspan.first_region = current_region; + // The rowspan starts at this region, precisely at this + // row. In other regions, it will start at dy = 0. + rowspan.dy = pos.y; + // When we layout the rowspan later, the full size of the + // pod must be equal to the full size of the first region + // it appears in. + rowspan.region_full = self.regions.full; + } + let amount_missing_heights = (current_region + 1) + .saturating_sub(rowspan.heights.len() + rowspan.first_region); + + // Ensure the vector of heights is long enough such that the + // last height is the one for the current region. + rowspan + .heights + .extend(std::iter::repeat(Abs::zero()).take(amount_missing_heights)); + + // Ensure that, in this region, the rowspan will span at least + // this row. + *rowspan.heights.last_mut().unwrap() += height; + + if is_last { + // Do not extend the rowspan through this row again, even + // if it is repeated in a future region. + rowspan.max_resolved_row = Some(y); + } + } + + // We use a for loop over indices to avoid borrow checking + // problems (we need to mutate the rowspans vector, so we can't + // have an iterator actively borrowing it). We keep a separate + // 'i' variable so we can step the counter back after removing + // a rowspan (see explanation below). + let mut i = 0; + while let Some(rowspan) = self.rowspans.get(i) { + // Layout any rowspans which end at this row, but only if this is + // this row's last frame (to avoid having the rowspan stop being + // laid out at the first frame of the row). + // Any rowspans ending before this row are laid out even + // on this row's first frame. + if laid_out_footer_start.map_or(true, |footer_start| { + // If this is a footer row, then only lay out this rowspan + // if the rowspan is contained within the footer. + y < footer_start || rowspan.y >= footer_start + }) && (rowspan.y + rowspan.rowspan < y + 1 + || rowspan.y + rowspan.rowspan == y + 1 && is_last) + { + // Rowspan ends at this or an earlier row, so we take + // it from the rowspans vector and lay it out. + // It's safe to pass the current region as a possible + // region for the rowspan to be laid out in, even if + // the rowspan's last row was at an earlier region, + // because the rowspan won't have an entry for this + // region in its 'heights' vector if it doesn't span + // any rows in this region. + // + // Here we don't advance the index counter ('i') because + // a new element we haven't checked yet in this loop + // will take the index of the now removed element, so + // 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)?; + } else { + i += 1; + } + } + + output.push_frame(pos, frame); + rrows.push(RowPiece { height, y }); + pos.y += height; + } + + self.finish_region_internal(output, rrows); + + if !last { + 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; + } + + Ok(()) + } + + /// Advances to the next region, registering the finished output and + /// resolved rows for the current region in the appropriate vectors. + pub(super) fn finish_region_internal( + &mut self, + output: Frame, + resolved_rows: Vec<RowPiece>, + ) { + self.finished.push(output); + self.rrows.push(resolved_rows); + self.regions.next(); + self.initial = self.regions.size; + } +} + +/// Turn an iterator of extents into an iterator of offsets before, in between, +/// and after the extents, e.g. [10mm, 5mm] -> [0mm, 10mm, 15mm]. +pub(super) fn points( + extents: impl IntoIterator<Item = Abs>, +) -> impl Iterator<Item = Abs> { + let mut offset = Abs::zero(); + std::iter::once(Abs::zero()).chain(extents).map(move |extent| { + offset += extent; + offset + }) +} + +/// Checks if the first region of a sequence of regions is the last usable +/// region, assuming that the last region will always be occupied by some +/// specific offset height, even after calling `.next()`, due to some +/// additional logic which adds content automatically on each region turn (in +/// our case, headers). +pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool { + regions.backlog.is_empty() + && regions.last.map_or(true, |height| regions.size.y + offset == height) +} diff --git a/crates/typst-layout/src/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs new file mode 100644 index 00000000..3e89612a --- /dev/null +++ b/crates/typst-layout/src/grid/lines.rs @@ -0,0 +1,1548 @@ +use std::num::NonZeroUsize; +use std::sync::Arc; + +use typst_library::foundations::{AlternativeFold, Fold}; +use typst_library::layout::Abs; +use typst_library::visualize::Stroke; + +use super::{CellGrid, LinePosition, Repeatable, RowPiece}; + +/// Represents an explicit grid line (horizontal or vertical) specified by the +/// user. +pub struct Line { + /// The index of the track after this line. This will be the index of the + /// row a horizontal line is above of, or of the column right after a + /// vertical line. + /// + /// Must be within `0..=tracks.len()` (where `tracks` is either `grid.cols` + /// or `grid.rows`, ignoring gutter tracks, as appropriate). + pub index: usize, + /// The index of the track at which this line starts being drawn. + /// This is the first column a horizontal line appears in, or the first row + /// a vertical line appears in. + /// + /// Must be within `0..tracks.len()` minus gutter tracks. + pub start: usize, + /// The index after the last track through which the line is drawn. + /// Thus, the line is drawn through tracks `start..end` (note that `end` is + /// exclusive). + /// + /// Must be within `1..=tracks.len()` minus gutter tracks. + /// `None` indicates the line should go all the way to the end. + pub end: Option<NonZeroUsize>, + /// The line's stroke. This is `None` when the line is explicitly used to + /// override a previously specified line. + pub stroke: Option<Arc<Stroke<Abs>>>, + /// The line's position in relation to the track with its index. + pub position: LinePosition, +} + +/// Indicates which priority a particular grid line segment should have, based +/// on the highest priority configuration that defined the segment's stroke. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum StrokePriority { + /// The stroke of the segment was derived solely from the grid's global + /// stroke setting, so it should have the lowest priority. + GridStroke = 0, + /// The segment's stroke was derived (even if partially) from a cell's + /// stroke override, so it should have priority over non-overridden cell + /// strokes and be drawn on top of them (when they have the same + /// thickness). + CellStroke = 1, + /// The segment's stroke was derived from a user's explicitly placed line + /// (hline or vline), and thus should have maximum priority, drawn on top + /// of any cell strokes (when they have the same thickness). + ExplicitLine = 2, +} + +/// Data for a particular line segment in the grid as generated by +/// `generate_line_segments`. +#[derive(Debug, PartialEq, Eq)] +pub struct LineSegment { + /// The stroke with which to draw this segment. + pub stroke: Arc<Stroke<Abs>>, + /// The offset of this segment since the beginning of its axis. + /// For a vertical line segment, this is the offset since the top of the + /// table in the current page; for a horizontal line segment, this is the + /// offset since the start border of the table. + pub offset: Abs, + /// The length of this segment. + pub length: Abs, + /// The segment's drawing priority, indicating on top of which other + /// segments this one should be drawn. + pub priority: StrokePriority, +} + +/// Generates the segments of lines that should be drawn alongside a certain +/// axis in the grid, going through the given tracks (orthogonal to the lines). +/// Each returned segment contains its stroke, its offset from the start, and +/// its length. +/// +/// Accepts, as parameters, the index of the lines that should be produced +/// (for example, the column at which vertical lines will be drawn); a list of +/// user-specified lines with the same index (the `lines` parameter); whether +/// the given index corresponds to the maximum index for the line's axis; and a +/// function which returns the final stroke that should be used for each track +/// the line goes through, alongside the priority of the returned stroke (its +/// parameters are the grid, the index of the line to be drawn, the number of +/// the track to draw at and the stroke of the user hline/vline override at +/// this index to fold with, if any). Contiguous segments with the same stroke +/// and priority are joined together automatically. +/// +/// The function should return `None` for positions at which the line would +/// otherwise cross a merged cell (for example, a vline could cross a colspan), +/// in which case a new segment should be drawn after the merged cell(s), even +/// if it would have the same stroke as the previous one. +/// +/// Regarding priority, the function should return a priority of ExplicitLine +/// when the user-defined line's stroke at the current position isn't None +/// (note that it is passed by parameter to the function). When it is None, the +/// function should return a priority of CellStroke if the stroke returned was +/// given or affected by a per-cell override of the grid's global stroke. +/// When that isn't the case, the returned stroke was entirely provided by the +/// grid's global stroke, and thus a priority of GridStroke should be returned. +/// +/// Note that we assume that the tracks are sorted according to ascending +/// number, and they must be iterable over pairs of (number, size). For +/// vertical lines, for instance, `tracks` would describe the rows in the +/// current region, as pairs (row index, row height). +pub fn generate_line_segments<'grid, F, I, L>( + grid: &'grid CellGrid, + tracks: I, + index: usize, + lines: L, + line_stroke_at_track: F, +) -> impl Iterator<Item = LineSegment> + 'grid +where + F: Fn( + &CellGrid, + usize, + usize, + Option<Option<Arc<Stroke<Abs>>>>, + ) -> Option<(Arc<Stroke<Abs>>, StrokePriority)> + + 'grid, + I: IntoIterator<Item = (usize, Abs)>, + I::IntoIter: 'grid, + L: IntoIterator<Item = &'grid Line>, + L::IntoIter: Clone + 'grid, +{ + // The segment currently being drawn. + // + // It is extended for each consecutive track through which the line would + // be drawn with the same stroke and priority. + // + // Starts as None to force us to create a new segment as soon as we find + // the first track through which we should draw. + let mut current_segment: Option<LineSegment> = None; + + // How far from the start (before the first track) have we gone so far. + // Used to determine the positions at which to draw each segment. + let mut offset = Abs::zero(); + + // How much to multiply line indices by to account for gutter. + let gutter_factor = if grid.has_gutter { 2 } else { 1 }; + + // Create an iterator of line segments, which will go through each track, + // from start to finish, to create line segments and extend them until they + // are interrupted and thus yielded through the iterator. We then repeat + // the process, picking up from the track after the one at which we had + // an interruption, until we have gone through all tracks. + // + // When going through each track, we check if the current segment would be + // interrupted, either because, at this track, we hit a merged cell over + // which we shouldn't draw, or because the line would have a different + // stroke or priority at this point (so we have to start a new segment). If + // so, the current segment is yielded and its variable is either set to + // 'None' (if no segment should be drawn at the point of interruption, + // meaning we might have to create a new segment later) or to the new + // segment (if we're starting to draw a segment with a different stroke or + // priority than before). + // Otherwise (if the current segment should span the current track), it is + // simply extended (or a new one is created, if it is 'None'), and no value + // is yielded for the current track, since the segment isn't yet complete + // (the next tracks might extend it further before it is interrupted and + // yielded). That is, we yield each segment only when it is interrupted, + // since then we will know its final length for sure. + // + // After the loop is done (and thus we went through all tracks), we + // interrupt the current segment one last time, to ensure the final segment + // is always interrupted and yielded, if it wasn't interrupted earlier. + let mut tracks = tracks.into_iter(); + let lines = lines.into_iter(); + std::iter::from_fn(move || { + // Each time this closure runs, we advance the track iterator as much + // as possible before returning because the current segment was + // interrupted. The for loop is resumed from where it stopped at the + // next call due to that, ensuring we go through all tracks and then + // stop. + for (track, size) in &mut tracks { + // Get the expected line stroke at this track by folding the + // strokes of each user-specified line (with priority to the + // user-specified line specified last). + let mut line_strokes = lines + .clone() + .filter(|line| { + line.end + .map(|end| { + // Subtract 1 from end index so we stop at the last + // cell before it (don't cross one extra gutter). + let end = if grid.has_gutter { + 2 * end.get() - 1 + } else { + end.get() + }; + (gutter_factor * line.start..end).contains(&track) + }) + .unwrap_or_else(|| track >= gutter_factor * line.start) + }) + .map(|line| line.stroke.clone()); + + // Distinguish between unspecified stroke (None, if no lines + // were matched above) and specified stroke of None (Some(None), + // if some lines were matched and the one specified last had a + // stroke of None) by conditionally folding after 'next()'. + let line_stroke = line_strokes.next().map(|first_stroke| { + line_strokes.fold(first_stroke, |acc, line_stroke| line_stroke.fold(acc)) + }); + + // The function shall determine if it is appropriate to draw + // the line at this position or not (i.e. whether or not it + // would cross a merged cell), and, if so, the final stroke it + // should have (because cells near this position could have + // stroke overrides, which have priority and should be folded + // with the stroke obtained above). + // + // If we are currently already drawing a segment and the function + // indicates we should, at this track, draw some other segment + // (with a different stroke or priority), or even no segment at + // all, we interrupt and yield the current segment (which was drawn + // up to the previous track) by returning it wrapped in 'Some()' + // (which indicates, in the context of 'std::iter::from_fn', that + // our iterator isn't over yet, and this should be its next value). + if let Some((stroke, priority)) = + line_stroke_at_track(grid, index, track, line_stroke) + { + // We should draw at this position. Let's check if we were + // already drawing in the previous position. + if let Some(current_segment) = &mut current_segment { + // We are currently building a segment. Let's check if + // we should extend it to this track as well. + if current_segment.stroke == stroke + && current_segment.priority == priority + { + // Extend the current segment so it covers at least + // this track as well, since we should use the same + // stroke as in the previous one when a line goes + // through this track, with the same priority. + current_segment.length += size; + } else { + // We got a different stroke or priority now, so create + // a new segment with the new stroke and spanning the + // current track. Yield the old segment, as it was + // interrupted and is thus complete. + let new_segment = + LineSegment { stroke, offset, length: size, priority }; + let old_segment = std::mem::replace(current_segment, new_segment); + offset += size; + return Some(old_segment); + } + } else { + // We should draw here, but there is no segment + // currently being drawn, either because the last + // position had a merged cell, had a stroke + // of 'None', or because this is the first track. + // Create a new segment to draw. We start spanning this + // track. + current_segment = + Some(LineSegment { stroke, offset, length: size, priority }); + } + } else if let Some(old_segment) = Option::take(&mut current_segment) { + // We shouldn't draw here (stroke of None), so we yield the + // current segment, as it was interrupted. + offset += size; + return Some(old_segment); + } + // Either the current segment is None (meaning we didn't start + // drawing a segment yet since the last yielded one), so we keep + // searching for a track where we should draw one; or the current + // segment is Some but wasn't interrupted at this track, so we keep + // looping through the following tracks until it is interrupted, + // or we reach the end. + offset += size; + } + + // Reached the end of all tracks, so we interrupt and finish + // the current segment. Note that, on future calls to this + // closure, the current segment will necessarily be 'None', + // so the iterator will necessarily end (that is, we will return None) + // after this. + // + // Note: Fully-qualified notation because rust-analyzer is confused. + Option::take(&mut current_segment) + }) +} + +/// Returns the correct stroke with which to draw a vline right before column +/// `x` when going through row `y`, given the stroke of the user-specified line +/// at this position, if any (note that a stroke of `None` is unspecified, +/// while `Some(None)` means specified to remove any stroke at this position). +/// Also returns the stroke's drawing priority, which depends on its source. +/// +/// If the vline would go through a colspan, returns None (shouldn't be drawn). +/// If the one (when at the border) or two (otherwise) cells to the left and +/// right of the vline have right and left stroke overrides, respectively, +/// then the cells' stroke overrides are folded together with the vline's +/// stroke (with priority to the vline's stroke, followed by the right cell's +/// stroke, and, finally, the left cell's) and returned. If only one of the two +/// cells around the vline (if there are two) has an override, that cell's +/// stroke is given priority when folding. If, however, the cells around the +/// vline at this row do not have any stroke overrides, then the vline's own +/// stroke, as defined by user-specified lines (if any), is returned. +/// +/// The priority associated with the returned stroke follows the rules +/// described in the docs for `generate_line_segment`. +pub fn vline_stroke_at_row( + grid: &CellGrid, + x: usize, + y: usize, + stroke: Option<Option<Arc<Stroke<Abs>>>>, +) -> Option<(Arc<Stroke<Abs>>, StrokePriority)> { + // When the vline isn't at the border, we need to check if a colspan would + // be present between columns 'x' and 'x-1' at row 'y', and thus overlap + // with the line. + // To do so, we analyze the cell right after this vline. If it is merged + // with a cell before this line (parent.x < x) which is at this row or + // above it (parent.y <= y, which is checked by + // 'effective_parent_cell_position'), this means it would overlap with the + // vline, so the vline must not be drawn at this row. + if x != 0 && x != grid.cols.len() { + // Use 'effective_parent_cell_position' to skip the gutters, if x or y + // represent gutter tracks. + // We would then analyze the cell one column after (if at a gutter + // column), and/or one row below (if at a gutter row), in order to + // check if it would be merged with a cell before the vline. + if let Some(parent) = grid.effective_parent_cell_position(x, y) { + if parent.x < x { + // There is a colspan cell going through this vline's position, + // so don't draw it here. + return None; + } + } + } + + let (left_cell_stroke, left_cell_prioritized) = x + .checked_sub(1) + .and_then(|left_x| { + // Let's find the parent cell of the position before us, in order + // to take its right stroke, even with gutter before us. + grid.effective_parent_cell_position(left_x, y) + }) + .map(|parent| { + let left_cell = grid.cell(parent.x, parent.y).unwrap(); + (left_cell.stroke.right.clone(), left_cell.stroke_overridden.right) + }) + .unwrap_or((None, false)); + + let (right_cell_stroke, right_cell_prioritized) = if x < grid.cols.len() { + // Let's find the parent cell of the position after us, in order + // to take its left stroke, even with gutter after us. + grid.effective_parent_cell_position(x, y) + .map(|parent| { + let right_cell = grid.cell(parent.x, parent.y).unwrap(); + (right_cell.stroke.left.clone(), right_cell.stroke_overridden.left) + }) + .unwrap_or((None, false)) + } else { + (None, false) + }; + + let priority = if stroke.is_some() { + StrokePriority::ExplicitLine + } else if left_cell_prioritized || right_cell_prioritized { + StrokePriority::CellStroke + } else { + StrokePriority::GridStroke + }; + + let (prioritized_cell_stroke, deprioritized_cell_stroke) = + if left_cell_prioritized && !right_cell_prioritized { + (left_cell_stroke, right_cell_stroke) + } else { + // When both cells' strokes have the same priority, we default to + // prioritizing the right cell's left stroke. + (right_cell_stroke, left_cell_stroke) + }; + + // When both cells specify a stroke for this line segment, fold + // both strokes, with priority to either the one prioritized cell, + // or to the right cell's left stroke in case of a tie. But when one of + // them doesn't specify a stroke, the other cell's stroke should be used + // instead, regardless of priority (hence the usage of 'fold_or'). + let cell_stroke = prioritized_cell_stroke.fold_or(deprioritized_cell_stroke); + + // Fold the line stroke and folded cell strokes, if possible. + // Give priority to the explicit line stroke. + // Otherwise, use whichever of the two isn't 'none' or unspecified. + let final_stroke = stroke.fold_or(Some(cell_stroke)).flatten(); + + final_stroke.zip(Some(priority)) +} + +/// Returns the correct stroke with which to draw a hline on top of row `y` +/// when going through column `x`, given the stroke of the user-specified line +/// at this position, if any (note that a stroke of `None` is unspecified, +/// while `Some(None)` means specified to remove any stroke at this position). +/// Also returns the stroke's drawing priority, which depends on its source. +/// +/// The `local_top_y` parameter indicates which row is effectively on top of +/// this hline at the current region. This is `None` if the hline is above the +/// first row in the region, for instance. The `in_last_region` parameter +/// indicates whether this is the last region of the table. If not and this is +/// a line at the bottom border, the bottom border's line gains priority. +/// +/// If the one (when at the border) or two (otherwise) cells above and below +/// the hline have bottom and top stroke overrides, respectively, then the +/// cells' stroke overrides are folded together with the hline's stroke (with +/// priority to hline's stroke, followed by the bottom cell's stroke, and, +/// finally, the top cell's) and returned. If only one of the two cells around +/// the vline (if there are two) has an override, that cell's stroke is given +/// priority when folding. If, however, the cells around the hline at this +/// column do not have any stroke overrides, then the hline's own stroke, as +/// defined by user-specified lines (if any), is directly returned. +/// +/// The priority associated with the returned stroke follows the rules +/// described in the docs for `generate_line_segment`. +/// +/// The rows argument is needed to know which rows are effectively present in +/// the current region, in order to avoid unnecessary hline splitting when a +/// rowspan's previous rows are either in a previous region or empty (and thus +/// wouldn't overlap with the hline, since its first row in the current region +/// is below the hline). +/// +/// This function assumes columns are sorted by increasing `x`, and rows are +/// sorted by increasing `y`. +pub fn hline_stroke_at_column( + grid: &CellGrid, + rows: &[RowPiece], + local_top_y: Option<usize>, + in_last_region: bool, + y: usize, + x: usize, + stroke: Option<Option<Arc<Stroke<Abs>>>>, +) -> Option<(Arc<Stroke<Abs>>, StrokePriority)> { + // When the hline isn't at the border, we need to check if a rowspan + // would be present between rows 'y' and 'y-1' at column 'x', and thus + // overlap with the line. + // To do so, we analyze the cell right below this hline. If it is + // merged with a cell above this line (parent.y < y) which is at this + // column or before it (parent.x <= x, which is checked by + // 'effective_parent_cell_position'), this means it would overlap with the + // hline, so the hline must not be drawn at this column. + if y != 0 && y != grid.rows.len() { + // Use 'effective_parent_cell_position' to skip the gutters, if x or y + // represent gutter tracks. + // We would then analyze the cell one column after (if at a gutter + // column), and/or one row below (if at a gutter row), in order to + // check if it would be merged with a cell before the hline. + if let Some(parent) = grid.effective_parent_cell_position(x, y) { + if parent.y < y { + // Get the first 'y' spanned by the possible rowspan in this region. + // The 'parent.y' row and any other spanned rows above 'y' could be + // missing from this region, which could have lead the check above + // to be triggered, even though there is no spanned row above the + // hline in the final layout of this region, and thus no overlap + // with the hline, allowing it to be drawn regardless of the + // theoretical presence of a rowspan going across its position. + let local_parent_y = rows + .iter() + .find(|row| row.y >= parent.y) + .map(|row| row.y) + .unwrap_or(y); + + if local_parent_y < y { + // There is a rowspan cell going through this hline's + // position, so don't draw it here. + return None; + } + } + } + } + + // When the hline is at the top of the region and this isn't the first + // region, fold with the top stroke of the topmost cell at this column, + // that is, the top border. + let use_top_border_stroke = local_top_y.is_none() && y != 0; + let (top_cell_stroke, top_cell_prioritized) = local_top_y + .or(use_top_border_stroke.then_some(0)) + .and_then(|top_y| { + // Let's find the parent cell of the position above us, in order + // to take its bottom stroke, even when we're below gutter. + grid.effective_parent_cell_position(x, top_y) + }) + .map(|parent| { + let top_cell = grid.cell(parent.x, parent.y).unwrap(); + if use_top_border_stroke { + (top_cell.stroke.top.clone(), top_cell.stroke_overridden.top) + } else { + (top_cell.stroke.bottom.clone(), top_cell.stroke_overridden.bottom) + } + }) + .unwrap_or((None, false)); + + // Use the bottom border stroke with priority if we're not in the last + // region, we have the last index, and (as a failsafe) we don't have the + // last row of cells above us. + let use_bottom_border_stroke = !in_last_region + && local_top_y.map_or(true, |top_y| top_y + 1 != grid.rows.len()) + && y == grid.rows.len(); + let bottom_y = + if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y }; + let (bottom_cell_stroke, bottom_cell_prioritized) = if bottom_y < grid.rows.len() { + // Let's find the parent cell of the position below us, in order + // to take its top stroke, even when we're above gutter. + grid.effective_parent_cell_position(x, bottom_y) + .map(|parent| { + let bottom_cell = grid.cell(parent.x, parent.y).unwrap(); + if use_bottom_border_stroke { + ( + bottom_cell.stroke.bottom.clone(), + bottom_cell.stroke_overridden.bottom, + ) + } else { + (bottom_cell.stroke.top.clone(), bottom_cell.stroke_overridden.top) + } + }) + .unwrap_or((None, false)) + } else { + // No cell below the bottom border. + (None, false) + }; + + let priority = if stroke.is_some() { + StrokePriority::ExplicitLine + } else if top_cell_prioritized || bottom_cell_prioritized { + StrokePriority::CellStroke + } else { + StrokePriority::GridStroke + }; + + // 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 + }); + + // Prioritize the footer's top stroke as well where applicable. + let bottom_stroke_comes_from_footer = grid + .footer + .as_ref() + .and_then(Repeatable::as_repeated) + .is_some_and(|footer| { + // Ensure the row below us is a repeated footer. + // FIXME: Make this check more robust when footers at arbitrary + // positions are added. + local_top_y.unwrap_or(0) + 1 < footer.start && y >= footer.start + }); + + let (prioritized_cell_stroke, deprioritized_cell_stroke) = + if !use_bottom_border_stroke + && !bottom_stroke_comes_from_footer + && (use_top_border_stroke + || top_stroke_comes_from_header + || top_cell_prioritized && !bottom_cell_prioritized) + { + // Top border must always be prioritized, even if it did not + // request for that explicitly. + (top_cell_stroke, bottom_cell_stroke) + } else { + // When both cells' strokes have the same priority, we default to + // prioritizing the bottom cell's top stroke. + // Additionally, the bottom border cell's stroke always has + // priority. Same for stroke above footers. + (bottom_cell_stroke, top_cell_stroke) + }; + + // When both cells specify a stroke for this line segment, fold + // both strokes, with priority to either the one prioritized cell, + // or to the bottom cell's top stroke in case of a tie. But when one of + // them doesn't specify a stroke, the other cell's stroke should be used + // instead, regardless of priority (hence the usage of 'fold_or'). + let cell_stroke = prioritized_cell_stroke.fold_or(deprioritized_cell_stroke); + + // Fold the line stroke and folded cell strokes, if possible. + // Give priority to the explicit line stroke. + // Otherwise, use whichever of the two isn't 'none' or unspecified. + let final_stroke = stroke.fold_or(Some(cell_stroke)).flatten(); + + final_stroke.zip(Some(priority)) +} + +#[cfg(test)] +mod test { + use typst_library::foundations::Content; + use typst_library::introspection::Locator; + use typst_library::layout::{Axes, Sides, Sizing}; + use typst_utils::NonZeroExt; + + use super::super::cells::Entry; + use super::super::Cell; + use super::*; + + fn sample_cell() -> Cell<'static> { + Cell { + body: Content::default(), + locator: Locator::root(), + fill: None, + colspan: NonZeroUsize::ONE, + rowspan: NonZeroUsize::ONE, + stroke: Sides::splat(Some(Arc::new(Stroke::default()))), + stroke_overridden: Sides::splat(false), + breakable: true, + } + } + + fn cell_with_colspan_rowspan(colspan: usize, rowspan: usize) -> Cell<'static> { + Cell { + body: Content::default(), + locator: Locator::root(), + fill: None, + colspan: NonZeroUsize::try_from(colspan).unwrap(), + rowspan: NonZeroUsize::try_from(rowspan).unwrap(), + stroke: Sides::splat(Some(Arc::new(Stroke::default()))), + stroke_overridden: Sides::splat(false), + breakable: true, + } + } + + fn sample_grid_for_vlines(gutters: bool) -> CellGrid<'static> { + const COLS: usize = 4; + const ROWS: usize = 6; + let entries = vec![ + // row 0 + Entry::Cell(sample_cell()), + Entry::Cell(sample_cell()), + Entry::Cell(cell_with_colspan_rowspan(2, 1)), + Entry::Merged { parent: 2 }, + // row 1 + Entry::Cell(sample_cell()), + Entry::Cell(cell_with_colspan_rowspan(3, 1)), + Entry::Merged { parent: 5 }, + Entry::Merged { parent: 5 }, + // row 2 + Entry::Merged { parent: 4 }, + Entry::Cell(sample_cell()), + Entry::Cell(cell_with_colspan_rowspan(2, 1)), + Entry::Merged { parent: 10 }, + // row 3 + Entry::Cell(sample_cell()), + Entry::Cell(cell_with_colspan_rowspan(3, 2)), + Entry::Merged { parent: 13 }, + Entry::Merged { parent: 13 }, + // row 4 + Entry::Cell(sample_cell()), + Entry::Merged { parent: 13 }, + Entry::Merged { parent: 13 }, + Entry::Merged { parent: 13 }, + // row 5 + Entry::Cell(sample_cell()), + Entry::Cell(sample_cell()), + Entry::Cell(cell_with_colspan_rowspan(2, 1)), + Entry::Merged { parent: 22 }, + ]; + CellGrid::new_internal( + Axes::with_x(&[Sizing::Auto; COLS]), + if gutters { + Axes::new(&[Sizing::Auto; COLS - 1], &[Sizing::Auto; ROWS - 1]) + } else { + Axes::default() + }, + vec![], + vec![], + None, + None, + entries, + ) + } + + #[test] + fn test_vline_splitting_without_gutter() { + let stroke = Arc::new(Stroke::default()); + let grid = sample_grid_for_vlines(false); + let rows = &[ + RowPiece { height: Abs::pt(1.0), y: 0 }, + RowPiece { height: Abs::pt(2.0), y: 1 }, + RowPiece { height: Abs::pt(4.0), y: 2 }, + RowPiece { height: Abs::pt(8.0), y: 3 }, + RowPiece { height: Abs::pt(16.0), y: 4 }, + RowPiece { height: Abs::pt(32.0), y: 5 }, + ]; + let expected_vline_splits = &[ + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), + priority: StrokePriority::GridStroke, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), + priority: StrokePriority::GridStroke, + }], + // interrupted a few times by colspans + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2.), + length: Abs::pt(4.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8. + 16.), + length: Abs::pt(32.), + priority: StrokePriority::GridStroke, + }, + ], + // interrupted every time by colspans + vec![], + vec![LineSegment { + stroke, + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), + priority: StrokePriority::GridStroke, + }], + ]; + for (x, expected_splits) in expected_vline_splits.iter().enumerate() { + let tracks = rows.iter().map(|row| (row.y, row.height)); + assert_eq!( + expected_splits, + &generate_line_segments(&grid, tracks, x, &[], vline_stroke_at_row) + .collect::<Vec<_>>(), + ); + } + } + + #[test] + fn test_vline_splitting_with_gutter_and_per_cell_stroke() { + let stroke = Arc::new(Stroke::default()); + let grid = sample_grid_for_vlines(true); + let rows = &[ + RowPiece { height: Abs::pt(1.0), y: 0 }, + RowPiece { height: Abs::pt(2.0), y: 1 }, + RowPiece { height: Abs::pt(4.0), y: 2 }, + RowPiece { height: Abs::pt(8.0), y: 3 }, + RowPiece { height: Abs::pt(16.0), y: 4 }, + RowPiece { height: Abs::pt(32.0), y: 5 }, + RowPiece { height: Abs::pt(64.0), y: 6 }, + RowPiece { height: Abs::pt(128.0), y: 7 }, + RowPiece { height: Abs::pt(256.0), y: 8 }, + RowPiece { height: Abs::pt(512.0), y: 9 }, + RowPiece { height: Abs::pt(1024.0), y: 10 }, + ]; + // Stroke is per-cell so we skip gutter + let expected_vline_splits = &[ + // left border + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1.), + priority: StrokePriority::GridStroke, + }, + // Covers the rowspan between (original) rows 1 and 2 + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2.), + length: Abs::pt(4. + 8. + 16.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), + length: Abs::pt(64.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128.), + length: Abs::pt(256.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt( + 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512., + ), + length: Abs::pt(1024.), + priority: StrokePriority::GridStroke, + }, + ], + // gutter line below + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1.), + priority: StrokePriority::GridStroke, + }, + // Covers the rowspan between (original) rows 1 and 2 + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2.), + length: Abs::pt(4. + 8. + 16.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), + length: Abs::pt(64.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128.), + length: Abs::pt(256.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt( + 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512., + ), + length: Abs::pt(1024.), + priority: StrokePriority::GridStroke, + }, + ], + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2.), + length: Abs::pt(4.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8.), + length: Abs::pt(16.), + priority: StrokePriority::GridStroke, + }, + // Covers the rowspan between (original) rows 3 and 4 + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), + length: Abs::pt(64. + 128. + 256.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt( + 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512., + ), + length: Abs::pt(1024.), + priority: StrokePriority::GridStroke, + }, + ], + // gutter line below + // the two lines below are interrupted multiple times by colspans + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8.), + length: Abs::pt(16.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt( + 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512., + ), + length: Abs::pt(1024.), + priority: StrokePriority::GridStroke, + }, + ], + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8.), + length: Abs::pt(16.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt( + 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512., + ), + length: Abs::pt(1024.), + priority: StrokePriority::GridStroke, + }, + ], + // gutter line below + // the two lines below can only cross certain gutter rows, because + // all non-gutter cells in the following column are merged with + // cells from the previous column. + vec![], + vec![], + // right border + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2.), + length: Abs::pt(4.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8.), + length: Abs::pt(16.), + priority: StrokePriority::GridStroke, + }, + // Covers the rowspan between (original) rows 3 and 4 + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), + length: Abs::pt(64. + 128. + 256.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt( + 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512., + ), + length: Abs::pt(1024.), + priority: StrokePriority::GridStroke, + }, + ], + ]; + for (x, expected_splits) in expected_vline_splits.iter().enumerate() { + let tracks = rows.iter().map(|row| (row.y, row.height)); + assert_eq!( + expected_splits, + &generate_line_segments(&grid, tracks, x, &[], vline_stroke_at_row) + .collect::<Vec<_>>(), + ); + } + } + + #[test] + fn test_vline_splitting_with_gutter_and_explicit_vlines() { + let stroke = Arc::new(Stroke::default()); + let grid = sample_grid_for_vlines(true); + let rows = &[ + RowPiece { height: Abs::pt(1.0), y: 0 }, + RowPiece { height: Abs::pt(2.0), y: 1 }, + RowPiece { height: Abs::pt(4.0), y: 2 }, + RowPiece { height: Abs::pt(8.0), y: 3 }, + RowPiece { height: Abs::pt(16.0), y: 4 }, + RowPiece { height: Abs::pt(32.0), y: 5 }, + RowPiece { height: Abs::pt(64.0), y: 6 }, + RowPiece { height: Abs::pt(128.0), y: 7 }, + RowPiece { height: Abs::pt(256.0), y: 8 }, + RowPiece { height: Abs::pt(512.0), y: 9 }, + RowPiece { height: Abs::pt(1024.0), y: 10 }, + ]; + let expected_vline_splits = &[ + // left border + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt( + 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024., + ), + priority: StrokePriority::ExplicitLine, + }], + // gutter line below + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt( + 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024., + ), + priority: StrokePriority::ExplicitLine, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt( + 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024., + ), + priority: StrokePriority::ExplicitLine, + }], + // gutter line below + // the two lines below are interrupted multiple times by colspans + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2.), + priority: StrokePriority::ExplicitLine, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4.), + length: Abs::pt(8. + 16. + 32.), + priority: StrokePriority::ExplicitLine, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.), + length: Abs::pt(512. + 1024.), + priority: StrokePriority::ExplicitLine, + }, + ], + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2.), + priority: StrokePriority::ExplicitLine, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4.), + length: Abs::pt(8. + 16. + 32.), + priority: StrokePriority::ExplicitLine, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.), + length: Abs::pt(512. + 1024.), + priority: StrokePriority::ExplicitLine, + }, + ], + // gutter line below + // the two lines below can only cross certain gutter rows, because + // all non-gutter cells in the following column are merged with + // cells from the previous column. + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1.), + length: Abs::pt(2.), + priority: StrokePriority::ExplicitLine, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4.), + length: Abs::pt(8.), + priority: StrokePriority::ExplicitLine, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8. + 16.), + length: Abs::pt(32.), + priority: StrokePriority::ExplicitLine, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.), + length: Abs::pt(512.), + priority: StrokePriority::ExplicitLine, + }, + ], + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1.), + length: Abs::pt(2.), + priority: StrokePriority::ExplicitLine, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4.), + length: Abs::pt(8.), + priority: StrokePriority::ExplicitLine, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8. + 16.), + length: Abs::pt(32.), + priority: StrokePriority::ExplicitLine, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.), + length: Abs::pt(512.), + priority: StrokePriority::ExplicitLine, + }, + ], + // right border + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt( + 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024., + ), + priority: StrokePriority::ExplicitLine, + }], + ]; + for (x, expected_splits) in expected_vline_splits.iter().enumerate() { + let tracks = rows.iter().map(|row| (row.y, row.height)); + assert_eq!( + expected_splits, + &generate_line_segments( + &grid, + tracks, + x, + &[ + Line { + index: x, + start: 0, + end: None, + stroke: Some(stroke.clone()), + position: LinePosition::Before + }, + Line { + index: x, + start: 0, + end: None, + stroke: Some(stroke.clone()), + position: LinePosition::After + }, + ], + vline_stroke_at_row + ) + .collect::<Vec<_>>(), + ); + } + } + + fn sample_grid_for_hlines(gutters: bool) -> CellGrid<'static> { + const COLS: usize = 4; + const ROWS: usize = 9; + let entries = vec![ + // row 0 + Entry::Cell(cell_with_colspan_rowspan(1, 2)), + Entry::Cell(sample_cell()), + Entry::Cell(cell_with_colspan_rowspan(2, 2)), + Entry::Merged { parent: 2 }, + // row 1 + Entry::Merged { parent: 0 }, + Entry::Cell(sample_cell()), + Entry::Merged { parent: 2 }, + Entry::Merged { parent: 2 }, + // row 2 + Entry::Cell(sample_cell()), + Entry::Cell(sample_cell()), + Entry::Cell(sample_cell()), + Entry::Cell(sample_cell()), + // row 3 + Entry::Cell(cell_with_colspan_rowspan(4, 2)), + Entry::Merged { parent: 12 }, + Entry::Merged { parent: 12 }, + Entry::Merged { parent: 12 }, + // row 4 + Entry::Merged { parent: 12 }, + Entry::Merged { parent: 12 }, + Entry::Merged { parent: 12 }, + Entry::Merged { parent: 12 }, + // row 5 + Entry::Cell(sample_cell()), + Entry::Cell(cell_with_colspan_rowspan(1, 2)), + Entry::Cell(cell_with_colspan_rowspan(2, 1)), + Entry::Merged { parent: 22 }, + // row 6 + Entry::Cell(sample_cell()), + Entry::Merged { parent: 21 }, + Entry::Cell(sample_cell()), + Entry::Cell(sample_cell()), + // row 7 (adjacent rowspans covering the whole row) + Entry::Cell(cell_with_colspan_rowspan(2, 2)), + Entry::Merged { parent: 28 }, + Entry::Cell(cell_with_colspan_rowspan(2, 2)), + Entry::Merged { parent: 30 }, + // row 8 + Entry::Merged { parent: 28 }, + Entry::Merged { parent: 28 }, + Entry::Merged { parent: 30 }, + Entry::Merged { parent: 30 }, + ]; + CellGrid::new_internal( + Axes::with_x(&[Sizing::Auto; COLS]), + if gutters { + Axes::new(&[Sizing::Auto; COLS - 1], &[Sizing::Auto; ROWS - 1]) + } else { + Axes::default() + }, + vec![], + vec![], + None, + None, + entries, + ) + } + + #[test] + fn test_hline_splitting_without_gutter() { + let stroke = Arc::new(Stroke::default()); + let grid = sample_grid_for_hlines(false); + let columns = &[Abs::pt(1.), Abs::pt(2.), Abs::pt(4.), Abs::pt(8.)]; + // Assume all rows would be drawn in the same region, and are available. + let rows = grid + .rows + .iter() + .enumerate() + .map(|(y, _)| RowPiece { height: Abs::pt(f64::from(2u32.pow(y as u32))), y }) + .collect::<Vec<_>>(); + let expected_hline_splits = &[ + // top border + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8.), + priority: StrokePriority::GridStroke, + }], + // interrupted a few times by rowspans + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1.), + length: Abs::pt(2.), + priority: StrokePriority::GridStroke, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8.), + priority: StrokePriority::GridStroke, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8.), + priority: StrokePriority::GridStroke, + }], + // interrupted every time by rowspans + vec![], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8.), + priority: StrokePriority::GridStroke, + }], + // interrupted once by rowspan + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2.), + length: Abs::pt(4. + 8.), + priority: StrokePriority::GridStroke, + }, + ], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8.), + priority: StrokePriority::GridStroke, + }], + // interrupted every time by successive rowspans + vec![], + // bottom border + vec![LineSegment { + stroke, + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8.), + priority: StrokePriority::GridStroke, + }], + ]; + for (y, expected_splits) in expected_hline_splits.iter().enumerate() { + let tracks = columns.iter().copied().enumerate(); + assert_eq!( + expected_splits, + &generate_line_segments(&grid, tracks, y, &[], |grid, y, x, stroke| { + hline_stroke_at_column( + grid, + &rows, + y.checked_sub(1), + true, + y, + x, + stroke, + ) + }) + .collect::<Vec<_>>(), + ); + } + } + + #[test] + fn test_hline_splitting_with_gutter_and_explicit_hlines() { + let stroke = Arc::new(Stroke::default()); + let grid = sample_grid_for_hlines(true); + let columns = &[ + Abs::pt(1.0), + Abs::pt(2.0), + Abs::pt(4.0), + Abs::pt(8.0), + Abs::pt(16.0), + Abs::pt(32.0), + Abs::pt(64.0), + ]; + // Assume all rows would be drawn in the same region, and are available. + let rows = grid + .rows + .iter() + .enumerate() + .map(|(y, _)| RowPiece { height: Abs::pt(f64::from(2u32.pow(y as u32))), y }) + .collect::<Vec<_>>(); + let expected_hline_splits = &[ + // top border + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + // gutter line below + // interrupted a few times by rowspans + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1.), + length: Abs::pt(2. + 4. + 8.), + priority: StrokePriority::ExplicitLine, + }], + // interrupted a few times by rowspans + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1.), + length: Abs::pt(2. + 4. + 8.), + priority: StrokePriority::ExplicitLine, + }], + // gutter line below + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + // gutter line below + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + // gutter line below + // interrupted every time by rowspans + vec![], + // interrupted every time by rowspans + vec![], + // gutter line below + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + // gutter line below + // interrupted once by rowspan + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2.), + priority: StrokePriority::ExplicitLine, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4.), + length: Abs::pt(8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }, + ], + // interrupted once by rowspan + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2.), + priority: StrokePriority::ExplicitLine, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4.), + length: Abs::pt(8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }, + ], + // gutter line below + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + // gutter line below + // there are two consecutive rowspans, but the gutter column + // between them is free. + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4.), + length: Abs::pt(8.), + priority: StrokePriority::ExplicitLine, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4.), + length: Abs::pt(8.), + priority: StrokePriority::ExplicitLine, + }], + // bottom border + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + ]; + for (y, expected_splits) in expected_hline_splits.iter().enumerate() { + let tracks = columns.iter().copied().enumerate(); + assert_eq!( + expected_splits, + &generate_line_segments( + &grid, + tracks, + y, + &[ + Line { + index: y, + start: 0, + end: None, + stroke: Some(stroke.clone()), + position: LinePosition::Before + }, + Line { + index: y, + start: 0, + end: None, + stroke: Some(stroke.clone()), + position: LinePosition::After + }, + ], + |grid, y, x, stroke| hline_stroke_at_column( + grid, + &rows, + y.checked_sub(1), + true, + y, + x, + stroke + ) + ) + .collect::<Vec<_>>(), + ); + } + } + + #[test] + fn test_hline_splitting_considers_absent_rows() { + let grid = sample_grid_for_hlines(false); + let columns = &[Abs::pt(1.), Abs::pt(2.), Abs::pt(4.), Abs::pt(8.)]; + // Assume row 3 is absent (even though there's a rowspan between rows + // 3 and 4) + // This can happen if it is an auto row which turns out to be fully + // empty. + let rows = grid + .rows + .iter() + .enumerate() + .filter(|(y, _)| *y != 3) + .map(|(y, _)| RowPiece { height: Abs::pt(f64::from(2u32.pow(y as u32))), y }) + .collect::<Vec<_>>(); + + // Hline above row 4 is no longer blocked, since the rowspan is now + // effectively spanning just one row (at least, visibly). + assert_eq!( + &vec![LineSegment { + stroke: Arc::new(Stroke::default()), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8.), + priority: StrokePriority::GridStroke + }], + &generate_line_segments( + &grid, + columns.iter().copied().enumerate(), + 4, + &[], + |grid, y, x, stroke| hline_stroke_at_column( + grid, + &rows, + if y == 4 { Some(2) } else { y.checked_sub(1) }, + true, + y, + x, + stroke + ) + ) + .collect::<Vec<_>>() + ); + } +} diff --git a/crates/typst-layout/src/grid/mod.rs b/crates/typst-layout/src/grid/mod.rs new file mode 100644 index 00000000..769bef8c --- /dev/null +++ b/crates/typst-layout/src/grid/mod.rs @@ -0,0 +1,416 @@ +mod cells; +mod layouter; +mod lines; +mod repeated; +mod rowspans; + +pub use self::cells::{Cell, CellGrid}; +pub use self::layouter::GridLayouter; + +use std::num::NonZeroUsize; +use std::sync::Arc; + +use ecow::eco_format; +use typst_library::diag::{SourceResult, Trace, Tracepoint}; +use typst_library::engine::Engine; +use typst_library::foundations::{Fold, Packed, Smart, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{ + Abs, Alignment, Axes, Dir, Fragment, GridCell, GridChild, GridElem, GridItem, Length, + OuterHAlignment, OuterVAlignment, Regions, Rel, Sides, +}; +use typst_library::model::{TableCell, TableChild, TableElem, TableItem}; +use typst_library::text::TextElem; +use typst_library::visualize::{Paint, Stroke}; +use typst_syntax::Span; + +use self::cells::{ + LinePosition, ResolvableCell, ResolvableGridChild, ResolvableGridItem, +}; +use self::layouter::RowPiece; +use self::lines::{ + generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Line, + LineSegment, +}; +use self::repeated::{Footer, Header, Repeatable}; +use self::rowspans::{Rowspan, UnbreakableRowGroup}; + +/// Layout the grid. +#[typst_macros::time(span = elem.span())] +pub fn layout_grid( + elem: &Packed<GridElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult<Fragment> { + let inset = elem.inset(styles); + let align = elem.align(styles); + let columns = elem.columns(styles); + let rows = elem.rows(styles); + let column_gutter = elem.column_gutter(styles); + let row_gutter = elem.row_gutter(styles); + let fill = elem.fill(styles); + let stroke = elem.stroke(styles); + + let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); + let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); + // Use trace to link back to the grid when a specific cell errors + let tracepoint = || Tracepoint::Call(Some(eco_format!("grid"))); + let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles); + let children = elem.children().iter().map(|child| match child { + GridChild::Header(header) => ResolvableGridChild::Header { + repeat: header.repeat(styles), + span: header.span(), + items: header.children().iter().map(resolve_item), + }, + GridChild::Footer(footer) => ResolvableGridChild::Footer { + repeat: footer.repeat(styles), + span: footer.span(), + items: footer.children().iter().map(resolve_item), + }, + GridChild::Item(item) => { + ResolvableGridChild::Item(grid_item_to_resolvable(item, styles)) + } + }); + let grid = CellGrid::resolve( + tracks, + gutter, + locator, + children, + fill, + align, + &inset, + &stroke, + engine, + styles, + elem.span(), + ) + .trace(engine.world, tracepoint, elem.span())?; + + let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); + + // Measure the columns and layout the grid row-by-row. + layouter.layout(engine) +} + +/// Layout the table. +#[typst_macros::time(span = elem.span())] +pub fn layout_table( + elem: &Packed<TableElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult<Fragment> { + let inset = elem.inset(styles); + let align = elem.align(styles); + let columns = elem.columns(styles); + let rows = elem.rows(styles); + let column_gutter = elem.column_gutter(styles); + let row_gutter = elem.row_gutter(styles); + let fill = elem.fill(styles); + let stroke = elem.stroke(styles); + + let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); + let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); + // Use trace to link back to the table when a specific cell errors + let tracepoint = || Tracepoint::Call(Some(eco_format!("table"))); + let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles); + let children = elem.children().iter().map(|child| match child { + TableChild::Header(header) => ResolvableGridChild::Header { + repeat: header.repeat(styles), + span: header.span(), + items: header.children().iter().map(resolve_item), + }, + TableChild::Footer(footer) => ResolvableGridChild::Footer { + repeat: footer.repeat(styles), + span: footer.span(), + items: footer.children().iter().map(resolve_item), + }, + TableChild::Item(item) => { + ResolvableGridChild::Item(table_item_to_resolvable(item, styles)) + } + }); + let grid = CellGrid::resolve( + tracks, + gutter, + locator, + children, + fill, + align, + &inset, + &stroke, + engine, + styles, + elem.span(), + ) + .trace(engine.world, tracepoint, elem.span())?; + + let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); + layouter.layout(engine) +} + +fn grid_item_to_resolvable( + item: &GridItem, + styles: StyleChain, +) -> ResolvableGridItem<Packed<GridCell>> { + match item { + GridItem::HLine(hline) => ResolvableGridItem::HLine { + y: hline.y(styles), + start: hline.start(styles), + end: hline.end(styles), + stroke: hline.stroke(styles), + span: hline.span(), + position: match hline.position(styles) { + OuterVAlignment::Top => LinePosition::Before, + OuterVAlignment::Bottom => LinePosition::After, + }, + }, + GridItem::VLine(vline) => ResolvableGridItem::VLine { + x: vline.x(styles), + start: vline.start(styles), + end: vline.end(styles), + stroke: vline.stroke(styles), + span: vline.span(), + position: match vline.position(styles) { + OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::After + } + OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::Before + } + OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before, + OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, + }, + }, + GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), + } +} + +fn table_item_to_resolvable( + item: &TableItem, + styles: StyleChain, +) -> ResolvableGridItem<Packed<TableCell>> { + match item { + TableItem::HLine(hline) => ResolvableGridItem::HLine { + y: hline.y(styles), + start: hline.start(styles), + end: hline.end(styles), + stroke: hline.stroke(styles), + span: hline.span(), + position: match hline.position(styles) { + OuterVAlignment::Top => LinePosition::Before, + OuterVAlignment::Bottom => LinePosition::After, + }, + }, + TableItem::VLine(vline) => ResolvableGridItem::VLine { + x: vline.x(styles), + start: vline.start(styles), + end: vline.end(styles), + stroke: vline.stroke(styles), + span: vline.span(), + position: match vline.position(styles) { + OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::After + } + OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::Before + } + OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before, + OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, + }, + }, + TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), + } +} + +impl ResolvableCell for Packed<TableCell> { + fn resolve_cell<'a>( + mut self, + x: usize, + y: usize, + fill: &Option<Paint>, + align: Smart<Alignment>, + inset: Sides<Option<Rel<Length>>>, + stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>, + breakable: bool, + locator: Locator<'a>, + styles: StyleChain, + ) -> Cell<'a> { + let cell = &mut *self; + let colspan = cell.colspan(styles); + let rowspan = cell.rowspan(styles); + let breakable = cell.breakable(styles).unwrap_or(breakable); + let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); + + let cell_stroke = cell.stroke(styles); + let stroke_overridden = + cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); + + // Using a typical 'Sides' fold, an unspecified side loses to a + // specified side. Additionally, when both are specified, an inner + // None wins over the outer Some, and vice-versa. When both are + // specified and Some, fold occurs, which, remarkably, leads to an Arc + // clone. + // + // In the end, we flatten because, for layout purposes, an unspecified + // cell stroke is the same as specifying 'none', so we equate the two + // concepts. + let stroke = cell_stroke.fold(stroke).map(Option::flatten); + cell.push_x(Smart::Custom(x)); + cell.push_y(Smart::Custom(y)); + cell.push_fill(Smart::Custom(fill.clone())); + cell.push_align(match align { + Smart::Custom(align) => { + Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) + } + // Don't fold if the table is using outer alignment. Use the + // cell's alignment instead (which, in the end, will fold with + // the outer alignment when it is effectively displayed). + Smart::Auto => cell.align(styles), + }); + cell.push_inset(Smart::Custom( + cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), + )); + cell.push_stroke( + // Here we convert the resolved stroke to a regular stroke, however + // with resolved units (that is, 'em' converted to absolute units). + // We also convert any stroke unspecified by both the cell and the + // outer stroke ('None' in the folded stroke) to 'none', that is, + // all sides are present in the resulting Sides object accessible + // by show rules on table cells. + stroke.as_ref().map(|side| { + Some(side.as_ref().map(|cell_stroke| { + Arc::new((**cell_stroke).clone().map(Length::from)) + })) + }), + ); + cell.push_breakable(Smart::Custom(breakable)); + Cell { + body: self.pack(), + locator, + fill, + colspan, + rowspan, + stroke, + stroke_overridden, + breakable, + } + } + + fn x(&self, styles: StyleChain) -> Smart<usize> { + (**self).x(styles) + } + + fn y(&self, styles: StyleChain) -> Smart<usize> { + (**self).y(styles) + } + + fn colspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).colspan(styles) + } + + fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).rowspan(styles) + } + + fn span(&self) -> Span { + Packed::span(self) + } +} + +impl ResolvableCell for Packed<GridCell> { + fn resolve_cell<'a>( + mut self, + x: usize, + y: usize, + fill: &Option<Paint>, + align: Smart<Alignment>, + inset: Sides<Option<Rel<Length>>>, + stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>, + breakable: bool, + locator: Locator<'a>, + styles: StyleChain, + ) -> Cell<'a> { + let cell = &mut *self; + let colspan = cell.colspan(styles); + let rowspan = cell.rowspan(styles); + let breakable = cell.breakable(styles).unwrap_or(breakable); + let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); + + let cell_stroke = cell.stroke(styles); + let stroke_overridden = + cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); + + // Using a typical 'Sides' fold, an unspecified side loses to a + // specified side. Additionally, when both are specified, an inner + // None wins over the outer Some, and vice-versa. When both are + // specified and Some, fold occurs, which, remarkably, leads to an Arc + // clone. + // + // In the end, we flatten because, for layout purposes, an unspecified + // cell stroke is the same as specifying 'none', so we equate the two + // concepts. + let stroke = cell_stroke.fold(stroke).map(Option::flatten); + cell.push_x(Smart::Custom(x)); + cell.push_y(Smart::Custom(y)); + cell.push_fill(Smart::Custom(fill.clone())); + cell.push_align(match align { + Smart::Custom(align) => { + Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) + } + // Don't fold if the grid is using outer alignment. Use the + // cell's alignment instead (which, in the end, will fold with + // the outer alignment when it is effectively displayed). + Smart::Auto => cell.align(styles), + }); + cell.push_inset(Smart::Custom( + cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), + )); + cell.push_stroke( + // Here we convert the resolved stroke to a regular stroke, however + // with resolved units (that is, 'em' converted to absolute units). + // We also convert any stroke unspecified by both the cell and the + // outer stroke ('None' in the folded stroke) to 'none', that is, + // all sides are present in the resulting Sides object accessible + // by show rules on grid cells. + stroke.as_ref().map(|side| { + Some(side.as_ref().map(|cell_stroke| { + Arc::new((**cell_stroke).clone().map(Length::from)) + })) + }), + ); + cell.push_breakable(Smart::Custom(breakable)); + Cell { + body: self.pack(), + locator, + fill, + colspan, + rowspan, + stroke, + stroke_overridden, + breakable, + } + } + + fn x(&self, styles: StyleChain) -> Smart<usize> { + (**self).x(styles) + } + + fn y(&self, styles: StyleChain) -> Smart<usize> { + (**self).y(styles) + } + + fn colspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).colspan(styles) + } + + fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).rowspan(styles) + } + + fn span(&self) -> Span { + Packed::span(self) + } +} diff --git a/crates/typst-layout/src/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs new file mode 100644 index 00000000..972179da --- /dev/null +++ b/crates/typst-layout/src/grid/repeated.rs @@ -0,0 +1,192 @@ +use typst_library::diag::SourceResult; +use typst_library::engine::Engine; +use typst_library::layout::{Abs, Axes, Frame, Regions}; + +use super::layouter::GridLayouter; +use super::rowspans::UnbreakableRowGroup; + +/// A repeatable grid header. Starts at the first row. +pub struct Header { + /// The index after the last row included in this header. + pub end: usize, +} + +/// A repeatable grid footer. Stops at the last row. +pub struct Footer { + /// The first row included in this footer. + pub start: usize, +} + +/// A possibly repeatable grid object. +/// 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> { + Repeated(T), + NotRepeated(T), +} + +impl<T> Repeatable<T> { + /// Gets the value inside this repeatable, regardless of whether + /// it repeats. + pub fn unwrap(&self) -> &T { + match self { + Self::Repeated(repeated) => repeated, + Self::NotRepeated(not_repeated) => not_repeated, + } + } + + /// Returns `Some` if the value is repeated, `None` otherwise. + pub fn as_repeated(&self) -> Option<&T> { + match self { + Self::Repeated(repeated) => Some(repeated), + Self::NotRepeated(_) => None, + } + } +} + +impl<'a> GridLayouter<'a> { + /// Layouts the header's rows. + /// Skips regions as necessary. + pub fn layout_header( + &mut self, + header: &Header, + engine: &mut Engine, + disambiguator: usize, + ) -> SourceResult<()> { + let header_rows = + self.simulate_header(header, &self.regions, engine, disambiguator)?; + 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() + { + // 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; + } + + // Reset the header height for this region. + // It will be re-calculated when laying out each header row. + self.header_height = Abs::zero(); + + 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 + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height; + } + } + + // Header 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)?; + } + Ok(()) + } + + /// Simulate the header's group of rows. + pub fn simulate_header( + &self, + header: &Header, + regions: &Regions<'_>, + engine: &mut Engine, + disambiguator: usize, + ) -> SourceResult<UnbreakableRowGroup> { + // Note that we assume the invariant that any rowspan in a header is + // fully contained within that header. Therefore, there won't be any + // unbreakable rowspans exceeding the header's rows, and we can safely + // 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), + regions, + engine, + disambiguator, + ) + } + + /// Updates `self.footer_height` by simulating the footer, and skips to fitting region. + pub fn prepare_footer( + &mut self, + footer: &Footer, + engine: &mut Engine, + disambiguator: usize, + ) -> SourceResult<()> { + let footer_height = self + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height; + let mut skipped_region = false; + while self.unbreakable_rows_left == 0 + && !self.regions.size.y.fits(footer_height) + && self.regions.may_progress() + { + // Advance regions without any output until we can place the + // footer. + self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]); + skipped_region = true; + } + + self.footer_height = if skipped_region { + // Simulate the footer again; the region's 'full' might have + // changed. + self.simulate_footer(footer, &self.regions, engine, disambiguator)? + .height + } else { + footer_height + }; + + Ok(()) + } + + /// Lays out all rows in the footer. + /// They are unbreakable. + pub fn layout_footer( + &mut self, + footer: &Footer, + engine: &mut Engine, + disambiguator: usize, + ) -> SourceResult<()> { + // 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; + + 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)?; + } + + Ok(()) + } + + // Simulate the footer's group of rows. + pub fn simulate_footer( + &self, + footer: &Footer, + regions: &Regions<'_>, + engine: &mut Engine, + disambiguator: usize, + ) -> SourceResult<UnbreakableRowGroup> { + // Note that we assume the invariant that any rowspan in a footer is + // fully contained within that footer. Therefore, there won't be any + // unbreakable rowspans exceeding the footer's rows, and we can safely + // assume that the amount of unbreakable rows following the first row + // 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), + regions, + engine, + disambiguator, + ) + } +} diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs new file mode 100644 index 00000000..03b4103f --- /dev/null +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -0,0 +1,1217 @@ +use typst_library::diag::SourceResult; +use typst_library::engine::Engine; +use typst_library::foundations::Resolve; +use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; +use typst_utils::MaybeReverseIter; + +use super::layouter::{in_last_with_offset, points, Row, RowPiece}; +use super::repeated::Repeatable; +use super::{Cell, GridLayouter}; + +/// All information needed to layout a single rowspan. +pub struct Rowspan { + /// First column of this rowspan. + pub x: usize, + /// First row of this rowspan. + pub y: usize, + /// The disambiguator for laying out the cells. + pub disambiguator: usize, + /// Amount of rows spanned by the cell at (x, y). + pub rowspan: usize, + /// Whether all rows of the rowspan are part of an unbreakable row group. + /// This is true e.g. in headers and footers, regardless of what the user + /// specified for the parent cell's `breakable` field. + pub is_effectively_unbreakable: bool, + /// The horizontal offset of this rowspan in all regions. + pub dx: Abs, + /// The vertical offset of this rowspan in the first region. + pub dy: Abs, + /// The index of the first region this rowspan appears in. + pub first_region: usize, + /// The full height in the first region this rowspan appears in, for + /// relative sizing. + pub region_full: Abs, + /// The vertical space available for this rowspan in each region. + pub heights: Vec<Abs>, + /// The index of the largest resolved spanned row so far. + /// Once a spanned row is resolved and its height added to `heights`, this + /// number is increased. Older rows, even if repeated through e.g. a + /// header, will no longer contribute height to this rowspan. + /// + /// This is `None` if no spanned rows were resolved in `finish_region` yet. + pub max_resolved_row: Option<usize>, +} + +/// The output of the simulation of an unbreakable row group. +#[derive(Default)] +pub struct UnbreakableRowGroup { + /// The rows in this group of unbreakable rows. + /// Includes their indices and their predicted heights. + pub rows: Vec<(usize, Abs)>, + /// The total height of this row group. + pub height: Abs, +} + +/// Data used to measure a cell in an auto row. +pub struct CellMeasurementData<'layouter> { + /// The available width for the cell across all regions. + pub width: Abs, + /// The available height for the cell in its first region. + /// Infinite when the auto row is unbreakable. + pub height: Abs, + /// The backlog of heights available for the cell in later regions. + /// + /// When this is `None`, the `custom_backlog` field should be used instead. + /// That's because, otherwise, this field would have to contain a reference + /// to the `custom_backlog` field, which isn't possible in Rust without + /// resorting to unsafe hacks. + pub backlog: Option<&'layouter [Abs]>, + /// If the backlog needs to be built from scratch instead of reusing the + /// one at the current region, which is the case of a multi-region rowspan + /// (needs to join its backlog of already laid out heights with the current + /// backlog), then this vector will store the new backlog. + pub custom_backlog: Vec<Abs>, + /// The full height of the first region of the cell. + /// Infinite when the auto row is unbreakable. + pub full: Abs, + /// The height of the last repeated region to use in the measurement pod, + /// if any. + pub last: Option<Abs>, + /// The total height of previous rows spanned by the cell in the current + /// region (so far). + pub height_in_this_region: Abs, + /// The amount of previous regions spanned by the cell. + /// They are skipped for measurement purposes. + pub frames_in_previous_regions: usize, +} + +impl<'a> GridLayouter<'a> { + /// 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`). + /// + /// We need to do this only once we already know the heights of all + /// spanned rows, which is only possible after laying out the last row + /// spanned by the rowspan (or some row immediately after the last one). + pub fn layout_rowspan( + &mut self, + rowspan_data: Rowspan, + current_region_data: Option<(&mut Frame, &[RowPiece])>, + engine: &mut Engine, + ) -> SourceResult<()> { + let Rowspan { + x, + y, + disambiguator, + rowspan, + is_effectively_unbreakable, + dx, + dy, + first_region, + region_full, + heights, + .. + } = rowspan_data; + let [first_height, backlog @ ..] = heights.as_slice() else { + // Nothing to layout. + return Ok(()); + }; + let first_column = self.rcols[x]; + let cell = self.grid.cell(x, y).unwrap(); + let width = self.cell_spanned_width(cell, x); + let dx = if self.is_rtl { dx - width + first_column } else { dx }; + + // Prepare regions. + let size = Size::new(width, *first_height); + let mut pod: Regions = Region::new(size, Axes::splat(true)).into(); + pod.backlog = backlog; + + if !is_effectively_unbreakable + && self.grid.rows[y..][..rowspan] + .iter() + .any(|spanned_row| spanned_row == &Sizing::Auto) + { + // If the rowspan spans an auto row and is breakable, it will see + // '100%' as the full page height, at least at its first region. + // This is consistent with how it is measured, and with how + // non-rowspan cells behave in auto rows. + pod.full = region_full; + } + + // Push the layouted frames directly into the finished frames. + let fragment = cell.layout(engine, disambiguator, self.styles, pod)?; + let (current_region, current_rrows) = current_region_data.unzip(); + for ((i, finished), frame) in self + .finished + .iter_mut() + .chain(current_region.into_iter()) + .skip(first_region) + .enumerate() + .zip(fragment) + { + let dy = if i == 0 { + // At first, we draw the rowspan starting at its expected + // vertical offset in the first region. + dy + } 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() + } + }; + + finished.push_frame(Point::new(dx, dy), frame); + } + + Ok(()) + } + + /// Checks if a row contains the beginning of one or more rowspan cells. + /// If so, adds them to the rowspans vector. + pub fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) { + // We will compute the horizontal offset of each rowspan in advance. + // For that reason, we must reverse the column order when using RTL. + let offsets = points(self.rcols.iter().copied().rev_if(self.is_rtl)); + for (x, dx) in (0..self.rcols.len()).rev_if(self.is_rtl).zip(offsets) { + let Some(cell) = self.grid.cell(x, y) else { + continue; + }; + let rowspan = self.grid.effective_rowspan_of_cell(cell); + if rowspan > 1 { + // Rowspan detected. We will lay it out later. + self.rowspans.push(Rowspan { + x, + y, + disambiguator, + rowspan, + // The field below will be updated in + // 'check_for_unbreakable_rows'. + is_effectively_unbreakable: !cell.breakable, + dx, + // The four fields below will be updated in 'finish_region'. + dy: Abs::zero(), + first_region: usize::MAX, + region_full: Abs::zero(), + heights: vec![], + max_resolved_row: None, + }); + } + } + } + + /// Checks if the upcoming rows will be grouped together under an + /// unbreakable row group, and, if so, advances regions until there is + /// enough space for them. This can be needed, for example, if there's an + /// unbreakable rowspan crossing those rows. + pub fn check_for_unbreakable_rows( + &mut self, + current_row: usize, + engine: &mut Engine, + ) -> SourceResult<()> { + if self.unbreakable_rows_left == 0 { + // By default, the amount of unbreakable rows starting at the + // 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. + amount_unbreakable_rows = Some(self.grid.rows.len() - footer.start); + } + } + + let row_group = self.simulate_unbreakable_row_group( + current_row, + amount_unbreakable_rows, + &self.regions, + engine, + 0, + )?; + + // Skip to fitting region. + while !self.regions.size.y.fits(row_group.height) + && !in_last_with_offset( + self.regions, + self.header_height + self.footer_height, + ) + { + self.finish_region(engine, false)?; + } + + // Update unbreakable rows left. + self.unbreakable_rows_left = row_group.rows.len(); + } + + if self.unbreakable_rows_left > 1 { + // Mark rowspans as effectively unbreakable where applicable + // (if all of their spanned rows would be in the same unbreakable + // row group). + // Not needed if only one unbreakable row is left, since, then, + // no rowspan will be effectively unbreakable, at least necessarily. + // Note that this function is called after 'check_for_rowspans' and + // potentially updates the amount of remaining unbreakable rows, so + // it wouldn't be accurate to only check for this condition in that + // function. We need to check here instead. + for rowspan_data in + self.rowspans.iter_mut().filter(|rowspan| rowspan.y == current_row) + { + rowspan_data.is_effectively_unbreakable |= + self.unbreakable_rows_left >= rowspan_data.rowspan; + } + } + + Ok(()) + } + + /// Simulates a group of unbreakable rows, starting with the index of the + /// first row in the group. If `amount_unbreakable_rows` is `None`, keeps + /// adding rows to the group until none have unbreakable cells in common. + /// Otherwise, adds specifically the given amount of rows to the group. + /// + /// This is used to figure out how much height the next unbreakable row + /// group (if any) needs. + pub fn simulate_unbreakable_row_group( + &self, + first_row: usize, + amount_unbreakable_rows: Option<usize>, + regions: &Regions<'_>, + engine: &mut Engine, + disambiguator: usize, + ) -> SourceResult<UnbreakableRowGroup> { + let mut row_group = UnbreakableRowGroup::default(); + let mut unbreakable_rows_left = amount_unbreakable_rows.unwrap_or(0); + for (y, row) in self.grid.rows.iter().enumerate().skip(first_row) { + if amount_unbreakable_rows.is_none() { + // When we don't set a fixed amount of unbreakable rows, + // determine the amount based on the rowspan of unbreakable + // cells in rows. + let additional_unbreakable_rows = self.check_for_unbreakable_cells(y); + unbreakable_rows_left = + unbreakable_rows_left.max(additional_unbreakable_rows); + } + if unbreakable_rows_left == 0 { + // This check is in case the first row does not have any + // unbreakable cells. Therefore, no unbreakable row group + // is formed. + break; + } + let height = match row { + Sizing::Rel(v) => v.resolve(self.styles).relative_to(regions.base().y), + + // No need to pass the regions to the auto row, since + // unbreakable auto rows are always measured with infinite + // height, ignore backlog, and do not invoke the rowspan + // simulation procedure at all. + Sizing::Auto => self + .measure_auto_row( + engine, + disambiguator, + y, + false, + unbreakable_rows_left, + Some(&row_group), + )? + .unwrap() + .first() + .copied() + .unwrap_or_else(Abs::zero), + // Fractional rows don't matter when calculating the space + // needed for unbreakable rows + Sizing::Fr(_) => Abs::zero(), + }; + row_group.height += height; + row_group.rows.push((y, height)); + unbreakable_rows_left -= 1; + if unbreakable_rows_left == 0 { + // This second check is necessary so we can tell distinct + // but consecutive unbreakable row groups apart. If the + // unbreakable row group ended at this row, we stop before + // checking the next one. + break; + } + } + + Ok(row_group) + } + + /// Checks if one or more of the cells at the given row are unbreakable. + /// If so, returns the largest rowspan among the unbreakable cells; + /// the spanned rows must, as a result, be laid out in the same region. + pub fn check_for_unbreakable_cells(&self, y: usize) -> usize { + (0..self.grid.cols.len()) + .filter_map(|x| self.grid.cell(x, y)) + .filter(|cell| !cell.breakable) + .map(|cell| self.grid.effective_rowspan_of_cell(cell)) + .max() + .unwrap_or(0) + } + + /// Used by `measure_auto_row` to gather data needed to measure the cell. + pub fn prepare_auto_row_cell_measurement( + &self, + parent: Axes<usize>, + cell: &Cell, + breakable: bool, + row_group_data: Option<&UnbreakableRowGroup>, + ) -> CellMeasurementData<'_> { + let rowspan = self.grid.effective_rowspan_of_cell(cell); + + // This variable is used to construct a custom backlog if the cell + // is a rowspan, or if headers or footers are used. When measuring, we + // join the heights from previous regions to the current backlog to + // form a rowspan's expected backlog. We also subtract the header's + // and footer's heights from all regions. + let mut custom_backlog: Vec<Abs> = vec![]; + + // This function is used to subtract the expected header and footer + // height from each upcoming region size in the current backlog and + // last region. + let mut subtract_header_footer_height_from_regions = || { + // Only breakable auto rows need to update their backlogs based + // on the presence of a header or footer, given that unbreakable + // auto rows don't depend on the backlog, as they only span one + // region. + if breakable + && (matches!(self.grid.header, Some(Repeatable::Repeated(_))) + || matches!(self.grid.footer, Some(Repeatable::Repeated(_)))) + { + // Subtract header and footer height from all upcoming regions + // when measuring the cell, including the last repeated region. + // + // This will update the 'custom_backlog' vector with the + // updated heights of the upcoming regions. + let mapped_regions = self.regions.map(&mut custom_backlog, |size| { + Size::new(size.x, size.y - self.header_height - self.footer_height) + }); + + // Callees must use the custom backlog instead of the current + // backlog, so we return 'None'. + return (None, mapped_regions.last); + } + + // No need to change the backlog or last region. + (Some(self.regions.backlog), self.regions.last) + }; + + // Each declaration, from top to bottom: + // 1. The height available to the cell in the first region. + // Usually, this will just be the size remaining in the current + // region. + // 2. The backlog of upcoming region heights to specify as + // available to the cell. + // 3. The full height of the first region of the cell. + // 4. Height of the last repeated region to use in the measurement pod. + // 5. The total height of the cell covered by previously spanned + // rows in this region. This is used by rowspans to be able to tell + // how much the auto row needs to expand. + // 6. The amount of frames laid out by this cell in previous + // regions. When the cell isn't a rowspan, this is always zero. + // These frames are skipped after measuring. + let height; + let backlog; + let full; + let last; + let height_in_this_region; + let frames_in_previous_regions; + + if rowspan == 1 { + // Not a rowspan, so the cell only occupies this row. Therefore: + // 1. When we measure the cell below, use the available height + // remaining in the region as the height it has available. + // However, if the auto row is unbreakable, measure with infinite + // height instead to see how much content expands. + // 2. Use the region's backlog and last region when measuring, + // however subtract the expected header and footer heights from + // each upcoming size, if there is a header or footer. + // 3. Use the same full region height. + // 4. No height occupied by this cell in this region so far. + // 5. Yes, this cell started in this region. + height = if breakable { self.regions.size.y } else { Abs::inf() }; + (backlog, last) = subtract_header_footer_height_from_regions(); + full = if breakable { self.regions.full } else { Abs::inf() }; + height_in_this_region = Abs::zero(); + frames_in_previous_regions = 0; + } else { + // Height of the rowspan covered by spanned rows in the current + // region. + let laid_out_height: Abs = self + .lrows + .iter() + .filter_map(|row| match row { + Row::Frame(frame, y, _) + if (parent.y..parent.y + rowspan).contains(y) => + { + Some(frame.height()) + } + // Either we have a row outside of the rowspan, or a + // fractional row, whose size we can't really guess. + _ => None, + }) + .sum(); + + // If we're currently simulating an unbreakable row group, also + // consider the height of previously spanned rows which are in + // the row group but not yet laid out. + let unbreakable_height: Abs = row_group_data + .into_iter() + .flat_map(|row_group| &row_group.rows) + .filter(|(y, _)| (parent.y..parent.y + rowspan).contains(y)) + .map(|(_, height)| height) + .sum(); + + height_in_this_region = laid_out_height + unbreakable_height; + + // Ensure we will measure the rowspan with the correct heights. + // For that, we will gather the total height spanned by this + // rowspan in previous regions. + if let Some((rowspan_full, [rowspan_height, rowspan_other_heights @ ..])) = + self.rowspans + .iter() + .find(|data| data.x == parent.x && data.y == parent.y) + .map(|data| (data.region_full, &*data.heights)) + { + // The rowspan started in a previous region (as it already + // has at least one region height). + // Therefore, its initial height will be the height in its + // first spanned region, and the backlog will be the + // remaining heights, plus the current region's size, plus + // the current backlog. + frames_in_previous_regions = rowspan_other_heights.len() + 1; + + let heights_up_to_current_region = rowspan_other_heights + .iter() + .copied() + .chain(std::iter::once(if breakable { + self.initial.y - self.header_height - self.footer_height + } else { + // When measuring unbreakable auto rows, infinite + // height is available for content to expand. + Abs::inf() + })); + + custom_backlog = if breakable { + // This auto row is breakable. Therefore, join the + // 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); + + heights_up_to_current_region.chain(backlog).collect::<Vec<_>>() + } else { + // No extra backlog if this is an unbreakable auto row. + // Ensure, when measuring, that the rowspan can be laid + // out through all spanned rows which were already laid + // out so far, but don't go further than this region. + heights_up_to_current_region.collect::<Vec<_>>() + }; + + height = *rowspan_height; + backlog = None; + full = rowspan_full; + last = self + .regions + .last + .map(|size| size - self.header_height - self.footer_height); + } else { + // The rowspan started in the current region, as its vector + // of heights in regions is currently empty. + // Therefore, the initial height it has available will be + // the current available size, plus the size spanned in + // previous rows in this region (and/or unbreakable row + // group, if it's being simulated). + // The backlog and full will be that of the current region. + // However, use infinite height instead if we're measuring an + // unbreakable auto row. + height = if breakable { + height_in_this_region + self.regions.size.y + } else { + Abs::inf() + }; + (backlog, last) = subtract_header_footer_height_from_regions(); + full = if breakable { self.regions.full } else { Abs::inf() }; + frames_in_previous_regions = 0; + } + } + + let width = self.cell_spanned_width(cell, parent.x); + CellMeasurementData { + width, + height, + backlog, + custom_backlog, + full, + last, + height_in_this_region, + frames_in_previous_regions, + } + } + + /// Used in `measure_auto_row` to prepare a rowspan's `sizes` vector. + /// Returns `true` if we'll need to run a simulation to more accurately + /// expand the auto row based on the rowspan's demanded size, or `false` + /// otherwise. + #[allow(clippy::too_many_arguments)] + pub fn prepare_rowspan_sizes( + &self, + auto_row_y: usize, + sizes: &mut Vec<Abs>, + cell: &Cell, + parent_y: usize, + rowspan: usize, + unbreakable_rows_left: usize, + measurement_data: &CellMeasurementData<'_>, + ) -> bool { + if sizes.len() <= 1 + && sizes.first().map_or(true, |&first_frame_size| { + first_frame_size <= measurement_data.height_in_this_region + }) + { + // Ignore a rowspan fully covered by rows in previous + // regions and/or in the current region. + sizes.clear(); + return false; + } + if let Some(first_frame_size) = sizes.first_mut() { + // Subtract already covered height from the size requested + // by this rowspan to the auto row in the first region. + *first_frame_size = (*first_frame_size + - measurement_data.height_in_this_region) + .max(Abs::zero()); + } + + let last_spanned_row = parent_y + rowspan - 1; + + // When the rowspan is unbreakable, or all of its upcoming + // spanned rows are in the same unbreakable row group, its + // spanned gutter will certainly be in the same region as all + // of its other spanned rows, thus gutters won't be removed, + // and we can safely reduce how much the auto row expands by + // without using simulation. + let is_effectively_unbreakable_rowspan = + !cell.breakable || auto_row_y + unbreakable_rows_left > last_spanned_row; + + // If the rowspan doesn't end at this row and the grid has + // gutter, we will need to run a simulation to find out how + // much to expand this row by later. This is because gutters + // spanned by this rowspan might be removed if they appear + // around a pagebreak, so the auto row might have to expand a + // bit more to compensate for the missing gutter height. + // However, unbreakable rowspans aren't affected by that + // problem. + if auto_row_y != last_spanned_row + && !sizes.is_empty() + && self.grid.has_gutter + && !is_effectively_unbreakable_rowspan + { + return true; + } + + // We can only predict the resolved size of upcoming fixed-size + // rows, but not fractional rows. In the future, we might be + // able to simulate and circumvent the problem with fractional + // rows. Relative rows are currently always measured relative + // to the first region as well. + // We can ignore auto rows since this is the last spanned auto + // row. + let will_be_covered_height: Abs = self + .grid + .rows + .iter() + .skip(auto_row_y + 1) + .take(last_spanned_row - auto_row_y) + .map(|row| match row { + Sizing::Rel(v) => { + v.resolve(self.styles).relative_to(self.regions.base().y) + } + _ => Abs::zero(), + }) + .sum(); + + // Remove or reduce the sizes of the rowspan at the current or future + // regions where it will already be covered by further rows spanned by + // it. + subtract_end_sizes(sizes, will_be_covered_height); + + // No need to run a simulation for this rowspan. + false + } + + /// Performs a simulation to predict by how much height the last spanned + /// auto row will have to expand, given the current sizes of the auto row + /// in each region and the pending rowspans' data (parent Y, rowspan amount + /// and vector of requested sizes). + #[allow(clippy::too_many_arguments)] + pub fn simulate_and_measure_rowspans_in_auto_row( + &self, + y: usize, + resolved: &mut Vec<Abs>, + pending_rowspans: &[(usize, usize, Vec<Abs>)], + unbreakable_rows_left: usize, + row_group_data: Option<&UnbreakableRowGroup>, + mut disambiguator: usize, + engine: &mut Engine, + ) -> SourceResult<()> { + // To begin our simulation, we have to unify the sizes demanded by + // each rowspan into one simple vector of sizes, as if they were + // all a single rowspan. These sizes will be appended to + // 'resolved' once we finish our simulation. + let mut simulated_sizes: Vec<Abs> = vec![]; + let last_resolved_size = resolved.last().copied(); + let mut max_spanned_row = y; + for (parent_y, rowspan, sizes) in pending_rowspans { + let mut sizes = sizes.iter(); + for (target, size) in resolved.iter_mut().zip(&mut sizes) { + // First, we update the already resolved sizes as required + // by this rowspan. No need to simulate this since the auto row + // will already expand throughout already resolved regions. + // Our simulation, therefore, won't otherwise change already + // resolved sizes, other than, perhaps, the last one (at the + // last currently resolved region, at which we can expand). + target.set_max(*size); + } + for (simulated_target, rowspan_size) in + simulated_sizes.iter_mut().zip(&mut sizes) + { + // The remaining sizes are exclusive to rowspans, since + // other cells in this row didn't require as many regions. + // We will perform a simulation to see how much of these sizes + // does the auto row actually need to expand by, and how much + // is already covered by upcoming rows spanned by the rowspans. + simulated_target.set_max(*rowspan_size); + } + simulated_sizes.extend(sizes); + max_spanned_row = max_spanned_row.max(parent_y + rowspan - 1); + } + if simulated_sizes.is_empty() && resolved.last() == last_resolved_size.as_ref() { + // The rowspans already fit in the already resolved sizes. + // No need for simulation. + return Ok(()); + } + + // We will be updating the last resolved size (expanding the auto + // row) as needed. Therefore, consider it as part of the simulation. + // At the end, we push it back. + if let Some(modified_last_resolved_size) = resolved.pop() { + simulated_sizes.insert(0, modified_last_resolved_size); + } + + // Prepare regions for simulation. + // If we're currently inside an unbreakable row group simulation, + // subtract the current row group height from the available space + // when simulating rowspans in said group. + let mut simulated_regions = self.regions; + simulated_regions.size.y -= + row_group_data.map_or(Abs::zero(), |row_group| row_group.height); + + for _ in 0..resolved.len() { + // Ensure we start at the region where we will expand the auto + // row. + // Note that we won't accidentally call '.next()' once more than + // desired (we won't skip the last resolved frame, where we will + // expand) because we popped the last resolved size from the + // resolved vector, above. + 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 + // heights while measuring cells. + simulated_regions.size.y -= self.header_height + self.footer_height; + } + + if let Some(original_last_resolved_size) = last_resolved_size { + // We're now at the (current) last region of this auto row. + // Consider resolved height as already taken space. + simulated_regions.size.y -= original_last_resolved_size; + } + + // Now we run the simulation to check how much the auto row needs to + // grow to ensure that rowspans have the height they need. + let simulations_stabilized = self.run_rowspan_simulation( + y, + max_spanned_row, + simulated_regions, + &mut simulated_sizes, + engine, + last_resolved_size, + unbreakable_rows_left, + disambiguator, + )?; + + if !simulations_stabilized { + // If the simulation didn't stabilize above, we will just pretend + // all gutters were removed, as a best effort. That means the auto + // row will expand more than it normally should, but there isn't + // much we can do. + let will_be_covered_height = self + .grid + .rows + .iter() + .enumerate() + .skip(y + 1) + .take(max_spanned_row - y) + .filter(|(y, _)| !self.grid.is_gutter_track(*y)) + .map(|(_, row)| match row { + Sizing::Rel(v) => { + v.resolve(self.styles).relative_to(self.regions.base().y) + } + _ => Abs::zero(), + }) + .sum(); + + subtract_end_sizes(&mut simulated_sizes, will_be_covered_height); + } + + resolved.extend(simulated_sizes); + + Ok(()) + } + + /// Performs a simulation of laying out multiple rowspans (consolidated + /// into a single vector of simulated sizes) ending in a certain auto row + /// in order to find out how much the auto row will need to expand to cover + /// the rowspans' requested sizes, considering how much size has been + /// covered by other rows and by gutter between rows. + /// + /// For example, for a rowspan cell containing a block of 8pt of height + /// spanning rows (1pt, auto, 0.5pt, 0.5pt), with a gutter of 1pt between + /// each row, we have that the rows it spans provide 1pt + 0.5pt + 0.5pt + /// = 2pt of height, plus 1pt + 1pt + 1pt = 3pt of gutter, with a total of + /// 2pt + 3pt = 5pt of height already covered by fixed-size rows and + /// gutters. This means that the auto row must (under normal conditions) + /// expand by 3pt (8pt - 5pt) so that the rowspan has enough height across + /// rows to fully draw its contents. + /// + /// However, it's possible that the last row is sent to the next page to + /// respect a pagebreak, and then the 1pt gutter before it disappears. This + /// would lead to our rowspan having a height of 7pt available if we fail + /// to predict this situation when measuring the auto row. + /// + /// The algorithm below will, thus, attempt to simulate the layout of each + /// spanned row, considering the space available in the current page and in + /// upcoming pages (through the region backlog), in order to predict which + /// rows will be sent to a new page and thus have their preceding gutter + /// spacing removed (meaning the auto row has to grow a bit more). After + /// simulating, we subtract the total height spanned by upcoming rows and + /// gutter from the total rowspan height - this will be how much our auto + /// row has to expand. We then simulate again to check if, if the auto row + /// expanded by that amount, that would prompt the auto row to need to + /// expand even more, because expanding the auto row might cause some other + /// larger gutter spacing to disappear (leading to the rowspan having less + /// space available instead of more); if so, we update the amount to expand + /// and run the simulation again. Otherwise (if it should expand by the + /// same amount, meaning we predicted correctly, or by less, meaning the + /// auto row will be a bit larger than it should be, but that's a + /// compromise we're willing to accept), we conclude the simulation + /// (consider it stabilized) and return the result. + /// + /// Tries up to 5 times. If two consecutive simulations stabilize, then + /// we subtract the predicted expansion height ('amount_to_grow') from the + /// total height requested by rowspans (the 'requested_rowspan_height') to + /// obtain how much height is covered by upcoming rows, according to our + /// simulation, and the result of that operation is used to reduce or + /// remove heights from the end of the vector of simulated sizes, such that + /// the remaining heights are exactly how much the auto row should expand + /// by. Then, we return `true`. + /// + /// If the simulations don't stabilize (they return 5 different and + /// successively larger values), aborts and returns `false`. + #[allow(clippy::too_many_arguments)] + fn run_rowspan_simulation( + &self, + y: usize, + max_spanned_row: usize, + mut simulated_regions: Regions<'_>, + simulated_sizes: &mut Vec<Abs>, + engine: &mut Engine, + last_resolved_size: Option<Abs>, + unbreakable_rows_left: usize, + mut disambiguator: usize, + ) -> SourceResult<bool> { + // The max amount this row can expand will be the total size requested + // by rowspans which was not yet resolved. It is worth noting that, + // earlier, we pushed the last resolved size to 'simulated_sizes' as + // row expansion starts with it, so it's possible a rowspan requested + // to extend that size (we will see, through the simulation, if that's + // needed); however, we must subtract that resolved size from the total + // sum of sizes, as it was already resolved and thus the auto row will + // already grow by at least that much in the last resolved region (we + // would grow by the same size twice otherwise). + let requested_rowspan_height = + simulated_sizes.iter().sum::<Abs>() - last_resolved_size.unwrap_or_default(); + + // The amount the row will effectively grow by, according to the latest + // simulation. + let mut amount_to_grow = Abs::zero(); + + // Try to simulate up to 5 times. If it doesn't stabilize at a value + // which, when used and combined with upcoming spanned rows, covers all + // of the requested rowspan height, we give up. + for _attempt in 0..5 { + let rowspan_simulator = RowspanSimulator::new( + disambiguator, + simulated_regions, + self.header_height, + self.footer_height, + ); + + let total_spanned_height = rowspan_simulator.simulate_rowspan_layout( + y, + max_spanned_row, + amount_to_grow, + requested_rowspan_height, + unbreakable_rows_left, + self, + engine, + )?; + + // If the total height spanned by upcoming spanned rows plus the + // current amount we predict the auto row will have to grow (from + // the previous iteration) are larger than the size requested by + // rowspans, this means the auto row will grow enough in order to + // cover the requested rowspan height, so we stop the simulation. + // + // If that's not yet the case, we will simulate again and make the + // auto row grow even more, and do so until either the auto row has + // grown enough, or we tried to do so over 5 times. + // + // A flaw of this approach is that we consider rowspans' content to + // be contiguous. That is, we treat rowspans' requested heights as + // a simple number, instead of properly using the vector of + // requested heights in each region. This can lead to some + // weirdness when using multi-page rowspans with content that + // reacts to the amount of space available, including paragraphs. + // However, this is probably the best we can do for now. + if (total_spanned_height + amount_to_grow).fits(requested_rowspan_height) { + // Reduce sizes by the amount to be covered by upcoming spanned + // rows, which is equivalent to the amount that we don't grow. + // We reduce from the end as that's where the spanned rows will + // cover. The remaining sizes will all be covered by the auto + // row instead (which will grow by those sizes). + subtract_end_sizes( + simulated_sizes, + requested_rowspan_height - amount_to_grow, + ); + + if let Some(last_resolved_size) = last_resolved_size { + // Ensure the first simulated size is at least as large as + // the last resolved size (its initial value). As it was + // already resolved before, we must not reduce below the + // resolved size to avoid problems with non-rowspan cells. + if let Some(first_simulated_size) = simulated_sizes.first_mut() { + first_simulated_size.set_max(last_resolved_size); + } else { + simulated_sizes.push(last_resolved_size); + } + } + + return Ok(true); + } + + // For the next simulation, we will test if the auto row can grow + // by precisely how much rowspan height is not covered by upcoming + // spanned rows, according to the current simulation. + // We know that the new amount to grow is larger (and thus the + // auto row only expands between each simulation), because we + // checked above if + // 'total_spanned_height + (now old_)amount_to_grow >= requested_rowspan_height', + // which was false, so it holds that + // 'total_spanned_height + old_amount_to_grow < requested_rowspan_height' + // Thus, + // 'old_amount_to_grow < requested_rowspan_height - total_spanned_height' + // Therefore, by definition, 'old_amount_to_grow < amount_to_grow'. + let old_amount_to_grow = std::mem::replace( + &mut amount_to_grow, + requested_rowspan_height - total_spanned_height, + ); + + // We advance the 'regions' variable accordingly, so that, in the + // next simulation, we consider already grown space as final. + // That is, we effectively simulate how rows would be placed if the + // auto row grew by precisely the new value of 'amount_to_grow'. + let mut extra_amount_to_grow = amount_to_grow - old_amount_to_grow; + while extra_amount_to_grow > Abs::zero() + && simulated_regions.size.y < extra_amount_to_grow + { + extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero()); + simulated_regions.next(); + simulated_regions.size.y -= self.header_height + self.footer_height; + disambiguator += 1; + } + simulated_regions.size.y -= extra_amount_to_grow; + } + + // Simulation didn't succeed in 5 attempts. + Ok(false) + } +} + +/// Auxiliary structure holding state during rowspan simulation. +struct RowspanSimulator<'a> { + /// The number of finished regions. + finished: usize, + /// The state of regions during the simulation. + regions: Regions<'a>, + /// The height of the header in the currently simulated region. + header_height: Abs, + /// The height of the footer in the currently simulated region. + footer_height: Abs, + /// The total spanned height so far in the simulation. + total_spanned_height: Abs, + /// Height of the latest spanned gutter row in the simulation. + /// Zero if it was removed. + latest_spanned_gutter_height: Abs, +} + +impl<'a> RowspanSimulator<'a> { + /// Creates new rowspan simulation state with the given regions and initial + /// header and footer heights. Other fields should always start as zero. + fn new( + finished: usize, + regions: Regions<'a>, + header_height: Abs, + footer_height: Abs, + ) -> Self { + Self { + finished, + regions, + header_height, + footer_height, + total_spanned_height: Abs::zero(), + latest_spanned_gutter_height: Abs::zero(), + } + } + + /// Calculates the total spanned height of the rowspan. + /// Stops calculating if, at any point in the simulation, the value of + /// `total_spanned_height + amount_to_grow` becomes larger than + /// `requested_rowspan_height`, as the results are not going to become any + /// more useful after that point. + #[allow(clippy::too_many_arguments)] + fn simulate_rowspan_layout( + mut self, + y: usize, + max_spanned_row: usize, + amount_to_grow: Abs, + requested_rowspan_height: Abs, + mut unbreakable_rows_left: usize, + layouter: &GridLayouter<'_>, + engine: &mut Engine, + ) -> SourceResult<Abs> { + let spanned_rows = &layouter.grid.rows[y + 1..=max_spanned_row]; + for (offset, row) in spanned_rows.iter().enumerate() { + if (self.total_spanned_height + amount_to_grow).fits(requested_rowspan_height) + { + // Stop the simulation, as the combination of upcoming + // spanned rows (so far) and the current amount the auto + // row expands by has already fully covered the height the + // rowspans need. + return Ok(self.total_spanned_height); + } + let spanned_y = y + 1 + offset; + let is_gutter = layouter.grid.is_gutter_track(spanned_y); + + if unbreakable_rows_left == 0 { + // Simulate unbreakable row groups, and skip regions until + // they fit. There is no risk of infinite recursion, as + // no auto rows participate in the simulation, so the + // unbreakable row group simulator won't recursively call + // 'measure_auto_row' or (consequently) this function. + let row_group = layouter.simulate_unbreakable_row_group( + spanned_y, + None, + &self.regions, + engine, + 0, + )?; + while !self.regions.size.y.fits(row_group.height) + && !in_last_with_offset( + self.regions, + self.header_height + self.footer_height, + ) + { + self.finish_region(layouter, engine)?; + } + + unbreakable_rows_left = row_group.rows.len(); + } + + match row { + // Fixed-size spanned rows are what we are interested in. + // They contribute a fixed amount of height to our rowspan. + Sizing::Rel(v) => { + let height = + v.resolve(layouter.styles).relative_to(self.regions.base().y); + self.total_spanned_height += height; + if is_gutter { + self.latest_spanned_gutter_height = height; + } + + let mut skipped_region = false; + while unbreakable_rows_left == 0 + && !self.regions.size.y.fits(height) + && !in_last_with_offset( + self.regions, + self.header_height + self.footer_height, + ) + { + self.finish_region(layouter, engine)?; + + skipped_region = true; + } + + if !skipped_region || !is_gutter { + // No gutter at the top of a new region, so don't + // account for it if we just skipped a region. + self.regions.size.y -= height; + } + } + Sizing::Auto => { + // We only simulate for rowspans which end at the + // current auto row. Therefore, there won't be any + // further auto rows. + unreachable!(); + } + // For now, we ignore fractional rows on simulation. + Sizing::Fr(_) if is_gutter => { + self.latest_spanned_gutter_height = Abs::zero(); + } + Sizing::Fr(_) => {} + } + + unbreakable_rows_left = unbreakable_rows_left.saturating_sub(1); + } + + Ok(self.total_spanned_height) + } + + fn simulate_header_footer_layout( + &mut self, + layouter: &GridLayouter<'_>, + engine: &mut Engine, + ) -> SourceResult<()> { + // We can't just use the initial header/footer height on each region, + // because header/footer height might vary depending on region size if + // it contains rows with relative lengths. Therefore, we re-simulate + // headers and footers on each new region. + // It's true that, when measuring cells, we reduce each height in the + // backlog to consider the initial header and footer heights; however, + // our simulation checks what happens AFTER the auto row, so we can + // just use the original backlog from `self.regions`. + let disambiguator = self.finished; + let header_height = + if let Some(Repeatable::Repeated(header)) = &layouter.grid.header { + layouter + .simulate_header(header, &self.regions, engine, disambiguator)? + .height + } else { + Abs::zero() + }; + + let footer_height = + if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer { + layouter + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height + } else { + Abs::zero() + }; + + let mut skipped_region = false; + + // Skip until we reach a fitting region for both header and footer. + while !self.regions.size.y.fits(header_height + footer_height) + && self.regions.may_progress() + { + self.regions.next(); + self.finished += 1; + skipped_region = true; + } + + if let Some(Repeatable::Repeated(header)) = &layouter.grid.header { + 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 + } else { + header_height + }; + } + + if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer { + self.footer_height = if skipped_region { + // Simulate footers again, at the new region, as + // the full region height may change. + layouter + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height + } else { + footer_height + }; + } + + // Consume the header's and footer's heights from the new region, + // but don't consider them spanned. The rowspan does not go over the + // header or footer (as an invariant, any rowspans spanning any header + // or footer rows are fully contained within that header's or footer's rows). + self.regions.size.y -= self.header_height + self.footer_height; + + Ok(()) + } + + fn finish_region( + &mut self, + layouter: &GridLayouter<'_>, + engine: &mut Engine, + ) -> SourceResult<()> { + // If a row was pushed to the next region, the immediately + // preceding gutter row is removed. + self.total_spanned_height -= self.latest_spanned_gutter_height; + self.latest_spanned_gutter_height = Abs::zero(); + self.regions.next(); + self.finished += 1; + + self.simulate_header_footer_layout(layouter, engine) + } +} + +/// Subtracts some size from the end of a vector of sizes. +/// For example, subtracting 5pt from \[2pt, 1pt, 3pt\] will result in \[1pt\]. +fn subtract_end_sizes(sizes: &mut Vec<Abs>, mut subtract: Abs) { + while subtract > Abs::zero() && sizes.last().is_some_and(|&size| size <= subtract) { + subtract -= sizes.pop().unwrap(); + } + if subtract > Abs::zero() { + if let Some(last_size) = sizes.last_mut() { + *last_size -= subtract; + } + } +} |
