diff --git a/crates/typst/src/layout/grid/layout.rs b/crates/typst/src/layout/grid/layout.rs index a27e42697..c18a50351 100644 --- a/crates/typst/src/layout/grid/layout.rs +++ b/crates/typst/src/layout/grid/layout.rs @@ -242,9 +242,15 @@ impl Entry { } } +/// A repeatable grid header. Starts at the first row. +pub(super) struct Header { + /// The index after the last row included in this header. + pub(super) end: usize, +} + /// A grid item, possibly affected by automatic cell positioning. Can be either /// a line or a cell. -pub enum GridItem { +pub enum ResolvableGridItem { /// A horizontal line in the grid. HLine { /// The row above which the horizontal line is drawn. @@ -275,6 +281,12 @@ pub enum GridItem { Cell(T), } +/// Any grid child, which can be either a header or an item. +pub enum ResolvableGridChild { + Header { repeat: bool, span: Span, items: I }, + Item(ResolvableGridItem), +} + /// Used for cell-like elements which are aware of their final properties in /// the table, and may have property overrides. pub trait ResolvableCell { @@ -327,6 +339,8 @@ pub struct CellGrid { /// Gutter rows are not included. /// Contains up to 'rows_without_gutter.len() + 1' vectors of lines. pub(super) hlines: Vec>, + /// The repeatable header of this grid. + pub(super) header: Option
, /// Whether this grid has gutters. pub(super) has_gutter: bool, } @@ -339,7 +353,7 @@ impl CellGrid { cells: impl IntoIterator, ) -> Self { let entries = cells.into_iter().map(Entry::Cell).collect(); - Self::new_internal(tracks, gutter, vec![], vec![], entries) + Self::new_internal(tracks, gutter, vec![], vec![], None, entries) } /// Resolves and positions all cells in the grid before creating it. @@ -349,10 +363,10 @@ impl CellGrid { /// 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( + pub fn resolve( tracks: Axes<&[Sizing]>, gutter: Axes<&[Sizing]>, - items: I, + children: C, fill: &Celled>, align: &Celled>, inset: &Celled>>>, @@ -363,8 +377,9 @@ impl CellGrid { ) -> SourceResult where T: ResolvableCell + Default, - I: IntoIterator>, - I::IntoIter: ExactSizeIterator, + I: Iterator>, + C: IntoIterator>, + C::IntoIter: ExactSizeIterator, { // Number of content columns: Always at least one. let c = tracks.x.len().max(1); @@ -380,6 +395,9 @@ impl CellGrid { 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; + // Resolve the breakability of a cell, based on whether or not it spans // an auto row. let resolve_breakable = |y, rowspan| { @@ -411,213 +429,361 @@ impl CellGrid { let mut auto_index: usize = 0; // We have to rebuild the grid to account for arbitrary positions. - // Create at least 'items.len()' positions, since there could be at - // least 'items.len()' cells (if no explicit lines were specified), + // 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 'items.len()' is already a multiple of 'c' (thus - // 'items.len() % c' would be zero). - let items = items.into_iter(); - let Some(item_count) = items.len().checked_add((c - items.len() % c) % c) else { + // 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(item_count); - for item in items { - let cell = match item { - GridItem::HLine { y, start, end, stroke, span, position } => { - let y = y.unwrap_or_else(|| { - // 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. - 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 mut resolved_cells: Vec> = Vec::with_capacity(child_count); + for child in children { + let mut is_header = false; + let mut header_start = usize::MAX; + let mut header_end = 0; + let mut header_span = Span::detached(); + let mut min_auto_index = 0; + + let (header_items, simple_item) = match child { + ResolvableGridChild::Header { repeat, span, items, .. } => { + if header.is_some() { + bail!(span, "cannot have more than one header"); } - 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)); - continue; + is_header = true; + header_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. + min_auto_index = auto_index.next_multiple_of(c); + + (Some(items), None) } - GridItem::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. - 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: 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; - } - GridItem::Cell(cell) => cell, + ResolvableGridChild::Item(item) => (None, Some(item)), }; - let cell_span = cell.span(); - // 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, &resolved_cells, &mut auto_index, c) - .at(cell_span)? - }; - let x = resolved_index % c; - let y = resolved_index / c; - let colspan = cell.colspan(styles).get(); - let rowspan = cell.rowspan(styles).get(); + let items = header_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 y = y.unwrap_or_else(|| { + // 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. + auto_index + .max(min_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 }; - 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), - 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(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. + // 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)); 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" - ) + 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 the current auto index from + // past iterations is smaller than the minimum auto + // index. 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. + auto_index + .checked_sub(1) + .filter(|last_auto_index| { + last_auto_index >= &min_auto_index + }) + .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; } - *slot = Some(Entry::Merged { parent: resolved_index }); + ResolvableGridItem::Cell(cell) => cell, + }; + let cell_span = cell.span(); + // 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, + &resolved_cells, + &mut auto_index, + min_auto_index, + c, + ) + .at(cell_span)? + }; + let x = resolved_index % c; + let y = resolved_index / c; + let colspan = cell.colspan(styles).get(); + let rowspan = cell.rowspan(styles).get(); + + 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), + 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(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 { + // Ensure each cell in a header is fully contained within + // the header. + header_start = header_start.min(y); + header_end = header_end.max(y + rowspan); + } + } + + if is_header { + if header_start == usize::MAX { + // Empty header: consider the header to be one row after + // the latest auto index. + header_start = auto_index.next_multiple_of(c) / c; + header_end = header_start + 1; + } + + if header_start != 0 { + bail!( + header_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: header_end, + }); + + // 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 * header_end); } } - // Replace absent entries by resolved empty cells, and produce a vector - // of 'Entry' from 'Option' (final step). + // 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. 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. let resolved_cells = resolved_cells .into_iter() .enumerate() .map(|(i, cell)| { if let Some(cell) = cell { + if let Some((parent_cell, header)) = + cell.as_cell().zip(header.as_mut()) + { + 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()); + } + } + Ok(cell) } else { let x = i % c; @@ -722,7 +888,36 @@ impl CellGrid { vlines[x].push(line); } - Ok(Self::new_internal(tracks, gutter, vlines, hlines, resolved_cells)) + // No point in storing the header if it shouldn't be repeated. + let header = header.filter(|_| repeat_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 + }); + + Ok(Self::new_internal(tracks, gutter, vlines, hlines, header, resolved_cells)) } /// Generates the cell grid, given the tracks and resolved entries. @@ -731,6 +926,7 @@ impl CellGrid { gutter: Axes<&[Sizing]>, vlines: Vec>, hlines: Vec>, + header: Option
, entries: Vec, ) -> Self { let mut cols = vec![]; @@ -777,7 +973,15 @@ impl CellGrid { rows.pop(); } - Self { cols, rows, entries, vlines, hlines, has_gutter } + Self { + cols, + rows, + entries, + vlines, + hlines, + header, + has_gutter, + } } /// Get the grid entry in column `x` and row `y`. @@ -904,11 +1108,18 @@ impl CellGrid { /// 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 `min_auto_index` parameter is used to bump the auto index to that value +/// if it is currently smaller than it and a cell requests fully automatic +/// positioning. Useful with headers: if a cell in a header has automatic +/// positioning, it should start at the header's first row, and not at the end +/// of the previous row. fn resolve_cell_position( cell_x: Smart, cell_y: Smart, resolved_cells: &[Option], auto_index: &mut usize, + min_auto_index: usize, columns: usize, ) -> HintedStrResult { // Translates a (x, y) position to the equivalent index in the final cell vector. @@ -924,7 +1135,7 @@ fn resolve_cell_position( (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; + let mut resolved_index = min_auto_index.max(*auto_index); 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 @@ -1021,6 +1232,13 @@ pub struct GridLayouter<'a> { pub(super) finished: Vec, /// Whether this is an RTL grid. pub(super) is_rtl: bool, + /// The simulated header height. + /// This field is reset in `layout_header` and properly updated by + /// `layout_auto_row` and `layout_relative_row`, and should not be read + /// before all header rows are fully laid out. It is usually fine because + /// header rows themselves are unbreakable, and unbreakable rows do not + /// need to read this field at all. + pub(super) header_height: Abs, /// The span of the grid element. pub(super) span: Span, } @@ -1046,6 +1264,16 @@ pub(super) enum Row { Fr(Fr, usize), } +impl Row { + /// Returns the `y` index of this row. + fn index(&self) -> usize { + match self { + Self::Frame(_, y, _) => *y, + Self::Fr(_, y) => *y, + } + } +} + impl<'a> GridLayouter<'a> { /// Create a new grid layouter. /// @@ -1074,6 +1302,7 @@ impl<'a> GridLayouter<'a> { initial: regions.size, finished: vec![], is_rtl: TextElem::dir_in(styles) == Dir::RTL, + header_height: Abs::zero(), span, } } @@ -1083,31 +1312,17 @@ impl<'a> GridLayouter<'a> { self.measure_columns(engine)?; for y in 0..self.grid.rows.len() { - // Skip to next region if current one is full, but only for content - // rows, not for gutter rows, and only if we aren't laying out an - // unbreakable group of rows. - let is_content_row = !self.grid.is_gutter_track(y); - if self.unbreakable_rows_left == 0 && self.regions.is_full() && is_content_row - { - self.finish_region(engine)?; - } - - if is_content_row { - // Gutter rows have no rowspans or possibly unbreakable cells. - self.check_for_rowspans(y); - self.check_for_unbreakable_rows(y, engine)?; - } - - // Don't layout gutter rows at the top of a region. - if is_content_row || !self.lrows.is_empty() { - match self.grid.rows[y] { - Sizing::Auto => self.layout_auto_row(engine, y)?, - Sizing::Rel(v) => self.layout_relative_row(engine, v, y)?, - Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y)), + if let Some(header) = &self.grid.header { + if y < header.end { + if y == 0 { + self.layout_header(header, engine)?; + } + // Skip header rows during normal layout. + continue; } } - self.unbreakable_rows_left = self.unbreakable_rows_left.saturating_sub(1); + self.layout_row(y, engine)?; } self.finish_region(engine)?; @@ -1130,6 +1345,36 @@ impl<'a> GridLayouter<'a> { self.render_fills_strokes() } + /// Layout the given row. + fn layout_row(&mut self, y: usize, engine: &mut Engine) -> SourceResult<()> { + // Skip to next region if current one is full, but only for content + // rows, not for gutter rows, and only if we aren't laying out an + // unbreakable group of rows. + let is_content_row = !self.grid.is_gutter_track(y); + if self.unbreakable_rows_left == 0 && self.regions.is_full() && is_content_row { + self.finish_region(engine)?; + } + + if is_content_row { + // Gutter rows have no rowspans or possibly unbreakable cells. + self.check_for_rowspans(y); + self.check_for_unbreakable_rows(y, engine)?; + } + + // Don't layout gutter rows at the top of a region. + if is_content_row || !self.lrows.is_empty() { + match self.grid.rows[y] { + Sizing::Auto => self.layout_auto_row(engine, y)?, + Sizing::Rel(v) => self.layout_relative_row(engine, v, y)?, + Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y)), + } + } + + self.unbreakable_rows_left = self.unbreakable_rows_left.saturating_sub(1); + + Ok(()) + } + /// Add lines and backgrounds. fn render_fills_strokes(mut self) -> SourceResult { let mut finished = std::mem::take(&mut self.finished); @@ -1150,11 +1395,34 @@ impl<'a> GridLayouter<'a> { // in quadratic complexity. let mut lines = vec![]; + // Which line position to look for in the list of lines for a + // track, such that placing lines with those positions will + // correspond to placing them before the given track index. + // + // If the index represents a gutter track, this means the list of + // lines will actually correspond to the list of lines in the + // previous index, so we must look for lines positioned after the + // previous index, and not before, to determine which lines should + // be placed before gutter. + // + // Note that the maximum index is always an odd number when + // there's gutter, so we must check for it to ensure we don't give + // it the same treatment as a line before a gutter track. + let expected_line_position = |index, is_max_index: bool| { + if self.grid.is_gutter_track(index) && !is_max_index { + LinePosition::After + } else { + LinePosition::Before + } + }; + // Render vertical lines. // Render them first so horizontal lines have priority later. for (x, dx) in points(self.rcols.iter().copied()).enumerate() { let dx = if self.is_rtl { self.width - dx } else { dx }; let is_end_border = x == self.grid.cols.len(); + let expected_vline_position = expected_line_position(x, is_end_border); + let vlines_at_column = self .grid .vlines @@ -1183,8 +1451,10 @@ impl<'a> GridLayouter<'a> { // lines before it, not after). x / 2 }) - .map(Vec::as_slice) - .unwrap_or(&[]); + .into_iter() + .flatten() + .filter(|line| line.position == expected_vline_position); + let tracks = rows.iter().map(|row| (row.y, row.height)); // Determine all different line segments we have to draw in @@ -1198,7 +1468,6 @@ impl<'a> GridLayouter<'a> { tracks, x, vlines_at_column, - is_end_border, vline_stroke_at_row, ) .map(|segment| { @@ -1234,35 +1503,124 @@ impl<'a> GridLayouter<'a> { .map(|piece| piece.y) .chain(std::iter::once(self.grid.rows.len())); - let mut prev_y = None; - for (y, dy) in hline_indices.zip(hline_offsets) { - let is_bottom_border = y == self.grid.rows.len(); - let hlines_at_row = self - .grid + // Converts a row to the corresponding index in the vector of + // hlines. + let hline_index_of_row = |y: usize| { + if !self.grid.has_gutter { + y + } else if y == self.grid.rows.len() { + y / 2 + 1 + } else { + // Check the vlines loop for an explanation regarding + // these index operations. + y / 2 + } + }; + + let get_hlines_at = |y| { + self.grid .hlines - .get(if !self.grid.has_gutter { - y - } else if is_bottom_border { - y / 2 + 1 - } else { - // Check the vlines loop for an explanation regarding - // these index operations. - y / 2 - }) + .get(hline_index_of_row(y)) .map(Vec::as_slice) .unwrap_or(&[]) + }; + + let mut prev_y = None; + for (y, dy) in hline_indices.zip(hline_offsets) { + // Position of lines below the row index in the previous iteration. + let expected_prev_line_position = prev_y + .map(|prev_y| { + expected_line_position( + prev_y + 1, + prev_y + 1 == self.grid.rows.len(), + ) + }) + .unwrap_or(LinePosition::Before); + + // If some grid rows were omitted between the previous resolved + // row and the current one, we ensure lines below the previous + // row don't "disappear" and are considered, albeit with less + // priority. However, don't do this when we're below a header, + // as it must have more priority instead of less, so it is + // chained later instead of before. + let prev_lines = prev_y + .filter(|prev_y| { + prev_y + 1 != y + && !self + .grid + .header + .as_ref() + .is_some_and(|header| prev_y + 1 == header.end) + }) + .map(|prev_y| get_hlines_at(prev_y + 1)) + .unwrap_or(&[]); + + let expected_hline_position = + expected_line_position(y, y == self.grid.rows.len()); + + let hlines_at_y = get_hlines_at(y) .iter() - .chain(if prev_y.is_none() && y != 0 { - // For lines at the top of the region, give priority to - // the lines at the top border. - self.grid.hlines.first().map(Vec::as_slice).unwrap_or(&[]) + .filter(|line| line.position == expected_hline_position); + + let top_border_hlines = if prev_y.is_none() && y != 0 { + // For lines at the top of the region, give priority to + // the lines at the top border. + get_hlines_at(0) + } else { + &[] + }; + + // The header lines, if any, will correspond to the lines under + // the previous row, so they function similarly to 'prev_lines'. + let expected_header_line_position = expected_prev_line_position; + let header_hlines = if let Some((header, prev_y)) = + self.grid.header.as_ref().zip(prev_y) + { + if prev_y + 1 != y + && prev_y + 1 == header.end + && !self.grid.has_gutter + { + // For lines below a header, give priority to the + // lines originally below the header rather than + // the lines of what's below the repeated header. + // However, no need to do that when we're laying + // out the header for the first time, since the + // lines being normally laid out then will be + // precisely the lines below the header. + // + // Additionally, we don't append header lines when + // gutter is enabled, since, in that case, there will + // be a gutter row between header and content, so no + // lines should overlap. + get_hlines_at(header.end) } else { - // When not at the top of the region, no border lines - // to consider. - // When at the top of the region but at the first row, - // its own lines are already the border lines. &[] - }); + } + } else { + &[] + }; + + // The effective hlines to be considered at this row index are + // chained in order of increasing priority: + // 1. Lines from the row right above us, if needed; + // 2. Lines from the current row (usually, only those are + // present); + // 3. Lines from the top border (above the top cells, hence + // 'before' position only); + // 4. Lines from the header above us, if present. + let hlines_at_row = + prev_lines + .iter() + .filter(|line| line.position == expected_prev_line_position) + .chain(hlines_at_y) + .chain( + top_border_hlines + .iter() + .filter(|line| line.position == LinePosition::Before), + ) + .chain(header_hlines.iter().filter(|line| { + line.position == expected_header_line_position + })); let tracks = self.rcols.iter().copied().enumerate(); @@ -1287,7 +1645,6 @@ impl<'a> GridLayouter<'a> { tracks, y, hlines_at_row, - is_bottom_border, |grid, y, x, stroke| { hline_stroke_at_column( grid, @@ -1695,19 +2052,29 @@ impl<'a> GridLayouter<'a> { if let &[first] = resolved.as_slice() { let frame = self.layout_single_row(engine, first, y)?; self.push_row(frame, y, true); + + if self.grid.header.as_ref().is_some_and(|header| y < header.end) { + // Add to header height. + self.header_height += first; + } + return Ok(()); } // Expand all but the last region. // Skip the first region if the space is eaten up by an fr row. let len = resolved.len(); - for (region, target) in self + for ((i, region), target) in self .regions .iter() + .enumerate() .zip(&mut resolved[..len - 1]) .skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize) { - target.set_max(region.y); + // Subtract header height from the region height when it's not the + // first. + target + .set_max(region.y - if i > 0 { self.header_height } else { Abs::zero() }); } // Layout into multiple regions. @@ -1809,6 +2176,8 @@ impl<'a> GridLayouter<'a> { pod.size = size; pod.backlog = backlog; pod.full = measurement_data.full; + pod.last = measurement_data.last; + pod }; @@ -1908,12 +2277,19 @@ impl<'a> GridLayouter<'a> { let resolved = v.resolve(self.styles).relative_to(self.regions.base().y); let frame = self.layout_single_row(engine, resolved, y)?; + if self.grid.header.as_ref().is_some_and(|header| y < header.end) { + // Add to header height. + self.header_height += resolved; + } + // Skip to fitting region, but only if we aren't part of an unbreakable - // row group. + // row group. We use 'in_last_with_offset' so our 'in_last' call + // properly considers that a header would be added on each region + // break. let height = frame.height(); while self.unbreakable_rows_left == 0 && !self.regions.size.y.fits(height) - && !self.regions.in_last() + && !in_last_with_offset(self.regions, self.header_height) { self.finish_region(engine)?; @@ -2036,13 +2412,26 @@ impl<'a> GridLayouter<'a> { /// Finish rows for one region. pub(super) fn finish_region(&mut self, engine: &mut Engine) -> SourceResult<()> { - if self.lrows.last().is_some_and(|row| { - let (Row::Frame(_, y, _) | Row::Fr(_, y)) = row; - self.grid.is_gutter_track(*y) - }) { + if self + .lrows + .last() + .is_some_and(|row| self.grid.is_gutter_track(row.index())) + { // Remove the last row in the region if it is a gutter row. self.lrows.pop().unwrap(); } + + if let Some(header) = &self.grid.header { + if self.grid.rows.len() > header.end + && self.lrows.last().is_some_and(|row| row.index() < header.end) + && !in_last_with_offset(self.regions, self.header_height) + { + // Header would be alone in this region, but there are more + // rows beyond the header. Push an empty region. + self.lrows.clear(); + } + } + // Determine the height of existing rows in the region. let mut used = Abs::zero(); let mut fr = Fr::zero(); @@ -2085,6 +2474,9 @@ impl<'a> GridLayouter<'a> { .rowspans .iter_mut() .filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y)) + .filter(|rowspan| { + rowspan.max_resolved_row.map_or(true, |max_row| y > max_row) + }) { // If the first region wasn't defined yet, it will have the the // initial value of usize::MAX, so we can set it to the current @@ -2111,39 +2503,47 @@ impl<'a> GridLayouter<'a> { // Ensure that, in this region, the rowspan will span at least // this row. *rowspan.heights.last_mut().unwrap() += height; + + if is_last { + // Do not extend the rowspan through this row again, even + // if it is repeated in a future region. + rowspan.max_resolved_row = Some(y); + } } - // Layout any rowspans which end at this row, but only if this is - // this row's last frame (to avoid having the rowspan stop being - // laid out at the first frame of the row). - if is_last { - // We use a for loop over indices to avoid borrow checking - // problems (we need to mutate the rowspans vector, so we can't - // have an iterator actively borrowing it). We keep a separate - // 'i' variable so we can step the counter back after removing - // a rowspan (see explanation below). - let mut i = 0; - while let Some(rowspan) = self.rowspans.get(i) { - if rowspan.y + rowspan.rowspan <= y + 1 { - // Rowspan ends at this or an earlier row, so we take - // it from the rowspans vector and lay it out. - // It's safe to pass the current region as a possible - // region for the rowspan to be laid out in, even if - // the rowspan's last row was at an earlier region, - // because the rowspan won't have an entry for this - // region in its 'heights' vector if it doesn't span - // any rows in this region. - // - // Here we don't advance the index counter ('i') because - // a new element we haven't checked yet in this loop - // will take the index of the now removed element, so - // we have to check the same index again in the next - // iteration. - let rowspan = self.rowspans.remove(i); - self.layout_rowspan(rowspan, Some(&mut output), engine)?; - } else { - i += 1; - } + // We use a for loop over indices to avoid borrow checking + // problems (we need to mutate the rowspans vector, so we can't + // have an iterator actively borrowing it). We keep a separate + // 'i' variable so we can step the counter back after removing + // a rowspan (see explanation below). + let mut i = 0; + while let Some(rowspan) = self.rowspans.get(i) { + // Layout any rowspans which end at this row, but only if this is + // this row's last frame (to avoid having the rowspan stop being + // laid out at the first frame of the row). + // Any rowspans ending before this row are laid out even + // on this row's first frame. + if rowspan.y + rowspan.rowspan < y + 1 + || rowspan.y + rowspan.rowspan == y + 1 && is_last + { + // Rowspan ends at this or an earlier row, so we take + // it from the rowspans vector and lay it out. + // It's safe to pass the current region as a possible + // region for the rowspan to be laid out in, even if + // the rowspan's last row was at an earlier region, + // because the rowspan won't have an entry for this + // region in its 'heights' vector if it doesn't span + // any rows in this region. + // + // Here we don't advance the index counter ('i') because + // a new element we haven't checked yet in this loop + // will take the index of the now removed element, so + // we have to check the same index again in the next + // iteration. + let rowspan = self.rowspans.remove(i); + self.layout_rowspan(rowspan, Some((&mut output, &rrows)), engine)?; + } else { + i += 1; } } @@ -2152,13 +2552,72 @@ impl<'a> GridLayouter<'a> { pos.y += height; } - self.finished.push(output); - self.rrows.push(rrows); - self.regions.next(); - self.initial = self.regions.size; + self.finish_region_internal(output, rrows); + + if let Some(header) = &self.grid.header { + // Add a header to the new region. + self.layout_header(header, engine)?; + } Ok(()) } + + /// Advances to the next region, registering the finished output and + /// resolved rows for the current region in the appropriate vectors. + fn finish_region_internal(&mut self, output: Frame, resolved_rows: Vec) { + self.finished.push(output); + self.rrows.push(resolved_rows); + self.regions.next(); + self.initial = self.regions.size; + } + + /// Layouts the header's rows. + /// Skips regions as necessary. + fn layout_header( + &mut self, + header: &Header, + engine: &mut Engine, + ) -> SourceResult<()> { + let header_rows = self.simulate_header(header, &self.regions, engine)?; + while self.unbreakable_rows_left == 0 + && !self.regions.size.y.fits(header_rows.height) + && !self.regions.in_last() + { + // Advance regions without any output until we can place the + // header. + self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]); + } + + // Reset the header height for this region. + self.header_height = Abs::zero(); + + // Header is unbreakable. + // Thus, no risk of 'finish_region' being recursively called from + // within 'layout_row'. + self.unbreakable_rows_left += header.end; + for y in 0..header.end { + self.layout_row(y, engine)?; + } + Ok(()) + } + + /// Simulate the header's group of rows. + pub(super) fn simulate_header( + &self, + header: &Header, + regions: &Regions<'_>, + engine: &mut Engine, + ) -> SourceResult { + // Note that we assume the invariant that any rowspan in a header is + // fully contained within that header. Therefore, there won't be any + // unbreakable rowspans exceeding the header's rows, and we can safely + // assume that the amount of unbreakable rows following the first row + // in the header will be precisely the rows in the header. + let header_row_group = + self.simulate_unbreakable_row_group(0, Some(header.end), regions, engine)?; + + Ok(header_row_group) + } } /// Turn an iterator of extents into an iterator of offsets before, in between, @@ -2172,3 +2631,13 @@ pub(super) fn points( offset }) } + +/// Checks if the first region of a sequence of regions is the last usable +/// region, assuming that the last region will always be occupied by some +/// specific offset height, even after calling `.next()`, due to some +/// additional logic which adds content automatically on each region turn (in +/// our case, headers). +pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool { + regions.backlog.is_empty() + && regions.last.map_or(true, |height| regions.size.y + offset == height) +} diff --git a/crates/typst/src/layout/grid/lines.rs b/crates/typst/src/layout/grid/lines.rs index 7084c71ad..b659ba77d 100644 --- a/crates/typst/src/layout/grid/lines.rs +++ b/crates/typst/src/layout/grid/lines.rs @@ -40,7 +40,7 @@ pub struct Line { /// 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(PartialEq, Eq)] +#[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, @@ -122,7 +122,6 @@ pub(super) fn generate_line_segments<'grid, F, I, L>( tracks: I, index: usize, lines: L, - is_max_index: bool, line_stroke_at_track: F, ) -> impl Iterator + 'grid where @@ -154,22 +153,6 @@ where // How much to multiply line indices by to account for gutter. let gutter_factor = if grid.has_gutter { 2 } else { 1 }; - // Which line position to look for in the given list of lines. - // - // If the index represents a gutter track, this means the list of lines - // parameter will actually correspond to the list of lines in the previous - // index, so we must look for lines positioned after the previous index, - // and not before, to determine which lines should be placed in gutter. - // - // Note that the maximum index is always an odd number when there's gutter, - // so we must check for it to ensure we don't give it the same treatment as - // a line before a gutter track. - let expected_line_position = if grid.is_gutter_track(index) && !is_max_index { - LinePosition::After - } else { - LinePosition::Before - }; - // Create an iterator of line segments, which will go through each track, // from start to finish, to create line segments and extend them until they // are interrupted and thus yielded through the iterator. We then repeat @@ -210,20 +193,18 @@ where let mut line_strokes = lines .clone() .filter(|line| { - line.position == expected_line_position - && line - .end - .map(|end| { - // Subtract 1 from end index so we stop at the last - // cell before it (don't cross one extra gutter). - let end = if grid.has_gutter { - 2 * end.get() - 1 - } else { - end.get() - }; - (gutter_factor * line.start..end).contains(&track) - }) - .unwrap_or_else(|| track >= gutter_factor * line.start) + line.end + .map(|end| { + // Subtract 1 from end index so we stop at the last + // cell before it (don't cross one extra gutter). + let end = if grid.has_gutter { + 2 * end.get() - 1 + } else { + end.get() + }; + (gutter_factor * line.start..end).contains(&track) + }) + .unwrap_or_else(|| track >= gutter_factor * line.start) }) .map(|line| line.stroke.clone()); @@ -554,9 +535,25 @@ pub(super) fn hline_stroke_at_column( StrokePriority::GridStroke }; + // Top border stroke and header stroke are generally prioritized, unless + // they don't have explicit hline overrides and one or more user-provided + // hlines would appear at the same position, which then are prioritized. + let top_stroke_comes_from_header = + grid.header + .as_ref() + .zip(local_top_y) + .is_some_and(|(header, local_top_y)| { + // Ensure the row above us is a repeated header. + // FIXME: Make this check more robust when headers at arbitrary + // positions are added. + local_top_y + 1 == header.end && y != header.end + }); + let (prioritized_cell_stroke, deprioritized_cell_stroke) = if !use_bottom_border_stroke - && (use_top_border_stroke || top_cell_prioritized && !bottom_cell_prioritized) + && (use_top_border_stroke + || top_stroke_comes_from_header + || top_cell_prioritized && !bottom_cell_prioritized) { // Top border must always be prioritized, even if it did not // request for that explicitly. @@ -660,6 +657,7 @@ mod test { }, vec![], vec![], + None, entries, ) } @@ -723,15 +721,8 @@ mod test { let tracks = rows.iter().map(|row| (row.y, row.height)); assert_eq!( expected_splits, - &generate_line_segments( - &grid, - tracks, - x, - &[], - x == grid.cols.len(), - vline_stroke_at_row - ) - .collect::>(), + &generate_line_segments(&grid, tracks, x, &[], vline_stroke_at_row) + .collect::>(), ); } } @@ -955,15 +946,8 @@ mod test { let tracks = rows.iter().map(|row| (row.y, row.height)); assert_eq!( expected_splits, - &generate_line_segments( - &grid, - tracks, - x, - &[], - x == grid.cols.len(), - vline_stroke_at_row - ) - .collect::>(), + &generate_line_segments(&grid, tracks, x, &[], vline_stroke_at_row) + .collect::>(), ); } } @@ -1144,7 +1128,6 @@ mod test { position: LinePosition::After }, ], - x == grid.cols.len(), vline_stroke_at_row ) .collect::>(), @@ -1211,6 +1194,7 @@ mod test { }, vec![], vec![], + None, entries, ) } @@ -1297,22 +1281,17 @@ mod test { let tracks = columns.iter().copied().enumerate(); assert_eq!( expected_splits, - &generate_line_segments( - &grid, - tracks, - y, - &[], - y == grid.rows.len(), - |grid, y, x, stroke| hline_stroke_at_column( + &generate_line_segments(&grid, tracks, y, &[], |grid, y, x, stroke| { + hline_stroke_at_column( grid, &rows, y.checked_sub(1), true, y, x, - stroke + stroke, ) - ) + }) .collect::>(), ); } @@ -1496,7 +1475,6 @@ mod test { position: LinePosition::After }, ], - y == grid.rows.len(), |grid, y, x, stroke| hline_stroke_at_column( grid, &rows, @@ -1542,7 +1520,6 @@ mod test { columns.iter().copied().enumerate(), 4, &[], - 4 == grid.rows.len(), |grid, y, x, stroke| hline_stroke_at_column( grid, &rows, diff --git a/crates/typst/src/layout/grid/mod.rs b/crates/typst/src/layout/grid/mod.rs index 6675f3e02..0f431bc65 100644 --- a/crates/typst/src/layout/grid/mod.rs +++ b/crates/typst/src/layout/grid/mod.rs @@ -2,13 +2,16 @@ mod layout; mod lines; mod rowspans; -pub use self::layout::{Cell, CellGrid, Celled, GridItem, GridLayouter, ResolvableCell}; +pub use self::layout::{ + Cell, CellGrid, Celled, GridLayouter, ResolvableCell, ResolvableGridChild, + ResolvableGridItem, +}; pub use self::lines::LinePosition; use std::num::NonZeroUsize; use std::sync::Arc; -use ecow::eco_format; +use ecow::{eco_format, EcoString}; use smallvec::{smallvec, SmallVec}; use crate::diag::{bail, SourceResult, StrResult, Trace, Tracepoint}; @@ -20,7 +23,7 @@ use crate::layout::{ Abs, AlignElem, Alignment, Axes, Dir, Fragment, LayoutMultiple, Length, OuterHAlignment, OuterVAlignment, Regions, Rel, Sides, Sizing, }; -use crate::model::{TableCell, TableHLine, TableVLine}; +use crate::model::{TableCell, TableHLine, TableHeader, TableVLine}; use crate::syntax::Span; use crate::text::TextElem; use crate::util::NonZeroExt; @@ -293,6 +296,9 @@ impl GridElem { #[elem] type GridVLine; + + #[elem] + type GridHeader; } impl LayoutMultiple for Packed { @@ -316,43 +322,20 @@ impl LayoutMultiple for Packed { 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 items = self.children().iter().map(|child| match child { - GridChild::HLine(hline) => GridItem::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, - }, + let children = self.children().iter().map(|child| match child { + GridChild::Header(header) => ResolvableGridChild::Header { + repeat: header.repeat(styles), + span: header.span(), + items: header.children().iter().map(|child| child.to_resolvable(styles)), }, - GridChild::VLine(vline) => GridItem::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, - }, - }, - GridChild::Cell(cell) => GridItem::Cell(cell.clone()), + GridChild::Item(item) => { + ResolvableGridChild::Item(item.to_resolvable(styles)) + } }); let grid = CellGrid::resolve( tracks, gutter, - items, + children, fill, align, &inset, @@ -385,52 +368,136 @@ cast! { /// Any child of a grid element. #[derive(Debug, PartialEq, Clone, Hash)] pub enum GridChild { + Header(Packed), + Item(GridItem), +} + +cast! { + GridChild, + self => match self { + Self::Header(header) => header.into_value(), + Self::Item(item) => item.into_value(), + }, + v: Content => { + v.try_into()? + }, +} + +impl TryFrom for GridChild { + type Error = EcoString; + fn try_from(value: Content) -> StrResult { + if value.is::() { + bail!("cannot use `table.header` as a grid header; use `grid.header` instead") + } + + value + .into_packed::() + .map(Self::Header) + .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), } +impl GridItem { + fn to_resolvable(&self, styles: StyleChain) -> ResolvableGridItem> { + match self { + Self::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, + }, + }, + Self::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, + }, + }, + Self::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), + } + } +} + cast! { - GridChild, + GridItem, self => match self { Self::HLine(hline) => hline.into_value(), Self::VLine(vline) => vline.into_value(), Self::Cell(cell) => cell.into_value(), }, v: Content => { - if v.is::() { - bail!( - "cannot use `table.cell` as a grid cell; use `grid.cell` instead" - ); - } - if v.is::() { - bail!( - "cannot use `table.hline` as a grid line; use `grid.hline` instead" - ); - } - if v.is::() { - bail!( - "cannot use `table.vline` as a grid line; use `grid.vline` instead" - ); - } - v.into() + v.try_into()? } } -impl From for GridChild { - fn from(value: Content) -> Self { - value +impl TryFrom for GridItem { + type Error = EcoString; + fn try_from(value: Content) -> StrResult { + if value.is::() { + bail!("cannot place a grid header within another header"); + } + if value.is::() { + bail!("cannot place a table header within another header"); + } + if value.is::() { + bail!("cannot use `table.cell` as a grid cell; use `grid.cell` instead"); + } + if value.is::() { + bail!("cannot use `table.hline` as a grid line; use `grid.hline` instead"); + } + if value.is::() { + bail!("cannot use `table.vline` as a grid line; use `grid.vline` instead"); + } + + Ok(value .into_packed::() - .map(GridChild::HLine) - .or_else(|value| value.into_packed::().map(GridChild::VLine)) - .or_else(|value| value.into_packed::().map(GridChild::Cell)) + .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(); - GridChild::Cell(Packed::new(GridCell::new(value)).spanned(span)) - }) + Self::Cell(Packed::new(GridCell::new(value)).spanned(span)) + })) } } +/// A repeatable grid header. +#[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 horizontal line in the grid. /// /// Overrides any per-cell stroke, including stroke specified through the diff --git a/crates/typst/src/layout/grid/rowspans.rs b/crates/typst/src/layout/grid/rowspans.rs index be63da5ce..d33b79434 100644 --- a/crates/typst/src/layout/grid/rowspans.rs +++ b/crates/typst/src/layout/grid/rowspans.rs @@ -6,7 +6,7 @@ use crate::layout::{ }; use crate::util::MaybeReverseIter; -use super::layout::{points, Row}; +use super::layout::{in_last_with_offset, points, Row, RowPiece}; /// All information needed to layout a single rowspan. pub(super) struct Rowspan { @@ -27,6 +27,13 @@ pub(super) struct Rowspan { pub(super) region_full: Abs, /// The vertical space available for this rowspan in each region. pub(super) heights: Vec, + /// The index of the largest resolved spanned row so far. + /// Once a spanned row is resolved and its height added to `heights`, this + /// number is increased. Older rows, even if repeated through e.g. a + /// header, will no longer contribute height to this rowspan. + /// + /// This is `None` if no spanned rows were resolved in `finish_region` yet. + pub(super) max_resolved_row: Option, } /// The output of the simulation of an unbreakable row group. @@ -44,9 +51,14 @@ pub(super) struct CellMeasurementData<'layouter> { /// The available width for the cell across all regions. pub(super) width: Abs, /// The available height for the cell in its first region. + /// Infinite when the auto row is unbreakable. pub(super) height: Abs, /// The backlog of heights available for the cell in later regions. + /// /// When this is `None`, the `custom_backlog` field should be used instead. + /// That's because, otherwise, this field would have to contain a reference + /// to the `custom_backlog` field, which isn't possible in Rust without + /// resorting to unsafe hacks. pub(super) backlog: Option<&'layouter [Abs]>, /// If the backlog needs to be built from scratch instead of reusing the /// one at the current region, which is the case of a multi-region rowspan @@ -54,7 +66,11 @@ pub(super) struct CellMeasurementData<'layouter> { /// backlog), then this vector will store the new backlog. pub(super) custom_backlog: Vec, /// The full height of the first region of the cell. + /// Infinite when the auto row is unbreakable. pub(super) full: Abs, + /// The height of the last repeated region to use in the measurement pod, + /// if any. + pub(super) last: Option, /// The total height of previous rows spanned by the cell in the current /// region (so far). pub(super) height_in_this_region: Abs, @@ -65,9 +81,10 @@ pub(super) struct CellMeasurementData<'layouter> { impl<'a> GridLayouter<'a> { /// Layout a rowspan over the already finished regions, plus the current - /// region, if it wasn't finished yet (because we're being called from - /// `finish_region`, but note that this function is also called once after - /// all regions are finished, in which case `current_region` is `None`). + /// region's frame and resolved rows, if it wasn't finished yet (because + /// we're being called from `finish_region`, but note that this function is + /// also called once after all regions are finished, in which case + /// `current_region_data` is `None`). /// /// We need to do this only once we already know the heights of all /// spanned rows, which is only possible after laying out the last row @@ -75,7 +92,7 @@ impl<'a> GridLayouter<'a> { pub(super) fn layout_rowspan( &mut self, rowspan_data: Rowspan, - current_region: Option<&mut Frame>, + current_region_data: Option<(&mut Frame, &[RowPiece])>, engine: &mut Engine, ) -> SourceResult<()> { let Rowspan { @@ -97,22 +114,42 @@ impl<'a> GridLayouter<'a> { pod.backlog = backlog; // Push the layouted frames directly into the finished frames. - // At first, we draw the rowspan starting at its expected offset - // in the first region. - let mut pos = Point::new(dx, dy); let fragment = cell.layout(engine, self.styles, pod)?; - for (finished, frame) in self + let (current_region, current_rrows) = current_region_data.unzip(); + for ((i, finished), frame) in self .finished .iter_mut() .chain(current_region.into_iter()) .skip(first_region) + .enumerate() .zip(fragment) { - finished.push_frame(pos, frame); + let dy = if i == 0 { + // At first, we draw the rowspan starting at its expected + // vertical offset in the first region. + dy + } else { + // The rowspan continuation starts after the header (thus, + // at a position after the sum of the laid out header + // rows). + if let Some(header) = &self.grid.header { + let header_rows = self + .rrows + .get(i) + .map(Vec::as_slice) + .or(current_rrows) + .unwrap_or(&[]) + .iter() + .take_while(|row| row.y < header.end); - // From the second region onwards, the rowspan's continuation - // starts at the very top. - pos.y = Abs::zero(); + header_rows.map(|row| row.height).sum() + } else { + // Without a header, start at the very top of the region. + Abs::zero() + } + }; + + finished.push_frame(Point::new(dx, dy), frame); } Ok(()) @@ -141,6 +178,7 @@ impl<'a> GridLayouter<'a> { first_region: usize::MAX, region_full: Abs::zero(), heights: vec![], + max_resolved_row: None, }); } } @@ -156,11 +194,17 @@ impl<'a> GridLayouter<'a> { engine: &mut Engine, ) -> SourceResult<()> { if self.unbreakable_rows_left == 0 { - let row_group = - self.simulate_unbreakable_row_group(current_row, &self.regions, engine)?; + let row_group = self.simulate_unbreakable_row_group( + current_row, + None, + &self.regions, + engine, + )?; // Skip to fitting region. - while !self.regions.size.y.fits(row_group.height) && !self.regions.in_last() { + while !self.regions.size.y.fits(row_group.height) + && !in_last_with_offset(self.regions, self.header_height) + { self.finish_region(engine)?; } self.unbreakable_rows_left = row_group.rows.len(); @@ -170,23 +214,30 @@ impl<'a> GridLayouter<'a> { } /// Simulates a group of unbreakable rows, starting with the index of the - /// first row in the group. Keeps adding rows to the group until none have - /// unbreakable cells in common. + /// first row in the group. If `amount_unbreakable_rows` is `None`, keeps + /// adding rows to the group until none have unbreakable cells in common. + /// Otherwise, adds specifically the given amount of rows to the group. /// /// This is used to figure out how much height the next unbreakable row /// group (if any) needs. pub(super) fn simulate_unbreakable_row_group( &self, first_row: usize, + amount_unbreakable_rows: Option, regions: &Regions<'_>, engine: &mut Engine, ) -> SourceResult { let mut row_group = UnbreakableRowGroup::default(); - let mut unbreakable_rows_left = 0; + let mut unbreakable_rows_left = amount_unbreakable_rows.unwrap_or(0); for (y, row) in self.grid.rows.iter().enumerate().skip(first_row) { - let additional_unbreakable_rows = self.check_for_unbreakable_cells(y); - unbreakable_rows_left = - unbreakable_rows_left.max(additional_unbreakable_rows); + if amount_unbreakable_rows.is_none() { + // When we don't set a fixed amount of unbreakable rows, + // determine the amount based on the rowspan of unbreakable + // cells in rows. + let additional_unbreakable_rows = self.check_for_unbreakable_cells(y); + unbreakable_rows_left = + unbreakable_rows_left.max(additional_unbreakable_rows); + } if unbreakable_rows_left == 0 { // This check is in case the first row does not have any // unbreakable cells. Therefore, no unbreakable row group @@ -254,10 +305,37 @@ impl<'a> GridLayouter<'a> { let rowspan = self.grid.effective_rowspan_of_cell(cell); // This variable is used to construct a custom backlog if the cell - // is a rowspan. When measuring, we join the heights from previous - // regions to the current backlog to form the rowspan's expected - // backlog. - let mut rowspan_backlog: Vec = vec![]; + // is a rowspan, or if headers are used. When measuring, we join + // the heights from previous regions to the current backlog to form + // a rowspan's expected backlog. We also subtract the header's + // height from all regions. + let mut custom_backlog: Vec = vec![]; + + // This function is used to subtract the expected header height from + // each upcoming region size in the current backlog and last region. + let mut subtract_header_height_from_regions = || { + // Only breakable auto rows need to update their backlogs based + // on the presence of a header, given that unbreakable auto + // rows don't depend on the backlog, as they only span one + // region. + if breakable && self.grid.header.is_some() { + // Subtract header height from all upcoming regions when + // measuring the cell, including the last repeated region. + // + // This will update the 'custom_backlog' vector with the + // updated heights of the upcoming regions. + let mapped_regions = self.regions.map(&mut custom_backlog, |size| { + Size::new(size.x, size.y - self.header_height) + }); + + // Callees must use the custom backlog instead of the current + // backlog, so we return 'None'. + return (None, mapped_regions.last); + } + + // No need to change the backlog or last region. + (Some(self.regions.backlog), self.regions.last) + }; // Each declaration, from top to bottom: // 1. The height available to the cell in the first region. @@ -266,25 +344,34 @@ impl<'a> GridLayouter<'a> { // 2. The backlog of upcoming region heights to specify as // available to the cell. // 3. The full height of the first region of the cell. - // 4. The total height of the cell covered by previously spanned + // 4. Height of the last repeated region to use in the measurement pod. + // 5. The total height of the cell covered by previously spanned // rows in this region. This is used by rowspans to be able to tell // how much the auto row needs to expand. - // 5. The amount of frames laid out by this cell in previous + // 6. The amount of frames laid out by this cell in previous // regions. When the cell isn't a rowspan, this is always zero. // These frames are skipped after measuring. - let (height, backlog, full, height_in_this_region, frames_in_previous_regions); + let height; + let backlog; + let full; + let last; + let height_in_this_region; + let frames_in_previous_regions; + if rowspan == 1 { // Not a rowspan, so the cell only occupies this row. Therefore: // 1. When we measure the cell below, use the available height // remaining in the region as the height it has available. // However, if the auto row is unbreakable, measure with infinite // height instead to see how much content expands. - // 2. Also use the region's backlog when measuring. + // 2. Use the region's backlog and last region when measuring, + // however subtract the expected header height from each upcoming + // size, if there is a header. // 3. Use the same full region height. // 4. No height occupied by this cell in this region so far. // 5. Yes, this cell started in this region. height = if breakable { self.regions.size.y } else { Abs::inf() }; - backlog = Some(self.regions.backlog); + (backlog, last) = subtract_header_height_from_regions(); full = if breakable { self.regions.full } else { Abs::inf() }; height_in_this_region = Abs::zero(); frames_in_previous_regions = 0; @@ -339,21 +426,25 @@ impl<'a> GridLayouter<'a> { .iter() .copied() .chain(std::iter::once(if breakable { - self.initial.y + self.initial.y - self.header_height } else { // When measuring unbreakable auto rows, infinite // height is available for content to expand. Abs::inf() })); - rowspan_backlog = if breakable { + custom_backlog = if breakable { // This auto row is breakable. Therefore, join the // rowspan's already laid out heights with the current // region's height and current backlog to ensure a good // level of accuracy in the measurements. - heights_up_to_current_region - .chain(self.regions.backlog.iter().copied()) - .collect::>() + let backlog = self + .regions + .backlog + .iter() + .map(|&size| size - self.header_height); + + heights_up_to_current_region.chain(backlog).collect::>() } else { // No extra backlog if this is an unbreakable auto row. // Ensure, when measuring, that the rowspan can be laid @@ -365,6 +456,7 @@ impl<'a> GridLayouter<'a> { height = *rowspan_height; backlog = None; full = rowspan_full; + last = self.regions.last.map(|size| size - self.header_height); } else { // The rowspan started in the current region, as its vector // of heights in regions is currently empty. @@ -380,7 +472,7 @@ impl<'a> GridLayouter<'a> { } else { Abs::inf() }; - backlog = Some(self.regions.backlog); + (backlog, last) = subtract_header_height_from_regions(); full = if breakable { self.regions.full } else { Abs::inf() }; frames_in_previous_regions = 0; } @@ -391,8 +483,9 @@ impl<'a> GridLayouter<'a> { width, height, backlog, - custom_backlog: rowspan_backlog, + custom_backlog, full, + last, height_in_this_region, frames_in_previous_regions, } @@ -561,7 +654,13 @@ impl<'a> GridLayouter<'a> { // expand) because we popped the last resolved size from the // resolved vector, above. simulated_regions.next(); + + // Subtract the initial header height, since that's the height we + // used when subtracting from the region backlog's heights while + // measuring cells. + simulated_regions.size.y -= self.header_height; } + if let Some(original_last_resolved_size) = last_resolved_size { // We're now at the (current) last region of this auto row. // Consider resolved height as already taken space. @@ -689,87 +788,18 @@ impl<'a> GridLayouter<'a> { // which, when used and combined with upcoming spanned rows, covers all // of the requested rowspan height, we give up. for _attempt in 0..5 { - let mut regions = simulated_regions; - let mut total_spanned_height = Abs::zero(); - let mut unbreakable_rows_left = unbreakable_rows_left; + let rowspan_simulator = + RowspanSimulator::new(simulated_regions, self.header_height); - // Height of the latest spanned gutter row. - // Zero if it was removed. - let mut latest_spanned_gutter_height = Abs::zero(); - let spanned_rows = &self.grid.rows[y + 1..=max_spanned_row]; - for (offset, row) in spanned_rows.iter().enumerate() { - if (total_spanned_height + amount_to_grow).fits(requested_rowspan_height) - { - // Stop the simulation, as the combination of upcoming - // spanned rows (so far) and the current amount the auto - // row expands by has already fully covered the height the - // rowspans need. - break; - } - let spanned_y = y + 1 + offset; - let is_gutter = self.grid.is_gutter_track(spanned_y); - - if unbreakable_rows_left == 0 { - // Simulate unbreakable row groups, and skip regions until - // they fit. There is no risk of infinite recursion, as - // no auto rows participate in the simulation, so the - // unbreakable row group simulator won't recursively call - // 'measure_auto_row' or (consequently) this function. - let row_group = - self.simulate_unbreakable_row_group(spanned_y, ®ions, engine)?; - while !regions.size.y.fits(row_group.height) && !regions.in_last() { - total_spanned_height -= latest_spanned_gutter_height; - latest_spanned_gutter_height = Abs::zero(); - regions.next(); - } - - unbreakable_rows_left = row_group.rows.len(); - } - - match row { - // Fixed-size spanned rows are what we are interested in. - // They contribute a fixed amount of height to our rowspan. - Sizing::Rel(v) => { - let height = v.resolve(self.styles).relative_to(regions.base().y); - total_spanned_height += height; - if is_gutter { - latest_spanned_gutter_height = height; - } - - let mut skipped_region = false; - while unbreakable_rows_left == 0 - && !regions.size.y.fits(height) - && !regions.in_last() - { - // A row was pushed to the next region. Therefore, - // the immediately preceding gutter row is removed. - total_spanned_height -= latest_spanned_gutter_height; - latest_spanned_gutter_height = Abs::zero(); - skipped_region = true; - regions.next(); - } - - if !skipped_region || !is_gutter { - // No gutter at the top of a new region, so don't - // account for it if we just skipped a region. - regions.size.y -= height; - } - } - Sizing::Auto => { - // We only simulate for rowspans which end at the - // current auto row. Therefore, there won't be any - // further auto rows. - unreachable!(); - } - // For now, we ignore fractional rows on simulation. - Sizing::Fr(_) if is_gutter => { - latest_spanned_gutter_height = Abs::zero(); - } - Sizing::Fr(_) => {} - } - - unbreakable_rows_left = unbreakable_rows_left.saturating_sub(1); - } + let total_spanned_height = rowspan_simulator.simulate_rowspan_layout( + y, + max_spanned_row, + amount_to_grow, + requested_rowspan_height, + unbreakable_rows_left, + self, + engine, + )?; // If the total height spanned by upcoming spanned rows plus the // current amount we predict the auto row will have to grow (from @@ -841,6 +871,7 @@ impl<'a> GridLayouter<'a> { { extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero()); simulated_regions.next(); + simulated_regions.size.y -= self.header_height; } simulated_regions.size.y -= extra_amount_to_grow; } @@ -850,6 +881,189 @@ impl<'a> GridLayouter<'a> { } } +/// Auxiliary structure holding state during rowspan simulation. +struct RowspanSimulator<'a> { + /// The state of regions during the simulation. + regions: Regions<'a>, + /// The height of the header in the currently simulated region. + header_height: Abs, + /// The total spanned height so far in the simulation. + total_spanned_height: Abs, + /// Height of the latest spanned gutter row in the simulation. + /// Zero if it was removed. + latest_spanned_gutter_height: Abs, +} + +impl<'a> RowspanSimulator<'a> { + /// Creates new rowspan simulation state with the given regions and initial + /// header height. Other fields should always start as zero. + fn new(regions: Regions<'a>, header_height: Abs) -> Self { + Self { + regions, + header_height, + total_spanned_height: Abs::zero(), + latest_spanned_gutter_height: Abs::zero(), + } + } + + /// Calculates the total spanned height of the rowspan. + /// Stops calculating if, at any point in the simulation, the value of + /// `total_spanned_height + amount_to_grow` becomes larger than + /// `requested_rowspan_height`, as the results are not going to become any + /// more useful after that point. + #[allow(clippy::too_many_arguments)] + fn simulate_rowspan_layout( + mut self, + y: usize, + max_spanned_row: usize, + amount_to_grow: Abs, + requested_rowspan_height: Abs, + mut unbreakable_rows_left: usize, + layouter: &GridLayouter<'_>, + engine: &mut Engine, + ) -> SourceResult { + let spanned_rows = &layouter.grid.rows[y + 1..=max_spanned_row]; + for (offset, row) in spanned_rows.iter().enumerate() { + if (self.total_spanned_height + amount_to_grow).fits(requested_rowspan_height) + { + // Stop the simulation, as the combination of upcoming + // spanned rows (so far) and the current amount the auto + // row expands by has already fully covered the height the + // rowspans need. + return Ok(self.total_spanned_height); + } + let spanned_y = y + 1 + offset; + let is_gutter = layouter.grid.is_gutter_track(spanned_y); + + if unbreakable_rows_left == 0 { + // Simulate unbreakable row groups, and skip regions until + // they fit. There is no risk of infinite recursion, as + // no auto rows participate in the simulation, so the + // unbreakable row group simulator won't recursively call + // 'measure_auto_row' or (consequently) this function. + let row_group = layouter.simulate_unbreakable_row_group( + spanned_y, + None, + &self.regions, + engine, + )?; + while !self.regions.size.y.fits(row_group.height) + && !in_last_with_offset(self.regions, self.header_height) + { + self.finish_region(layouter, engine)?; + } + + unbreakable_rows_left = row_group.rows.len(); + } + + match row { + // Fixed-size spanned rows are what we are interested in. + // They contribute a fixed amount of height to our rowspan. + Sizing::Rel(v) => { + let height = + v.resolve(layouter.styles).relative_to(self.regions.base().y); + self.total_spanned_height += height; + if is_gutter { + self.latest_spanned_gutter_height = height; + } + + let mut skipped_region = false; + while unbreakable_rows_left == 0 + && !self.regions.size.y.fits(height) + && !in_last_with_offset(self.regions, self.header_height) + { + self.finish_region(layouter, engine)?; + + skipped_region = true; + } + + if !skipped_region || !is_gutter { + // No gutter at the top of a new region, so don't + // account for it if we just skipped a region. + self.regions.size.y -= height; + } + } + Sizing::Auto => { + // We only simulate for rowspans which end at the + // current auto row. Therefore, there won't be any + // further auto rows. + unreachable!(); + } + // For now, we ignore fractional rows on simulation. + Sizing::Fr(_) if is_gutter => { + self.latest_spanned_gutter_height = Abs::zero(); + } + Sizing::Fr(_) => {} + } + + unbreakable_rows_left = unbreakable_rows_left.saturating_sub(1); + } + + Ok(self.total_spanned_height) + } + + fn simulate_header_layout( + &mut self, + layouter: &GridLayouter<'_>, + engine: &mut Engine, + ) -> SourceResult<()> { + if let Some(header) = &layouter.grid.header { + // We can't just use the initial header height on each + // region, because header height might vary depending + // on region size if it contains rows with relative + // lengths. Therefore, we re-simulate headers on each + // new region. + // It's true that, when measuring cells, we reduce each + // height in the backlog to consider the initial header + // height; however, our simulation checks what happens + // AFTER the auto row, so we can just use the original + // backlog from `self.regions`. + let header_row_group = + layouter.simulate_header(header, &self.regions, engine)?; + let mut skipped_region = false; + + // Skip until we reach a fitting region for this header. + while !self.regions.size.y.fits(header_row_group.height) + && !self.regions.in_last() + { + self.regions.next(); + skipped_region = true; + } + + self.header_height = if skipped_region { + // Simulate headers again, at the new region, as + // the full region height may change. + layouter.simulate_header(header, &self.regions, engine)?.height + } else { + header_row_group.height + }; + + // Consume the header's height from the new region, + // but don't consider it spanned. The rowspan + // does not go over the header (as an invariant, + // any rowspans spanning a header row are fully + // contained within that header's rows). + self.regions.size.y -= self.header_height; + } + + Ok(()) + } + + fn finish_region( + &mut self, + layouter: &GridLayouter<'_>, + engine: &mut Engine, + ) -> SourceResult<()> { + // If a row was pushed to the next region, the immediately + // preceding gutter row is removed. + self.total_spanned_height -= self.latest_spanned_gutter_height; + self.latest_spanned_gutter_height = Abs::zero(); + self.regions.next(); + + self.simulate_header_layout(layouter, engine) + } +} + /// Subtracts some size from the end of a vector of sizes. /// For example, subtracting 5pt from \[2pt, 1pt, 3pt\] will result in \[1pt\]. fn subtract_end_sizes(sizes: &mut Vec, mut subtract: Abs) { diff --git a/crates/typst/src/model/bibliography.rs b/crates/typst/src/model/bibliography.rs index e34640262..576d1171b 100644 --- a/crates/typst/src/model/bibliography.rs +++ b/crates/typst/src/model/bibliography.rs @@ -29,8 +29,8 @@ use crate::foundations::{ }; use crate::introspection::{Introspector, Locatable, Location}; use crate::layout::{ - BlockElem, Em, GridCell, GridChild, GridElem, HElem, PadElem, Sizing, TrackSizings, - VElem, + BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, Sizing, + TrackSizings, VElem, }; use crate::model::{ CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, @@ -238,13 +238,13 @@ impl Show for Packed { if references.iter().any(|(prefix, _)| prefix.is_some()) { let mut cells = vec![]; for (prefix, reference) in references { - cells.push(GridChild::Cell( + cells.push(GridChild::Item(GridItem::Cell( Packed::new(GridCell::new(prefix.clone().unwrap_or_default())) .spanned(span), - )); - cells.push(GridChild::Cell( + ))); + cells.push(GridChild::Item(GridItem::Cell( Packed::new(GridCell::new(reference.clone())).spanned(span), - )); + ))); } seq.push(VElem::new(row_gutter).with_weakness(3).pack()); @@ -948,8 +948,12 @@ impl ElemRenderer<'_> { if let Some(prefix) = suf_prefix { const COLUMN_GUTTER: Em = Em::new(0.65); content = GridElem::new(vec![ - GridChild::Cell(Packed::new(GridCell::new(prefix)).spanned(self.span)), - GridChild::Cell(Packed::new(GridCell::new(content)).spanned(self.span)), + GridChild::Item(GridItem::Cell( + Packed::new(GridCell::new(prefix)).spanned(self.span), + )), + GridChild::Item(GridItem::Cell( + Packed::new(GridCell::new(content)).spanned(self.span), + )), ]) .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) diff --git a/crates/typst/src/model/table.rs b/crates/typst/src/model/table.rs index 2285e9a74..250a527cf 100644 --- a/crates/typst/src/model/table.rs +++ b/crates/typst/src/model/table.rs @@ -1,18 +1,18 @@ use std::num::NonZeroUsize; use std::sync::Arc; -use ecow::eco_format; +use ecow::{eco_format, EcoString}; -use crate::diag::{bail, SourceResult, Trace, Tracepoint}; +use crate::diag::{bail, SourceResult, StrResult, Trace, Tracepoint}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, Content, Fold, Packed, Show, Smart, StyleChain, }; use crate::layout::{ show_grid_cell, Abs, Alignment, Axes, Cell, CellGrid, Celled, Dir, Fragment, - GridCell, GridHLine, GridItem, GridLayouter, GridVLine, LayoutMultiple, Length, - LinePosition, OuterHAlignment, OuterVAlignment, Regions, Rel, ResolvableCell, Sides, - TrackSizings, + GridCell, GridHLine, GridHeader, GridLayouter, GridVLine, LayoutMultiple, Length, + LinePosition, OuterHAlignment, OuterVAlignment, Regions, Rel, ResolvableCell, + ResolvableGridChild, ResolvableGridItem, Sides, TrackSizings, }; use crate::model::Figurable; use crate::syntax::Span; @@ -221,6 +221,9 @@ impl TableElem { #[elem] type TableVLine; + + #[elem] + type TableHeader; } impl LayoutMultiple for Packed { @@ -244,43 +247,20 @@ impl LayoutMultiple for Packed { 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 items = self.children().iter().map(|child| match child { - TableChild::HLine(hline) => GridItem::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, - }, + let children = self.children().iter().map(|child| match child { + TableChild::Header(header) => ResolvableGridChild::Header { + repeat: header.repeat(styles), + span: header.span(), + items: header.children().iter().map(|child| child.to_resolvable(styles)), }, - TableChild::VLine(vline) => GridItem::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, - }, - }, - TableChild::Cell(cell) => GridItem::Cell(cell.clone()), + TableChild::Item(item) => { + ResolvableGridChild::Item(item.to_resolvable(styles)) + } }); let grid = CellGrid::resolve( tracks, gutter, - items, + children, fill, align, &inset, @@ -338,50 +318,138 @@ impl Figurable for Packed {} /// Any child of a table element. #[derive(Debug, PartialEq, Clone, Hash)] pub enum TableChild { + Header(Packed), + Item(TableItem), +} + +cast! { + TableChild, + self => match self { + Self::Header(header) => header.into_value(), + Self::Item(item) => item.into_value(), + }, + v: Content => { + v.try_into()? + }, +} + +impl TryFrom for TableChild { + type Error = EcoString; + + fn try_from(value: Content) -> StrResult { + if value.is::() { + bail!( + "cannot use `grid.header` as a table header; use `table.header` instead" + ) + } + + value + .into_packed::() + .map(Self::Header) + .or_else(|value| TableItem::try_from(value).map(Self::Item)) + } +} + +/// A table item, which is the basic unit of table specification. +#[derive(Debug, PartialEq, Clone, Hash)] +pub enum TableItem { HLine(Packed), VLine(Packed), Cell(Packed), } +impl TableItem { + fn to_resolvable(&self, styles: StyleChain) -> ResolvableGridItem> { + match self { + Self::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, + }, + }, + Self::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, + }, + }, + Self::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), + } + } +} + cast! { - TableChild, + TableItem, self => match self { Self::HLine(hline) => hline.into_value(), Self::VLine(vline) => vline.into_value(), Self::Cell(cell) => cell.into_value(), }, v: Content => { - if v.is::() { - bail!( - "cannot use `grid.cell` as a table cell; use `table.cell` instead" - ); + v.try_into()? + }, +} + +impl TryFrom for TableItem { + type Error = EcoString; + + fn try_from(value: Content) -> StrResult { + if value.is::() { + bail!("cannot place a grid header within another header"); } - if v.is::() { - bail!( - "cannot use `grid.hline` as a table line; use `table.hline` instead" - ); + if value.is::() { + bail!("cannot place a table header within another header"); } - if v.is::() { - bail!( - "cannot use `grid.vline` as a table line; use `table.vline` instead" - ); + if value.is::() { + bail!("cannot use `grid.cell` as a table cell; use `table.cell` instead"); } - v.into() + if value.is::() { + bail!("cannot use `grid.hline` as a table line; use `table.hline` instead"); + } + if value.is::() { + bail!("cannot use `grid.vline` as a table line; use `table.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(TableCell::new(value)).spanned(span)) + })) } } -impl From for TableChild { - fn from(value: Content) -> Self { - value - .into_packed::() - .map(TableChild::HLine) - .or_else(|value| value.into_packed::().map(TableChild::VLine)) - .or_else(|value| value.into_packed::().map(TableChild::Cell)) - .unwrap_or_else(|value| { - let span = value.span(); - TableChild::Cell(Packed::new(TableCell::new(value)).spanned(span)) - }) - } +/// A repeatable table header. +#[elem(name = "header", title = "Table Header")] +pub struct TableHeader { + /// 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 horizontal line in the table. See the docs for diff --git a/tests/ref/layout/grid-headers-1.png b/tests/ref/layout/grid-headers-1.png new file mode 100644 index 000000000..7ae2d8d34 Binary files /dev/null and b/tests/ref/layout/grid-headers-1.png differ diff --git a/tests/ref/layout/grid-headers-2.png b/tests/ref/layout/grid-headers-2.png new file mode 100644 index 000000000..3dbc07c88 Binary files /dev/null and b/tests/ref/layout/grid-headers-2.png differ diff --git a/tests/ref/layout/grid-headers-3.png b/tests/ref/layout/grid-headers-3.png new file mode 100644 index 000000000..9ee77d50b Binary files /dev/null and b/tests/ref/layout/grid-headers-3.png differ diff --git a/tests/ref/layout/grid-headers-4.png b/tests/ref/layout/grid-headers-4.png new file mode 100644 index 000000000..e60877d83 Binary files /dev/null and b/tests/ref/layout/grid-headers-4.png differ diff --git a/tests/ref/layout/grid-rowspan-basic.png b/tests/ref/layout/grid-rowspan-basic.png index 966c8fd9d..783991b3c 100644 Binary files a/tests/ref/layout/grid-rowspan-basic.png and b/tests/ref/layout/grid-rowspan-basic.png differ diff --git a/tests/ref/layout/grid-rtl.png b/tests/ref/layout/grid-rtl.png index 3cf0b9aa5..d628ee8ae 100644 Binary files a/tests/ref/layout/grid-rtl.png and b/tests/ref/layout/grid-rtl.png differ diff --git a/tests/typ/layout/grid-headers-1.typ b/tests/typ/layout/grid-headers-1.typ new file mode 100644 index 000000000..f1afe70e9 --- /dev/null +++ b/tests/typ/layout/grid-headers-1.typ @@ -0,0 +1,162 @@ +#set page(width: auto, height: 12em) +#table( + columns: 5, + align: center + horizon, + table.header( + table.cell(colspan: 5)[*Cool Zone*], + table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*], + table.hline(start: 2, end: 3, stroke: yellow) + ), + ..range(0, 6).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten() +) + +--- +// Disable repetition +#set page(width: auto, height: 12em) +#table( + columns: 5, + align: center + horizon, + table.header( + table.cell(colspan: 5)[*Cool Zone*], + table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*], + table.hline(start: 2, end: 3, stroke: yellow), + repeat: false + ), + ..range(0, 6).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten() +) + +--- +#set page(width: auto, height: 12em) +#table( + columns: 5, + align: center + horizon, + gutter: 3pt, + table.header( + table.cell(colspan: 5)[*Cool Zone*], + table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*], + table.hline(start: 2, end: 3, stroke: yellow), + ), + ..range(0, 6).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten() +) + +--- +// Relative lengths +#set page(height: 10em) +#table( + rows: (30%, 30%, auto), + table.header( + [*A*], + [*B*] + ), + [C], + [C] +) + +--- +#grid( + grid.cell(y: 1)[a], + grid.header(grid.cell(y: 0)[b]), + grid.cell(y: 2)[c] +) + +--- +// When the header is the last grid child, it shouldn't include the gutter row +// after it, because there is none. +#grid( + columns: 2, + gutter: 3pt, + grid.header( + [a], [b], + [c], [d] + ) +) + +--- +#set page(height: 14em) +#let t(n) = table( + columns: 3, + align: center + horizon, + gutter: 3pt, + table.header( + table.cell(colspan: 3)[*Cool Zone #n*], + [*Name*], [*Num*], [*Data*] + ), + ..range(0, 5).map(i => ([\##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456])).flatten() +) +#grid( + gutter: 3pt, + t(0), + t(1) +) + +--- +// Test line positioning in header +#table( + columns: 3, + stroke: none, + table.hline(stroke: red, end: 2), + table.vline(stroke: red, end: 3), + table.header( + table.hline(stroke: aqua, start: 2), + table.vline(stroke: aqua, start: 3), [*A*], table.hline(stroke: orange), table.vline(stroke: orange), [*B*], + [*C*], [*D*] + ), + [a], [b], + [c], [d], + [e], [f] +) + +--- +// Error: 3:3-3:19 header must start at the first row +// Hint: 3:3-3:19 remove any rows before the header +#grid( + [a], + grid.header([b]) +) + +--- +// Error: 4:3-4:19 header must start at the first row +// Hint: 4:3-4:19 remove any rows before the header +#grid( + columns: 2, + [a], + grid.header([b]) +) + +--- +// Error: 3:3-3:19 cannot have more than one header +#grid( + grid.header([a]), + grid.header([b]), + [a], +) + +--- +// Error: 2:3-2:20 cannot use `table.header` as a grid header; use `grid.header` instead +#grid( + table.header([a]), + [a], +) + +--- +// Error: 2:3-2:19 cannot use `grid.header` as a table header; use `table.header` instead +#table( + grid.header([a]), + [a], +) + +--- +// Error: 14-28 cannot place a grid header within another header +#grid.header(grid.header[a]) + +--- +// Error: 14-29 cannot place a table header within another header +#grid.header(table.header[a]) + +--- +// Error: 15-29 cannot place a grid header within another header +#table.header(grid.header[a]) + +--- +// Error: 15-30 cannot place a table header within another header +#table.header(table.header[a]) diff --git a/tests/typ/layout/grid-headers-2.typ b/tests/typ/layout/grid-headers-2.typ new file mode 100644 index 000000000..75c9b330d --- /dev/null +++ b/tests/typ/layout/grid-headers-2.typ @@ -0,0 +1,52 @@ +#set page(height: 15em) +#table( + rows: (auto, 2.5em, auto), + table.header( + [*Hello*], + [*World*] + ), + block(width: 2em, height: 20em, fill: red) +) + +--- +// Rowspan sizing algorithm doesn't do the best job at non-contiguous content +// ATM. +#set page(height: 15em) + +#table( + rows: (auto, 2.5em, 2em, auto, 5em), + table.header( + [*Hello*], + [*World*] + ), + table.cell(rowspan: 3, lorem(40)) +) + +--- +// Rowspan sizing algorithm doesn't do the best job at non-contiguous content +// ATM. +#set page(height: 15em) + +#table( + rows: (auto, 2.5em, 2em, auto, 5em), + gutter: 3pt, + table.header( + [*Hello*], + [*World*] + ), + table.cell(rowspan: 3, lorem(40)) +) + +--- +// This should look right +#set page(height: 15em) + +#table( + rows: (auto, 2.5em, 2em, auto), + gutter: 3pt, + table.header( + [*Hello*], + [*World*] + ), + table.cell(rowspan: 3, lorem(40)) +) diff --git a/tests/typ/layout/grid-headers-3.typ b/tests/typ/layout/grid-headers-3.typ new file mode 100644 index 000000000..e7437cf77 --- /dev/null +++ b/tests/typ/layout/grid-headers-3.typ @@ -0,0 +1,35 @@ +// Test lack of space for header + text. +#set page(height: 9em) + +#table( + rows: (auto, 2.5em, auto, auto, 10em), + gutter: 3pt, + table.header( + [*Hello*], + [*World*] + ), + table.cell(rowspan: 3, lorem(80)) +) + +--- +// Orphan header prevention test +#set page(height: 12em) +#v(8em) +#grid( + columns: 3, + grid.header( + [*Mui*], [*A*], grid.cell(rowspan: 2, fill: orange)[*B*], + [*Header*], [*Header* #v(0.1em)] + ), + ..([Test], [Test], [Test]) * 20 +) + +--- +// Empty header should just be a repeated blank row +#set page(height: 12em) +#table( + columns: 4, + align: center + horizon, + table.header(), + ..range(0, 4).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789])).flatten() +) diff --git a/tests/typ/layout/grid-headers-4.typ b/tests/typ/layout/grid-headers-4.typ new file mode 100644 index 000000000..13fd41dd1 --- /dev/null +++ b/tests/typ/layout/grid-headers-4.typ @@ -0,0 +1,58 @@ +// When a header has a rowspan with an empty row, it should be displayed +// properly +#set page(height: 10em) + +#let count = counter("g") +#table( + rows: (auto, 2em, auto, auto), + table.header( + [eeec], + table.cell(rowspan: 2, count.step() + count.display()), + ), + [d], + block(width: 5em, fill: yellow, lorem(15)), + [d] +) +#count.display() + +--- +// Ensure header expands to fit cell placed in it after its declaration +#set page(height: 10em) +#table( + columns: 2, + table.header( + [a], [b], + [c], + ), + table.cell(x: 1, y: 1, rowspan: 2, lorem(80)) +) + +--- +// Nested table with header should repeat both headers +#set page(height: 10em) +#table( + table.header( + [a] + ), + table( + table.header( + [b] + ), + [a\ b\ c\ d] + ) +) + +--- +#set page(height: 12em) +#table( + table.header( + table( + table.header( + [b] + ), + [c], + [d] + ) + ), + [a\ b] +) diff --git a/tests/typ/layout/grid-rowspan-basic.typ b/tests/typ/layout/grid-rowspan-basic.typ index 49164fa69..1cc7289b4 100644 --- a/tests/typ/layout/grid-rowspan-basic.typ +++ b/tests/typ/layout/grid-rowspan-basic.typ @@ -209,3 +209,24 @@ grid.cell(y: 6, breakable: false)[m], grid.cell(y: 6, breakable: true)[n], grid.cell(y: 7, breakable: false)[o], grid.cell(y: 7, breakable: true)[p], grid.cell(y: 7, breakable: auto)[q] ) + +--- +#table( + columns: 2, + table.cell(stroke: (bottom: red))[a], [b], + table.hline(stroke: green), + table.cell(stroke: (top: yellow, left: green, right: aqua, bottom: blue), colspan: 1, rowspan: 2)[d], table.cell(colspan: 1, rowspan: 2)[e], + [f], + [g] +) + +--- +#table( + columns: 2, + gutter: 3pt, + table.cell(stroke: (bottom: red))[a], [b], + table.hline(stroke: green), + table.cell(stroke: (top: yellow, left: green, right: aqua, bottom: blue), colspan: 1, rowspan: 2)[d], table.cell(colspan: 1, rowspan: 2)[e], + [f], + [g] +) diff --git a/tests/typ/layout/grid-rtl.typ b/tests/typ/layout/grid-rtl.typ index be9fac516..cea67d96f 100644 --- a/tests/typ/layout/grid-rtl.typ +++ b/tests/typ/layout/grid-rtl.typ @@ -178,3 +178,18 @@ [e], [f] ) + +--- +// Headers +#set page(height: 15em) +#set text(dir: rtl) +#table( + columns: 5, + align: center + horizon, + table.header( + table.cell(colspan: 5)[*Cool Zone*], + table.cell(stroke: red)[*N1*], table.cell(stroke: aqua)[*N2*], [*D1*], [*D2*], [*Etc*], + table.hline(start: 2, end: 3, stroke: yellow) + ), + ..range(0, 10).map(i => ([\##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten() +)