summaryrefslogtreecommitdiff
path: root/crates/typst-layout/src/grid/lines.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-layout/src/grid/lines.rs')
-rw-r--r--crates/typst-layout/src/grid/lines.rs1548
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<_>>()
+ );
+ }
+}