diff --git a/crates/typst/src/layout/grid/layout.rs b/crates/typst/src/layout/grid/layout.rs index 83dbf069..a27e4269 100644 --- a/crates/typst/src/layout/grid/layout.rs +++ b/crates/typst/src/layout/grid/layout.rs @@ -9,6 +9,7 @@ use super::lines::{ generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Line, LinePosition, LineSegment, }; +use super::rowspans::{Rowspan, UnbreakableRowGroup}; use crate::diag::{ bail, At, Hint, HintedStrResult, HintedString, SourceResult, StrResult, }; @@ -172,6 +173,8 @@ pub struct Cell { pub fill: Option, /// The amount of columns spanned by the cell. pub colspan: NonZeroUsize, + /// The amount of rows spanned by the cell. + pub rowspan: NonZeroUsize, /// The cell's stroke. /// /// We use an Arc to avoid unnecessary space usage when all sides are the @@ -185,6 +188,10 @@ pub struct Cell { /// override their own stroke properties (and thus have less priority when /// defining with which stroke to draw grid lines around this cell). pub stroke_overridden: Sides, + /// Whether rows spanned by this cell can be placed in different pages. + /// By default, a cell spanning only fixed-size rows is unbreakable, while + /// a cell spanning at least one `auto`-sized row is breakable. + pub breakable: bool, } impl From for Cell { @@ -194,8 +201,10 @@ impl From for Cell { body, fill: None, colspan: NonZeroUsize::ONE, + rowspan: NonZeroUsize::ONE, stroke: Sides::splat(None), stroke_overridden: Sides::splat(false), + breakable: true, } } } @@ -245,7 +254,7 @@ pub enum GridItem { stroke: Option>>, /// The span of the corresponding line element. span: Span, - /// The line's position. "before" here means on top of row 'y', while + /// The line's position. "before" here means on top of row `y`, while /// "after" means below it. position: LinePosition, }, @@ -258,7 +267,7 @@ pub enum GridItem { stroke: Option>>, /// The span of the corresponding line element. span: Span, - /// The line's position. "before" here means to the left of column 'x', + /// The line's position. "before" here means to the left of column `x`, /// while "after" means to its right (both considering LTR). position: LinePosition, }, @@ -270,7 +279,8 @@ pub enum GridItem { /// the table, and may have property overrides. pub trait ResolvableCell { /// Resolves the cell's fields, given its coordinates and default grid-wide - /// fill, align, inset and stroke properties. + /// fill, align, inset and stroke properties, plus the expected value of + /// the `breakable` field. /// Returns a final Cell. #[allow(clippy::too_many_arguments)] fn resolve_cell( @@ -281,6 +291,7 @@ pub trait ResolvableCell { align: Smart, inset: Sides>>, stroke: Sides>>>>, + breakable: bool, styles: StyleChain, ) -> Cell; @@ -293,6 +304,9 @@ pub trait ResolvableCell { /// The amount of columns spanned by this cell. fn colspan(&self, styles: StyleChain) -> NonZeroUsize; + /// The amount of rows spanned by this cell. + fn rowspan(&self, styles: StyleChain) -> NonZeroUsize; + /// The cell's span, for errors. fn span(&self) -> Span; } @@ -366,6 +380,27 @@ impl CellGrid { let mut pending_vlines: Vec<(Span, Line)> = vec![]; let has_gutter = gutter.any(|tracks| !tracks.is_empty()); + // Resolve the breakability of a cell, based on whether or not it spans + // an auto row. + let resolve_breakable = |y, rowspan| { + let auto = Sizing::Auto; + let zero = Sizing::Rel(Rel::zero()); + tracks + .y + .iter() + .chain(std::iter::repeat(tracks.y.last().unwrap_or(&auto))) + .skip(y) + .take(rowspan) + .any(|row| row == &Sizing::Auto) + || gutter + .y + .iter() + .chain(std::iter::repeat(gutter.y.last().unwrap_or(&zero))) + .skip(y) + .take(rowspan - 1) + .any(|row_gutter| row_gutter == &Sizing::Auto) + }; + // We can't just use the cell's index in the 'cells' vector to // determine its automatic position, since cells could have arbitrary // positions, so the position of a cell in 'cells' can differ from its @@ -471,6 +506,7 @@ impl CellGrid { 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!( @@ -480,11 +516,17 @@ impl CellGrid { ) } - let Some(largest_index) = resolved_index.checked_add(colspan - 1) else { + 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 colspan" + hint: "try reducing the cell's rowspan or colspan" ) }; @@ -497,6 +539,7 @@ impl CellGrid { align.resolve(engine, styles, x, y)?, inset.resolve(engine, styles, x, y)?, stroke.resolve(engine, styles, x, y)?, + resolve_breakable(y, rowspan), styles, ); @@ -542,23 +585,29 @@ impl CellGrid { *slot = Some(Entry::Cell(cell)); - // Now, if the cell spans more than one column, we fill the spanned - // positions in the grid with Entry::Merged pointing to the + // 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 (offset, slot) in resolved_cells[resolved_index..][..colspan] - .iter_mut() - .enumerate() - .skip(1) - { - if slot.is_some() { - let spanned_x = x + offset; - bail!( - cell_span, - "cell would span a previously placed cell at column {spanned_x}, row {y}"; - hint: "try specifying your cells in a different order or reducing the cell's colspan" - ) + 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 }); } - *slot = Some(Entry::Merged { parent: resolved_index }); } } @@ -583,6 +632,7 @@ impl CellGrid { align.resolve(engine, styles, x, y)?, inset.resolve(engine, styles, x, y)?, stroke.resolve(engine, styles, x, y)?, + resolve_breakable(y, 1), styles, ); Ok(Entry::Cell(new_cell)) @@ -760,16 +810,6 @@ impl CellGrid { self.entry(x, y).and_then(Entry::as_cell) } - /// Returns the parent cell of the grid entry at the given position. - /// - If the entry at the given position is a cell, returns it. - /// - If it is a merged cell, returns the parent cell. - /// - If it is a gutter cell, returns None. - #[track_caller] - pub(super) fn parent_cell(&self, x: usize, y: usize) -> Option<&Cell> { - self.parent_cell_position(x, y) - .and_then(|Axes { x, y }| self.cell(x, y)) - } - /// Returns the position of the parent cell of the grid entry at the given /// position. It is guaranteed to have a non-gutter, non-merged cell at /// the returned position, due to how the grid is built. @@ -792,6 +832,72 @@ impl CellGrid { } }) } + + /// Returns the position of the actual parent cell of a merged position, + /// even if the given position is gutter, in which case we return the + /// parent of the nearest adjacent content cell which could possibly span + /// the given gutter position. If the given position is not a gutter cell, + /// then this function will return the same as `parent_cell_position` would. + /// If the given position is a gutter cell, but no cell spans it, returns + /// `None`. + /// + /// This is useful for lines. A line needs to check if a cell next to it + /// has a stroke override - even at a gutter position there could be a + /// stroke override, since a cell could be merged with two cells at both + /// ends of the gutter cell (e.g. to its left and to its right), and thus + /// that cell would impose a stroke under the gutter. This function allows + /// getting the position of that cell (which spans the given gutter + /// position, if it is gutter), if it exists; otherwise returns None (it's + /// gutter and no cell spans it). + #[track_caller] + pub(super) fn effective_parent_cell_position( + &self, + x: usize, + y: usize, + ) -> Option> { + if self.has_gutter { + // If (x, y) is a gutter cell, we skip it (skip a gutter column and + // row) to the nearest adjacent content cell, in the direction + // which merged cells grow toward (increasing x and increasing y), + // such that we can verify if that adjacent cell is merged with the + // gutter cell by checking if its parent would come before (x, y). + // Otherwise, no cell is merged with this gutter cell, and we + // return None. + self.parent_cell_position(x + x % 2, y + y % 2) + .filter(|&parent| parent.x <= x && parent.y <= y) + } else { + self.parent_cell_position(x, y) + } + } + + /// Checks if the track with the given index is gutter. + /// Does not check if the index is a valid track. + #[inline] + pub(super) fn is_gutter_track(&self, index: usize) -> bool { + self.has_gutter && index % 2 == 1 + } + + /// Returns the effective colspan of a cell, considering the gutters it + /// might span if the grid has gutters. + #[inline] + pub(super) fn effective_colspan_of_cell(&self, cell: &Cell) -> usize { + if self.has_gutter { + 2 * cell.colspan.get() - 1 + } else { + cell.colspan.get() + } + } + + /// Returns the effective rowspan of a cell, considering the gutters it + /// might span if the grid has gutters. + #[inline] + pub(super) fn effective_rowspan_of_cell(&self, cell: &Cell) -> usize { + if self.has_gutter { + 2 * cell.rowspan.get() - 1 + } else { + cell.rowspan.get() + } + } } /// Given a cell's requested x and y, the vector with the resolved cell @@ -889,27 +995,34 @@ fn resolve_cell_position( /// Performs grid layout. pub struct GridLayouter<'a> { /// The grid of cells. - grid: &'a CellGrid, + pub(super) grid: &'a CellGrid, /// The regions to layout children into. - regions: Regions<'a>, + pub(super) regions: Regions<'a>, /// The inherited styles. - styles: StyleChain<'a>, + pub(super) styles: StyleChain<'a>, /// Resolved column sizes. - rcols: Vec, + pub(super) rcols: Vec, /// The sum of `rcols`. - width: Abs, + pub(super) width: Abs, /// Resolve row sizes, by region. - rrows: Vec>, + pub(super) rrows: Vec>, /// Rows in the current region. - lrows: Vec, + pub(super) lrows: Vec, + /// The amount of unbreakable rows remaining to be laid out in the + /// current unbreakable row group. While this is positive, no region breaks + /// should occur. + pub(super) unbreakable_rows_left: usize, + /// Rowspans not yet laid out because not all of their spanned rows were + /// laid out yet. + pub(super) rowspans: Vec, /// The initial size of the current region before we started subtracting. - initial: Size, + pub(super) initial: Size, /// Frames for finished regions. - finished: Vec, + pub(super) finished: Vec, /// Whether this is an RTL grid. - is_rtl: bool, + pub(super) is_rtl: bool, /// The span of the grid element. - span: Span, + pub(super) span: Span, } /// Details about a resulting row piece. @@ -923,9 +1036,12 @@ pub struct RowPiece { /// Produced by initial row layout, auto and relative rows are already finished, /// fractional rows not yet. -enum Row { +pub(super) enum Row { /// Finished row frame of auto or relative row with y index. - Frame(Frame, usize), + /// The last parameter indicates whether or not this is the last region + /// where this row is laid out, and it can only be false when a row uses + /// `layout_multi_row`, which in turn is only used by breakable auto rows. + Frame(Frame, usize, bool), /// Fractional row with y index. Fr(Fr, usize), } @@ -953,6 +1069,8 @@ impl<'a> GridLayouter<'a> { width: Abs::zero(), rrows: vec![], lrows: vec![], + unbreakable_rows_left: 0, + rowspans: vec![], initial: regions.size, finished: vec![], is_rtl: TextElem::dir_in(styles) == Dir::RTL, @@ -966,27 +1084,59 @@ impl<'a> GridLayouter<'a> { 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. - if self.regions.is_full() && (!self.grid.has_gutter || y % 2 == 0) { + // 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)?; } - 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 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); } self.finish_region(engine)?; + // Layout any missing rowspans. + // There are only two possibilities for rowspans not yet laid out + // (usually, a rowspan is laid out as soon as its last row, or any row + // after it, is laid out): + // 1. The rowspan was fully empty and only spanned fully empty auto + // rows, which were all prevented from being laid out. Those rowspans + // are ignored by 'layout_rowspan', and are not of any concern. + // + // 2. The rowspan's last row was an auto row at the last region which + // was not laid out, and no other rows were laid out after it. Those + // might still need to be laid out, so we check for them. + for rowspan in std::mem::take(&mut self.rowspans) { + self.layout_rowspan(rowspan, None, engine)?; + } + self.render_fills_strokes() } /// Add lines and backgrounds. fn render_fills_strokes(mut self) -> SourceResult { let mut finished = std::mem::take(&mut self.finished); - for (frame, rows) in finished.iter_mut().zip(&self.rrows) { + let frame_amount = finished.len(); + for ((frame_index, frame), rows) in + finished.iter_mut().enumerate().zip(&self.rrows) + { if self.rcols.is_empty() || rows.is_empty() { continue; } @@ -1033,7 +1183,7 @@ impl<'a> GridLayouter<'a> { // lines before it, not after). x / 2 }) - .map(|vlines| &**vlines) + .map(Vec::as_slice) .unwrap_or(&[]); let tracks = rows.iter().map(|row| (row.y, row.height)); @@ -1077,13 +1227,14 @@ impl<'a> GridLayouter<'a> { // Additionally, determine their indices (the indices of the // rows they are drawn on top of). In principle, this will // correspond to the rows' indices directly, except for the - // first and last hlines, which must be 0 and (amount of rows) - // respectively, as they are always drawn (due to being part of - // the table's border). - let hline_indices = std::iter::once(0) - .chain(rows.iter().map(|piece| piece.y).skip(1)) + // last hline index, which must be (amount of rows) in order to + // draw the table's bottom border. + let hline_indices = rows + .iter() + .map(|piece| piece.y) .chain(std::iter::once(self.grid.rows.len())); + 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 @@ -1098,10 +1249,37 @@ impl<'a> GridLayouter<'a> { // these index operations. y / 2 }) - .map(|hlines| &**hlines) - .unwrap_or(&[]); + .map(Vec::as_slice) + .unwrap_or(&[]) + .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(&[]) + } 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. + &[] + }); + let tracks = self.rcols.iter().copied().enumerate(); + // Normally, given an hline above row y, the row above it is + // 'y - 1' (if y > 0). However, sometimes that's not true, for + // example if 'y - 1' is in a previous region, or if 'y - 1' + // was an empty auto row which was removed. Therefore, we tell + // the hlines at this index which row is actually above them in + // the laid out region so they can include that row's bottom + // strokes in the folding process. + let local_top_y = prev_y; + + // When we're in the last region, the bottom border stroke + // doesn't necessarily gain priority like it does in previous + // regions. + let in_last_region = frame_index + 1 == frame_amount; + // Determine all different line segments we have to draw in // this row, and convert them to points and shapes. let segments = generate_line_segments( @@ -1110,7 +1288,17 @@ impl<'a> GridLayouter<'a> { y, hlines_at_row, is_bottom_border, - hline_stroke_at_column, + |grid, y, x, stroke| { + hline_stroke_at_column( + grid, + rows, + local_top_y, + in_last_region, + y, + x, + stroke, + ) + }, ) .map(|segment| { let LineSegment { stroke, offset: dx, length, priority } = segment; @@ -1130,6 +1318,8 @@ impl<'a> GridLayouter<'a> { // Draw later (after we sort all lines below.) lines.extend(segments); + + prev_y = Some(y); } // Sort by increasing thickness, so that we draw larger strokes @@ -1151,10 +1341,72 @@ impl<'a> GridLayouter<'a> { for (x, &col) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { let mut dy = Abs::zero(); for row in rows { - if let Some(cell) = self.grid.cell(x, row.y) { + // We want to only draw the fill starting at the parent + // positions of cells. However, sometimes the parent + // position is absent from the current region, either + // because the first few rows of a rowspan were empty auto + // rows and thus removed from layout, or because the parent + // cell was in a previous region (in which case we'd want + // to draw its fill again, in the current region). + // Therefore, we first analyze the parent position to see + // if the current row would be the first row spanned by the + // parent cell in this region. If so, this means we have to + // start drawing the cell's fill here. If not, we ignore + // the position `(x, row.y)`, as its fill will already have + // been rendered before. + // + // Note: In the case of gutter rows, we have to check the + // row below before discarding them fully, because a + // gutter row might be the first row spanned by a rowspan + // in this region (e.g. if the first row was empty and + // therefore removed), so its fill could start in that + // gutter row. That's why we use + // 'effective_parent_cell_position'. + let parent = self + .grid + .effective_parent_cell_position(x, row.y) + .filter(|parent| { + // Ensure this is the first column spanned by the + // cell before drawing its fill, otherwise we + // already rendered its fill in a previous + // iteration of the outer loop (and/or this is a + // gutter column, which we ignore). + // + // Additionally, we should only draw the fill when + // this row is the local parent Y for this cell, + // that is, the first row spanned by the cell's + // parent in this region, because if the parent + // cell's fill was already drawn in a previous + // region, we must render it again in later regions + // spanned by that cell. Note that said condition + // always holds when the current cell has a rowspan + // of 1 and we're not currently at a gutter row. + parent.x == x + && (parent.y == row.y + || rows + .iter() + .find(|row| row.y >= parent.y) + .is_some_and(|first_spanned_row| { + first_spanned_row.y == row.y + })) + }); + + if let Some(parent) = parent { + let cell = self.grid.cell(parent.x, parent.y).unwrap(); let fill = cell.fill.clone(); if let Some(fill) = fill { - let width = self.cell_spanned_width(x, cell.colspan.get()); + let rowspan = self.grid.effective_rowspan_of_cell(cell); + let height = if rowspan == 1 { + row.height + } else { + rows.iter() + .filter(|row| { + (parent.y..parent.y + rowspan).contains(&row.y) + }) + .map(|row| row.height) + .sum() + }; + let width = self.cell_spanned_width(cell, x); // In the grid, cell colspans expand to the right, // so we're at the leftmost (lowest 'x') column // spanned by the cell. However, in RTL, cells @@ -1167,7 +1419,7 @@ impl<'a> GridLayouter<'a> { let offset = if self.is_rtl { -width + col } else { Abs::zero() }; let pos = Point::new(dx + offset, dy); - let size = Size::new(width, row.height); + let size = Size::new(width, height); let rect = Geometry::Rect(size).filled(fill); fills.push((pos, FrameItem::Shape(rect, self.span))); } @@ -1237,12 +1489,9 @@ impl<'a> GridLayouter<'a> { /// Total width spanned by the cell (among resolved columns). /// Includes spanned gutter columns. - fn cell_spanned_width(&self, x: usize, colspan: usize) -> Abs { - self.rcols - .iter() - .skip(x) - .take(if self.grid.has_gutter { 2 * colspan - 1 } else { colspan }) - .sum() + pub(super) fn cell_spanned_width(&self, cell: &Cell, x: usize) -> Abs { + let colspan = self.grid.effective_colspan_of_cell(cell); + self.rcols.iter().skip(x).take(colspan).sum() } /// Measure the size that is available to auto columns. @@ -1272,25 +1521,23 @@ impl<'a> GridLayouter<'a> { let mut resolved = Abs::zero(); for y in 0..self.grid.rows.len() { // We get the parent cell in case this is a merged position. - let Some(Axes { x: parent_x, y: parent_y }) = - self.grid.parent_cell_position(x, y) - else { + let Some(parent) = self.grid.parent_cell_position(x, y) else { continue; }; - let cell = self.grid.cell(parent_x, parent_y).unwrap(); - let colspan = cell.colspan.get(); + if parent.y != y { + // Don't check the width of rowspans more than once. + continue; + } + let cell = self.grid.cell(parent.x, parent.y).unwrap(); + let colspan = self.grid.effective_colspan_of_cell(cell); if colspan > 1 { let last_spanned_auto_col = self .grid .cols .iter() .enumerate() - .skip(parent_x) - .take(if self.grid.has_gutter { - 2 * colspan - 1 - } else { - colspan - }) + .skip(parent.x) + .take(colspan) .rev() .find(|(_, col)| **col == Sizing::Auto) .map(|(x, _)| x); @@ -1307,7 +1554,7 @@ impl<'a> GridLayouter<'a> { && !all_frac_cols.is_empty() && all_frac_cols .iter() - .all(|x| (parent_x..parent_x + colspan).contains(x)) + .all(|x| (parent.x..parent.x + colspan).contains(x)) { // Additionally, as a heuristic, a colspan won't affect the // size of auto columns if it already spans all fractional @@ -1319,14 +1566,30 @@ impl<'a> GridLayouter<'a> { continue; } - // For relative rows, we can already resolve the correct - // base and for auto and fr we could only guess anyway. - let height = match self.grid.rows[y] { - Sizing::Rel(v) => { - v.resolve(self.styles).relative_to(self.regions.base().y) - } - _ => self.regions.base().y, - }; + // Sum the heights of spanned rows to find the expected + // available height for the cell, unless it spans a fractional + // or auto column. + let rowspan = self.grid.effective_rowspan_of_cell(cell); + let height = self + .grid + .rows + .iter() + .skip(y) + .take(rowspan) + .try_fold(Abs::zero(), |acc, col| { + // For relative rows, we can already resolve the correct + // base and for auto and fr we could only guess anyway. + match col { + Sizing::Rel(v) => Some( + acc + v + .resolve(self.styles) + .relative_to(self.regions.base().y), + ), + _ => None, + } + }) + .unwrap_or_else(|| self.regions.base().y); + // Don't expand this auto column more than the cell actually // needs. To do this, we check how much the other, previously // resolved columns provide to the cell in terms of width @@ -1341,7 +1604,7 @@ impl<'a> GridLayouter<'a> { // an auto column. One mitigation for this is the heuristic // used above to not expand the last auto column spanned by a // cell if it spans all fractional columns in a finite region. - let already_covered_width = self.cell_spanned_width(parent_x, colspan); + let already_covered_width = self.cell_spanned_width(cell, parent.x); let size = Size::new(available, height); let pod = Regions::one(size, Axes::splat(false)); @@ -1408,11 +1671,18 @@ impl<'a> GridLayouter<'a> { fn layout_auto_row(&mut self, engine: &mut Engine, y: usize) -> SourceResult<()> { // Determine the size for each region of the row. If the first region // ends up empty for some column, skip the region and remeasure. - let mut resolved = match self.measure_auto_row(engine, y, true)? { + let mut resolved = match self.measure_auto_row( + engine, + y, + true, + self.unbreakable_rows_left, + None, + )? { Some(resolved) => resolved, None => { self.finish_region(engine)?; - self.measure_auto_row(engine, y, false)?.unwrap() + self.measure_auto_row(engine, y, false, self.unbreakable_rows_left, None)? + .unwrap() } }; @@ -1424,7 +1694,7 @@ impl<'a> GridLayouter<'a> { // Layout into a single region. if let &[first] = resolved.as_slice() { let frame = self.layout_single_row(engine, first, y)?; - self.push_row(frame, y); + self.push_row(frame, y, true); return Ok(()); } @@ -1444,7 +1714,7 @@ impl<'a> GridLayouter<'a> { let fragment = self.layout_multi_row(engine, &resolved, y)?; let len = fragment.len(); for (i, frame) in fragment.into_iter().enumerate() { - self.push_row(frame, y); + self.push_row(frame, y, i + 1 == len); if i + 1 < len { self.finish_region(engine)?; } @@ -1455,43 +1725,175 @@ impl<'a> GridLayouter<'a> { /// Measure the regions sizes of an auto row. The option is always `Some(_)` /// if `can_skip` is false. - fn measure_auto_row( - &mut self, + /// If `unbreakable_rows_left` is positive, this function shall only return + /// a single frame. Useful when an unbreakable rowspan crosses this auto + /// row. + /// The `row_group_data` option is used within the unbreakable row group + /// simulator to predict the height of the auto row if previous rows in the + /// group were placed in the same region. + pub(super) fn measure_auto_row( + &self, engine: &mut Engine, y: usize, can_skip: bool, + unbreakable_rows_left: usize, + row_group_data: Option<&UnbreakableRowGroup>, ) -> SourceResult>> { + let breakable = unbreakable_rows_left == 0; let mut resolved: Vec = vec![]; + let mut pending_rowspans: Vec<(usize, usize, Vec)> = vec![]; for x in 0..self.rcols.len() { - if let Some(cell) = self.grid.cell(x, y) { - let mut pod = self.regions; - pod.size.x = self.cell_spanned_width(x, cell.colspan.get()); - - let frames = cell.measure(engine, self.styles, pod)?.into_frames(); - - // Skip the first region if one cell in it is empty. Then, - // remeasure. - if let [first, rest @ ..] = frames.as_slice() { - if can_skip - && first.is_empty() - && rest.iter().any(|frame| !frame.is_empty()) - { - return Ok(None); - } - } - - let mut sizes = frames.iter().map(|frame| frame.height()); - for (target, size) in resolved.iter_mut().zip(&mut sizes) { - target.set_max(size); - } - - // New heights are maximal by virtue of being new. Note that - // this extend only uses the rest of the sizes iterator. - resolved.extend(sizes); + // Get the parent cell in case this is a merged position. + let Some(parent) = self.grid.parent_cell_position(x, y) else { + // Skip gutter columns. + continue; + }; + if parent.x != x { + // Only check the height of a colspan once. + continue; } + // The parent cell is never a gutter or merged position. + let cell = self.grid.cell(parent.x, parent.y).unwrap(); + let rowspan = self.grid.effective_rowspan_of_cell(cell); + + if rowspan > 1 { + let last_spanned_auto_row = self + .grid + .rows + .iter() + .enumerate() + .skip(parent.y) + .take(rowspan) + .rev() + .find(|(_, &row)| row == Sizing::Auto) + .map(|(y, _)| y); + + if last_spanned_auto_row != Some(y) { + // A rowspan should only affect the height of its last + // spanned auto row. + continue; + } + } + + let measurement_data = self.prepare_auto_row_cell_measurement( + parent, + cell, + breakable, + row_group_data, + ); + let size = Axes::new(measurement_data.width, measurement_data.height); + let backlog = + measurement_data.backlog.unwrap_or(&measurement_data.custom_backlog); + + let pod = if !breakable { + // Force cell to fit into a single region when the row is + // unbreakable, even when it is a breakable rowspan, as a best + // effort. + let mut pod = Regions::one(size, self.regions.expand); + pod.full = measurement_data.full; + + if measurement_data.frames_in_previous_regions > 0 { + // Best effort to conciliate a breakable rowspan which + // started at a previous region going through an + // unbreakable auto row. Ensure it goes through previously + // laid out regions, but stops at this one when measuring. + pod.backlog = backlog; + } + + pod + } else { + // This row is breakable, so measure the cell normally, with + // the initial height and backlog determined previously. + let mut pod = self.regions; + pod.size = size; + pod.backlog = backlog; + pod.full = measurement_data.full; + pod + }; + + let frames = cell.measure(engine, self.styles, pod)?.into_frames(); + + // Skip the first region if one cell in it is empty. Then, + // remeasure. + if let Some([first, rest @ ..]) = + frames.get(measurement_data.frames_in_previous_regions..) + { + if can_skip + && breakable + && first.is_empty() + && rest.iter().any(|frame| !frame.is_empty()) + { + return Ok(None); + } + } + + // Skip frames from previous regions if applicable. + let mut sizes = frames + .iter() + .skip(measurement_data.frames_in_previous_regions) + .map(|frame| frame.height()) + .collect::>(); + + // Don't expand this row more than the cell needs. + // To figure out how much height the cell needs, we must first + // subtract, from the cell's expected height, the already resolved + // heights of its spanned rows. Note that this is the last spanned + // auto row, so all previous auto rows were already resolved, as + // well as fractional rows in previous regions. + // Additionally, we subtract the heights of fixed-size rows which + // weren't laid out yet, since those heights won't change in + // principle. + // Upcoming fractional rows are ignored. + // Upcoming gutter rows might be removed, so we need to simulate + // them. + if rowspan > 1 { + let should_simulate = self.prepare_rowspan_sizes( + y, + &mut sizes, + cell, + parent.y, + rowspan, + unbreakable_rows_left, + &measurement_data, + ); + + if should_simulate { + // Rowspan spans gutter and is breakable. We'll need to + // run a simulation to predict how much this auto row needs + // to expand so that the rowspan's contents fit into the + // table. + pending_rowspans.push((parent.y, rowspan, sizes)); + continue; + } + } + + let mut sizes = sizes.into_iter(); + + for (target, size) in resolved.iter_mut().zip(&mut sizes) { + target.set_max(size); + } + + // New heights are maximal by virtue of being new. Note that + // this extend only uses the rest of the sizes iterator. + resolved.extend(sizes); } + // Simulate the upcoming regions in order to predict how much we need + // to expand this auto row for rowspans which span gutter. + if !pending_rowspans.is_empty() { + self.simulate_and_measure_rowspans_in_auto_row( + y, + &mut resolved, + &pending_rowspans, + unbreakable_rows_left, + row_group_data, + engine, + )?; + } + + debug_assert!(breakable || resolved.len() <= 1); + Ok(Some(resolved)) } @@ -1506,18 +1908,22 @@ 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)?; - // Skip to fitting region. + // Skip to fitting region, but only if we aren't part of an unbreakable + // row group. let height = frame.height(); - while !self.regions.size.y.fits(height) && !self.regions.in_last() { + while self.unbreakable_rows_left == 0 + && !self.regions.size.y.fits(height) + && !self.regions.in_last() + { self.finish_region(engine)?; // Don't skip multiple regions for gutter and don't push a row. - if self.grid.has_gutter && y % 2 == 1 { + if self.grid.is_gutter_track(y) { return Ok(()); } } - self.push_row(frame, y); + self.push_row(frame, y, true); Ok(()) } @@ -1539,27 +1945,31 @@ impl<'a> GridLayouter<'a> { // Reverse the column order when using RTL. for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { if let Some(cell) = self.grid.cell(x, y) { - let width = self.cell_spanned_width(x, cell.colspan.get()); - let size = Size::new(width, height); - let mut pod = Regions::one(size, Axes::splat(true)); - if self.grid.rows[y] == Sizing::Auto { - pod.full = self.regions.full; + // Rowspans have a separate layout step + if cell.rowspan.get() == 1 { + let width = self.cell_spanned_width(cell, x); + let size = Size::new(width, height); + let mut pod = Regions::one(size, Axes::splat(true)); + if self.grid.rows[y] == Sizing::Auto { + pod.full = self.regions.full; + } + let frame = cell.layout(engine, self.styles, pod)?.into_frame(); + let mut pos = pos; + if self.is_rtl { + // In the grid, cell colspans expand to the right, + // so we're at the leftmost (lowest 'x') column + // spanned by the cell. However, in RTL, cells + // expand to the left. Therefore, without the + // offset below, the cell's contents would be laid out + // starting at its rightmost visual position and extend + // over to unrelated cells to its right in RTL. + // We avoid this by ensuring the rendered cell starts at + // the very left of the cell, even with colspan > 1. + let offset = -width + rcol; + pos.x += offset; + } + output.push_frame(pos, frame); } - let mut frame = cell.layout(engine, self.styles, pod)?.into_frame(); - if self.is_rtl { - // In the grid, cell colspans expand to the right, - // so we're at the leftmost (lowest 'x') column - // spanned by the cell. However, in RTL, cells - // expand to the left. Therefore, without the - // offset below, the cell's contents would be laid out - // starting at its rightmost visual position and extend - // over to unrelated cells to its right in RTL. - // We avoid this by ensuring the rendered cell starts at - // the very left of the cell, even with colspan > 1. - let offset = Point::with_x(-width + rcol); - frame.translate(offset); - } - output.push_frame(pos, frame); } pos.x += rcol; @@ -1591,17 +2001,21 @@ impl<'a> GridLayouter<'a> { let mut pos = Point::zero(); for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { if let Some(cell) = self.grid.cell(x, y) { - let width = self.cell_spanned_width(x, cell.colspan.get()); - pod.size.x = width; + // Rowspans have a separate layout step + if cell.rowspan.get() == 1 { + let width = self.cell_spanned_width(cell, x); + pod.size.x = width; - // Push the layouted frames into the individual output frames. - let fragment = cell.layout(engine, self.styles, pod)?; - for (output, mut frame) in outputs.iter_mut().zip(fragment) { - if self.is_rtl { - let offset = Point::with_x(-width + rcol); - frame.translate(offset); + // Push the layouted frames into the individual output frames. + let fragment = cell.layout(engine, self.styles, pod)?; + for (output, frame) in outputs.iter_mut().zip(fragment) { + let mut pos = pos; + if self.is_rtl { + let offset = -width + rcol; + pos.x += offset; + } + output.push_frame(pos, frame); } - output.push_frame(pos, frame); } } @@ -1612,19 +2026,29 @@ impl<'a> GridLayouter<'a> { } /// Push a row frame into the current region. - fn push_row(&mut self, frame: Frame, y: usize) { + /// The `is_last` parameter must be `true` if this is the last frame which + /// will be pushed for this particular row. It can be `false` for rows + /// spanning multiple regions. + fn push_row(&mut self, frame: Frame, y: usize, is_last: bool) { self.regions.size.y -= frame.height(); - self.lrows.push(Row::Frame(frame, y)); + self.lrows.push(Row::Frame(frame, y, is_last)); } /// Finish rows for one region. - fn finish_region(&mut self, engine: &mut Engine) -> SourceResult<()> { + 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) + }) { + // Remove the last row in the region if it is a gutter row. + self.lrows.pop().unwrap(); + } // Determine the height of existing rows in the region. let mut used = Abs::zero(); let mut fr = Fr::zero(); for row in &self.lrows { match row { - Row::Frame(frame, _) => used += frame.height(), + Row::Frame(frame, _, _) => used += frame.height(), Row::Fr(v, _) => fr += *v, } } @@ -1640,19 +2064,89 @@ impl<'a> GridLayouter<'a> { let mut output = Frame::soft(size); let mut pos = Point::zero(); let mut rrows = vec![]; + let current_region = self.finished.len(); // Place finished rows and layout fractional rows. for row in std::mem::take(&mut self.lrows) { - let (frame, y) = match row { - Row::Frame(frame, y) => (frame, y), + let (frame, y, is_last) = match row { + Row::Frame(frame, y, is_last) => (frame, y, is_last), Row::Fr(v, y) => { let remaining = self.regions.full - used; let height = v.share(fr, remaining); - (self.layout_single_row(engine, height, y)?, y) + (self.layout_single_row(engine, height, y)?, y, true) } }; let height = frame.height(); + + // Ensure rowspans which span this row will have enough space to + // be laid out over it later. + for rowspan in self + .rowspans + .iter_mut() + .filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y)) + { + // 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 + // region's index. + if rowspan.first_region > current_region { + rowspan.first_region = current_region; + // The rowspan starts at this region, precisely at this + // row. In other regions, it will start at dy = 0. + rowspan.dy = pos.y; + // When we layout the rowspan later, the full size of the + // pod must be equal to the full size of the first region + // it appears in. + rowspan.region_full = self.regions.full; + } + let amount_missing_heights = (current_region + 1) + .saturating_sub(rowspan.heights.len() + rowspan.first_region); + + // Ensure the vector of heights is long enough such that the + // last height is the one for the current region. + rowspan + .heights + .extend(std::iter::repeat(Abs::zero()).take(amount_missing_heights)); + + // Ensure that, in this region, the rowspan will span at least + // this row. + *rowspan.heights.last_mut().unwrap() += height; + } + + // 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; + } + } + } + output.push_frame(pos, frame); rrows.push(RowPiece { height, y }); pos.y += height; @@ -1669,7 +2163,9 @@ impl<'a> GridLayouter<'a> { /// Turn an iterator of extents into an iterator of offsets before, in between, /// and after the extents, e.g. [10mm, 5mm] -> [0mm, 10mm, 15mm]. -fn points(extents: impl IntoIterator) -> impl Iterator { +pub(super) fn points( + extents: impl IntoIterator, +) -> impl Iterator { let mut offset = Abs::zero(); std::iter::once(Abs::zero()).chain(extents).map(move |extent| { offset += extent; diff --git a/crates/typst/src/layout/grid/lines.rs b/crates/typst/src/layout/grid/lines.rs index 6eb43c9a..7084c71a 100644 --- a/crates/typst/src/layout/grid/lines.rs +++ b/crates/typst/src/layout/grid/lines.rs @@ -1,9 +1,9 @@ use std::num::NonZeroUsize; use std::sync::Arc; -use super::layout::CellGrid; +use super::layout::{CellGrid, RowPiece}; use crate::foundations::{AlternativeFold, Fold}; -use crate::layout::{Abs, Axes}; +use crate::layout::Abs; use crate::visualize::Stroke; /// Represents an explicit grid line (horizontal or vertical) specified by the @@ -67,7 +67,7 @@ pub(super) enum StrokePriority { } /// Data for a particular line segment in the grid as generated by -/// 'generate_line_segments'. +/// `generate_line_segments`. #[derive(Debug, PartialEq, Eq)] pub(super) struct LineSegment { /// The stroke with which to draw this segment. @@ -100,7 +100,7 @@ pub(super) struct LineSegment { /// this index to fold with, if any). Contiguous segments with the same stroke /// and priority are joined together automatically. /// -/// The function should return 'None' for positions at which the line would +/// The function should return `None` for positions at which the line would /// otherwise cross a merged cell (for example, a vline could cross a colspan), /// in which case a new segment should be drawn after the merged cell(s), even /// if it would have the same stroke as the previous one. @@ -115,13 +115,13 @@ pub(super) struct LineSegment { /// /// Note that we assume that the tracks are sorted according to ascending /// number, and they must be iterable over pairs of (number, size). For -/// vertical lines, for instance, 'tracks' would describe the rows in the +/// vertical lines, for instance, `tracks` would describe the rows in the /// current region, as pairs (row index, row height). -pub(super) fn generate_line_segments<'grid, F, I>( +pub(super) fn generate_line_segments<'grid, F, I, L>( grid: &'grid CellGrid, tracks: I, index: usize, - lines: &'grid [Line], + lines: L, is_max_index: bool, line_stroke_at_track: F, ) -> impl Iterator + 'grid @@ -135,6 +135,8 @@ where + 'grid, I: IntoIterator, I::IntoIter: 'grid, + L: IntoIterator, + L::IntoIter: Clone + 'grid, { // The segment currently being drawn. // @@ -162,7 +164,7 @@ where // 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.has_gutter && index % 2 == 1 && !is_max_index { + let expected_line_position = if grid.is_gutter_track(index) && !is_max_index { LinePosition::After } else { LinePosition::Before @@ -194,6 +196,7 @@ where // interrupt the current segment one last time, to ensure the final segment // is always interrupted and yielded, if it wasn't interrupted earlier. let mut tracks = tracks.into_iter(); + let lines = lines.into_iter(); std::iter::from_fn(move || { // Each time this closure runs, we advance the track iterator as much // as possible before returning because the current segment was @@ -205,7 +208,7 @@ where // strokes of each user-specified line (with priority to the // user-specified line specified last). let mut line_strokes = lines - .iter() + .clone() .filter(|line| { line.position == expected_line_position && line @@ -332,45 +335,48 @@ pub(super) fn vline_stroke_at_row( y: usize, stroke: Option>>>, ) -> Option<(Arc>, StrokePriority)> { + // When the vline isn't at the border, we need to check if a colspan would + // be present between columns 'x' and 'x-1' at row 'y', and thus overlap + // with the line. + // To do so, we analyze the cell right after this vline. If it is merged + // with a cell before this line (parent.x < x) which is at this row or + // above it (parent.y <= y, which is checked by + // 'effective_parent_cell_position'), this means it would overlap with the + // vline, so the vline must not be drawn at this row. if x != 0 && x != grid.cols.len() { - // When the vline isn't at the border, we need to check if a colspan would - // be present between columns 'x' and 'x-1' at row 'y', and thus overlap - // with the line. - // To do so, we analyze the cell right after this vline. If it is merged - // with a cell before this line (parent_x < x) which is at this row or - // above it (parent_y <= y), this means it would overlap with the vline, - // so the vline must not be drawn at this row. - let first_adjacent_cell = if grid.has_gutter { - // Skip the gutters, if x or y represent gutter tracks. - // We would then analyze the cell one column after (if at a gutter - // column), and/or one row below (if at a gutter row), in order to - // check if it would be merged with a cell before the vline. - (x + x % 2, y + y % 2) - } else { - (x, y) - }; - let Axes { x: parent_x, y: parent_y } = grid - .parent_cell_position(first_adjacent_cell.0, first_adjacent_cell.1) - .unwrap(); - - if parent_x < x && parent_y <= y { - // There is a colspan cell going through this vline's position, - // so don't draw it here. - return None; + // Use 'effective_parent_cell_position' to skip the gutters, if x or y + // represent gutter tracks. + // We would then analyze the cell one column after (if at a gutter + // column), and/or one row below (if at a gutter row), in order to + // check if it would be merged with a cell before the vline. + if let Some(parent) = grid.effective_parent_cell_position(x, y) { + if parent.x < x { + // There is a colspan cell going through this vline's position, + // so don't draw it here. + return None; + } } } let (left_cell_stroke, left_cell_prioritized) = x .checked_sub(1) - .and_then(|left_x| grid.parent_cell(left_x, y)) - .map(|left_cell| { + .and_then(|left_x| { + // Let's find the parent cell of the position before us, in order + // to take its right stroke, even with gutter before us. + grid.effective_parent_cell_position(left_x, y) + }) + .map(|parent| { + let left_cell = grid.cell(parent.x, parent.y).unwrap(); (left_cell.stroke.right.clone(), left_cell.stroke_overridden.right) }) .unwrap_or((None, false)); let (right_cell_stroke, right_cell_prioritized) = if x < grid.cols.len() { - grid.parent_cell(x, y) - .map(|right_cell| { + // Let's find the parent cell of the position after us, in order + // to take its left stroke, even with gutter after us. + grid.effective_parent_cell_position(x, y) + .map(|parent| { + let right_cell = grid.cell(parent.x, parent.y).unwrap(); (right_cell.stroke.left.clone(), right_cell.stroke_overridden.left) }) .unwrap_or((None, false)) @@ -416,6 +422,12 @@ pub(super) fn vline_stroke_at_row( /// while `Some(None)` means specified to remove any stroke at this position). /// Also returns the stroke's drawing priority, which depends on its source. /// +/// The `local_top_y` parameter indicates which row is effectively on top of +/// this hline at the current region. This is `None` if the hline is above the +/// first row in the region, for instance. The `in_last_region` parameter +/// indicates whether this is the last region of the table. If not and this is +/// a line at the bottom border, the bottom border's line gains priority. +/// /// If the one (when at the border) or two (otherwise) cells above and below /// the hline have bottom and top stroke overrides, respectively, then the /// cells' stroke overrides are folded together with the hline's stroke (with @@ -428,58 +440,105 @@ pub(super) fn vline_stroke_at_row( /// /// The priority associated with the returned stroke follows the rules /// described in the docs for `generate_line_segment`. +/// +/// The rows argument is needed to know which rows are effectively present in +/// the current region, in order to avoid unnecessary hline splitting when a +/// rowspan's previous rows are either in a previous region or empty (and thus +/// wouldn't overlap with the hline, since its first row in the current region +/// is below the hline). +/// +/// This function assumes columns are sorted by increasing `x`, and rows are +/// sorted by increasing `y`. pub(super) fn hline_stroke_at_column( grid: &CellGrid, + rows: &[RowPiece], + local_top_y: Option, + in_last_region: bool, y: usize, x: usize, stroke: Option>>>, ) -> Option<(Arc>, StrokePriority)> { - // There are no rowspans yet, so no need to add a check here. The line will - // always be drawn, if it has a stroke. - let cell_x = if grid.has_gutter { - // Skip the gutter column this hline is in. - // This is because positions above and below it, even if gutter, could - // be part of a colspan, so we have to check the following cell. - // However, this is only valid if we're not in a gutter row. - x + x % 2 - } else { - x - }; + // When the hline isn't at the border, we need to check if a rowspan + // would be present between rows 'y' and 'y-1' at column 'x', and thus + // overlap with the line. + // To do so, we analyze the cell right below this hline. If it is + // merged with a cell above this line (parent.y < y) which is at this + // column or before it (parent.x <= x, which is checked by + // 'effective_parent_cell_position'), this means it would overlap with the + // hline, so the hline must not be drawn at this column. + if y != 0 && y != grid.rows.len() { + // Use 'effective_parent_cell_position' to skip the gutters, if x or y + // represent gutter tracks. + // We would then analyze the cell one column after (if at a gutter + // column), and/or one row below (if at a gutter row), in order to + // check if it would be merged with a cell before the hline. + if let Some(parent) = grid.effective_parent_cell_position(x, y) { + if parent.y < y { + // Get the first 'y' spanned by the possible rowspan in this region. + // The 'parent.y' row and any other spanned rows above 'y' could be + // missing from this region, which could have lead the check above + // to be triggered, even though there is no spanned row above the + // hline in the final layout of this region, and thus no overlap + // with the hline, allowing it to be drawn regardless of the + // theoretical presence of a rowspan going across its position. + let local_parent_y = rows + .iter() + .find(|row| row.y >= parent.y) + .map(|row| row.y) + .unwrap_or(y); - let (top_cell_stroke, top_cell_prioritized) = y - .checked_sub(1) + if local_parent_y < y { + // There is a rowspan cell going through this hline's + // position, so don't draw it here. + return None; + } + } + } + } + + // When the hline is at the top of the region and this isn't the first + // region, fold with the top stroke of the topmost cell at this column, + // that is, the top border. + let use_top_border_stroke = local_top_y.is_none() && y != 0; + let (top_cell_stroke, top_cell_prioritized) = local_top_y + .or(use_top_border_stroke.then_some(0)) .and_then(|top_y| { // Let's find the parent cell of the position above us, in order // to take its bottom stroke, even when we're below gutter. - grid.parent_cell_position(cell_x, top_y) + grid.effective_parent_cell_position(x, top_y) }) - .filter(|Axes { x: parent_x, .. }| { - // Only use the stroke of the cell above us but one column to the - // right if it is merged with a cell before this line's column. - // If the position above us is a simple non-merged cell, or the - // parent of a colspan, this will also evaluate to true. - parent_x <= &x - }) - .map(|Axes { x: parent_x, y: parent_y }| { - let top_cell = grid.cell(parent_x, parent_y).unwrap(); - (top_cell.stroke.bottom.clone(), top_cell.stroke_overridden.bottom) + .map(|parent| { + let top_cell = grid.cell(parent.x, parent.y).unwrap(); + if use_top_border_stroke { + (top_cell.stroke.top.clone(), top_cell.stroke_overridden.top) + } else { + (top_cell.stroke.bottom.clone(), top_cell.stroke_overridden.bottom) + } }) .unwrap_or((None, false)); - let (bottom_cell_stroke, bottom_cell_prioritized) = if y < grid.rows.len() { + // Use the bottom border stroke with priority if we're not in the last + // region, we have the last index, and (as a failsafe) we don't have the + // last row of cells above us. + let use_bottom_border_stroke = !in_last_region + && local_top_y.map_or(true, |top_y| top_y + 1 != grid.rows.len()) + && y == grid.rows.len(); + let bottom_y = + if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y }; + let (bottom_cell_stroke, bottom_cell_prioritized) = if bottom_y < grid.rows.len() { // Let's find the parent cell of the position below us, in order // to take its top stroke, even when we're above gutter. - grid.parent_cell_position(cell_x, y) - .filter(|Axes { x: parent_x, .. }| { - // Only use the stroke of the cell below us but one column to the - // right if it is merged with a cell before this line's column. - // If the position below us is a simple non-merged cell, or the - // parent of a colspan, this will also evaluate to true. - parent_x <= &x - }) - .map(|Axes { x: parent_x, y: parent_y }| { - let bottom_cell = grid.cell(parent_x, parent_y).unwrap(); - (bottom_cell.stroke.top.clone(), bottom_cell.stroke_overridden.top) + grid.effective_parent_cell_position(x, bottom_y) + .map(|parent| { + let bottom_cell = grid.cell(parent.x, parent.y).unwrap(); + if use_bottom_border_stroke { + ( + bottom_cell.stroke.bottom.clone(), + bottom_cell.stroke_overridden.bottom, + ) + } else { + (bottom_cell.stroke.top.clone(), bottom_cell.stroke_overridden.top) + } }) .unwrap_or((None, false)) } else { @@ -496,11 +555,17 @@ pub(super) fn hline_stroke_at_column( }; let (prioritized_cell_stroke, deprioritized_cell_stroke) = - if top_cell_prioritized && !bottom_cell_prioritized { + if !use_bottom_border_stroke + && (use_top_border_stroke || top_cell_prioritized && !bottom_cell_prioritized) + { + // Top border must always be prioritized, even if it did not + // request for that explicitly. (top_cell_stroke, bottom_cell_stroke) } else { // When both cells' strokes have the same priority, we default to // prioritizing the bottom cell's top stroke. + // Additionally, the bottom border cell's stroke always has + // priority. (bottom_cell_stroke, top_cell_stroke) }; @@ -524,7 +589,7 @@ mod test { use super::super::layout::{Entry, RowPiece}; use super::*; use crate::foundations::Content; - use crate::layout::{Cell, Sides, Sizing}; + use crate::layout::{Axes, Cell, Sides, Sizing}; use crate::util::NonZeroExt; fn sample_cell() -> Cell { @@ -532,43 +597,47 @@ mod test { body: Content::default(), fill: None, colspan: NonZeroUsize::ONE, + rowspan: NonZeroUsize::ONE, stroke: Sides::splat(Some(Arc::new(Stroke::default()))), stroke_overridden: Sides::splat(false), + breakable: true, } } - fn cell_with_colspan(colspan: usize) -> Cell { + fn cell_with_colspan_rowspan(colspan: usize, rowspan: usize) -> Cell { Cell { body: Content::default(), fill: None, colspan: NonZeroUsize::try_from(colspan).unwrap(), + rowspan: NonZeroUsize::try_from(rowspan).unwrap(), stroke: Sides::splat(Some(Arc::new(Stroke::default()))), stroke_overridden: Sides::splat(false), + breakable: true, } } - fn sample_grid(gutters: bool) -> CellGrid { + fn sample_grid_for_vlines(gutters: bool) -> CellGrid { const COLS: usize = 4; const ROWS: usize = 6; let entries = vec![ // row 0 Entry::Cell(sample_cell()), Entry::Cell(sample_cell()), - Entry::Cell(cell_with_colspan(2)), + Entry::Cell(cell_with_colspan_rowspan(2, 1)), Entry::Merged { parent: 2 }, // row 1 Entry::Cell(sample_cell()), - Entry::Cell(cell_with_colspan(3)), + Entry::Cell(cell_with_colspan_rowspan(3, 1)), Entry::Merged { parent: 5 }, Entry::Merged { parent: 5 }, // row 2 Entry::Merged { parent: 4 }, Entry::Cell(sample_cell()), - Entry::Cell(cell_with_colspan(2)), + Entry::Cell(cell_with_colspan_rowspan(2, 1)), Entry::Merged { parent: 10 }, // row 3 Entry::Cell(sample_cell()), - Entry::Cell(cell_with_colspan(3)), + Entry::Cell(cell_with_colspan_rowspan(3, 2)), Entry::Merged { parent: 13 }, Entry::Merged { parent: 13 }, // row 4 @@ -579,7 +648,7 @@ mod test { // row 5 Entry::Cell(sample_cell()), Entry::Cell(sample_cell()), - Entry::Cell(cell_with_colspan(2)), + Entry::Cell(cell_with_colspan_rowspan(2, 1)), Entry::Merged { parent: 22 }, ]; CellGrid::new_internal( @@ -598,7 +667,7 @@ mod test { #[test] fn test_vline_splitting_without_gutter() { let stroke = Arc::new(Stroke::default()); - let grid = sample_grid(false); + let grid = sample_grid_for_vlines(false); let rows = &[ RowPiece { height: Abs::pt(1.0), y: 0 }, RowPiece { height: Abs::pt(2.0), y: 1 }, @@ -670,7 +739,7 @@ mod test { #[test] fn test_vline_splitting_with_gutter_and_per_cell_stroke() { let stroke = Arc::new(Stroke::default()); - let grid = sample_grid(true); + let grid = sample_grid_for_vlines(true); let rows = &[ RowPiece { height: Abs::pt(1.0), y: 0 }, RowPiece { height: Abs::pt(2.0), y: 1 }, @@ -694,16 +763,11 @@ mod test { length: Abs::pt(1.), priority: StrokePriority::GridStroke, }, + // Covers the rowspan between (original) rows 1 and 2 LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2.), - length: Abs::pt(4.), - priority: StrokePriority::GridStroke, - }, - LineSegment { - stroke: stroke.clone(), - offset: Abs::pt(1. + 2. + 4. + 8.), - length: Abs::pt(16.), + length: Abs::pt(4. + 8. + 16.), priority: StrokePriority::GridStroke, }, LineSegment { @@ -735,16 +799,11 @@ mod test { length: Abs::pt(1.), priority: StrokePriority::GridStroke, }, + // Covers the rowspan between (original) rows 1 and 2 LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2.), - length: Abs::pt(4.), - priority: StrokePriority::GridStroke, - }, - LineSegment { - stroke: stroke.clone(), - offset: Abs::pt(1. + 2. + 4. + 8.), - length: Abs::pt(16.), + length: Abs::pt(4. + 8. + 16.), priority: StrokePriority::GridStroke, }, LineSegment { @@ -787,16 +846,11 @@ mod test { length: Abs::pt(16.), priority: StrokePriority::GridStroke, }, + // Covers the rowspan between (original) rows 3 and 4 LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), - length: Abs::pt(64.), - priority: StrokePriority::GridStroke, - }, - LineSegment { - stroke: stroke.clone(), - offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128.), - length: Abs::pt(256.), + length: Abs::pt(64. + 128. + 256.), priority: StrokePriority::GridStroke, }, LineSegment { @@ -880,16 +934,11 @@ mod test { length: Abs::pt(16.), priority: StrokePriority::GridStroke, }, + // Covers the rowspan between (original) rows 3 and 4 LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), - length: Abs::pt(64.), - priority: StrokePriority::GridStroke, - }, - LineSegment { - stroke: stroke.clone(), - offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128.), - length: Abs::pt(256.), + length: Abs::pt(64. + 128. + 256.), priority: StrokePriority::GridStroke, }, LineSegment { @@ -922,7 +971,7 @@ mod test { #[test] fn test_vline_splitting_with_gutter_and_explicit_vlines() { let stroke = Arc::new(Stroke::default()); - let grid = sample_grid(true); + let grid = sample_grid_for_vlines(true); let rows = &[ RowPiece { height: Abs::pt(1.0), y: 0 }, RowPiece { height: Abs::pt(2.0), y: 1 }, @@ -1102,4 +1151,409 @@ mod test { ); } } + + fn sample_grid_for_hlines(gutters: bool) -> CellGrid { + const COLS: usize = 4; + const ROWS: usize = 9; + let entries = vec![ + // row 0 + Entry::Cell(cell_with_colspan_rowspan(1, 2)), + Entry::Cell(sample_cell()), + Entry::Cell(cell_with_colspan_rowspan(2, 2)), + Entry::Merged { parent: 2 }, + // row 1 + Entry::Merged { parent: 0 }, + Entry::Cell(sample_cell()), + Entry::Merged { parent: 2 }, + Entry::Merged { parent: 2 }, + // row 2 + Entry::Cell(sample_cell()), + Entry::Cell(sample_cell()), + Entry::Cell(sample_cell()), + Entry::Cell(sample_cell()), + // row 3 + Entry::Cell(cell_with_colspan_rowspan(4, 2)), + Entry::Merged { parent: 12 }, + Entry::Merged { parent: 12 }, + Entry::Merged { parent: 12 }, + // row 4 + Entry::Merged { parent: 12 }, + Entry::Merged { parent: 12 }, + Entry::Merged { parent: 12 }, + Entry::Merged { parent: 12 }, + // row 5 + Entry::Cell(sample_cell()), + Entry::Cell(cell_with_colspan_rowspan(1, 2)), + Entry::Cell(cell_with_colspan_rowspan(2, 1)), + Entry::Merged { parent: 22 }, + // row 6 + Entry::Cell(sample_cell()), + Entry::Merged { parent: 21 }, + Entry::Cell(sample_cell()), + Entry::Cell(sample_cell()), + // row 7 (adjacent rowspans covering the whole row) + Entry::Cell(cell_with_colspan_rowspan(2, 2)), + Entry::Merged { parent: 28 }, + Entry::Cell(cell_with_colspan_rowspan(2, 2)), + Entry::Merged { parent: 30 }, + // row 8 + Entry::Merged { parent: 28 }, + Entry::Merged { parent: 28 }, + Entry::Merged { parent: 30 }, + Entry::Merged { parent: 30 }, + ]; + CellGrid::new_internal( + Axes::with_x(&[Sizing::Auto; COLS]), + if gutters { + Axes::new(&[Sizing::Auto; COLS - 1], &[Sizing::Auto; ROWS - 1]) + } else { + Axes::default() + }, + vec![], + vec![], + entries, + ) + } + + #[test] + fn test_hline_splitting_without_gutter() { + let stroke = Arc::new(Stroke::default()); + let grid = sample_grid_for_hlines(false); + let columns = &[Abs::pt(1.), Abs::pt(2.), Abs::pt(4.), Abs::pt(8.)]; + // Assume all rows would be drawn in the same region, and are available. + let rows = grid + .rows + .iter() + .enumerate() + .map(|(y, _)| RowPiece { height: Abs::pt(f64::from(2u32.pow(y as u32))), y }) + .collect::>(); + let expected_hline_splits = &[ + // top border + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8.), + priority: StrokePriority::GridStroke, + }], + // interrupted a few times by rowspans + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1.), + length: Abs::pt(2.), + priority: StrokePriority::GridStroke, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8.), + priority: StrokePriority::GridStroke, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8.), + priority: StrokePriority::GridStroke, + }], + // interrupted every time by rowspans + vec![], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8.), + priority: StrokePriority::GridStroke, + }], + // interrupted once by rowspan + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1.), + priority: StrokePriority::GridStroke, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2.), + length: Abs::pt(4. + 8.), + priority: StrokePriority::GridStroke, + }, + ], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8.), + priority: StrokePriority::GridStroke, + }], + // interrupted every time by successive rowspans + vec![], + // bottom border + vec![LineSegment { + stroke, + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8.), + priority: StrokePriority::GridStroke, + }], + ]; + for (y, expected_splits) in expected_hline_splits.iter().enumerate() { + let tracks = columns.iter().copied().enumerate(); + assert_eq!( + expected_splits, + &generate_line_segments( + &grid, + tracks, + y, + &[], + y == grid.rows.len(), + |grid, y, x, stroke| hline_stroke_at_column( + grid, + &rows, + y.checked_sub(1), + true, + y, + x, + stroke + ) + ) + .collect::>(), + ); + } + } + + #[test] + fn test_hline_splitting_with_gutter_and_explicit_hlines() { + let stroke = Arc::new(Stroke::default()); + let grid = sample_grid_for_hlines(true); + let columns = &[ + Abs::pt(1.0), + Abs::pt(2.0), + Abs::pt(4.0), + Abs::pt(8.0), + Abs::pt(16.0), + Abs::pt(32.0), + Abs::pt(64.0), + ]; + // Assume all rows would be drawn in the same region, and are available. + let rows = grid + .rows + .iter() + .enumerate() + .map(|(y, _)| RowPiece { height: Abs::pt(f64::from(2u32.pow(y as u32))), y }) + .collect::>(); + let expected_hline_splits = &[ + // top border + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + // gutter line below + // interrupted a few times by rowspans + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1.), + length: Abs::pt(2. + 4. + 8.), + priority: StrokePriority::ExplicitLine, + }], + // interrupted a few times by rowspans + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1.), + length: Abs::pt(2. + 4. + 8.), + priority: StrokePriority::ExplicitLine, + }], + // gutter line below + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + // gutter line below + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + // gutter line below + // interrupted every time by rowspans + vec![], + // interrupted every time by rowspans + vec![], + // gutter line below + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + // gutter line below + // interrupted once by rowspan + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2.), + priority: StrokePriority::ExplicitLine, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4.), + length: Abs::pt(8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }, + ], + // interrupted once by rowspan + vec![ + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2.), + priority: StrokePriority::ExplicitLine, + }, + LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4.), + length: Abs::pt(8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }, + ], + // gutter line below + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + // gutter line below + // there are two consecutive rowspans, but the gutter column + // between them is free. + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4.), + length: Abs::pt(8.), + priority: StrokePriority::ExplicitLine, + }], + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(1. + 2. + 4.), + length: Abs::pt(8.), + priority: StrokePriority::ExplicitLine, + }], + // bottom border + vec![LineSegment { + stroke: stroke.clone(), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.), + priority: StrokePriority::ExplicitLine, + }], + ]; + for (y, expected_splits) in expected_hline_splits.iter().enumerate() { + let tracks = columns.iter().copied().enumerate(); + assert_eq!( + expected_splits, + &generate_line_segments( + &grid, + tracks, + y, + &[ + Line { + index: y, + start: 0, + end: None, + stroke: Some(stroke.clone()), + position: LinePosition::Before + }, + Line { + index: y, + start: 0, + end: None, + stroke: Some(stroke.clone()), + position: LinePosition::After + }, + ], + y == grid.rows.len(), + |grid, y, x, stroke| hline_stroke_at_column( + grid, + &rows, + y.checked_sub(1), + true, + y, + x, + stroke + ) + ) + .collect::>(), + ); + } + } + + #[test] + fn test_hline_splitting_considers_absent_rows() { + let grid = sample_grid_for_hlines(false); + let columns = &[Abs::pt(1.), Abs::pt(2.), Abs::pt(4.), Abs::pt(8.)]; + // Assume row 3 is absent (even though there's a rowspan between rows + // 3 and 4) + // This can happen if it is an auto row which turns out to be fully + // empty. + let rows = grid + .rows + .iter() + .enumerate() + .filter(|(y, _)| *y != 3) + .map(|(y, _)| RowPiece { height: Abs::pt(f64::from(2u32.pow(y as u32))), y }) + .collect::>(); + + // Hline above row 4 is no longer blocked, since the rowspan is now + // effectively spanning just one row (at least, visibly). + assert_eq!( + &vec![LineSegment { + stroke: Arc::new(Stroke::default()), + offset: Abs::pt(0.), + length: Abs::pt(1. + 2. + 4. + 8.), + priority: StrokePriority::GridStroke + }], + &generate_line_segments( + &grid, + columns.iter().copied().enumerate(), + 4, + &[], + 4 == grid.rows.len(), + |grid, y, x, stroke| hline_stroke_at_column( + grid, + &rows, + if y == 4 { Some(2) } else { y.checked_sub(1) }, + true, + y, + x, + stroke + ) + ) + .collect::>() + ); + } } diff --git a/crates/typst/src/layout/grid/mod.rs b/crates/typst/src/layout/grid/mod.rs index fc884e80..6675f3e0 100644 --- a/crates/typst/src/layout/grid/mod.rs +++ b/crates/typst/src/layout/grid/mod.rs @@ -1,5 +1,6 @@ mod layout; mod lines; +mod rowspans; pub use self::layout::{Cell, CellGrid, Celled, GridItem, GridLayouter, ResolvableCell}; pub use self::lines::LinePosition; @@ -644,6 +645,10 @@ pub struct GridCell { #[default(NonZeroUsize::ONE)] pub colspan: NonZeroUsize, + /// The amount of rows spanned by this cell. + #[default(NonZeroUsize::ONE)] + pub rowspan: NonZeroUsize, + /// The cell's fill override. pub fill: Smart>, @@ -657,6 +662,12 @@ pub struct GridCell { #[resolve] #[fold] pub stroke: Sides>>>, + + /// Whether rows spanned by this cell can be placed in different pages. + /// When equal to `{auto}`, a cell spanning only fixed-size rows is + /// unbreakable, while a cell spanning at least one `{auto}`-sized row is + /// breakable. + pub breakable: Smart, } cast! { @@ -679,10 +690,13 @@ impl ResolvableCell for Packed { align: Smart, inset: Sides>>, stroke: Sides>>>>, + breakable: bool, styles: StyleChain, ) -> Cell { let cell = &mut *self; let colspan = cell.colspan(styles); + let rowspan = cell.rowspan(styles); + let breakable = cell.breakable(styles).unwrap_or(breakable); let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); let cell_stroke = cell.stroke(styles); @@ -727,12 +741,15 @@ impl ResolvableCell for Packed { })) }), ); + cell.push_breakable(Smart::Custom(breakable)); Cell { body: self.pack(), fill, colspan, + rowspan, stroke, stroke_overridden, + breakable, } } @@ -748,6 +765,10 @@ impl ResolvableCell for Packed { (**self).colspan(styles) } + fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).rowspan(styles) + } + fn span(&self) -> Span { Packed::span(self) } diff --git a/crates/typst/src/layout/grid/rowspans.rs b/crates/typst/src/layout/grid/rowspans.rs new file mode 100644 index 00000000..be63da5c --- /dev/null +++ b/crates/typst/src/layout/grid/rowspans.rs @@ -0,0 +1,864 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::Resolve; +use crate::layout::{ + Abs, Axes, Cell, Frame, GridLayouter, LayoutMultiple, Point, Regions, Size, Sizing, +}; +use crate::util::MaybeReverseIter; + +use super::layout::{points, Row}; + +/// All information needed to layout a single rowspan. +pub(super) struct Rowspan { + // First column of this rowspan. + pub(super) x: usize, + // First row of this rowspan. + pub(super) y: usize, + // Amount of rows spanned by the cell at (x, y). + pub(super) rowspan: usize, + /// The horizontal offset of this rowspan in all regions. + pub(super) dx: Abs, + /// The vertical offset of this rowspan in the first region. + pub(super) dy: Abs, + /// The index of the first region this rowspan appears in. + pub(super) first_region: usize, + /// The full height in the first region this rowspan appears in, for + /// relative sizing. + pub(super) region_full: Abs, + /// The vertical space available for this rowspan in each region. + pub(super) heights: Vec, +} + +/// The output of the simulation of an unbreakable row group. +#[derive(Default)] +pub(super) struct UnbreakableRowGroup { + /// The rows in this group of unbreakable rows. + /// Includes their indices and their predicted heights. + pub(super) rows: Vec<(usize, Abs)>, + /// The total height of this row group. + pub(super) height: Abs, +} + +/// Data used to measure a cell in an auto row. +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. + 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. + 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 + /// (needs to join its backlog of already laid out heights with the current + /// backlog), then this vector will store the new backlog. + pub(super) custom_backlog: Vec, + /// The full height of the first region of the cell. + pub(super) full: Abs, + /// The total height of previous rows spanned by the cell in the current + /// region (so far). + pub(super) height_in_this_region: Abs, + /// The amount of previous regions spanned by the cell. + /// They are skipped for measurement purposes. + pub(super) frames_in_previous_regions: usize, +} + +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`). + /// + /// We need to do this only once we already know the heights of all + /// spanned rows, which is only possible after laying out the last row + /// spanned by the rowspan (or some row immediately after the last one). + pub(super) fn layout_rowspan( + &mut self, + rowspan_data: Rowspan, + current_region: Option<&mut Frame>, + engine: &mut Engine, + ) -> SourceResult<()> { + let Rowspan { + x, y, dx, dy, first_region, region_full, heights, .. + } = rowspan_data; + let [first_height, backlog @ ..] = heights.as_slice() else { + // Nothing to layout. + return Ok(()); + }; + let first_column = self.rcols[x]; + let cell = self.grid.cell(x, y).unwrap(); + let width = self.cell_spanned_width(cell, x); + let dx = if self.is_rtl { dx - width + first_column } else { dx }; + + // Prepare regions. + let size = Size::new(width, *first_height); + let mut pod = Regions::one(size, Axes::splat(true)); + pod.full = region_full; + 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 + .finished + .iter_mut() + .chain(current_region.into_iter()) + .skip(first_region) + .zip(fragment) + { + finished.push_frame(pos, frame); + + // From the second region onwards, the rowspan's continuation + // starts at the very top. + pos.y = Abs::zero(); + } + + Ok(()) + } + + /// Checks if a row contains the beginning of one or more rowspan cells. + /// If so, adds them to the rowspans vector. + pub(super) fn check_for_rowspans(&mut self, y: usize) { + // We will compute the horizontal offset of each rowspan in advance. + // For that reason, we must reverse the column order when using RTL. + let offsets = points(self.rcols.iter().copied().rev_if(self.is_rtl)); + for (x, dx) in (0..self.rcols.len()).rev_if(self.is_rtl).zip(offsets) { + let Some(cell) = self.grid.cell(x, y) else { + continue; + }; + let rowspan = self.grid.effective_rowspan_of_cell(cell); + if rowspan > 1 { + // Rowspan detected. We will lay it out later. + self.rowspans.push(Rowspan { + x, + y, + rowspan, + dx, + // The four fields below will be updated in 'finish_region'. + dy: Abs::zero(), + first_region: usize::MAX, + region_full: Abs::zero(), + heights: vec![], + }); + } + } + } + + /// Checks if the upcoming rows will be grouped together under an + /// unbreakable row group, and, if so, advances regions until there is + /// enough space for them. This can be needed, for example, if there's an + /// unbreakable rowspan crossing those rows. + pub(super) fn check_for_unbreakable_rows( + &mut self, + current_row: usize, + engine: &mut Engine, + ) -> SourceResult<()> { + if self.unbreakable_rows_left == 0 { + let row_group = + self.simulate_unbreakable_row_group(current_row, &self.regions, engine)?; + + // Skip to fitting region. + while !self.regions.size.y.fits(row_group.height) && !self.regions.in_last() { + self.finish_region(engine)?; + } + self.unbreakable_rows_left = row_group.rows.len(); + } + + Ok(()) + } + + /// 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. + /// + /// 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, + regions: &Regions<'_>, + engine: &mut Engine, + ) -> SourceResult { + let mut row_group = UnbreakableRowGroup::default(); + let mut unbreakable_rows_left = 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 unbreakable_rows_left == 0 { + // This check is in case the first row does not have any + // unbreakable cells. Therefore, no unbreakable row group + // is formed. + break; + } + let height = match row { + Sizing::Rel(v) => v.resolve(self.styles).relative_to(regions.base().y), + + // No need to pass the regions to the auto row, since + // unbreakable auto rows are always measured with infinite + // height, ignore backlog, and do not invoke the rowspan + // simulation procedure at all. + Sizing::Auto => self + .measure_auto_row( + engine, + y, + false, + unbreakable_rows_left, + Some(&row_group), + )? + .unwrap() + .first() + .copied() + .unwrap_or_else(Abs::zero), + // Fractional rows don't matter when calculating the space + // needed for unbreakable rows + Sizing::Fr(_) => Abs::zero(), + }; + row_group.height += height; + row_group.rows.push((y, height)); + unbreakable_rows_left -= 1; + if unbreakable_rows_left == 0 { + // This second check is necessary so we can tell distinct + // but consecutive unbreakable row groups apart. If the + // unbreakable row group ended at this row, we stop before + // checking the next one. + break; + } + } + + Ok(row_group) + } + + /// Checks if one or more of the cells at the given row are unbreakable. + /// If so, returns the largest rowspan among the unbreakable cells; + /// the spanned rows must, as a result, be laid out in the same region. + pub(super) fn check_for_unbreakable_cells(&self, y: usize) -> usize { + (0..self.grid.cols.len()) + .filter_map(|x| self.grid.cell(x, y)) + .filter(|cell| !cell.breakable) + .map(|cell| self.grid.effective_rowspan_of_cell(cell)) + .max() + .unwrap_or(0) + } + + /// Used by `measure_auto_row` to gather data needed to measure the cell. + pub(super) fn prepare_auto_row_cell_measurement( + &self, + parent: Axes, + cell: &Cell, + breakable: bool, + row_group_data: Option<&UnbreakableRowGroup>, + ) -> CellMeasurementData<'_> { + let rowspan = self.grid.effective_rowspan_of_cell(cell); + + // This variable is used to construct a custom backlog if the cell + // is a rowspan. 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![]; + + // Each declaration, from top to bottom: + // 1. The height available to the cell in the first region. + // Usually, this will just be the size remaining in the current + // region. + // 2. The backlog of upcoming region heights to specify as + // available to the cell. + // 3. The full height of the first region of the cell. + // 4. 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 + // 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); + 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. + // 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); + full = if breakable { self.regions.full } else { Abs::inf() }; + height_in_this_region = Abs::zero(); + frames_in_previous_regions = 0; + } else { + // Height of the rowspan covered by spanned rows in the current + // region. + let laid_out_height: Abs = self + .lrows + .iter() + .filter_map(|row| match row { + Row::Frame(frame, y, _) + if (parent.y..parent.y + rowspan).contains(y) => + { + Some(frame.height()) + } + // Either we have a row outside of the rowspan, or a + // fractional row, whose size we can't really guess. + _ => None, + }) + .sum(); + + // If we're currently simulating an unbreakable row group, also + // consider the height of previously spanned rows which are in + // the row group but not yet laid out. + let unbreakable_height: Abs = row_group_data + .into_iter() + .flat_map(|row_group| &row_group.rows) + .filter(|(y, _)| (parent.y..parent.y + rowspan).contains(y)) + .map(|(_, height)| height) + .sum(); + + height_in_this_region = laid_out_height + unbreakable_height; + + // Ensure we will measure the rowspan with the correct heights. + // For that, we will gather the total height spanned by this + // rowspan in previous regions. + if let Some((rowspan_full, [rowspan_height, rowspan_other_heights @ ..])) = + self.rowspans + .iter() + .find(|data| data.x == parent.x && data.y == parent.y) + .map(|data| (data.region_full, &*data.heights)) + { + // The rowspan started in a previous region (as it already + // has at least one region height). + // Therefore, its initial height will be the height in its + // first spanned region, and the backlog will be the + // remaining heights, plus the current region's size, plus + // the current backlog. + frames_in_previous_regions = rowspan_other_heights.len() + 1; + + let heights_up_to_current_region = rowspan_other_heights + .iter() + .copied() + .chain(std::iter::once(if breakable { + self.initial.y + } else { + // When measuring unbreakable auto rows, infinite + // height is available for content to expand. + Abs::inf() + })); + + rowspan_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::>() + } else { + // No extra backlog if this is an unbreakable auto row. + // Ensure, when measuring, that the rowspan can be laid + // out through all spanned rows which were already laid + // out so far, but don't go further than this region. + heights_up_to_current_region.collect::>() + }; + + height = *rowspan_height; + backlog = None; + full = rowspan_full; + } else { + // The rowspan started in the current region, as its vector + // of heights in regions is currently empty. + // Therefore, the initial height it has available will be + // the current available size, plus the size spanned in + // previous rows in this region (and/or unbreakable row + // group, if it's being simulated). + // The backlog and full will be that of the current region. + // However, use infinite height instead if we're measuring an + // unbreakable auto row. + height = if breakable { + height_in_this_region + self.regions.size.y + } else { + Abs::inf() + }; + backlog = Some(self.regions.backlog); + full = if breakable { self.regions.full } else { Abs::inf() }; + frames_in_previous_regions = 0; + } + } + + let width = self.cell_spanned_width(cell, parent.x); + CellMeasurementData { + width, + height, + backlog, + custom_backlog: rowspan_backlog, + full, + height_in_this_region, + frames_in_previous_regions, + } + } + + /// Used in `measure_auto_row` to prepare a rowspan's `sizes` vector. + /// Returns `true` if we'll need to run a simulation to more accurately + /// expand the auto row based on the rowspan's demanded size, or `false` + /// otherwise. + #[allow(clippy::too_many_arguments)] + pub(super) fn prepare_rowspan_sizes( + &self, + auto_row_y: usize, + sizes: &mut Vec, + cell: &Cell, + parent_y: usize, + rowspan: usize, + unbreakable_rows_left: usize, + measurement_data: &CellMeasurementData<'_>, + ) -> bool { + if sizes.len() <= 1 + && sizes.first().map_or(true, |&first_frame_size| { + first_frame_size <= measurement_data.height_in_this_region + }) + { + // Ignore a rowspan fully covered by rows in previous + // regions and/or in the current region. + sizes.clear(); + return false; + } + if let Some(first_frame_size) = sizes.first_mut() { + // Subtract already covered height from the size requested + // by this rowspan to the auto row in the first region. + *first_frame_size = (*first_frame_size + - measurement_data.height_in_this_region) + .max(Abs::zero()); + } + + let last_spanned_row = parent_y + rowspan - 1; + + // When the rowspan is unbreakable, or all of its upcoming + // spanned rows are in the same unbreakable row group, its + // spanned gutter will certainly be in the same region as all + // of its other spanned rows, thus gutters won't be removed, + // and we can safely reduce how much the auto row expands by + // without using simulation. + let is_effectively_unbreakable_rowspan = + !cell.breakable || auto_row_y + unbreakable_rows_left > last_spanned_row; + + // If the rowspan doesn't end at this row and the grid has + // gutter, we will need to run a simulation to find out how + // much to expand this row by later. This is because gutters + // spanned by this rowspan might be removed if they appear + // around a pagebreak, so the auto row might have to expand a + // bit more to compensate for the missing gutter height. + // However, unbreakable rowspans aren't affected by that + // problem. + if auto_row_y != last_spanned_row + && !sizes.is_empty() + && self.grid.has_gutter + && !is_effectively_unbreakable_rowspan + { + return true; + } + + // We can only predict the resolved size of upcoming fixed-size + // rows, but not fractional rows. In the future, we might be + // able to simulate and circumvent the problem with fractional + // rows. Relative rows are currently always measured relative + // to the first region as well. + // We can ignore auto rows since this is the last spanned auto + // row. + let will_be_covered_height: Abs = self + .grid + .rows + .iter() + .skip(auto_row_y + 1) + .take(last_spanned_row - auto_row_y) + .map(|row| match row { + Sizing::Rel(v) => { + v.resolve(self.styles).relative_to(self.regions.base().y) + } + _ => Abs::zero(), + }) + .sum(); + + // Remove or reduce the sizes of the rowspan at the current or future + // regions where it will already be covered by further rows spanned by + // it. + subtract_end_sizes(sizes, will_be_covered_height); + + // No need to run a simulation for this rowspan. + false + } + + /// Performs a simulation to predict by how much height the last spanned + /// auto row will have to expand, given the current sizes of the auto row + /// in each region and the pending rowspans' data (parent Y, rowspan amount + /// and vector of requested sizes). + pub(super) fn simulate_and_measure_rowspans_in_auto_row( + &self, + y: usize, + resolved: &mut Vec, + pending_rowspans: &[(usize, usize, Vec)], + unbreakable_rows_left: usize, + row_group_data: Option<&UnbreakableRowGroup>, + engine: &mut Engine, + ) -> SourceResult<()> { + // To begin our simulation, we have to unify the sizes demanded by + // each rowspan into one simple vector of sizes, as if they were + // all a single rowspan. These sizes will be appended to + // 'resolved' once we finish our simulation. + let mut simulated_sizes: Vec = vec![]; + let last_resolved_size = resolved.last().copied(); + let mut max_spanned_row = y; + for (parent_y, rowspan, sizes) in pending_rowspans { + let mut sizes = sizes.iter(); + for (target, size) in resolved.iter_mut().zip(&mut sizes) { + // First, we update the already resolved sizes as required + // by this rowspan. No need to simulate this since the auto row + // will already expand throughout already resolved regions. + // Our simulation, therefore, won't otherwise change already + // resolved sizes, other than, perhaps, the last one (at the + // last currently resolved region, at which we can expand). + target.set_max(*size); + } + for (simulated_target, rowspan_size) in + simulated_sizes.iter_mut().zip(&mut sizes) + { + // The remaining sizes are exclusive to rowspans, since + // other cells in this row didn't require as many regions. + // We will perform a simulation to see how much of these sizes + // does the auto row actually need to expand by, and how much + // is already covered by upcoming rows spanned by the rowspans. + simulated_target.set_max(*rowspan_size); + } + simulated_sizes.extend(sizes); + max_spanned_row = max_spanned_row.max(parent_y + rowspan - 1); + } + if simulated_sizes.is_empty() && resolved.last() == last_resolved_size.as_ref() { + // The rowspans already fit in the already resolved sizes. + // No need for simulation. + return Ok(()); + } + + // We will be updating the last resolved size (expanding the auto + // row) as needed. Therefore, consider it as part of the simulation. + // At the end, we push it back. + if let Some(modified_last_resolved_size) = resolved.pop() { + simulated_sizes.insert(0, modified_last_resolved_size); + } + + // Prepare regions for simulation. + // If we're currently inside an unbreakable row group simulation, + // subtract the current row group height from the available space + // when simulating rowspans in said group. + let mut simulated_regions = self.regions; + simulated_regions.size.y -= + row_group_data.map_or(Abs::zero(), |row_group| row_group.height); + + for _ in 0..resolved.len() { + // Ensure we start at the region where we will expand the auto + // row. + // Note that we won't accidentally call '.next()' once more than + // desired (we won't skip the last resolved frame, where we will + // expand) because we popped the last resolved size from the + // resolved vector, above. + simulated_regions.next(); + } + if let Some(original_last_resolved_size) = last_resolved_size { + // We're now at the (current) last region of this auto row. + // Consider resolved height as already taken space. + simulated_regions.size.y -= original_last_resolved_size; + } + + // Now we run the simulation to check how much the auto row needs to + // grow to ensure that rowspans have the height they need. + let simulations_stabilized = self.run_rowspan_simulation( + y, + max_spanned_row, + simulated_regions, + &mut simulated_sizes, + engine, + last_resolved_size, + unbreakable_rows_left, + )?; + + if !simulations_stabilized { + // If the simulation didn't stabilize above, we will just pretend + // all gutters were removed, as a best effort. That means the auto + // row will expand more than it normally should, but there isn't + // much we can do. + let will_be_covered_height = self + .grid + .rows + .iter() + .enumerate() + .skip(y + 1) + .take(max_spanned_row - y) + .filter(|(y, _)| !self.grid.is_gutter_track(*y)) + .map(|(_, row)| match row { + Sizing::Rel(v) => { + v.resolve(self.styles).relative_to(self.regions.base().y) + } + _ => Abs::zero(), + }) + .sum(); + + subtract_end_sizes(&mut simulated_sizes, will_be_covered_height); + } + + resolved.extend(simulated_sizes); + + Ok(()) + } + + /// Performs a simulation of laying out multiple rowspans (consolidated + /// into a single vector of simulated sizes) ending in a certain auto row + /// in order to find out how much the auto row will need to expand to cover + /// the rowspans' requested sizes, considering how much size has been + /// covered by other rows and by gutter between rows. + /// + /// For example, for a rowspan cell containing a block of 8pt of height + /// spanning rows (1pt, auto, 0.5pt, 0.5pt), with a gutter of 1pt between + /// each row, we have that the rows it spans provide 1pt + 0.5pt + 0.5pt + /// = 2pt of height, plus 1pt + 1pt + 1pt = 3pt of gutter, with a total of + /// 2pt + 3pt = 5pt of height already covered by fixed-size rows and + /// gutters. This means that the auto row must (under normal conditions) + /// expand by 3pt (8pt - 5pt) so that the rowspan has enough height across + /// rows to fully draw its contents. + /// + /// However, it's possible that the last row is sent to the next page to + /// respect a pagebreak, and then the 1pt gutter before it disappears. This + /// would lead to our rowspan having a height of 7pt available if we fail + /// to predict this situation when measuring the auto row. + /// + /// The algorithm below will, thus, attempt to simulate the layout of each + /// spanned row, considering the space available in the current page and in + /// upcoming pages (through the region backlog), in order to predict which + /// rows will be sent to a new page and thus have their preceding gutter + /// spacing removed (meaning the auto row has to grow a bit more). After + /// simulating, we subtract the total height spanned by upcoming rows and + /// gutter from the total rowspan height - this will be how much our auto + /// row has to expand. We then simulate again to check if, if the auto row + /// expanded by that amount, that would prompt the auto row to need to + /// expand even more, because expanding the auto row might cause some other + /// larger gutter spacing to disappear (leading to the rowspan having less + /// space available instead of more); if so, we update the amount to expand + /// and run the simulation again. Otherwise (if it should expand by the + /// same amount, meaning we predicted correctly, or by less, meaning the + /// auto row will be a bit larger than it should be, but that's a + /// compromise we're willing to accept), we conclude the simulation + /// (consider it stabilized) and return the result. + /// + /// Tries up to 5 times. If two consecutive simulations stabilize, then + /// we subtract the predicted expansion height ('amount_to_grow') from the + /// total height requested by rowspans (the 'requested_rowspan_height') to + /// obtain how much height is covered by upcoming rows, according to our + /// simulation, and the result of that operation is used to reduce or + /// remove heights from the end of the vector of simulated sizes, such that + /// the remaining heights are exactly how much the auto row should expand + /// by. Then, we return `true`. + /// + /// If the simulations don't stabilize (they return 5 different and + /// successively larger values), aborts and returns `false`. + #[allow(clippy::too_many_arguments)] + fn run_rowspan_simulation( + &self, + y: usize, + max_spanned_row: usize, + mut simulated_regions: Regions<'_>, + simulated_sizes: &mut Vec, + engine: &mut Engine, + last_resolved_size: Option, + unbreakable_rows_left: usize, + ) -> SourceResult { + // The max amount this row can expand will be the total size requested + // by rowspans which was not yet resolved. It is worth noting that, + // earlier, we pushed the last resolved size to 'simulated_sizes' as + // row expansion starts with it, so it's possible a rowspan requested + // to extend that size (we will see, through the simulation, if that's + // needed); however, we must subtract that resolved size from the total + // sum of sizes, as it was already resolved and thus the auto row will + // already grow by at least that much in the last resolved region (we + // would grow by the same size twice otherwise). + let requested_rowspan_height = + simulated_sizes.iter().sum::() - last_resolved_size.unwrap_or_default(); + + // The amount the row will effectively grow by, according to the latest + // simulation. + let mut amount_to_grow = Abs::zero(); + + // Try to simulate up to 5 times. If it doesn't stabilize at a value + // which, when used and combined with upcoming spanned rows, covers all + // of the requested rowspan height, we give up. + for _attempt in 0..5 { + let mut regions = simulated_regions; + let mut total_spanned_height = Abs::zero(); + let mut unbreakable_rows_left = unbreakable_rows_left; + + // 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); + } + + // If the total height spanned by upcoming spanned rows plus the + // current amount we predict the auto row will have to grow (from + // the previous iteration) are larger than the size requested by + // rowspans, this means the auto row will grow enough in order to + // cover the requested rowspan height, so we stop the simulation. + // + // If that's not yet the case, we will simulate again and make the + // auto row grow even more, and do so until either the auto row has + // grown enough, or we tried to do so over 5 times. + // + // A flaw of this approach is that we consider rowspans' content to + // be contiguous. That is, we treat rowspans' requested heights as + // a simple number, instead of properly using the vector of + // requested heights in each region. This can lead to some + // weirdness when using multi-page rowspans with content that + // reacts to the amount of space available, including paragraphs. + // However, this is probably the best we can do for now. + if (total_spanned_height + amount_to_grow).fits(requested_rowspan_height) { + // Reduce sizes by the amount to be covered by upcoming spanned + // rows, which is equivalent to the amount that we don't grow. + // We reduce from the end as that's where the spanned rows will + // cover. The remaining sizes will all be covered by the auto + // row instead (which will grow by those sizes). + subtract_end_sizes( + simulated_sizes, + requested_rowspan_height - amount_to_grow, + ); + + if let Some(last_resolved_size) = last_resolved_size { + // Ensure the first simulated size is at least as large as + // the last resolved size (its initial value). As it was + // already resolved before, we must not reduce below the + // resolved size to avoid problems with non-rowspan cells. + if let Some(first_simulated_size) = simulated_sizes.first_mut() { + first_simulated_size.set_max(last_resolved_size); + } else { + simulated_sizes.push(last_resolved_size); + } + } + + return Ok(true); + } + + // For the next simulation, we will test if the auto row can grow + // by precisely how much rowspan height is not covered by upcoming + // spanned rows, according to the current simulation. + // We know that the new amount to grow is larger (and thus the + // auto row only expands between each simulation), because we + // checked above if + // 'total_spanned_height + (now old_)amount_to_grow >= requested_rowspan_height', + // which was false, so it holds that + // 'total_spanned_height + old_amount_to_grow < requested_rowspan_height' + // Thus, + // 'old_amount_to_grow < requested_rowspan_height - total_spanned_height' + // Therefore, by definition, 'old_amount_to_grow < amount_to_grow'. + let old_amount_to_grow = std::mem::replace( + &mut amount_to_grow, + requested_rowspan_height - total_spanned_height, + ); + + // We advance the 'regions' variable accordingly, so that, in the + // next simulation, we consider already grown space as final. + // That is, we effectively simulate how rows would be placed if the + // auto row grew by precisely the new value of 'amount_to_grow'. + let mut extra_amount_to_grow = amount_to_grow - old_amount_to_grow; + while extra_amount_to_grow > Abs::zero() + && simulated_regions.size.y < extra_amount_to_grow + { + extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero()); + simulated_regions.next(); + } + simulated_regions.size.y -= extra_amount_to_grow; + } + + // Simulation didn't succeed in 5 attempts. + Ok(false) + } +} + +/// 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) { + while subtract > Abs::zero() && sizes.last().is_some_and(|&size| size <= subtract) { + subtract -= sizes.pop().unwrap(); + } + if subtract > Abs::zero() { + if let Some(last_size) = sizes.last_mut() { + *last_size -= subtract; + } + } +} diff --git a/crates/typst/src/model/table.rs b/crates/typst/src/model/table.rs index a51fd1e7..79521f2d 100644 --- a/crates/typst/src/model/table.rs +++ b/crates/typst/src/model/table.rs @@ -535,6 +535,10 @@ pub struct TableCell { #[default(NonZeroUsize::ONE)] pub colspan: NonZeroUsize, + /// The amount of rows spanned by this cell. + #[default(NonZeroUsize::ONE)] + rowspan: NonZeroUsize, + /// The cell's alignment override. pub align: Smart, @@ -545,6 +549,12 @@ pub struct TableCell { #[resolve] #[fold] pub stroke: Sides>>>, + + /// Whether rows spanned by this cell can be placed in different pages. + /// When equal to `{auto}`, a cell spanning only fixed-size rows is + /// unbreakable, while a cell spanning at least one `{auto}`-sized row is + /// breakable. + pub breakable: Smart, } cast! { @@ -567,10 +577,13 @@ impl ResolvableCell for Packed { align: Smart, inset: Sides>>, stroke: Sides>>>>, + breakable: bool, styles: StyleChain, ) -> Cell { let cell = &mut *self; let colspan = cell.colspan(styles); + let rowspan = cell.rowspan(styles); + let breakable = cell.breakable(styles).unwrap_or(breakable); let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); let cell_stroke = cell.stroke(styles); @@ -615,12 +628,15 @@ impl ResolvableCell for Packed { })) }), ); + cell.push_breakable(Smart::Custom(breakable)); Cell { body: self.pack(), fill, colspan, + rowspan, stroke, stroke_overridden, + breakable, } } @@ -632,10 +648,14 @@ impl ResolvableCell for Packed { (**self).y(styles) } - fn colspan(&self, styles: StyleChain) -> std::num::NonZeroUsize { + fn colspan(&self, styles: StyleChain) -> NonZeroUsize { (**self).colspan(styles) } + fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).rowspan(styles) + } + fn span(&self) -> Span { Packed::span(self) } diff --git a/tests/ref/bugs/grid-4.png b/tests/ref/bugs/grid-4.png new file mode 100644 index 00000000..475f561e Binary files /dev/null and b/tests/ref/bugs/grid-4.png differ diff --git a/tests/ref/layout/grid-rowspan-basic.png b/tests/ref/layout/grid-rowspan-basic.png new file mode 100644 index 00000000..966c8fd9 Binary files /dev/null and b/tests/ref/layout/grid-rowspan-basic.png differ diff --git a/tests/ref/layout/grid-rowspan-split-1.png b/tests/ref/layout/grid-rowspan-split-1.png new file mode 100644 index 00000000..12cd5fc6 Binary files /dev/null and b/tests/ref/layout/grid-rowspan-split-1.png differ diff --git a/tests/ref/layout/grid-rowspan-split-2.png b/tests/ref/layout/grid-rowspan-split-2.png new file mode 100644 index 00000000..e55c5e23 Binary files /dev/null and b/tests/ref/layout/grid-rowspan-split-2.png differ diff --git a/tests/ref/layout/grid-rowspan-split-3.png b/tests/ref/layout/grid-rowspan-split-3.png new file mode 100644 index 00000000..3d809123 Binary files /dev/null and b/tests/ref/layout/grid-rowspan-split-3.png differ diff --git a/tests/ref/layout/grid-rtl.png b/tests/ref/layout/grid-rtl.png index f81e992e..3cf0b9aa 100644 Binary files a/tests/ref/layout/grid-rtl.png and b/tests/ref/layout/grid-rtl.png differ diff --git a/tests/ref/layout/grid-stroke.png b/tests/ref/layout/grid-stroke.png index 0f0b562a..409d10f1 100644 Binary files a/tests/ref/layout/grid-stroke.png and b/tests/ref/layout/grid-stroke.png differ diff --git a/tests/typ/bugs/grid-4.typ b/tests/typ/bugs/grid-4.typ new file mode 100644 index 00000000..691bf877 --- /dev/null +++ b/tests/typ/bugs/grid-4.typ @@ -0,0 +1,17 @@ +// Ensure gutter rows at the top or bottom of a region are skipped. + +--- +#set page(height: 10em) + +#table( + row-gutter: 1.5em, + inset: 0pt, + rows: (1fr, auto), + [a], + [], + [], + [f], + [e\ e], + [], + [a] +) diff --git a/tests/typ/layout/grid-colspan.typ b/tests/typ/layout/grid-colspan.typ index 3fd1a0fd..1bdadcf1 100644 --- a/tests/typ/layout/grid-colspan.typ +++ b/tests/typ/layout/grid-colspan.typ @@ -81,7 +81,7 @@ --- // Error: 4:8-4:32 cell would span a previously placed cell at column 2, row 0 -// Hint: 4:8-4:32 try specifying your cells in a different order or reducing the cell's colspan +// Hint: 4:8-4:32 try specifying your cells in a different order or reducing the cell's rowspan or colspan #grid( columns: 3, grid.cell(x: 2, y: 0)[x], diff --git a/tests/typ/layout/grid-positioning.typ b/tests/typ/layout/grid-positioning.typ index ca71cb37..5461fb1c 100644 --- a/tests/typ/layout/grid-positioning.typ +++ b/tests/typ/layout/grid-positioning.typ @@ -221,3 +221,11 @@ fill: (x, y) => if calc.odd(x + y) { red.lighten(50%) } else { green }, table.cell(x: 2, y: 6148914691236517206)[a], ) + +--- +// Error: 3:3-3:45 cell would span an exceedingly large position +// Hint: 3:3-3:45 try reducing the cell's rowspan or colspan +#grid( + columns: 500, + grid.cell(rowspan: 6148914691236517206)[a] +) diff --git a/tests/typ/layout/grid-rowspan-basic.typ b/tests/typ/layout/grid-rowspan-basic.typ new file mode 100644 index 00000000..49164fa6 --- /dev/null +++ b/tests/typ/layout/grid-rowspan-basic.typ @@ -0,0 +1,211 @@ +#grid( + columns: 4, + fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) }, + inset: 5pt, + align: center, + grid.cell(rowspan: 2, fill: orange)[*Left*], + [Right A], [Right A], [Right A], + [Right B], grid.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide], + [Left A], [Left A], + [Left B], [Left B], grid.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long] +) + +#table( + columns: 4, + fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) }, + inset: 5pt, + align: center, + table.cell(rowspan: 2, fill: orange)[*Left*], + [Right A], [Right A], [Right A], + [Right B], table.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide], + [Left A], [Left A], + [Left B], [Left B], table.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long] +) + +--- +#grid( + columns: 4, + fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) }, + inset: 5pt, + align: center, + gutter: 3pt, + grid.cell(rowspan: 2, fill: orange)[*Left*], + [Right A], [Right A], [Right A], + [Right B], grid.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide], + [Left A], [Left A], + [Left B], [Left B], grid.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long] +) + +#table( + columns: 4, + fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) }, + inset: 5pt, + align: center, + gutter: 3pt, + table.cell(rowspan: 2, fill: orange)[*Left*], + [Right A], [Right A], [Right A], + [Right B], table.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide], + [Left A], [Left A], + [Left B], [Left B], table.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long] +) + +--- +// Fixed-size rows +#set page(height: 10em) +#grid( + columns: 2, + rows: 1.5em, + fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) }, + grid.cell(rowspan: 3)[R1], [b], + [c], + [d], + [e], [f], + grid.cell(rowspan: 5)[R2], [h], + [i], + [j], + [k], + [l], + [m], [n] +) + +--- +// Cell coordinate tests +#set page(height: 10em) +#show table.cell: it => [(#it.x, #it.y)] +#table( + columns: 3, + fill: red, + [a], [b], table.cell(rowspan: 2)[c], + table.cell(colspan: 2)[d], + table.cell(colspan: 3, rowspan: 10)[a], + table.cell(colspan: 2)[b], +) +#table( + columns: 3, + gutter: 3pt, + fill: red, + [a], [b], table.cell(rowspan: 2)[c], + table.cell(colspan: 2)[d], + table.cell(colspan: 3, rowspan: 9)[a], + table.cell(colspan: 2)[b], +) + +--- +// Auto row expansion +#set page(height: 10em) +#grid( + columns: (1em, 1em), + rows: (0.5em, 0.5em, auto), + fill: orange, + gutter: 3pt, + grid.cell(rowspan: 4, [x x x x] + place(bottom)[*Bot*]), + [a], + [b], + [c], + [d] +) + +--- +// Excessive rowspan (no gutter) +#set page(height: 10em) +#table( + columns: 4, + fill: red, + [a], [b], table.cell(rowspan: 2)[c], [d], + table.cell(colspan: 2, stroke: (bottom: aqua + 2pt))[e], table.cell(stroke: (bottom: aqua))[f], + table.cell(colspan: 2, rowspan: 10)[R1], table.cell(colspan: 2, rowspan: 10)[R2], + [b], +) + +--- +// Excessive rowspan (with gutter) +#set page(height: 10em) +#table( + columns: 4, + gutter: 3pt, + fill: red, + [a], [b], table.cell(rowspan: 2)[c], [d], + table.cell(colspan: 2, stroke: (bottom: aqua + 2pt))[e], table.cell(stroke: (bottom: aqua))[f], + table.cell(colspan: 2, rowspan: 10)[R1], table.cell(colspan: 2, rowspan: 10)[R2], + [b], +) + +--- +// Fractional rows +// They cause the auto row to expand more than needed. +#set page(height: 10em) +#grid( + fill: red, + gutter: 3pt, + columns: 3, + rows: (1em, auto, 1fr), + [a], [b], grid.cell(rowspan: 3, block(height: 4em, width: 1em, fill: orange)), + [c], [d], + [e], [f] +) + +--- +// Fractional rows +#set page(height: 10em) +#grid( + fill: red, + gutter: 3pt, + columns: 3, + rows: (1fr, auto, 1em), + [a], [b], grid.cell(rowspan: 3, block(height: 4em, width: 1em, fill: orange)), + [c], [d], + [e], [f] +) + +--- +// Cell order +#let count = counter("count") +#show grid.cell: it => { + count.step() + count.display() +} + +#grid( + columns: (2em,) * 3, + stroke: aqua, + rows: 1.2em, + fill: (x, y) => if calc.odd(x + y) { red } else { orange }, + [a], grid.cell(rowspan: 2)[b], grid.cell(rowspan: 2)[c], + [d], + grid.cell(rowspan: 2)[f], [g], [h], + [i], [j], + [k], [l], [m], + grid.cell(rowspan: 2)[n], [o], [p], + [q], [r], + [s], [t], [u] +) + +--- +#table( + columns: 3, + rows: (auto, auto, auto, 2em), + gutter: 3pt, + table.cell(rowspan: 4)[a \ b\ c\ d\ e], [c], [d], + [e], table.cell(breakable: false, rowspan: 2)[f], + [g] +) + +--- +// Test cell breakability +#show grid.cell: it => { + assert.eq(it.breakable, (it.x, it.y) != (0, 6) and (it.y in (2, 5, 6) or (it.x, it.y) in ((0, 1), (2, 3), (1, 7)))) + it.breakable +} +#grid( + columns: 3, + rows: (6pt, 1fr, auto, 1%, 1em, auto, auto, 0.2in), + row-gutter: (0pt, 0pt, 0pt, auto), + [a], [b], [c], + grid.cell(rowspan: 3)[d], [e], [f], + [g], [h], + [i], grid.cell(rowspan: 2)[j], + [k], + grid.cell(y: 5)[l], + 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] +) diff --git a/tests/typ/layout/grid-rowspan-split-1.typ b/tests/typ/layout/grid-rowspan-split-1.typ new file mode 100644 index 00000000..e247fa80 --- /dev/null +++ b/tests/typ/layout/grid-rowspan-split-1.typ @@ -0,0 +1,89 @@ +// Rowspan split tests + +--- +#set page(height: 10em) +#table( + columns: 2, + rows: (auto, auto, 3em), + fill: red, + [a], table.cell(rowspan: 3, block(width: 50%, height: 10em, fill: orange) + place(bottom)[*ZD*]), + [e], + [f] +) + +--- +#set page(height: 10em) +#table( + columns: 2, + rows: (auto, auto, 3em), + row-gutter: 1em, + fill: red, + [a], table.cell(rowspan: 3, block(width: 50%, height: 10em, fill: orange) + place(bottom)[*ZD*]), + [e], + [f] +) + +--- +#set page(height: 5em) +#table( + columns: 2, + fill: red, + inset: 0pt, + table.cell(fill: orange, rowspan: 10, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]), + ..([y],) * 10, + [a], [b], +) + +--- +#set page(height: 5em) +#table( + columns: 2, + fill: red, + inset: 0pt, + gutter: 2pt, + table.cell(fill: orange, rowspan: 10, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]), + ..([y],) * 10, + [a], [b], +) + +--- +#set page(height: 5em) +#table( + columns: 2, + fill: red, + inset: 0pt, + table.cell(fill: orange, rowspan: 10, breakable: false, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]), + ..([y],) * 10, + [a], [b], +) + +--- +#set page(height: 5em) +#table( + columns: 2, + fill: red, + inset: 0pt, + gutter: 2pt, + table.cell(fill: orange, rowspan: 10, breakable: false, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]), + ..([y],) * 10, + [a], [b], +) + +--- +#set page(height: 5em) +#grid( + columns: 2, + stroke: red, + inset: 5pt, + grid.cell(rowspan: 5)[a\ b\ c\ d\ e] +) + +--- +#set page(height: 5em) +#table( + columns: 2, + gutter: 3pt, + stroke: red, + inset: 5pt, + table.cell(rowspan: 5)[a\ b\ c\ d\ e] +) diff --git a/tests/typ/layout/grid-rowspan-split-2.typ b/tests/typ/layout/grid-rowspan-split-2.typ new file mode 100644 index 00000000..189feed3 --- /dev/null +++ b/tests/typ/layout/grid-rowspan-split-2.typ @@ -0,0 +1,37 @@ +// Rowspan split without ending at the auto row + +--- +#set page(height: 6em) +#table( + rows: (4em,) * 7 + (auto,) + (4em,) * 7, + columns: 2, + column-gutter: 1em, + row-gutter: (1em, 2em) * 4, + fill: (x, y) => if calc.odd(x + y) { orange.lighten(20%) } else { red }, + table.cell(rowspan: 15, [a \ ] * 15), + [] * 15 +) + +--- +#set page(height: 6em) +#table( + rows: (4em,) * 7 + (auto,) + (4em,) * 7, + columns: 2, + column-gutter: 1em, + row-gutter: (1em, 2em) * 4, + fill: (x, y) => if calc.odd(x + y) { green } else { green.darken(40%) }, + table.cell(rowspan: 15, block(fill: blue, width: 2em, height: 4em * 14 + 3em)), + [] * 15 +) + +--- +#set page(height: 6em) +#table( + rows: (3em,) * 15, + columns: 2, + column-gutter: 1em, + row-gutter: (1em, 2em) * 4, + fill: (x, y) => if calc.odd(x + y) { aqua } else { blue }, + table.cell(breakable: true, rowspan: 15, [a \ ] * 15), + [] * 15 +) diff --git a/tests/typ/layout/grid-rowspan-split-3.typ b/tests/typ/layout/grid-rowspan-split-3.typ new file mode 100644 index 00000000..4c3ce7d8 --- /dev/null +++ b/tests/typ/layout/grid-rowspan-split-3.typ @@ -0,0 +1,108 @@ +// Some splitting corner cases + +--- +// Inside the larger rowspan's range, there's an unbreakable rowspan and a +// breakable rowspan. This should work normally. +// The auto row will also expand ignoring the last fractional row. +#set page(height: 10em) +#table( + gutter: 0.5em, + columns: 2, + rows: (2em,) * 10 + (auto, auto, 2em, 1fr), + fill: (_, y) => if calc.even(y) { aqua } else { blue }, + table.cell(rowspan: 14, block(width: 2em, height: 2em * 10 + 2em + 5em, fill: red)[]), + ..([a],) * 5, + table.cell(rowspan: 3)[a\ b], + table.cell(rowspan: 5, [a\ b\ c\ d\ e\ f\ g\ h]), + [z] +) + +--- +// Inset moving to next region bug +#set page(width: 10cm, height: 2.5cm, margin: 0.5cm) +#set text(size: 11pt) +#table( + columns: (1fr, 1fr, 1fr), + [A], + [B], + [C], + [D], + table.cell(rowspan: 2, lorem(4)), + [E], + [F], + [G], +) + +--- +// Second lorem must be sent to the next page, too big +#set page(width: 10cm, height: 9cm, margin: 1cm) +#set text(size: 11pt) +#table( + columns: (1fr, 1fr, 1fr), + align: center, + rows: (4cm, auto), + [A], [B], [C], + table.cell(rowspan: 4, breakable: false, lorem(10)), + [D], + table.cell(rowspan: 2, breakable: false, lorem(20)), + [E], +) + +--- +// Auto row must expand properly in both cases +#set text(10pt) +#show table.cell: it => if it.x == 0 { it } else { layout(size => size.height) } +#table( + columns: 2, + rows: (1em, auto, 2em, 3em, 4em), + gutter: 3pt, + table.cell(rowspan: 5, block(fill: orange, height: 15em)[a]), + [b], + [c], + [d], + [e], + [f] +) + +#table( + columns: 2, + rows: (1em, auto, 2em, 3em, 4em), + gutter: 3pt, + table.cell(rowspan: 5, breakable: false, block(fill: orange, height: 15em)[a]), + [b], + [c], + [d], + [e], + [f] +) + +--- +// Expanding on unbreakable auto row +#set page(height: 7em, margin: (bottom: 2em)) +#grid( + columns: 2, + rows: (1em, 1em, auto, 1em, 1em, 1em), + fill: (x, y) => if x == 0 { aqua } else { blue }, + stroke: black, + gutter: 2pt, + grid.cell(rowspan: 5, block(height: 10em)[a]), + [a], + [b], + grid.cell(breakable: false, v(3em) + [c]), + [d], + [e], + [f], [g] +) + +--- +#show table.cell.where(x: 0): strong +#show table.cell.where(y: 0): strong +#set page(height: 13em) +#let lets-repeat(thing, n) = ((thing + colbreak(),) * (calc.max(0, n - 1)) + (thing,)).join() +#table( + columns: 4, + fill: (x, y) => if x == 0 or y == 0 { gray }, + [], [Test 1], [Test 2], [Test 3], + table.cell(rowspan: 15, align: horizon, lets-repeat((rotate(-90deg, reflow: true)[*All Tests*]), 3)), + ..([123], [456], [789]) * 15 +) diff --git a/tests/typ/layout/grid-rtl.typ b/tests/typ/layout/grid-rtl.typ index dcac9810..be9fac51 100644 --- a/tests/typ/layout/grid-rtl.typ +++ b/tests/typ/layout/grid-rtl.typ @@ -137,3 +137,44 @@ #grid( [a], grid.vline(position: left) ) + +--- +#set text(dir: rtl) + +#grid( + columns: 4, + fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) }, + inset: 5pt, + align: center, + grid.cell(rowspan: 2, fill: orange)[*Left*], + [Right A], [Right A], [Right A], + [Right B], grid.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide], + [Left A], [Left A], + [Left B], [Left B], grid.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long] +) + +#table( + columns: 4, + fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) }, + inset: 5pt, + align: center, + gutter: 3pt, + table.cell(rowspan: 2, fill: orange)[*Left*], + [Right A], [Right A], [Right A], + [Right B], table.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide], + [Left A], [Left A], + [Left B], [Left B], table.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long] +) + +--- +#set page(height: 10em) +#set text(dir: rtl) +#table( + columns: 2, + rows: (auto, auto, 3em), + row-gutter: 1em, + fill: red, + [a], table.cell(rowspan: 3, block(width: 50%, height: 10em, fill: orange) + place(bottom)[*ZD*]), + [e], + [f] +) diff --git a/tests/typ/layout/grid-stroke.typ b/tests/typ/layout/grid-stroke.typ index 51b810d9..9d01e1cd 100644 --- a/tests/typ/layout/grid-stroke.typ +++ b/tests/typ/layout/grid-stroke.typ @@ -274,6 +274,20 @@ table.hline(position: bottom) ) +--- +// Test partial border line overrides +#set page(width: auto, height: 7em, margin: (bottom: 1em)) +#table( + columns: 4, + stroke: (x, y) => if y == 0 or y == 4 { orange } else { aqua }, + table.hline(stroke: blue, start: 1, end: 2), table.cell(stroke: red, v(3em)), table.cell(stroke: blue)[b], table.cell(stroke: green)[c], [M], + [a], [b], [c], [M], + [d], [e], [f], [M], + [g], [h], [i], [M], + table.cell(stroke: red)[a], table.cell(stroke: blue)[b], table.cell(stroke: green)[c], [M], + table.hline(stroke: blue, start: 1, end: 2), +) + --- // Error: 8:3-8:32 cannot place horizontal line at the 'bottom' position of the bottom border (y = 2) // Hint: 8:3-8:32 set the line's position to 'top' or place it at a smaller 'y' index