From 5c876535cc89912b32bc29a17c753ae9b1f03938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Mon, 6 Jan 2025 16:20:28 +0100 Subject: Move `CellGrid` from `typst-layout` to `typst-library` (#5585) --- crates/typst-layout/src/grid/cells.rs | 1349 ----------------- crates/typst-layout/src/grid/layouter.rs | 18 +- crates/typst-layout/src/grid/lines.rs | 38 +- crates/typst-layout/src/grid/mod.rs | 410 +----- crates/typst-layout/src/grid/repeated.rs | 41 +- crates/typst-layout/src/grid/rowspans.rs | 6 +- crates/typst-layout/src/lists.rs | 3 +- crates/typst-library/src/layout/grid.rs | 924 ------------ crates/typst-library/src/layout/grid/mod.rs | 926 ++++++++++++ crates/typst-library/src/layout/grid/resolve.rs | 1773 +++++++++++++++++++++++ crates/typst-library/src/layout/mod.rs | 2 +- 11 files changed, 2750 insertions(+), 2740 deletions(-) delete mode 100644 crates/typst-layout/src/grid/cells.rs delete mode 100644 crates/typst-library/src/layout/grid.rs create mode 100644 crates/typst-library/src/layout/grid/mod.rs create mode 100644 crates/typst-library/src/layout/grid/resolve.rs diff --git a/crates/typst-layout/src/grid/cells.rs b/crates/typst-layout/src/grid/cells.rs deleted file mode 100644 index 175e2183..00000000 --- a/crates/typst-layout/src/grid/cells.rs +++ /dev/null @@ -1,1349 +0,0 @@ -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, - align: Smart, - inset: Sides>>, - stroke: Sides>>>>, - breakable: bool, - locator: Locator<'a>, - styles: StyleChain, - ) -> Cell<'a>; - - /// Returns this cell's column override. - fn x(&self, styles: StyleChain) -> Smart; - - /// Returns this cell's row override. - fn y(&self, styles: StyleChain) -> Smart; - - /// 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 { - /// A horizontal line in the grid. - HLine { - /// The row above which the horizontal line is drawn. - y: Smart, - start: usize, - end: Option, - stroke: Option>>, - /// 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, - start: usize, - end: Option, - stroke: Option>>, - /// 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, - /// 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>>>, - /// 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, - /// 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 { - 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 { - Header { repeat: bool, span: Span, items: I }, - Footer { repeat: bool, span: Span, items: I }, - Item(ResolvableGridItem), -} - -/// A grid of cells, including the columns, rows, and cell data. -pub struct CellGrid<'a> { - /// The grid cells. - pub entries: Vec>, - /// The column tracks including gutter tracks. - pub cols: Vec, - /// The row tracks including gutter tracks. - pub rows: Vec, - /// 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>, - /// 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>, - /// The repeatable header of this grid. - pub header: Option>, - /// The repeatable footer of this grid. - pub footer: Option>, - /// 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>, - ) -> 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( - tracks: Axes<&[Sizing]>, - gutter: Axes<&[Sizing]>, - locator: Locator<'a>, - children: C, - fill: &Celled>, - align: &Celled>, - inset: &Celled>>>, - stroke: &ResolvedCelled>>>>, - engine: &mut Engine, - styles: StyleChain, - span: Span, - ) -> SourceResult - where - T: ResolvableCell + Default, - I: Iterator>, - C: IntoIterator>, - 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
= 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> = 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'. - // 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::>>()?; - - // 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![]; - let mut hlines: Vec> = 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>, - hlines: Vec>, - header: Option>, - footer: Option>, - entries: Vec>, - ) -> 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> { - 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> { - 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, - cell_y: Smart, - colspan: usize, - rowspan: usize, - resolved_cells: &[Option], - auto_index: &mut usize, - start_new_row: &mut bool, - columns: usize, -) -> HintedStrResult { - // 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], - 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], - 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 index 7c94617d..1f9cf679 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -3,6 +3,7 @@ use std::fmt::Debug; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{Resolve, StyleChain}; +use typst_library::layout::grid::resolve::{Cell, CellGrid, LinePosition, Repeatable}; use typst_library::layout::{ Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel, Size, Sizing, @@ -13,8 +14,8 @@ 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, + generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row, + LineSegment, Rowspan, UnbreakableRowGroup, }; /// Performs grid layout. @@ -843,7 +844,8 @@ impl<'a> GridLayouter<'a> { 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(); + let frame = + layout_cell(cell, engine, 0, self.styles, pod.into())?.into_frame(); resolved.set_max(frame.width() - already_covered_width); } @@ -1086,7 +1088,7 @@ impl<'a> GridLayouter<'a> { }; let frames = - cell.layout(engine, disambiguator, self.styles, pod)?.into_frames(); + layout_cell(cell, engine, disambiguator, self.styles, pod)?.into_frames(); // Skip the first region if one cell in it is empty. Then, // remeasure. @@ -1252,9 +1254,9 @@ impl<'a> GridLayouter<'a> { // rows. pod.full = self.regions.full; } - let frame = cell - .layout(engine, disambiguator, self.styles, pod)? - .into_frame(); + let frame = + layout_cell(cell, engine, disambiguator, self.styles, pod)? + .into_frame(); let mut pos = pos; if self.is_rtl { // In the grid, cell colspans expand to the right, @@ -1310,7 +1312,7 @@ impl<'a> GridLayouter<'a> { // Push the layouted frames into the individual output frames. let fragment = - cell.layout(engine, disambiguator, self.styles, pod)?; + layout_cell(cell, engine, disambiguator, self.styles, pod)?; for (output, frame) in outputs.iter_mut().zip(fragment) { let mut pos = pos; if self.is_rtl { diff --git a/crates/typst-layout/src/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs index 3e89612a..1227953d 100644 --- a/crates/typst-layout/src/grid/lines.rs +++ b/crates/typst-layout/src/grid/lines.rs @@ -1,41 +1,11 @@ -use std::num::NonZeroUsize; use std::sync::Arc; use typst_library::foundations::{AlternativeFold, Fold}; +use typst_library::layout::grid::resolve::{CellGrid, Line, Repeatable}; 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, - /// The line's stroke. This is `None` when the line is explicitly used to - /// override a previously specified line. - pub stroke: Option>>, - /// The line's position in relation to the track with its index. - pub position: LinePosition, -} +use super::RowPiece; /// Indicates which priority a particular grid line segment should have, based /// on the highest priority configuration that defined the segment's stroke. @@ -588,13 +558,13 @@ pub fn hline_stroke_at_column( #[cfg(test)] mod test { + use std::num::NonZeroUsize; use typst_library::foundations::Content; use typst_library::introspection::Locator; + use typst_library::layout::grid::resolve::{Cell, Entry, LinePosition}; 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> { diff --git a/crates/typst-layout/src/grid/mod.rs b/crates/typst-layout/src/grid/mod.rs index 769bef8c..1b4380f0 100644 --- a/crates/typst-layout/src/grid/mod.rs +++ b/crates/typst-layout/src/grid/mod.rs @@ -1,40 +1,44 @@ -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::diag::SourceResult; use typst_library::engine::Engine; -use typst_library::foundations::{Fold, Packed, Smart, StyleChain}; +use typst_library::foundations::{Packed, 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 typst_library::layout::grid::resolve::{grid_to_cellgrid, table_to_cellgrid, Cell}; +use typst_library::layout::{Fragment, GridElem, Regions}; +use typst_library::model::TableElem; -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, + generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, LineSegment, }; -use self::repeated::{Footer, Header, Repeatable}; use self::rowspans::{Rowspan, UnbreakableRowGroup}; +/// 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_cell( + cell: &Cell, + engine: &mut Engine, + disambiguator: usize, + styles: StyleChain, + regions: Regions, +) -> SourceResult { + let mut locator = cell.locator.relayout(); + if disambiguator > 0 { + locator = locator.split().next_inner(disambiguator as u128); + } + crate::layout_fragment(engine, &cell.body, locator, styles, regions) +} + /// Layout the grid. #[typst_macros::time(span = elem.span())] pub fn layout_grid( @@ -44,54 +48,8 @@ pub fn layout_grid( styles: StyleChain, regions: Regions, ) -> SourceResult { - 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) + let grid = grid_to_cellgrid(elem, engine, locator, styles)?; + GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine) } /// Layout the table. @@ -103,314 +61,6 @@ pub fn layout_table( styles: StyleChain, regions: Regions, ) -> SourceResult { - 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> { - 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> { - 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 { - fn resolve_cell<'a>( - mut self, - x: usize, - y: usize, - fill: &Option, - align: Smart, - inset: Sides>>, - stroke: Sides>>>>, - 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 { - (**self).x(styles) - } - - fn y(&self, styles: StyleChain) -> Smart { - (**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 { - fn resolve_cell<'a>( - mut self, - x: usize, - y: usize, - fill: &Option, - align: Smart, - inset: Sides>>, - stroke: Sides>>>>, - 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 { - (**self).x(styles) - } - - fn y(&self, styles: StyleChain) -> Smart { - (**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) - } + let grid = table_to_cellgrid(elem, engine, locator, styles)?; + GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine) } diff --git a/crates/typst-layout/src/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs index 8d08d56d..22d2a09e 100644 --- a/crates/typst-layout/src/grid/repeated.rs +++ b/crates/typst-layout/src/grid/repeated.rs @@ -1,50 +1,11 @@ use typst_library::diag::SourceResult; use typst_library::engine::Engine; +use typst_library::layout::grid::resolve::{Footer, Header, Repeatable}; use typst_library::layout::{Abs, Axes, Frame, Regions}; use super::layouter::GridLayouter; use super::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 { - Repeated(T), - NotRepeated(T), -} - -impl Repeatable { - /// 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 GridLayouter<'_> { /// Layouts the header's rows. /// Skips regions as necessary. diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs index 93d4c960..5039695d 100644 --- a/crates/typst-layout/src/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -1,12 +1,12 @@ use typst_library::diag::SourceResult; use typst_library::engine::Engine; use typst_library::foundations::Resolve; +use typst_library::layout::grid::resolve::Repeatable; use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; use typst_utils::MaybeReverseIter; use super::layouter::{in_last_with_offset, points, Row, RowPiece}; -use super::repeated::Repeatable; -use super::{Cell, GridLayouter}; +use super::{layout_cell, Cell, GridLayouter}; /// All information needed to layout a single rowspan. pub struct Rowspan { @@ -141,7 +141,7 @@ impl GridLayouter<'_> { } // Push the layouted frames directly into the finished frames. - let fragment = cell.layout(engine, disambiguator, self.styles, pod)?; + let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?; let (current_region, current_rrows) = current_region_data.unzip(); for ((i, finished), frame) in self .finished diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs index 0d51a1e4..9479959b 100644 --- a/crates/typst-layout/src/lists.rs +++ b/crates/typst-layout/src/lists.rs @@ -4,11 +4,12 @@ use typst_library::diag::SourceResult; use typst_library::engine::Engine; use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain}; use typst_library::introspection::Locator; +use typst_library::layout::grid::resolve::{Cell, CellGrid}; use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment}; use typst_library::model::{EnumElem, ListElem, Numbering, ParElem}; use typst_library::text::TextElem; -use crate::grid::{Cell, CellGrid, GridLayouter}; +use crate::grid::GridLayouter; /// Layout the list. #[typst_macros::time(span = elem.span())] diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst-library/src/layout/grid.rs deleted file mode 100644 index 2e1e9abc..00000000 --- a/crates/typst-library/src/layout/grid.rs +++ /dev/null @@ -1,924 +0,0 @@ -use std::num::NonZeroUsize; -use std::sync::Arc; - -use comemo::Track; -use smallvec::{smallvec, SmallVec}; -use typst_utils::NonZeroExt; - -use crate::diag::{bail, At, HintedStrResult, HintedString, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func, - IntoValue, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, Value, -}; -use crate::layout::{ - Alignment, BlockElem, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing, -}; -use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine}; -use crate::visualize::{Paint, Stroke}; - -/// Arranges content in a grid. -/// -/// The grid element allows you to arrange content in a grid. You can define the -/// number of rows and columns, as well as the size of the gutters between them. -/// There are multiple sizing modes for columns and rows that can be used to -/// create complex layouts. -/// -/// While the grid and table elements work very similarly, they are intended for -/// different use cases and carry different semantics. The grid element is -/// intended for presentational and layout purposes, while the -/// [`{table}`]($table) element is intended for, in broad terms, presenting -/// multiple related data points. In the future, Typst will annotate its output -/// such that screenreaders will announce content in `table` as tabular while a -/// grid's content will be announced no different than multiple content blocks -/// in the document flow. Set and show rules on one of these elements do not -/// affect the other. -/// -/// A grid's sizing is determined by the track sizes specified in the arguments. -/// Because each of the sizing parameters accepts the same values, we will -/// explain them just once, here. Each sizing argument accepts an array of -/// individual track sizes. A track size is either: -/// -/// - `{auto}`: The track will be sized to fit its contents. It will be at most -/// as large as the remaining space. If there is more than one `{auto}` track -/// width, and together they claim more than the available space, the `{auto}` -/// tracks will fairly distribute the available space among themselves. -/// -/// - A fixed or relative length (e.g. `{10pt}` or `{20% - 1cm}`): The track -/// will be exactly of this size. -/// -/// - A fractional length (e.g. `{1fr}`): Once all other tracks have been sized, -/// the remaining space will be divided among the fractional tracks according -/// to their fractions. For example, if there are two fractional tracks, each -/// with a fraction of `{1fr}`, they will each take up half of the remaining -/// space. -/// -/// To specify a single track, the array can be omitted in favor of a single -/// value. To specify multiple `{auto}` tracks, enter the number of tracks -/// instead of an array. For example, `columns:` `{3}` is equivalent to -/// `columns:` `{(auto, auto, auto)}`. -/// -/// # Examples -/// The example below demonstrates the different track sizing options. It also -/// shows how you can use [`grid.cell`]($grid.cell) to make an individual cell -/// span two grid tracks. -/// -/// ```example -/// // We use `rect` to emphasize the -/// // area of cells. -/// #set rect( -/// inset: 8pt, -/// fill: rgb("e4e5ea"), -/// width: 100%, -/// ) -/// -/// #grid( -/// columns: (60pt, 1fr, 2fr), -/// rows: (auto, 60pt), -/// gutter: 3pt, -/// rect[Fixed width, auto height], -/// rect[1/3 of the remains], -/// rect[2/3 of the remains], -/// rect(height: 100%)[Fixed height], -/// grid.cell( -/// colspan: 2, -/// image("tiger.jpg", width: 100%), -/// ), -/// ) -/// ``` -/// -/// You can also [spread]($arguments/#spreading) an array of strings or content -/// into a grid to populate its cells. -/// -/// ```example -/// #grid( -/// columns: 5, -/// gutter: 5pt, -/// ..range(25).map(str) -/// ) -/// ``` -/// -/// # Styling the grid -/// The grid's appearance can be customized through different parameters. These -/// are the most important ones: -/// -/// - [`fill`]($grid.fill) to give all cells a background -/// - [`align`]($grid.align) to change how cells are aligned -/// - [`inset`]($grid.inset) to optionally add internal padding to each cell -/// - [`stroke`]($grid.stroke) to optionally enable grid lines with a certain -/// stroke -/// -/// If you need to override one of the above options for a single cell, you can -/// use the [`grid.cell`]($grid.cell) element. Likewise, you can override -/// individual grid lines with the [`grid.hline`]($grid.hline) and -/// [`grid.vline`]($grid.vline) elements. -/// -/// Alternatively, if you need the appearance options to depend on a cell's -/// position (column and row), you may specify a function to `fill` or `align` -/// of the form `(column, row) => value`. You may also use a show rule on -/// [`grid.cell`]($grid.cell) - see that element's examples or the examples -/// below for more information. -/// -/// Locating most of your styling in set and show rules is recommended, as it -/// keeps the grid's or table's actual usages clean and easy to read. It also -/// allows you to easily change the grid's appearance in one place. -/// -/// ## Stroke styling precedence -/// There are three ways to set the stroke of a grid cell: through -/// [`{grid.cell}`'s `stroke` field]($grid.cell.stroke), by using -/// [`{grid.hline}`]($grid.hline) and [`{grid.vline}`]($grid.vline), or by -/// setting the [`{grid}`'s `stroke` field]($grid.stroke). When multiple of -/// these settings are present and conflict, the `hline` and `vline` settings -/// take the highest precedence, followed by the `cell` settings, and finally -/// the `grid` settings. -/// -/// Furthermore, strokes of a repeated grid header or footer will take -/// precedence over regular cell strokes. -#[elem(scope, Show)] -pub struct GridElem { - /// The column sizes. - /// - /// Either specify a track size array or provide an integer to create a grid - /// with that many `{auto}`-sized columns. Note that opposed to rows and - /// gutters, providing a single track size will only ever create a single - /// column. - #[borrowed] - pub columns: TrackSizings, - - /// The row sizes. - /// - /// If there are more cells than fit the defined rows, the last row is - /// repeated until there are no more cells. - #[borrowed] - pub rows: TrackSizings, - - /// The gaps between rows and columns. - /// - /// If there are more gutters than defined sizes, the last gutter is - /// repeated. - /// - /// This is a shorthand to set `column-gutter` and `row-gutter` to the same - /// value. - #[external] - pub gutter: TrackSizings, - - /// The gaps between columns. - #[parse( - let gutter = args.named("gutter")?; - args.named("column-gutter")?.or_else(|| gutter.clone()) - )] - #[borrowed] - pub column_gutter: TrackSizings, - - /// The gaps between rows. - #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] - #[borrowed] - pub row_gutter: TrackSizings, - - /// How to fill the cells. - /// - /// This can be a color or a function that returns a color. The function - /// receives the cells' column and row indices, starting from zero. This can - /// be used to implement striped grids. - /// - /// ```example - /// #grid( - /// fill: (x, y) => - /// if calc.even(x + y) { luma(230) } - /// else { white }, - /// align: center + horizon, - /// columns: 4, - /// inset: 2pt, - /// [X], [O], [X], [O], - /// [O], [X], [O], [X], - /// [X], [O], [X], [O], - /// [O], [X], [O], [X], - /// ) - /// ``` - #[borrowed] - pub fill: Celled>, - - /// How to align the cells' content. - /// - /// This can either be a single alignment, an array of alignments - /// (corresponding to each column) or a function that returns an alignment. - /// The function receives the cells' column and row indices, starting from - /// zero. If set to `{auto}`, the outer alignment is used. - /// - /// You can find an example for this argument at the - /// [`table.align`]($table.align) parameter. - #[borrowed] - pub align: Celled>, - - /// How to [stroke]($stroke) the cells. - /// - /// Grids have no strokes by default, which can be changed by setting this - /// option to the desired stroke. - /// - /// If it is necessary to place lines which can cross spacing between cells - /// produced by the `gutter` option, or to override the stroke between - /// multiple specific cells, consider specifying one or more of - /// [`grid.hline`]($grid.hline) and [`grid.vline`]($grid.vline) alongside - /// your grid cells. - /// - /// ```example - /// #set page(height: 13em, width: 26em) - /// - /// #let cv(..jobs) = grid( - /// columns: 2, - /// inset: 5pt, - /// stroke: (x, y) => if x == 0 and y > 0 { - /// (right: ( - /// paint: luma(180), - /// thickness: 1.5pt, - /// dash: "dotted" - /// )) - /// }, - /// grid.header(grid.cell(colspan: 2)[ - /// *Professional Experience* - /// #box(width: 1fr, line(length: 100%, stroke: luma(180))) - /// ]), - /// ..{ - /// let last = none - /// for job in jobs.pos() { - /// ( - /// if job.year != last [*#job.year*], - /// [ - /// *#job.company* - #job.role _(#job.timeframe)_ \ - /// #job.details - /// ] - /// ) - /// last = job.year - /// } - /// } - /// ) - /// - /// #cv( - /// ( - /// year: 2012, - /// company: [Pear Seed & Co.], - /// role: [Lead Engineer], - /// timeframe: [Jul - Dec], - /// details: [ - /// - Raised engineers from 3x to 10x - /// - Did a great job - /// ], - /// ), - /// ( - /// year: 2012, - /// company: [Mega Corp.], - /// role: [VP of Sales], - /// timeframe: [Mar - Jun], - /// details: [- Closed tons of customers], - /// ), - /// ( - /// year: 2013, - /// company: [Tiny Co.], - /// role: [CEO], - /// timeframe: [Jan - Dec], - /// details: [- Delivered 4x more shareholder value], - /// ), - /// ( - /// year: 2014, - /// company: [Glorbocorp Ltd], - /// role: [CTO], - /// timeframe: [Jan - Mar], - /// details: [- Drove containerization forward], - /// ), - /// ) - /// ``` - #[resolve] - #[fold] - pub stroke: Celled>>>>, - - /// How much to pad the cells' content. - /// - /// You can find an example for this argument at the - /// [`table.inset`]($table.inset) parameter. - #[fold] - pub inset: Celled>>>, - - /// The contents of the grid cells, plus any extra grid lines specified - /// with the [`grid.hline`]($grid.hline) and [`grid.vline`]($grid.vline) - /// elements. - /// - /// The cells are populated in row-major order. - #[variadic] - pub children: Vec, -} - -#[scope] -impl GridElem { - #[elem] - type GridCell; - - #[elem] - type GridHLine; - - #[elem] - type GridVLine; - - #[elem] - type GridHeader; - - #[elem] - type GridFooter; -} - -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_grid) - .pack() - .spanned(self.span())) - } -} - -/// Track sizing definitions. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct TrackSizings(pub SmallVec<[Sizing; 4]>); - -cast! { - TrackSizings, - self => self.0.into_value(), - sizing: Sizing => Self(smallvec![sizing]), - count: NonZeroUsize => Self(smallvec![Sizing::Auto; count.get()]), - values: Array => Self(values.into_iter().map(Value::cast).collect::>()?), -} - -/// Any child of a grid element. -#[derive(Debug, PartialEq, Clone, Hash)] -pub enum GridChild { - Header(Packed), - Footer(Packed), - Item(GridItem), -} - -cast! { - GridChild, - self => match self { - Self::Header(header) => header.into_value(), - Self::Footer(footer) => footer.into_value(), - Self::Item(item) => item.into_value(), - }, - v: Content => { - v.try_into()? - }, -} - -impl TryFrom for GridChild { - type Error = HintedString; - fn try_from(value: Content) -> HintedStrResult { - if value.is::() { - bail!( - "cannot use `table.header` as a grid header"; - hint: "use `grid.header` instead" - ) - } - if value.is::() { - bail!( - "cannot use `table.footer` as a grid footer"; - hint: "use `grid.footer` instead" - ) - } - - value - .into_packed::() - .map(Self::Header) - .or_else(|value| value.into_packed::().map(Self::Footer)) - .or_else(|value| GridItem::try_from(value).map(Self::Item)) - } -} - -/// A grid item, which is the basic unit of grid specification. -#[derive(Debug, PartialEq, Clone, Hash)] -pub enum GridItem { - HLine(Packed), - VLine(Packed), - Cell(Packed), -} - -cast! { - GridItem, - self => match self { - Self::HLine(hline) => hline.into_value(), - Self::VLine(vline) => vline.into_value(), - Self::Cell(cell) => cell.into_value(), - }, - v: Content => { - v.try_into()? - } -} - -impl TryFrom for GridItem { - type Error = HintedString; - fn try_from(value: Content) -> HintedStrResult { - if value.is::() { - bail!("cannot place a grid header within another header or footer"); - } - if value.is::() { - bail!("cannot place a table header within another header or footer"); - } - if value.is::() { - bail!("cannot place a grid footer within another footer or header"); - } - if value.is::() { - bail!("cannot place a table footer within another footer or header"); - } - if value.is::() { - bail!( - "cannot use `table.cell` as a grid cell"; - hint: "use `grid.cell` instead" - ); - } - if value.is::() { - bail!( - "cannot use `table.hline` as a grid line"; - hint: "use `grid.hline` instead" - ); - } - if value.is::() { - bail!( - "cannot use `table.vline` as a grid line"; - hint: "use `grid.vline` instead" - ); - } - - Ok(value - .into_packed::() - .map(Self::HLine) - .or_else(|value| value.into_packed::().map(Self::VLine)) - .or_else(|value| value.into_packed::().map(Self::Cell)) - .unwrap_or_else(|value| { - let span = value.span(); - Self::Cell(Packed::new(GridCell::new(value)).spanned(span)) - })) - } -} - -/// A repeatable grid header. -/// -/// If `repeat` is set to `true`, the header will be repeated across pages. For -/// an example, refer to the [`table.header`]($table.header) element and the -/// [`grid.stroke`]($grid.stroke) parameter. -#[elem(name = "header", title = "Grid Header")] -pub struct GridHeader { - /// Whether this header should be repeated across pages. - #[default(true)] - pub repeat: bool, - - /// The cells and lines within the header. - #[variadic] - pub children: Vec, -} - -/// A repeatable grid footer. -/// -/// Just like the [`grid.header`]($grid.header) element, the footer can repeat -/// itself on every page of the table. -/// -/// No other grid cells may be placed after the footer. -#[elem(name = "footer", title = "Grid Footer")] -pub struct GridFooter { - /// Whether this footer should be repeated across pages. - #[default(true)] - pub repeat: bool, - - /// The cells and lines within the footer. - #[variadic] - pub children: Vec, -} - -/// A horizontal line in the grid. -/// -/// Overrides any per-cell stroke, including stroke specified through the grid's -/// `stroke` field. Can cross spacing between cells created through the grid's -/// `column-gutter` option. -/// -/// An example for this function can be found at the -/// [`table.hline`]($table.hline) element. -#[elem(name = "hline", title = "Grid Horizontal Line")] -pub struct GridHLine { - /// The row above which the horizontal line is placed (zero-indexed). - /// If the `position` field is set to `{bottom}`, the line is placed below - /// the row with the given index instead (see that field's docs for - /// details). - /// - /// Specifying `{auto}` causes the line to be placed at the row below the - /// last automatically positioned cell (that is, cell without coordinate - /// overrides) before the line among the grid's children. If there is no - /// such cell before the line, it is placed at the top of the grid (row 0). - /// Note that specifying for this option exactly the total amount of rows - /// in the grid causes this horizontal line to override the bottom border - /// of the grid, while a value of 0 overrides the top border. - pub y: Smart, - - /// The column at which the horizontal line starts (zero-indexed, inclusive). - pub start: usize, - - /// The column before which the horizontal line ends (zero-indexed, - /// exclusive). - /// Therefore, the horizontal line will be drawn up to and across column - /// `end - 1`. - /// - /// A value equal to `{none}` or to the amount of columns causes it to - /// extend all the way towards the end of the grid. - pub end: Option, - - /// The line's stroke. - /// - /// Specifying `{none}` removes any lines previously placed across this - /// line's range, including hlines or per-cell stroke below it. - #[resolve] - #[fold] - #[default(Some(Arc::new(Stroke::default())))] - pub stroke: Option>, - - /// The position at which the line is placed, given its row (`y`) - either - /// `{top}` to draw above it or `{bottom}` to draw below it. - /// - /// This setting is only relevant when row gutter is enabled (and - /// shouldn't be used otherwise - prefer just increasing the `y` field by - /// one instead), since then the position below a row becomes different - /// from the position above the next row due to the spacing between both. - #[default(OuterVAlignment::Top)] - pub position: OuterVAlignment, -} - -/// A vertical line in the grid. -/// -/// Overrides any per-cell stroke, including stroke specified through the -/// grid's `stroke` field. Can cross spacing between cells created through -/// the grid's `row-gutter` option. -#[elem(name = "vline", title = "Grid Vertical Line")] -pub struct GridVLine { - /// The column before which the horizontal line is placed (zero-indexed). - /// If the `position` field is set to `{end}`, the line is placed after the - /// column with the given index instead (see that field's docs for - /// details). - /// - /// Specifying `{auto}` causes the line to be placed at the column after - /// the last automatically positioned cell (that is, cell without - /// coordinate overrides) before the line among the grid's children. If - /// there is no such cell before the line, it is placed before the grid's - /// first column (column 0). - /// Note that specifying for this option exactly the total amount of - /// columns in the grid causes this vertical line to override the end - /// border of the grid (right in LTR, left in RTL), while a value of 0 - /// overrides the start border (left in LTR, right in RTL). - pub x: Smart, - - /// The row at which the vertical line starts (zero-indexed, inclusive). - pub start: usize, - - /// The row on top of which the vertical line ends (zero-indexed, - /// exclusive). - /// Therefore, the vertical line will be drawn up to and across row - /// `end - 1`. - /// - /// A value equal to `{none}` or to the amount of rows causes it to extend - /// all the way towards the bottom of the grid. - pub end: Option, - - /// The line's stroke. - /// - /// Specifying `{none}` removes any lines previously placed across this - /// line's range, including vlines or per-cell stroke below it. - #[resolve] - #[fold] - #[default(Some(Arc::new(Stroke::default())))] - pub stroke: Option>, - - /// The position at which the line is placed, given its column (`x`) - - /// either `{start}` to draw before it or `{end}` to draw after it. - /// - /// The values `{left}` and `{right}` are also accepted, but discouraged as - /// they cause your grid to be inconsistent between left-to-right and - /// right-to-left documents. - /// - /// This setting is only relevant when column gutter is enabled (and - /// shouldn't be used otherwise - prefer just increasing the `x` field by - /// one instead), since then the position after a column becomes different - /// from the position before the next column due to the spacing between - /// both. - #[default(OuterHAlignment::Start)] - pub position: OuterHAlignment, -} - -/// A cell in the grid. You can use this function in the argument list of a grid -/// to override grid style properties for an individual cell or manually -/// positioning it within the grid. You can also use this function in show rules -/// to apply certain styles to multiple cells at once. -/// -/// For example, you can override the position and stroke for a single cell: -/// -/// ```example -/// >>> #set page(width: auto) -/// >>> #set text(15pt, font: "Noto Sans Symbols 2", bottom-edge: -.2em) -/// <<< #set text(15pt, font: "Noto Sans Symbols 2") -/// #show regex("[♚-♟︎]"): set text(fill: rgb("21212A")) -/// #show regex("[♔-♙]"): set text(fill: rgb("111015")) -/// -/// #grid( -/// fill: (x, y) => rgb( -/// if calc.odd(x + y) { "7F8396" } -/// else { "EFF0F3" } -/// ), -/// columns: (1em,) * 8, -/// rows: 1em, -/// align: center + horizon, -/// -/// [♖], [♘], [♗], [♕], [♔], [♗], [♘], [♖], -/// [♙], [♙], [♙], [♙], [], [♙], [♙], [♙], -/// grid.cell( -/// x: 4, y: 3, -/// stroke: blue.transparentize(60%) -/// )[♙], -/// -/// ..(grid.cell(y: 6)[♟],) * 8, -/// ..([♜], [♞], [♝], [♛], [♚], [♝], [♞], [♜]) -/// .map(grid.cell.with(y: 7)), -/// ) -/// ``` -/// -/// You may also apply a show rule on `grid.cell` to style all cells at once, -/// which allows you, for example, to apply styles based on a cell's position. -/// Refer to the examples of the [`table.cell`]($table.cell) element to learn -/// more about this. -#[elem(name = "cell", title = "Grid Cell", Show)] -pub struct GridCell { - /// The cell's body. - #[required] - pub body: Content, - - /// The cell's column (zero-indexed). - /// This field may be used in show rules to style a cell depending on its - /// column. - /// - /// You may override this field to pick in which column the cell must - /// be placed. If no row (`y`) is chosen, the cell will be placed in the - /// first row (starting at row 0) with that column available (or a new row - /// if none). If both `x` and `y` are chosen, however, the cell will be - /// placed in that exact position. An error is raised if that position is - /// not available (thus, it is usually wise to specify cells with a custom - /// position before cells with automatic positions). - /// - /// ```example - /// #let circ(c) = circle( - /// fill: c, width: 5mm - /// ) - /// - /// #grid( - /// columns: 4, - /// rows: 7mm, - /// stroke: .5pt + blue, - /// align: center + horizon, - /// inset: 1mm, - /// - /// grid.cell(x: 2, y: 2, circ(aqua)), - /// circ(yellow), - /// grid.cell(x: 3, circ(green)), - /// circ(black), - /// ) - /// ``` - pub x: Smart, - - /// The cell's row (zero-indexed). - /// This field may be used in show rules to style a cell depending on its - /// row. - /// - /// You may override this field to pick in which row the cell must be - /// placed. If no column (`x`) is chosen, the cell will be placed in the - /// first column (starting at column 0) available in the chosen row. If all - /// columns in the chosen row are already occupied, an error is raised. - /// - /// ```example - /// #let tri(c) = polygon.regular( - /// fill: c, - /// size: 5mm, - /// vertices: 3, - /// ) - /// - /// #grid( - /// columns: 2, - /// stroke: blue, - /// inset: 1mm, - /// - /// tri(black), - /// grid.cell(y: 1, tri(teal)), - /// grid.cell(y: 1, tri(red)), - /// grid.cell(y: 2, tri(orange)) - /// ) - /// ``` - pub y: Smart, - - /// The amount of columns spanned by this cell. - #[default(NonZeroUsize::ONE)] - pub colspan: NonZeroUsize, - - /// The amount of rows spanned by this cell. - #[default(NonZeroUsize::ONE)] - pub rowspan: NonZeroUsize, - - /// The cell's [fill]($grid.fill) override. - pub fill: Smart>, - - /// The cell's [alignment]($grid.align) override. - pub align: Smart, - - /// The cell's [inset]($grid.inset) override. - pub inset: Smart>>>, - - /// The cell's [stroke]($grid.stroke) override. - #[resolve] - #[fold] - pub stroke: Sides>>>, - - /// Whether rows spanned by this cell can be placed in different pages. - /// When equal to `{auto}`, a cell spanning only fixed-size rows is - /// unbreakable, while a cell spanning at least one `{auto}`-sized row is - /// breakable. - pub breakable: Smart, -} - -cast! { - GridCell, - v: Content => v.into(), -} - -impl Show for Packed { - fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { - show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) - } -} - -impl Default for Packed { - fn default() -> Self { - Packed::new(GridCell::new(Content::default())) - } -} - -impl From for GridCell { - fn from(value: Content) -> Self { - #[allow(clippy::unwrap_or_default)] - value.unpack::().unwrap_or_else(Self::new) - } -} - -/// Function with common code to display a grid cell or table cell. -pub(crate) fn show_grid_cell( - mut body: Content, - inset: Smart>>>, - align: Smart, -) -> SourceResult { - let inset = inset.unwrap_or_default().map(Option::unwrap_or_default); - - if inset != Sides::default() { - // Only pad if some inset is not 0pt. - // Avoids a bug where using .padded() in any way inside Show causes - // alignment in align(...) to break. - body = body.padded(inset); - } - - if let Smart::Custom(alignment) = align { - body = body.aligned(alignment); - } - - Ok(body) -} - -/// A value that can be configured per cell. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Celled { - /// A bare value, the same for all cells. - Value(T), - /// A closure mapping from cell coordinates to a value. - Func(Func), - /// An array of alignment values corresponding to each column. - Array(Vec), -} - -impl Celled { - /// Resolve the value based on the cell position. - pub fn resolve( - &self, - engine: &mut Engine, - styles: StyleChain, - x: usize, - y: usize, - ) -> SourceResult { - Ok(match self { - Self::Value(value) => value.clone(), - Self::Func(func) => func - .call(engine, Context::new(None, Some(styles)).track(), [x, y])? - .cast() - .at(func.span())?, - Self::Array(array) => x - .checked_rem(array.len()) - .and_then(|i| array.get(i)) - .cloned() - .unwrap_or_default(), - }) - } -} - -impl Default for Celled { - fn default() -> Self { - Self::Value(T::default()) - } -} - -impl Reflect for Celled { - fn input() -> CastInfo { - T::input() + Array::input() + Func::input() - } - - fn output() -> CastInfo { - T::output() + Array::output() + Func::output() - } - - fn castable(value: &Value) -> bool { - Array::castable(value) || Func::castable(value) || T::castable(value) - } -} - -impl IntoValue for Celled { - fn into_value(self) -> Value { - match self { - Self::Value(value) => value.into_value(), - Self::Func(func) => func.into_value(), - Self::Array(arr) => arr.into_value(), - } - } -} - -impl FromValue for Celled { - fn from_value(value: Value) -> HintedStrResult { - match value { - Value::Func(v) => Ok(Self::Func(v)), - Value::Array(array) => Ok(Self::Array( - array.into_iter().map(T::from_value).collect::>()?, - )), - v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), - v => Err(Self::error(&v)), - } - } -} - -impl Fold for Celled { - fn fold(self, outer: Self) -> Self { - match (self, outer) { - (Self::Value(inner), Self::Value(outer)) => Self::Value(inner.fold(outer)), - (self_, _) => self_, - } - } -} - -impl Resolve for Celled { - type Output = ResolvedCelled; - - fn resolve(self, styles: StyleChain) -> Self::Output { - match self { - Self::Value(value) => ResolvedCelled(Celled::Value(value.resolve(styles))), - Self::Func(func) => ResolvedCelled(Celled::Func(func)), - Self::Array(values) => ResolvedCelled(Celled::Array( - values.into_iter().map(|value| value.resolve(styles)).collect(), - )), - } - } -} - -/// The result of resolving a Celled's value according to styles. -/// Holds resolved values which depend on each grid cell's position. -/// When it is a closure, however, it is only resolved when the closure is -/// called. -#[derive(Default, Clone)] -pub struct ResolvedCelled(Celled); - -impl ResolvedCelled -where - T: FromValue + Resolve, - ::Output: Default + Clone, -{ - /// Resolve the value based on the cell position. - pub fn resolve( - &self, - engine: &mut Engine, - styles: StyleChain, - x: usize, - y: usize, - ) -> SourceResult { - Ok(match &self.0 { - Celled::Value(value) => value.clone(), - Celled::Func(func) => func - .call(engine, Context::new(None, Some(styles)).track(), [x, y])? - .cast::() - .at(func.span())? - .resolve(styles), - Celled::Array(array) => x - .checked_rem(array.len()) - .and_then(|i| array.get(i)) - .cloned() - .unwrap_or_default(), - }) - } -} diff --git a/crates/typst-library/src/layout/grid/mod.rs b/crates/typst-library/src/layout/grid/mod.rs new file mode 100644 index 00000000..e46440fb --- /dev/null +++ b/crates/typst-library/src/layout/grid/mod.rs @@ -0,0 +1,926 @@ +pub mod resolve; + +use std::num::NonZeroUsize; +use std::sync::Arc; + +use comemo::Track; +use smallvec::{smallvec, SmallVec}; +use typst_utils::NonZeroExt; + +use crate::diag::{bail, At, HintedStrResult, HintedString, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func, + IntoValue, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, Value, +}; +use crate::layout::{ + Alignment, BlockElem, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing, +}; +use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine}; +use crate::visualize::{Paint, Stroke}; + +/// Arranges content in a grid. +/// +/// The grid element allows you to arrange content in a grid. You can define the +/// number of rows and columns, as well as the size of the gutters between them. +/// There are multiple sizing modes for columns and rows that can be used to +/// create complex layouts. +/// +/// While the grid and table elements work very similarly, they are intended for +/// different use cases and carry different semantics. The grid element is +/// intended for presentational and layout purposes, while the +/// [`{table}`]($table) element is intended for, in broad terms, presenting +/// multiple related data points. In the future, Typst will annotate its output +/// such that screenreaders will announce content in `table` as tabular while a +/// grid's content will be announced no different than multiple content blocks +/// in the document flow. Set and show rules on one of these elements do not +/// affect the other. +/// +/// A grid's sizing is determined by the track sizes specified in the arguments. +/// Because each of the sizing parameters accepts the same values, we will +/// explain them just once, here. Each sizing argument accepts an array of +/// individual track sizes. A track size is either: +/// +/// - `{auto}`: The track will be sized to fit its contents. It will be at most +/// as large as the remaining space. If there is more than one `{auto}` track +/// width, and together they claim more than the available space, the `{auto}` +/// tracks will fairly distribute the available space among themselves. +/// +/// - A fixed or relative length (e.g. `{10pt}` or `{20% - 1cm}`): The track +/// will be exactly of this size. +/// +/// - A fractional length (e.g. `{1fr}`): Once all other tracks have been sized, +/// the remaining space will be divided among the fractional tracks according +/// to their fractions. For example, if there are two fractional tracks, each +/// with a fraction of `{1fr}`, they will each take up half of the remaining +/// space. +/// +/// To specify a single track, the array can be omitted in favor of a single +/// value. To specify multiple `{auto}` tracks, enter the number of tracks +/// instead of an array. For example, `columns:` `{3}` is equivalent to +/// `columns:` `{(auto, auto, auto)}`. +/// +/// # Examples +/// The example below demonstrates the different track sizing options. It also +/// shows how you can use [`grid.cell`]($grid.cell) to make an individual cell +/// span two grid tracks. +/// +/// ```example +/// // We use `rect` to emphasize the +/// // area of cells. +/// #set rect( +/// inset: 8pt, +/// fill: rgb("e4e5ea"), +/// width: 100%, +/// ) +/// +/// #grid( +/// columns: (60pt, 1fr, 2fr), +/// rows: (auto, 60pt), +/// gutter: 3pt, +/// rect[Fixed width, auto height], +/// rect[1/3 of the remains], +/// rect[2/3 of the remains], +/// rect(height: 100%)[Fixed height], +/// grid.cell( +/// colspan: 2, +/// image("tiger.jpg", width: 100%), +/// ), +/// ) +/// ``` +/// +/// You can also [spread]($arguments/#spreading) an array of strings or content +/// into a grid to populate its cells. +/// +/// ```example +/// #grid( +/// columns: 5, +/// gutter: 5pt, +/// ..range(25).map(str) +/// ) +/// ``` +/// +/// # Styling the grid +/// The grid's appearance can be customized through different parameters. These +/// are the most important ones: +/// +/// - [`fill`]($grid.fill) to give all cells a background +/// - [`align`]($grid.align) to change how cells are aligned +/// - [`inset`]($grid.inset) to optionally add internal padding to each cell +/// - [`stroke`]($grid.stroke) to optionally enable grid lines with a certain +/// stroke +/// +/// If you need to override one of the above options for a single cell, you can +/// use the [`grid.cell`]($grid.cell) element. Likewise, you can override +/// individual grid lines with the [`grid.hline`]($grid.hline) and +/// [`grid.vline`]($grid.vline) elements. +/// +/// Alternatively, if you need the appearance options to depend on a cell's +/// position (column and row), you may specify a function to `fill` or `align` +/// of the form `(column, row) => value`. You may also use a show rule on +/// [`grid.cell`]($grid.cell) - see that element's examples or the examples +/// below for more information. +/// +/// Locating most of your styling in set and show rules is recommended, as it +/// keeps the grid's or table's actual usages clean and easy to read. It also +/// allows you to easily change the grid's appearance in one place. +/// +/// ## Stroke styling precedence +/// There are three ways to set the stroke of a grid cell: through +/// [`{grid.cell}`'s `stroke` field]($grid.cell.stroke), by using +/// [`{grid.hline}`]($grid.hline) and [`{grid.vline}`]($grid.vline), or by +/// setting the [`{grid}`'s `stroke` field]($grid.stroke). When multiple of +/// these settings are present and conflict, the `hline` and `vline` settings +/// take the highest precedence, followed by the `cell` settings, and finally +/// the `grid` settings. +/// +/// Furthermore, strokes of a repeated grid header or footer will take +/// precedence over regular cell strokes. +#[elem(scope, Show)] +pub struct GridElem { + /// The column sizes. + /// + /// Either specify a track size array or provide an integer to create a grid + /// with that many `{auto}`-sized columns. Note that opposed to rows and + /// gutters, providing a single track size will only ever create a single + /// column. + #[borrowed] + pub columns: TrackSizings, + + /// The row sizes. + /// + /// If there are more cells than fit the defined rows, the last row is + /// repeated until there are no more cells. + #[borrowed] + pub rows: TrackSizings, + + /// The gaps between rows and columns. + /// + /// If there are more gutters than defined sizes, the last gutter is + /// repeated. + /// + /// This is a shorthand to set `column-gutter` and `row-gutter` to the same + /// value. + #[external] + pub gutter: TrackSizings, + + /// The gaps between columns. + #[parse( + let gutter = args.named("gutter")?; + args.named("column-gutter")?.or_else(|| gutter.clone()) + )] + #[borrowed] + pub column_gutter: TrackSizings, + + /// The gaps between rows. + #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] + #[borrowed] + pub row_gutter: TrackSizings, + + /// How to fill the cells. + /// + /// This can be a color or a function that returns a color. The function + /// receives the cells' column and row indices, starting from zero. This can + /// be used to implement striped grids. + /// + /// ```example + /// #grid( + /// fill: (x, y) => + /// if calc.even(x + y) { luma(230) } + /// else { white }, + /// align: center + horizon, + /// columns: 4, + /// inset: 2pt, + /// [X], [O], [X], [O], + /// [O], [X], [O], [X], + /// [X], [O], [X], [O], + /// [O], [X], [O], [X], + /// ) + /// ``` + #[borrowed] + pub fill: Celled>, + + /// How to align the cells' content. + /// + /// This can either be a single alignment, an array of alignments + /// (corresponding to each column) or a function that returns an alignment. + /// The function receives the cells' column and row indices, starting from + /// zero. If set to `{auto}`, the outer alignment is used. + /// + /// You can find an example for this argument at the + /// [`table.align`]($table.align) parameter. + #[borrowed] + pub align: Celled>, + + /// How to [stroke]($stroke) the cells. + /// + /// Grids have no strokes by default, which can be changed by setting this + /// option to the desired stroke. + /// + /// If it is necessary to place lines which can cross spacing between cells + /// produced by the `gutter` option, or to override the stroke between + /// multiple specific cells, consider specifying one or more of + /// [`grid.hline`]($grid.hline) and [`grid.vline`]($grid.vline) alongside + /// your grid cells. + /// + /// ```example + /// #set page(height: 13em, width: 26em) + /// + /// #let cv(..jobs) = grid( + /// columns: 2, + /// inset: 5pt, + /// stroke: (x, y) => if x == 0 and y > 0 { + /// (right: ( + /// paint: luma(180), + /// thickness: 1.5pt, + /// dash: "dotted" + /// )) + /// }, + /// grid.header(grid.cell(colspan: 2)[ + /// *Professional Experience* + /// #box(width: 1fr, line(length: 100%, stroke: luma(180))) + /// ]), + /// ..{ + /// let last = none + /// for job in jobs.pos() { + /// ( + /// if job.year != last [*#job.year*], + /// [ + /// *#job.company* - #job.role _(#job.timeframe)_ \ + /// #job.details + /// ] + /// ) + /// last = job.year + /// } + /// } + /// ) + /// + /// #cv( + /// ( + /// year: 2012, + /// company: [Pear Seed & Co.], + /// role: [Lead Engineer], + /// timeframe: [Jul - Dec], + /// details: [ + /// - Raised engineers from 3x to 10x + /// - Did a great job + /// ], + /// ), + /// ( + /// year: 2012, + /// company: [Mega Corp.], + /// role: [VP of Sales], + /// timeframe: [Mar - Jun], + /// details: [- Closed tons of customers], + /// ), + /// ( + /// year: 2013, + /// company: [Tiny Co.], + /// role: [CEO], + /// timeframe: [Jan - Dec], + /// details: [- Delivered 4x more shareholder value], + /// ), + /// ( + /// year: 2014, + /// company: [Glorbocorp Ltd], + /// role: [CTO], + /// timeframe: [Jan - Mar], + /// details: [- Drove containerization forward], + /// ), + /// ) + /// ``` + #[resolve] + #[fold] + pub stroke: Celled>>>>, + + /// How much to pad the cells' content. + /// + /// You can find an example for this argument at the + /// [`table.inset`]($table.inset) parameter. + #[fold] + pub inset: Celled>>>, + + /// The contents of the grid cells, plus any extra grid lines specified + /// with the [`grid.hline`]($grid.hline) and [`grid.vline`]($grid.vline) + /// elements. + /// + /// The cells are populated in row-major order. + #[variadic] + pub children: Vec, +} + +#[scope] +impl GridElem { + #[elem] + type GridCell; + + #[elem] + type GridHLine; + + #[elem] + type GridVLine; + + #[elem] + type GridHeader; + + #[elem] + type GridFooter; +} + +impl Show for Packed { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { + Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_grid) + .pack() + .spanned(self.span())) + } +} + +/// Track sizing definitions. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct TrackSizings(pub SmallVec<[Sizing; 4]>); + +cast! { + TrackSizings, + self => self.0.into_value(), + sizing: Sizing => Self(smallvec![sizing]), + count: NonZeroUsize => Self(smallvec![Sizing::Auto; count.get()]), + values: Array => Self(values.into_iter().map(Value::cast).collect::>()?), +} + +/// Any child of a grid element. +#[derive(Debug, PartialEq, Clone, Hash)] +pub enum GridChild { + Header(Packed), + Footer(Packed), + Item(GridItem), +} + +cast! { + GridChild, + self => match self { + Self::Header(header) => header.into_value(), + Self::Footer(footer) => footer.into_value(), + Self::Item(item) => item.into_value(), + }, + v: Content => { + v.try_into()? + }, +} + +impl TryFrom for GridChild { + type Error = HintedString; + fn try_from(value: Content) -> HintedStrResult { + if value.is::() { + bail!( + "cannot use `table.header` as a grid header"; + hint: "use `grid.header` instead" + ) + } + if value.is::() { + bail!( + "cannot use `table.footer` as a grid footer"; + hint: "use `grid.footer` instead" + ) + } + + value + .into_packed::() + .map(Self::Header) + .or_else(|value| value.into_packed::().map(Self::Footer)) + .or_else(|value| GridItem::try_from(value).map(Self::Item)) + } +} + +/// A grid item, which is the basic unit of grid specification. +#[derive(Debug, PartialEq, Clone, Hash)] +pub enum GridItem { + HLine(Packed), + VLine(Packed), + Cell(Packed), +} + +cast! { + GridItem, + self => match self { + Self::HLine(hline) => hline.into_value(), + Self::VLine(vline) => vline.into_value(), + Self::Cell(cell) => cell.into_value(), + }, + v: Content => { + v.try_into()? + } +} + +impl TryFrom for GridItem { + type Error = HintedString; + fn try_from(value: Content) -> HintedStrResult { + if value.is::() { + bail!("cannot place a grid header within another header or footer"); + } + if value.is::() { + bail!("cannot place a table header within another header or footer"); + } + if value.is::() { + bail!("cannot place a grid footer within another footer or header"); + } + if value.is::() { + bail!("cannot place a table footer within another footer or header"); + } + if value.is::() { + bail!( + "cannot use `table.cell` as a grid cell"; + hint: "use `grid.cell` instead" + ); + } + if value.is::() { + bail!( + "cannot use `table.hline` as a grid line"; + hint: "use `grid.hline` instead" + ); + } + if value.is::() { + bail!( + "cannot use `table.vline` as a grid line"; + hint: "use `grid.vline` instead" + ); + } + + Ok(value + .into_packed::() + .map(Self::HLine) + .or_else(|value| value.into_packed::().map(Self::VLine)) + .or_else(|value| value.into_packed::().map(Self::Cell)) + .unwrap_or_else(|value| { + let span = value.span(); + Self::Cell(Packed::new(GridCell::new(value)).spanned(span)) + })) + } +} + +/// A repeatable grid header. +/// +/// If `repeat` is set to `true`, the header will be repeated across pages. For +/// an example, refer to the [`table.header`]($table.header) element and the +/// [`grid.stroke`]($grid.stroke) parameter. +#[elem(name = "header", title = "Grid Header")] +pub struct GridHeader { + /// Whether this header should be repeated across pages. + #[default(true)] + pub repeat: bool, + + /// The cells and lines within the header. + #[variadic] + pub children: Vec, +} + +/// A repeatable grid footer. +/// +/// Just like the [`grid.header`]($grid.header) element, the footer can repeat +/// itself on every page of the table. +/// +/// No other grid cells may be placed after the footer. +#[elem(name = "footer", title = "Grid Footer")] +pub struct GridFooter { + /// Whether this footer should be repeated across pages. + #[default(true)] + pub repeat: bool, + + /// The cells and lines within the footer. + #[variadic] + pub children: Vec, +} + +/// A horizontal line in the grid. +/// +/// Overrides any per-cell stroke, including stroke specified through the grid's +/// `stroke` field. Can cross spacing between cells created through the grid's +/// `column-gutter` option. +/// +/// An example for this function can be found at the +/// [`table.hline`]($table.hline) element. +#[elem(name = "hline", title = "Grid Horizontal Line")] +pub struct GridHLine { + /// The row above which the horizontal line is placed (zero-indexed). + /// If the `position` field is set to `{bottom}`, the line is placed below + /// the row with the given index instead (see that field's docs for + /// details). + /// + /// Specifying `{auto}` causes the line to be placed at the row below the + /// last automatically positioned cell (that is, cell without coordinate + /// overrides) before the line among the grid's children. If there is no + /// such cell before the line, it is placed at the top of the grid (row 0). + /// Note that specifying for this option exactly the total amount of rows + /// in the grid causes this horizontal line to override the bottom border + /// of the grid, while a value of 0 overrides the top border. + pub y: Smart, + + /// The column at which the horizontal line starts (zero-indexed, inclusive). + pub start: usize, + + /// The column before which the horizontal line ends (zero-indexed, + /// exclusive). + /// Therefore, the horizontal line will be drawn up to and across column + /// `end - 1`. + /// + /// A value equal to `{none}` or to the amount of columns causes it to + /// extend all the way towards the end of the grid. + pub end: Option, + + /// The line's stroke. + /// + /// Specifying `{none}` removes any lines previously placed across this + /// line's range, including hlines or per-cell stroke below it. + #[resolve] + #[fold] + #[default(Some(Arc::new(Stroke::default())))] + pub stroke: Option>, + + /// The position at which the line is placed, given its row (`y`) - either + /// `{top}` to draw above it or `{bottom}` to draw below it. + /// + /// This setting is only relevant when row gutter is enabled (and + /// shouldn't be used otherwise - prefer just increasing the `y` field by + /// one instead), since then the position below a row becomes different + /// from the position above the next row due to the spacing between both. + #[default(OuterVAlignment::Top)] + pub position: OuterVAlignment, +} + +/// A vertical line in the grid. +/// +/// Overrides any per-cell stroke, including stroke specified through the +/// grid's `stroke` field. Can cross spacing between cells created through +/// the grid's `row-gutter` option. +#[elem(name = "vline", title = "Grid Vertical Line")] +pub struct GridVLine { + /// The column before which the horizontal line is placed (zero-indexed). + /// If the `position` field is set to `{end}`, the line is placed after the + /// column with the given index instead (see that field's docs for + /// details). + /// + /// Specifying `{auto}` causes the line to be placed at the column after + /// the last automatically positioned cell (that is, cell without + /// coordinate overrides) before the line among the grid's children. If + /// there is no such cell before the line, it is placed before the grid's + /// first column (column 0). + /// Note that specifying for this option exactly the total amount of + /// columns in the grid causes this vertical line to override the end + /// border of the grid (right in LTR, left in RTL), while a value of 0 + /// overrides the start border (left in LTR, right in RTL). + pub x: Smart, + + /// The row at which the vertical line starts (zero-indexed, inclusive). + pub start: usize, + + /// The row on top of which the vertical line ends (zero-indexed, + /// exclusive). + /// Therefore, the vertical line will be drawn up to and across row + /// `end - 1`. + /// + /// A value equal to `{none}` or to the amount of rows causes it to extend + /// all the way towards the bottom of the grid. + pub end: Option, + + /// The line's stroke. + /// + /// Specifying `{none}` removes any lines previously placed across this + /// line's range, including vlines or per-cell stroke below it. + #[resolve] + #[fold] + #[default(Some(Arc::new(Stroke::default())))] + pub stroke: Option>, + + /// The position at which the line is placed, given its column (`x`) - + /// either `{start}` to draw before it or `{end}` to draw after it. + /// + /// The values `{left}` and `{right}` are also accepted, but discouraged as + /// they cause your grid to be inconsistent between left-to-right and + /// right-to-left documents. + /// + /// This setting is only relevant when column gutter is enabled (and + /// shouldn't be used otherwise - prefer just increasing the `x` field by + /// one instead), since then the position after a column becomes different + /// from the position before the next column due to the spacing between + /// both. + #[default(OuterHAlignment::Start)] + pub position: OuterHAlignment, +} + +/// A cell in the grid. You can use this function in the argument list of a grid +/// to override grid style properties for an individual cell or manually +/// positioning it within the grid. You can also use this function in show rules +/// to apply certain styles to multiple cells at once. +/// +/// For example, you can override the position and stroke for a single cell: +/// +/// ```example +/// >>> #set page(width: auto) +/// >>> #set text(15pt, font: "Noto Sans Symbols 2", bottom-edge: -.2em) +/// <<< #set text(15pt, font: "Noto Sans Symbols 2") +/// #show regex("[♚-♟︎]"): set text(fill: rgb("21212A")) +/// #show regex("[♔-♙]"): set text(fill: rgb("111015")) +/// +/// #grid( +/// fill: (x, y) => rgb( +/// if calc.odd(x + y) { "7F8396" } +/// else { "EFF0F3" } +/// ), +/// columns: (1em,) * 8, +/// rows: 1em, +/// align: center + horizon, +/// +/// [♖], [♘], [♗], [♕], [♔], [♗], [♘], [♖], +/// [♙], [♙], [♙], [♙], [], [♙], [♙], [♙], +/// grid.cell( +/// x: 4, y: 3, +/// stroke: blue.transparentize(60%) +/// )[♙], +/// +/// ..(grid.cell(y: 6)[♟],) * 8, +/// ..([♜], [♞], [♝], [♛], [♚], [♝], [♞], [♜]) +/// .map(grid.cell.with(y: 7)), +/// ) +/// ``` +/// +/// You may also apply a show rule on `grid.cell` to style all cells at once, +/// which allows you, for example, to apply styles based on a cell's position. +/// Refer to the examples of the [`table.cell`]($table.cell) element to learn +/// more about this. +#[elem(name = "cell", title = "Grid Cell", Show)] +pub struct GridCell { + /// The cell's body. + #[required] + pub body: Content, + + /// The cell's column (zero-indexed). + /// This field may be used in show rules to style a cell depending on its + /// column. + /// + /// You may override this field to pick in which column the cell must + /// be placed. If no row (`y`) is chosen, the cell will be placed in the + /// first row (starting at row 0) with that column available (or a new row + /// if none). If both `x` and `y` are chosen, however, the cell will be + /// placed in that exact position. An error is raised if that position is + /// not available (thus, it is usually wise to specify cells with a custom + /// position before cells with automatic positions). + /// + /// ```example + /// #let circ(c) = circle( + /// fill: c, width: 5mm + /// ) + /// + /// #grid( + /// columns: 4, + /// rows: 7mm, + /// stroke: .5pt + blue, + /// align: center + horizon, + /// inset: 1mm, + /// + /// grid.cell(x: 2, y: 2, circ(aqua)), + /// circ(yellow), + /// grid.cell(x: 3, circ(green)), + /// circ(black), + /// ) + /// ``` + pub x: Smart, + + /// The cell's row (zero-indexed). + /// This field may be used in show rules to style a cell depending on its + /// row. + /// + /// You may override this field to pick in which row the cell must be + /// placed. If no column (`x`) is chosen, the cell will be placed in the + /// first column (starting at column 0) available in the chosen row. If all + /// columns in the chosen row are already occupied, an error is raised. + /// + /// ```example + /// #let tri(c) = polygon.regular( + /// fill: c, + /// size: 5mm, + /// vertices: 3, + /// ) + /// + /// #grid( + /// columns: 2, + /// stroke: blue, + /// inset: 1mm, + /// + /// tri(black), + /// grid.cell(y: 1, tri(teal)), + /// grid.cell(y: 1, tri(red)), + /// grid.cell(y: 2, tri(orange)) + /// ) + /// ``` + pub y: Smart, + + /// The amount of columns spanned by this cell. + #[default(NonZeroUsize::ONE)] + pub colspan: NonZeroUsize, + + /// The amount of rows spanned by this cell. + #[default(NonZeroUsize::ONE)] + pub rowspan: NonZeroUsize, + + /// The cell's [fill]($grid.fill) override. + pub fill: Smart>, + + /// The cell's [alignment]($grid.align) override. + pub align: Smart, + + /// The cell's [inset]($grid.inset) override. + pub inset: Smart>>>, + + /// The cell's [stroke]($grid.stroke) override. + #[resolve] + #[fold] + pub stroke: Sides>>>, + + /// Whether rows spanned by this cell can be placed in different pages. + /// When equal to `{auto}`, a cell spanning only fixed-size rows is + /// unbreakable, while a cell spanning at least one `{auto}`-sized row is + /// breakable. + pub breakable: Smart, +} + +cast! { + GridCell, + v: Content => v.into(), +} + +impl Show for Packed { + fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { + show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) + } +} + +impl Default for Packed { + fn default() -> Self { + Packed::new(GridCell::new(Content::default())) + } +} + +impl From for GridCell { + fn from(value: Content) -> Self { + #[allow(clippy::unwrap_or_default)] + value.unpack::().unwrap_or_else(Self::new) + } +} + +/// Function with common code to display a grid cell or table cell. +pub(crate) fn show_grid_cell( + mut body: Content, + inset: Smart>>>, + align: Smart, +) -> SourceResult { + let inset = inset.unwrap_or_default().map(Option::unwrap_or_default); + + if inset != Sides::default() { + // Only pad if some inset is not 0pt. + // Avoids a bug where using .padded() in any way inside Show causes + // alignment in align(...) to break. + body = body.padded(inset); + } + + if let Smart::Custom(alignment) = align { + body = body.aligned(alignment); + } + + Ok(body) +} + +/// A value that can be configured per cell. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Celled { + /// A bare value, the same for all cells. + Value(T), + /// A closure mapping from cell coordinates to a value. + Func(Func), + /// An array of alignment values corresponding to each column. + Array(Vec), +} + +impl Celled { + /// Resolve the value based on the cell position. + pub fn resolve( + &self, + engine: &mut Engine, + styles: StyleChain, + x: usize, + y: usize, + ) -> SourceResult { + Ok(match self { + Self::Value(value) => value.clone(), + Self::Func(func) => func + .call(engine, Context::new(None, Some(styles)).track(), [x, y])? + .cast() + .at(func.span())?, + Self::Array(array) => x + .checked_rem(array.len()) + .and_then(|i| array.get(i)) + .cloned() + .unwrap_or_default(), + }) + } +} + +impl Default for Celled { + fn default() -> Self { + Self::Value(T::default()) + } +} + +impl Reflect for Celled { + fn input() -> CastInfo { + T::input() + Array::input() + Func::input() + } + + fn output() -> CastInfo { + T::output() + Array::output() + Func::output() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) || Func::castable(value) || T::castable(value) + } +} + +impl IntoValue for Celled { + fn into_value(self) -> Value { + match self { + Self::Value(value) => value.into_value(), + Self::Func(func) => func.into_value(), + Self::Array(arr) => arr.into_value(), + } + } +} + +impl FromValue for Celled { + fn from_value(value: Value) -> HintedStrResult { + match value { + Value::Func(v) => Ok(Self::Func(v)), + Value::Array(array) => Ok(Self::Array( + array.into_iter().map(T::from_value).collect::>()?, + )), + v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), + v => Err(Self::error(&v)), + } + } +} + +impl Fold for Celled { + fn fold(self, outer: Self) -> Self { + match (self, outer) { + (Self::Value(inner), Self::Value(outer)) => Self::Value(inner.fold(outer)), + (self_, _) => self_, + } + } +} + +impl Resolve for Celled { + type Output = ResolvedCelled; + + fn resolve(self, styles: StyleChain) -> Self::Output { + match self { + Self::Value(value) => ResolvedCelled(Celled::Value(value.resolve(styles))), + Self::Func(func) => ResolvedCelled(Celled::Func(func)), + Self::Array(values) => ResolvedCelled(Celled::Array( + values.into_iter().map(|value| value.resolve(styles)).collect(), + )), + } + } +} + +/// The result of resolving a Celled's value according to styles. +/// Holds resolved values which depend on each grid cell's position. +/// When it is a closure, however, it is only resolved when the closure is +/// called. +#[derive(Default, Clone)] +pub struct ResolvedCelled(Celled); + +impl ResolvedCelled +where + T: FromValue + Resolve, + ::Output: Default + Clone, +{ + /// Resolve the value based on the cell position. + pub fn resolve( + &self, + engine: &mut Engine, + styles: StyleChain, + x: usize, + y: usize, + ) -> SourceResult { + Ok(match &self.0 { + Celled::Value(value) => value.clone(), + Celled::Func(func) => func + .call(engine, Context::new(None, Some(styles)).track(), [x, y])? + .cast::() + .at(func.span())? + .resolve(styles), + Celled::Array(array) => x + .checked_rem(array.len()) + .and_then(|i| array.get(i)) + .cloned() + .unwrap_or_default(), + }) + } +} diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs new file mode 100644 index 00000000..adaff1c1 --- /dev/null +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -0,0 +1,1773 @@ +use std::num::NonZeroUsize; +use std::sync::Arc; + +use ecow::eco_format; +use typst_library::diag::{ + bail, At, Hint, HintedStrResult, HintedString, SourceResult, Trace, Tracepoint, +}; +use typst_library::engine::Engine; +use typst_library::foundations::{Content, Fold, Packed, Smart, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{ + Abs, Alignment, Axes, Celled, GridCell, GridChild, GridElem, GridItem, Length, + OuterHAlignment, OuterVAlignment, Rel, ResolvedCelled, Sides, Sizing, +}; +use typst_library::model::{TableCell, TableChild, TableElem, TableItem}; +use typst_library::text::TextElem; +use typst_library::visualize::{Paint, Stroke}; +use typst_library::Dir; + +use typst_syntax::Span; +use typst_utils::NonZeroExt; + +/// Convert a grid to a cell grid. +#[typst_macros::time(span = elem.span())] +pub fn grid_to_cellgrid<'a>( + elem: &Packed, + engine: &mut Engine, + locator: Locator<'a>, + styles: StyleChain, +) -> SourceResult> { + 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)) + } + }); + CellGrid::resolve( + tracks, + gutter, + locator, + children, + fill, + align, + &inset, + &stroke, + engine, + styles, + elem.span(), + ) + .trace(engine.world, tracepoint, elem.span()) +} + +/// Convert a table to a cell grid. +#[typst_macros::time(span = elem.span())] +pub fn table_to_cellgrid<'a>( + elem: &Packed, + engine: &mut Engine, + locator: Locator<'a>, + styles: StyleChain, +) -> SourceResult> { + 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)) + } + }); + CellGrid::resolve( + tracks, + gutter, + locator, + children, + fill, + align, + &inset, + &stroke, + engine, + styles, + elem.span(), + ) + .trace(engine.world, tracepoint, elem.span()) +} + +fn grid_item_to_resolvable( + item: &GridItem, + styles: StyleChain, +) -> ResolvableGridItem> { + 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> { + 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 { + fn resolve_cell<'a>( + mut self, + x: usize, + y: usize, + fill: &Option, + align: Smart, + inset: Sides>>, + stroke: Sides>>>>, + 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 { + (**self).x(styles) + } + + fn y(&self, styles: StyleChain) -> Smart { + (**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 { + fn resolve_cell<'a>( + mut self, + x: usize, + y: usize, + fill: &Option, + align: Smart, + inset: Sides>>, + stroke: Sides>>>>, + 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 { + (**self).x(styles) + } + + fn y(&self, styles: StyleChain) -> Smart { + (**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) + } +} + +/// 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, + /// The line's stroke. This is `None` when the line is explicitly used to + /// override a previously specified line. + pub stroke: Option>>, + /// The line's position in relation to the track with its index. + pub position: LinePosition, +} + +/// 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 { + Repeated(T), + NotRepeated(T), +} + +impl Repeatable { + /// 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, + } + } +} + +/// 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, + align: Smart, + inset: Sides>>, + stroke: Sides>>>>, + breakable: bool, + locator: Locator<'a>, + styles: StyleChain, + ) -> Cell<'a>; + + /// Returns this cell's column override. + fn x(&self, styles: StyleChain) -> Smart; + + /// Returns this cell's row override. + fn y(&self, styles: StyleChain) -> Smart; + + /// 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 { + /// A horizontal line in the grid. + HLine { + /// The row above which the horizontal line is drawn. + y: Smart, + start: usize, + end: Option, + stroke: Option>>, + /// 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, + start: usize, + end: Option, + stroke: Option>>, + /// 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, + /// 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>>>, + /// 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, + /// 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, + } + } +} + +/// 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 { + Header { repeat: bool, span: Span, items: I }, + Footer { repeat: bool, span: Span, items: I }, + Item(ResolvableGridItem), +} + +/// A grid of cells, including the columns, rows, and cell data. +pub struct CellGrid<'a> { + /// The grid cells. + pub entries: Vec>, + /// The column tracks including gutter tracks. + pub cols: Vec, + /// The row tracks including gutter tracks. + pub rows: Vec, + /// 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>, + /// 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>, + /// The repeatable header of this grid. + pub header: Option>, + /// The repeatable footer of this grid. + pub footer: Option>, + /// 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>, + ) -> 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( + tracks: Axes<&[Sizing]>, + gutter: Axes<&[Sizing]>, + locator: Locator<'a>, + children: C, + fill: &Celled>, + align: &Celled>, + inset: &Celled>>>, + stroke: &ResolvedCelled>>>>, + engine: &mut Engine, + styles: StyleChain, + span: Span, + ) -> SourceResult + where + T: ResolvableCell + Default, + I: Iterator>, + C: IntoIterator>, + 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
= 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> = 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'. + // 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::>>()?; + + // 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![]; + let mut hlines: Vec> = 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>, + hlines: Vec>, + header: Option>, + footer: Option>, + entries: Vec>, + ) -> 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> { + 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> { + 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, + cell_y: Smart, + colspan: usize, + rowspan: usize, + resolved_cells: &[Option], + auto_index: &mut usize, + start_new_row: &mut bool, + columns: usize, +) -> HintedStrResult { + // 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], + 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], + 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-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs index b54d6906..574a2830 100644 --- a/crates/typst-library/src/layout/mod.rs +++ b/crates/typst-library/src/layout/mod.rs @@ -12,7 +12,7 @@ mod em; mod fr; mod fragment; mod frame; -mod grid; +pub mod grid; mod hide; #[path = "layout.rs"] mod layout_; -- cgit v1.2.3