use std::sync::Arc; use typst_library::foundations::{AlternativeFold, Fold}; use typst_library::layout::grid::resolve::{CellGrid, Line, Repeatable}; use typst_library::layout::Abs; use typst_library::visualize::Stroke; use super::RowPiece; /// Indicates which priority a particular grid line segment should have, based /// on the highest priority configuration that defined the segment's stroke. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum StrokePriority { /// The stroke of the segment was derived solely from the grid's global /// stroke setting, so it should have the lowest priority. GridStroke = 0, /// The segment's stroke was derived (even if partially) from a cell's /// stroke override, so it should have priority over non-overridden cell /// strokes and be drawn on top of them (when they have the same /// thickness). CellStroke = 1, /// The segment's stroke was derived from a user's explicitly placed line /// (hline or vline), and thus should have maximum priority, drawn on top /// of any cell strokes (when they have the same thickness). ExplicitLine = 2, } /// Data for a particular line segment in the grid as generated by /// `generate_line_segments`. #[derive(Debug, PartialEq, Eq)] pub struct LineSegment { /// The stroke with which to draw this segment. pub stroke: Arc>, /// The offset of this segment since the beginning of its axis. /// For a vertical line segment, this is the offset since the top of the /// table in the current page; for a horizontal line segment, this is the /// offset since the start border of the table. pub offset: Abs, /// The length of this segment. pub length: Abs, /// The segment's drawing priority, indicating on top of which other /// segments this one should be drawn. pub priority: StrokePriority, } /// Generates the segments of lines that should be drawn alongside a certain /// axis in the grid, going through the given tracks (orthogonal to the lines). /// Each returned segment contains its stroke, its offset from the start, and /// its length. /// /// Accepts, as parameters, the index of the lines that should be produced /// (for example, the column at which vertical lines will be drawn); a list of /// user-specified lines with the same index (the `lines` parameter); whether /// the given index corresponds to the maximum index for the line's axis; and a /// function which returns the final stroke that should be used for each track /// the line goes through, alongside the priority of the returned stroke (its /// parameters are the grid, the index of the line to be drawn, the number of /// the track to draw at and the stroke of the user hline/vline override at /// this index to fold with, if any). Contiguous segments with the same stroke /// and priority are joined together automatically. /// /// The function should return `None` for positions at which the line would /// otherwise cross a merged cell (for example, a vline could cross a colspan), /// in which case a new segment should be drawn after the merged cell(s), even /// if it would have the same stroke as the previous one. /// /// Regarding priority, the function should return a priority of ExplicitLine /// when the user-defined line's stroke at the current position isn't None /// (note that it is passed by parameter to the function). When it is None, the /// function should return a priority of CellStroke if the stroke returned was /// given or affected by a per-cell override of the grid's global stroke. /// When that isn't the case, the returned stroke was entirely provided by the /// grid's global stroke, and thus a priority of GridStroke should be returned. /// /// Note that we assume that the tracks are sorted according to ascending /// number, and they must be iterable over pairs of (number, size). For /// vertical lines, for instance, `tracks` would describe the rows in the /// current region, as pairs (row index, row height). pub fn generate_line_segments<'grid, F, I, L>( grid: &'grid CellGrid, tracks: I, index: usize, lines: L, line_stroke_at_track: F, ) -> impl Iterator + 'grid where F: Fn( &CellGrid, usize, usize, Option>>>, ) -> Option<(Arc>, StrokePriority)> + 'grid, I: IntoIterator, I::IntoIter: 'grid, L: IntoIterator, L::IntoIter: Clone + 'grid, { // The segment currently being drawn. // // It is extended for each consecutive track through which the line would // be drawn with the same stroke and priority. // // Starts as None to force us to create a new segment as soon as we find // the first track through which we should draw. let mut current_segment: Option = None; // How far from the start (before the first track) have we gone so far. // Used to determine the positions at which to draw each segment. let mut offset = Abs::zero(); // How much to multiply line indices by to account for gutter. let gutter_factor = if grid.has_gutter { 2 } else { 1 }; // Create an iterator of line segments, which will go through each track, // from start to finish, to create line segments and extend them until they // are interrupted and thus yielded through the iterator. We then repeat // the process, picking up from the track after the one at which we had // an interruption, until we have gone through all tracks. // // When going through each track, we check if the current segment would be // interrupted, either because, at this track, we hit a merged cell over // which we shouldn't draw, or because the line would have a different // stroke or priority at this point (so we have to start a new segment). If // so, the current segment is yielded and its variable is either set to // 'None' (if no segment should be drawn at the point of interruption, // meaning we might have to create a new segment later) or to the new // segment (if we're starting to draw a segment with a different stroke or // priority than before). // Otherwise (if the current segment should span the current track), it is // simply extended (or a new one is created, if it is 'None'), and no value // is yielded for the current track, since the segment isn't yet complete // (the next tracks might extend it further before it is interrupted and // yielded). That is, we yield each segment only when it is interrupted, // since then we will know its final length for sure. // // After the loop is done (and thus we went through all tracks), we // interrupt the current segment one last time, to ensure the final segment // is always interrupted and yielded, if it wasn't interrupted earlier. let mut tracks = tracks.into_iter(); let lines = lines.into_iter(); std::iter::from_fn(move || { // Each time this closure runs, we advance the track iterator as much // as possible before returning because the current segment was // interrupted. The for loop is resumed from where it stopped at the // next call due to that, ensuring we go through all tracks and then // stop. for (track, size) in &mut tracks { // Get the expected line stroke at this track by folding the // strokes of each user-specified line (with priority to the // user-specified line specified last). let mut line_strokes = lines .clone() .filter(|line| { line.end .map(|end| { // Subtract 1 from end index so we stop at the last // cell before it (don't cross one extra gutter). let end = if grid.has_gutter { 2 * end.get() - 1 } else { end.get() }; (gutter_factor * line.start..end).contains(&track) }) .unwrap_or_else(|| track >= gutter_factor * line.start) }) .map(|line| line.stroke.clone()); // Distinguish between unspecified stroke (None, if no lines // were matched above) and specified stroke of None (Some(None), // if some lines were matched and the one specified last had a // stroke of None) by conditionally folding after 'next()'. let line_stroke = line_strokes.next().map(|first_stroke| { line_strokes.fold(first_stroke, |acc, line_stroke| line_stroke.fold(acc)) }); // The function shall determine if it is appropriate to draw // the line at this position or not (i.e. whether or not it // would cross a merged cell), and, if so, the final stroke it // should have (because cells near this position could have // stroke overrides, which have priority and should be folded // with the stroke obtained above). // // If we are currently already drawing a segment and the function // indicates we should, at this track, draw some other segment // (with a different stroke or priority), or even no segment at // all, we interrupt and yield the current segment (which was drawn // up to the previous track) by returning it wrapped in 'Some()' // (which indicates, in the context of 'std::iter::from_fn', that // our iterator isn't over yet, and this should be its next value). if let Some((stroke, priority)) = line_stroke_at_track(grid, index, track, line_stroke) { // We should draw at this position. Let's check if we were // already drawing in the previous position. if let Some(current_segment) = &mut current_segment { // We are currently building a segment. Let's check if // we should extend it to this track as well. if current_segment.stroke == stroke && current_segment.priority == priority { // Extend the current segment so it covers at least // this track as well, since we should use the same // stroke as in the previous one when a line goes // through this track, with the same priority. current_segment.length += size; } else { // We got a different stroke or priority now, so create // a new segment with the new stroke and spanning the // current track. Yield the old segment, as it was // interrupted and is thus complete. let new_segment = LineSegment { stroke, offset, length: size, priority }; let old_segment = std::mem::replace(current_segment, new_segment); offset += size; return Some(old_segment); } } else { // We should draw here, but there is no segment // currently being drawn, either because the last // position had a merged cell, had a stroke // of 'None', or because this is the first track. // Create a new segment to draw. We start spanning this // track. current_segment = Some(LineSegment { stroke, offset, length: size, priority }); } } else if let Some(old_segment) = Option::take(&mut current_segment) { // We shouldn't draw here (stroke of None), so we yield the // current segment, as it was interrupted. offset += size; return Some(old_segment); } // Either the current segment is None (meaning we didn't start // drawing a segment yet since the last yielded one), so we keep // searching for a track where we should draw one; or the current // segment is Some but wasn't interrupted at this track, so we keep // looping through the following tracks until it is interrupted, // or we reach the end. offset += size; } // Reached the end of all tracks, so we interrupt and finish // the current segment. Note that, on future calls to this // closure, the current segment will necessarily be 'None', // so the iterator will necessarily end (that is, we will return None) // after this. // // Note: Fully-qualified notation because rust-analyzer is confused. Option::take(&mut current_segment) }) } /// Returns the correct stroke with which to draw a vline right before column /// `x` when going through row `y`, given the stroke of the user-specified line /// at this position, if any (note that a stroke of `None` is unspecified, /// while `Some(None)` means specified to remove any stroke at this position). /// Also returns the stroke's drawing priority, which depends on its source. /// /// If the vline would go through a colspan, returns None (shouldn't be drawn). /// If the one (when at the border) or two (otherwise) cells to the left and /// right of the vline have right and left stroke overrides, respectively, /// then the cells' stroke overrides are folded together with the vline's /// stroke (with priority to the vline's stroke, followed by the right cell's /// stroke, and, finally, the left cell's) and returned. If only one of the two /// cells around the vline (if there are two) has an override, that cell's /// stroke is given priority when folding. If, however, the cells around the /// vline at this row do not have any stroke overrides, then the vline's own /// stroke, as defined by user-specified lines (if any), is returned. /// /// The priority associated with the returned stroke follows the rules /// described in the docs for `generate_line_segment`. pub fn vline_stroke_at_row( grid: &CellGrid, x: usize, y: usize, stroke: Option>>>, ) -> Option<(Arc>, StrokePriority)> { // When the vline isn't at the border, we need to check if a colspan would // be present between columns 'x' and 'x-1' at row 'y', and thus overlap // with the line. // To do so, we analyze the cell right after this vline. If it is merged // with a cell before this line (parent.x < x) which is at this row or // above it (parent.y <= y, which is checked by // 'effective_parent_cell_position'), this means it would overlap with the // vline, so the vline must not be drawn at this row. if x != 0 && x != grid.cols.len() { // Use 'effective_parent_cell_position' to skip the gutters, if x or y // represent gutter tracks. // We would then analyze the cell one column after (if at a gutter // column), and/or one row below (if at a gutter row), in order to // check if it would be merged with a cell before the vline. if let Some(parent) = grid.effective_parent_cell_position(x, y) { if parent.x < x { // There is a colspan cell going through this vline's position, // so don't draw it here. return None; } } } let (left_cell_stroke, left_cell_prioritized) = x .checked_sub(1) .and_then(|left_x| { // Let's find the parent cell of the position before us, in order // to take its right stroke, even with gutter before us. grid.effective_parent_cell_position(left_x, y) }) .map(|parent| { let left_cell = grid.cell(parent.x, parent.y).unwrap(); (left_cell.stroke.right.clone(), left_cell.stroke_overridden.right) }) .unwrap_or((None, false)); let (right_cell_stroke, right_cell_prioritized) = if x < grid.cols.len() { // Let's find the parent cell of the position after us, in order // to take its left stroke, even with gutter after us. grid.effective_parent_cell_position(x, y) .map(|parent| { let right_cell = grid.cell(parent.x, parent.y).unwrap(); (right_cell.stroke.left.clone(), right_cell.stroke_overridden.left) }) .unwrap_or((None, false)) } else { (None, false) }; let priority = if stroke.is_some() { StrokePriority::ExplicitLine } else if left_cell_prioritized || right_cell_prioritized { StrokePriority::CellStroke } else { StrokePriority::GridStroke }; let (prioritized_cell_stroke, deprioritized_cell_stroke) = if left_cell_prioritized && !right_cell_prioritized { (left_cell_stroke, right_cell_stroke) } else { // When both cells' strokes have the same priority, we default to // prioritizing the right cell's left stroke. (right_cell_stroke, left_cell_stroke) }; // When both cells specify a stroke for this line segment, fold // both strokes, with priority to either the one prioritized cell, // or to the right cell's left stroke in case of a tie. But when one of // them doesn't specify a stroke, the other cell's stroke should be used // instead, regardless of priority (hence the usage of 'fold_or'). let cell_stroke = prioritized_cell_stroke.fold_or(deprioritized_cell_stroke); // Fold the line stroke and folded cell strokes, if possible. // Give priority to the explicit line stroke. // Otherwise, use whichever of the two isn't 'none' or unspecified. let final_stroke = stroke.fold_or(Some(cell_stroke)).flatten(); final_stroke.zip(Some(priority)) } /// Returns the correct stroke with which to draw a hline on top of row `y` /// when going through column `x`, given the stroke of the user-specified line /// at this position, if any (note that a stroke of `None` is unspecified, /// while `Some(None)` means specified to remove any stroke at this position). /// Also returns the stroke's drawing priority, which depends on its source. /// /// The `local_top_y` parameter indicates which row is effectively on top of /// this hline at the current region. This is `None` if the hline is above the /// first row in the region, for instance. The `in_last_region` parameter /// indicates whether this is the last region of the table. If not and this is /// a line at the bottom border, the bottom border's line gains priority. /// /// If the one (when at the border) or two (otherwise) cells above and below /// the hline have bottom and top stroke overrides, respectively, then the /// cells' stroke overrides are folded together with the hline's stroke (with /// priority to hline's stroke, followed by the bottom cell's stroke, and, /// finally, the top cell's) and returned. If only one of the two cells around /// the vline (if there are two) has an override, that cell's stroke is given /// priority when folding. If, however, the cells around the hline at this /// column do not have any stroke overrides, then the hline's own stroke, as /// defined by user-specified lines (if any), is directly returned. /// /// The priority associated with the returned stroke follows the rules /// described in the docs for `generate_line_segment`. /// /// The rows argument is needed to know which rows are effectively present in /// the current region, in order to avoid unnecessary hline splitting when a /// rowspan's previous rows are either in a previous region or empty (and thus /// wouldn't overlap with the hline, since its first row in the current region /// is below the hline). /// /// This function assumes columns are sorted by increasing `x`, and rows are /// sorted by increasing `y`. pub fn hline_stroke_at_column( grid: &CellGrid, rows: &[RowPiece], local_top_y: Option, in_last_region: bool, y: usize, x: usize, stroke: Option>>>, ) -> Option<(Arc>, StrokePriority)> { // When the hline isn't at the border, we need to check if a rowspan // would be present between rows 'y' and 'y-1' at column 'x', and thus // overlap with the line. // To do so, we analyze the cell right below this hline. If it is // merged with a cell above this line (parent.y < y) which is at this // column or before it (parent.x <= x, which is checked by // 'effective_parent_cell_position'), this means it would overlap with the // hline, so the hline must not be drawn at this column. if y != 0 && y != grid.rows.len() { // Use 'effective_parent_cell_position' to skip the gutters, if x or y // represent gutter tracks. // We would then analyze the cell one column after (if at a gutter // column), and/or one row below (if at a gutter row), in order to // check if it would be merged with a cell before the hline. if let Some(parent) = grid.effective_parent_cell_position(x, y) { if parent.y < y { // Get the first 'y' spanned by the possible rowspan in this region. // The 'parent.y' row and any other spanned rows above 'y' could be // missing from this region, which could have lead the check above // to be triggered, even though there is no spanned row above the // hline in the final layout of this region, and thus no overlap // with the hline, allowing it to be drawn regardless of the // theoretical presence of a rowspan going across its position. let local_parent_y = rows .iter() .find(|row| row.y >= parent.y) .map(|row| row.y) .unwrap_or(y); if local_parent_y < y { // There is a rowspan cell going through this hline's // position, so don't draw it here. return None; } } } } // When the hline is at the top of the region and this isn't the first // region, fold with the top stroke of the topmost cell at this column, // that is, the top border. let use_top_border_stroke = local_top_y.is_none() && y != 0; let (top_cell_stroke, top_cell_prioritized) = local_top_y .or(use_top_border_stroke.then_some(0)) .and_then(|top_y| { // Let's find the parent cell of the position above us, in order // to take its bottom stroke, even when we're below gutter. grid.effective_parent_cell_position(x, top_y) }) .map(|parent| { let top_cell = grid.cell(parent.x, parent.y).unwrap(); if use_top_border_stroke { (top_cell.stroke.top.clone(), top_cell.stroke_overridden.top) } else { (top_cell.stroke.bottom.clone(), top_cell.stroke_overridden.bottom) } }) .unwrap_or((None, false)); // Use the bottom border stroke with priority if we're not in the last // region, we have the last index, and (as a failsafe) we don't have the // last row of cells above us. let use_bottom_border_stroke = !in_last_region && local_top_y.map_or(true, |top_y| top_y + 1 != grid.rows.len()) && y == grid.rows.len(); let bottom_y = if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y }; let (bottom_cell_stroke, bottom_cell_prioritized) = if bottom_y < grid.rows.len() { // Let's find the parent cell of the position below us, in order // to take its top stroke, even when we're above gutter. grid.effective_parent_cell_position(x, bottom_y) .map(|parent| { let bottom_cell = grid.cell(parent.x, parent.y).unwrap(); if use_bottom_border_stroke { ( bottom_cell.stroke.bottom.clone(), bottom_cell.stroke_overridden.bottom, ) } else { (bottom_cell.stroke.top.clone(), bottom_cell.stroke_overridden.top) } }) .unwrap_or((None, false)) } else { // No cell below the bottom border. (None, false) }; let priority = if stroke.is_some() { StrokePriority::ExplicitLine } else if top_cell_prioritized || bottom_cell_prioritized { StrokePriority::CellStroke } else { StrokePriority::GridStroke }; // Top border stroke and header stroke are generally prioritized, unless // they don't have explicit hline overrides and one or more user-provided // hlines would appear at the same position, which then are prioritized. let top_stroke_comes_from_header = grid .header .as_ref() .and_then(Repeatable::as_repeated) .zip(local_top_y) .is_some_and(|(header, local_top_y)| { // Ensure the row above us is a repeated header. // FIXME: Make this check more robust when headers at arbitrary // positions are added. local_top_y < header.end && y > header.end }); // Prioritize the footer's top stroke as well where applicable. let bottom_stroke_comes_from_footer = grid .footer .as_ref() .and_then(Repeatable::as_repeated) .is_some_and(|footer| { // Ensure the row below us is a repeated footer. // FIXME: Make this check more robust when footers at arbitrary // positions are added. local_top_y.unwrap_or(0) + 1 < footer.start && y >= footer.start }); let (prioritized_cell_stroke, deprioritized_cell_stroke) = if !use_bottom_border_stroke && !bottom_stroke_comes_from_footer && (use_top_border_stroke || top_stroke_comes_from_header || top_cell_prioritized && !bottom_cell_prioritized) { // Top border must always be prioritized, even if it did not // request for that explicitly. (top_cell_stroke, bottom_cell_stroke) } else { // When both cells' strokes have the same priority, we default to // prioritizing the bottom cell's top stroke. // Additionally, the bottom border cell's stroke always has // priority. Same for stroke above footers. (bottom_cell_stroke, top_cell_stroke) }; // When both cells specify a stroke for this line segment, fold // both strokes, with priority to either the one prioritized cell, // or to the bottom cell's top stroke in case of a tie. But when one of // them doesn't specify a stroke, the other cell's stroke should be used // instead, regardless of priority (hence the usage of 'fold_or'). let cell_stroke = prioritized_cell_stroke.fold_or(deprioritized_cell_stroke); // Fold the line stroke and folded cell strokes, if possible. // Give priority to the explicit line stroke. // Otherwise, use whichever of the two isn't 'none' or unspecified. let final_stroke = stroke.fold_or(Some(cell_stroke)).flatten(); final_stroke.zip(Some(priority)) } #[cfg(test)] mod test { use std::num::NonZeroUsize; use typst_library::foundations::Content; use typst_library::introspection::Locator; use typst_library::layout::grid::resolve::{Cell, Entry, LinePosition}; use typst_library::layout::{Axes, Sides, Sizing}; use typst_utils::NonZeroExt; use super::*; fn sample_cell() -> Cell<'static> { Cell { body: Content::default(), locator: Locator::root(), fill: None, colspan: NonZeroUsize::ONE, rowspan: NonZeroUsize::ONE, stroke: Sides::splat(Some(Arc::new(Stroke::default()))), stroke_overridden: Sides::splat(false), breakable: true, } } fn cell_with_colspan_rowspan(colspan: usize, rowspan: usize) -> Cell<'static> { Cell { body: Content::default(), locator: Locator::root(), fill: None, colspan: NonZeroUsize::try_from(colspan).unwrap(), rowspan: NonZeroUsize::try_from(rowspan).unwrap(), stroke: Sides::splat(Some(Arc::new(Stroke::default()))), stroke_overridden: Sides::splat(false), breakable: true, } } fn sample_grid_for_vlines(gutters: bool) -> CellGrid<'static> { const COLS: usize = 4; const ROWS: usize = 6; let entries = vec![ // row 0 Entry::Cell(sample_cell()), Entry::Cell(sample_cell()), Entry::Cell(cell_with_colspan_rowspan(2, 1)), Entry::Merged { parent: 2 }, // row 1 Entry::Cell(sample_cell()), Entry::Cell(cell_with_colspan_rowspan(3, 1)), Entry::Merged { parent: 5 }, Entry::Merged { parent: 5 }, // row 2 Entry::Merged { parent: 4 }, Entry::Cell(sample_cell()), Entry::Cell(cell_with_colspan_rowspan(2, 1)), Entry::Merged { parent: 10 }, // row 3 Entry::Cell(sample_cell()), Entry::Cell(cell_with_colspan_rowspan(3, 2)), Entry::Merged { parent: 13 }, Entry::Merged { parent: 13 }, // row 4 Entry::Cell(sample_cell()), Entry::Merged { parent: 13 }, Entry::Merged { parent: 13 }, Entry::Merged { parent: 13 }, // row 5 Entry::Cell(sample_cell()), Entry::Cell(sample_cell()), Entry::Cell(cell_with_colspan_rowspan(2, 1)), Entry::Merged { parent: 22 }, ]; CellGrid::new_internal( Axes::with_x(&[Sizing::Auto; COLS]), if gutters { Axes::new(&[Sizing::Auto; COLS - 1], &[Sizing::Auto; ROWS - 1]) } else { Axes::default() }, vec![], vec![], None, None, entries, ) } #[test] fn test_vline_splitting_without_gutter() { let stroke = Arc::new(Stroke::default()); let grid = sample_grid_for_vlines(false); let rows = &[ RowPiece { height: Abs::pt(1.0), y: 0 }, RowPiece { height: Abs::pt(2.0), y: 1 }, RowPiece { height: Abs::pt(4.0), y: 2 }, RowPiece { height: Abs::pt(8.0), y: 3 }, RowPiece { height: Abs::pt(16.0), y: 4 }, RowPiece { height: Abs::pt(32.0), y: 5 }, ]; let expected_vline_splits = &[ vec![LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), priority: StrokePriority::GridStroke, }], vec![LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), priority: StrokePriority::GridStroke, }], // interrupted a few times by colspans vec![ LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2.), length: Abs::pt(4.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16.), length: Abs::pt(32.), priority: StrokePriority::GridStroke, }, ], // interrupted every time by colspans vec![], vec![LineSegment { stroke, offset: Abs::pt(0.), length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), priority: StrokePriority::GridStroke, }], ]; for (x, expected_splits) in expected_vline_splits.iter().enumerate() { let tracks = rows.iter().map(|row| (row.y, row.height)); assert_eq!( expected_splits, &generate_line_segments(&grid, tracks, x, &[], vline_stroke_at_row) .collect::>(), ); } } #[test] fn test_vline_splitting_with_gutter_and_per_cell_stroke() { let stroke = Arc::new(Stroke::default()); let grid = sample_grid_for_vlines(true); let rows = &[ RowPiece { height: Abs::pt(1.0), y: 0 }, RowPiece { height: Abs::pt(2.0), y: 1 }, RowPiece { height: Abs::pt(4.0), y: 2 }, RowPiece { height: Abs::pt(8.0), y: 3 }, RowPiece { height: Abs::pt(16.0), y: 4 }, RowPiece { height: Abs::pt(32.0), y: 5 }, RowPiece { height: Abs::pt(64.0), y: 6 }, RowPiece { height: Abs::pt(128.0), y: 7 }, RowPiece { height: Abs::pt(256.0), y: 8 }, RowPiece { height: Abs::pt(512.0), y: 9 }, RowPiece { height: Abs::pt(1024.0), y: 10 }, ]; // Stroke is per-cell so we skip gutter let expected_vline_splits = &[ // left border vec![ LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1.), priority: StrokePriority::GridStroke, }, // Covers the rowspan between (original) rows 1 and 2 LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2.), length: Abs::pt(4. + 8. + 16.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), length: Abs::pt(64.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128.), length: Abs::pt(256.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt( 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512., ), length: Abs::pt(1024.), priority: StrokePriority::GridStroke, }, ], // gutter line below vec![ LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1.), priority: StrokePriority::GridStroke, }, // Covers the rowspan between (original) rows 1 and 2 LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2.), length: Abs::pt(4. + 8. + 16.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), length: Abs::pt(64.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128.), length: Abs::pt(256.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt( 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512., ), length: Abs::pt(1024.), priority: StrokePriority::GridStroke, }, ], vec![ LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2.), length: Abs::pt(4.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8.), length: Abs::pt(16.), priority: StrokePriority::GridStroke, }, // Covers the rowspan between (original) rows 3 and 4 LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), length: Abs::pt(64. + 128. + 256.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt( 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512., ), length: Abs::pt(1024.), priority: StrokePriority::GridStroke, }, ], // gutter line below // the two lines below are interrupted multiple times by colspans vec![ LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8.), length: Abs::pt(16.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt( 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512., ), length: Abs::pt(1024.), priority: StrokePriority::GridStroke, }, ], vec![ LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8.), length: Abs::pt(16.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt( 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512., ), length: Abs::pt(1024.), priority: StrokePriority::GridStroke, }, ], // gutter line below // the two lines below can only cross certain gutter rows, because // all non-gutter cells in the following column are merged with // cells from the previous column. vec![], vec![], // right border vec![ LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2.), length: Abs::pt(4.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8.), length: Abs::pt(16.), priority: StrokePriority::GridStroke, }, // Covers the rowspan between (original) rows 3 and 4 LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.), length: Abs::pt(64. + 128. + 256.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt( 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512., ), length: Abs::pt(1024.), priority: StrokePriority::GridStroke, }, ], ]; for (x, expected_splits) in expected_vline_splits.iter().enumerate() { let tracks = rows.iter().map(|row| (row.y, row.height)); assert_eq!( expected_splits, &generate_line_segments(&grid, tracks, x, &[], vline_stroke_at_row) .collect::>(), ); } } #[test] fn test_vline_splitting_with_gutter_and_explicit_vlines() { let stroke = Arc::new(Stroke::default()); let grid = sample_grid_for_vlines(true); let rows = &[ RowPiece { height: Abs::pt(1.0), y: 0 }, RowPiece { height: Abs::pt(2.0), y: 1 }, RowPiece { height: Abs::pt(4.0), y: 2 }, RowPiece { height: Abs::pt(8.0), y: 3 }, RowPiece { height: Abs::pt(16.0), y: 4 }, RowPiece { height: Abs::pt(32.0), y: 5 }, RowPiece { height: Abs::pt(64.0), y: 6 }, RowPiece { height: Abs::pt(128.0), y: 7 }, RowPiece { height: Abs::pt(256.0), y: 8 }, RowPiece { height: Abs::pt(512.0), y: 9 }, RowPiece { height: Abs::pt(1024.0), y: 10 }, ]; let expected_vline_splits = &[ // left border vec![LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt( 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024., ), priority: StrokePriority::ExplicitLine, }], // gutter line below vec![LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt( 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024., ), priority: StrokePriority::ExplicitLine, }], vec![LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt( 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024., ), priority: StrokePriority::ExplicitLine, }], // gutter line below // the two lines below are interrupted multiple times by colspans vec![ LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1. + 2.), priority: StrokePriority::ExplicitLine, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4.), length: Abs::pt(8. + 16. + 32.), priority: StrokePriority::ExplicitLine, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.), length: Abs::pt(512. + 1024.), priority: StrokePriority::ExplicitLine, }, ], vec![ LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1. + 2.), priority: StrokePriority::ExplicitLine, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4.), length: Abs::pt(8. + 16. + 32.), priority: StrokePriority::ExplicitLine, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.), length: Abs::pt(512. + 1024.), priority: StrokePriority::ExplicitLine, }, ], // gutter line below // the two lines below can only cross certain gutter rows, because // all non-gutter cells in the following column are merged with // cells from the previous column. vec![ LineSegment { stroke: stroke.clone(), offset: Abs::pt(1.), length: Abs::pt(2.), priority: StrokePriority::ExplicitLine, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4.), length: Abs::pt(8.), priority: StrokePriority::ExplicitLine, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16.), length: Abs::pt(32.), priority: StrokePriority::ExplicitLine, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.), length: Abs::pt(512.), priority: StrokePriority::ExplicitLine, }, ], vec![ LineSegment { stroke: stroke.clone(), offset: Abs::pt(1.), length: Abs::pt(2.), priority: StrokePriority::ExplicitLine, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4.), length: Abs::pt(8.), priority: StrokePriority::ExplicitLine, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16.), length: Abs::pt(32.), priority: StrokePriority::ExplicitLine, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.), length: Abs::pt(512.), priority: StrokePriority::ExplicitLine, }, ], // right border vec![LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt( 1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024., ), priority: StrokePriority::ExplicitLine, }], ]; for (x, expected_splits) in expected_vline_splits.iter().enumerate() { let tracks = rows.iter().map(|row| (row.y, row.height)); assert_eq!( expected_splits, &generate_line_segments( &grid, tracks, x, &[ Line { index: x, start: 0, end: None, stroke: Some(stroke.clone()), position: LinePosition::Before }, Line { index: x, start: 0, end: None, stroke: Some(stroke.clone()), position: LinePosition::After }, ], vline_stroke_at_row ) .collect::>(), ); } } fn sample_grid_for_hlines(gutters: bool) -> CellGrid<'static> { const COLS: usize = 4; const ROWS: usize = 9; let entries = vec![ // row 0 Entry::Cell(cell_with_colspan_rowspan(1, 2)), Entry::Cell(sample_cell()), Entry::Cell(cell_with_colspan_rowspan(2, 2)), Entry::Merged { parent: 2 }, // row 1 Entry::Merged { parent: 0 }, Entry::Cell(sample_cell()), Entry::Merged { parent: 2 }, Entry::Merged { parent: 2 }, // row 2 Entry::Cell(sample_cell()), Entry::Cell(sample_cell()), Entry::Cell(sample_cell()), Entry::Cell(sample_cell()), // row 3 Entry::Cell(cell_with_colspan_rowspan(4, 2)), Entry::Merged { parent: 12 }, Entry::Merged { parent: 12 }, Entry::Merged { parent: 12 }, // row 4 Entry::Merged { parent: 12 }, Entry::Merged { parent: 12 }, Entry::Merged { parent: 12 }, Entry::Merged { parent: 12 }, // row 5 Entry::Cell(sample_cell()), Entry::Cell(cell_with_colspan_rowspan(1, 2)), Entry::Cell(cell_with_colspan_rowspan(2, 1)), Entry::Merged { parent: 22 }, // row 6 Entry::Cell(sample_cell()), Entry::Merged { parent: 21 }, Entry::Cell(sample_cell()), Entry::Cell(sample_cell()), // row 7 (adjacent rowspans covering the whole row) Entry::Cell(cell_with_colspan_rowspan(2, 2)), Entry::Merged { parent: 28 }, Entry::Cell(cell_with_colspan_rowspan(2, 2)), Entry::Merged { parent: 30 }, // row 8 Entry::Merged { parent: 28 }, Entry::Merged { parent: 28 }, Entry::Merged { parent: 30 }, Entry::Merged { parent: 30 }, ]; CellGrid::new_internal( Axes::with_x(&[Sizing::Auto; COLS]), if gutters { Axes::new(&[Sizing::Auto; COLS - 1], &[Sizing::Auto; ROWS - 1]) } else { Axes::default() }, vec![], vec![], None, None, entries, ) } #[test] fn test_hline_splitting_without_gutter() { let stroke = Arc::new(Stroke::default()); let grid = sample_grid_for_hlines(false); let columns = &[Abs::pt(1.), Abs::pt(2.), Abs::pt(4.), Abs::pt(8.)]; // Assume all rows would be drawn in the same region, and are available. let rows = grid .rows .iter() .enumerate() .map(|(y, _)| RowPiece { height: Abs::pt(f64::from(2u32.pow(y as u32))), y }) .collect::>(); let expected_hline_splits = &[ // top border vec![LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1. + 2. + 4. + 8.), priority: StrokePriority::GridStroke, }], // interrupted a few times by rowspans vec![LineSegment { stroke: stroke.clone(), offset: Abs::pt(1.), length: Abs::pt(2.), priority: StrokePriority::GridStroke, }], vec![LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1. + 2. + 4. + 8.), priority: StrokePriority::GridStroke, }], vec![LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1. + 2. + 4. + 8.), priority: StrokePriority::GridStroke, }], // interrupted every time by rowspans vec![], vec![LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1. + 2. + 4. + 8.), priority: StrokePriority::GridStroke, }], // interrupted once by rowspan vec![ LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1.), priority: StrokePriority::GridStroke, }, LineSegment { stroke: stroke.clone(), offset: Abs::pt(1. + 2.), length: Abs::pt(4. + 8.), priority: StrokePriority::GridStroke, }, ], vec![LineSegment { stroke: stroke.clone(), offset: Abs::pt(0.), length: Abs::pt(1. + 2. + 4. + 8.), priority: StrokePriority::GridStroke, }], // interrupted every time by successive rowspans vec![], // bottom border vec![LineSegment { stroke, offset: Abs::pt(0.), length: Abs::pt(1. + 2. + 4. + 8.), priority: StrokePriority::GridStroke, }], ]; for (y, expected_splits) in expected_hline_splits.iter().enumerate() { let tracks = columns.iter().copied().enumerate(); assert_eq!( expected_splits, &generate_line_segments(&grid, tracks, y, &[], |grid, y, x, stroke| { hline_stroke_at_column( grid, &rows, y.checked_sub(1), true, y, x, stroke, ) }) .collect::>(), ); } } #[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 }, ], |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, &[], |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::>() ); } }