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