1519 lines
63 KiB
Rust

use std::sync::Arc;
use typst_library::foundations::{AlternativeFold, Fold};
use typst_library::layout::grid::resolve::{CellGrid, Line, Repeatable};
use typst_library::layout::Abs;
use typst_library::visualize::Stroke;
use super::RowPiece;
/// Indicates which priority a particular grid line segment should have, based
/// on the highest priority configuration that defined the segment's stroke.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum StrokePriority {
/// The stroke of the segment was derived solely from the grid's global
/// stroke setting, so it should have the lowest priority.
GridStroke = 0,
/// The segment's stroke was derived (even if partially) from a cell's
/// stroke override, so it should have priority over non-overridden cell
/// strokes and be drawn on top of them (when they have the same
/// thickness).
CellStroke = 1,
/// The segment's stroke was derived from a user's explicitly placed line
/// (hline or vline), and thus should have maximum priority, drawn on top
/// of any cell strokes (when they have the same thickness).
ExplicitLine = 2,
}
/// Data for a particular line segment in the grid as generated by
/// `generate_line_segments`.
#[derive(Debug, PartialEq, Eq)]
pub struct LineSegment {
/// The stroke with which to draw this segment.
pub stroke: Arc<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 std::num::NonZeroUsize;
use typst_library::foundations::Content;
use typst_library::introspection::Locator;
use typst_library::layout::grid::resolve::{Cell, Entry, LinePosition};
use typst_library::layout::{Axes, Sides, Sizing};
use typst_utils::NonZeroExt;
use super::*;
fn sample_cell() -> Cell<'static> {
Cell {
body: Content::default(),
locator: Locator::root(),
fill: None,
colspan: NonZeroUsize::ONE,
rowspan: NonZeroUsize::ONE,
stroke: Sides::splat(Some(Arc::new(Stroke::default()))),
stroke_overridden: Sides::splat(false),
breakable: true,
}
}
fn cell_with_colspan_rowspan(colspan: usize, rowspan: usize) -> Cell<'static> {
Cell {
body: Content::default(),
locator: Locator::root(),
fill: None,
colspan: NonZeroUsize::try_from(colspan).unwrap(),
rowspan: NonZeroUsize::try_from(rowspan).unwrap(),
stroke: Sides::splat(Some(Arc::new(Stroke::default()))),
stroke_overridden: Sides::splat(false),
breakable: true,
}
}
fn sample_grid_for_vlines(gutters: bool) -> CellGrid<'static> {
const COLS: usize = 4;
const ROWS: usize = 6;
let entries = vec![
// row 0
Entry::Cell(sample_cell()),
Entry::Cell(sample_cell()),
Entry::Cell(cell_with_colspan_rowspan(2, 1)),
Entry::Merged { parent: 2 },
// row 1
Entry::Cell(sample_cell()),
Entry::Cell(cell_with_colspan_rowspan(3, 1)),
Entry::Merged { parent: 5 },
Entry::Merged { parent: 5 },
// row 2
Entry::Merged { parent: 4 },
Entry::Cell(sample_cell()),
Entry::Cell(cell_with_colspan_rowspan(2, 1)),
Entry::Merged { parent: 10 },
// row 3
Entry::Cell(sample_cell()),
Entry::Cell(cell_with_colspan_rowspan(3, 2)),
Entry::Merged { parent: 13 },
Entry::Merged { parent: 13 },
// row 4
Entry::Cell(sample_cell()),
Entry::Merged { parent: 13 },
Entry::Merged { parent: 13 },
Entry::Merged { parent: 13 },
// row 5
Entry::Cell(sample_cell()),
Entry::Cell(sample_cell()),
Entry::Cell(cell_with_colspan_rowspan(2, 1)),
Entry::Merged { parent: 22 },
];
CellGrid::new_internal(
Axes::with_x(&[Sizing::Auto; COLS]),
if gutters {
Axes::new(&[Sizing::Auto; COLS - 1], &[Sizing::Auto; ROWS - 1])
} else {
Axes::default()
},
vec![],
vec![],
None,
None,
entries,
)
}
#[test]
fn test_vline_splitting_without_gutter() {
let stroke = Arc::new(Stroke::default());
let grid = sample_grid_for_vlines(false);
let rows = &[
RowPiece { height: Abs::pt(1.0), y: 0 },
RowPiece { height: Abs::pt(2.0), y: 1 },
RowPiece { height: Abs::pt(4.0), y: 2 },
RowPiece { height: Abs::pt(8.0), y: 3 },
RowPiece { height: Abs::pt(16.0), y: 4 },
RowPiece { height: Abs::pt(32.0), y: 5 },
];
let expected_vline_splits = &[
vec![LineSegment {
stroke: stroke.clone(),
offset: Abs::pt(0.),
length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.),
priority: StrokePriority::GridStroke,
}],
vec![LineSegment {
stroke: stroke.clone(),
offset: Abs::pt(0.),
length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.),
priority: StrokePriority::GridStroke,
}],
// interrupted a few times by colspans
vec![
LineSegment {
stroke: stroke.clone(),
offset: Abs::pt(0.),
length: Abs::pt(1.),
priority: StrokePriority::GridStroke,
},
LineSegment {
stroke: stroke.clone(),
offset: Abs::pt(1. + 2.),
length: Abs::pt(4.),
priority: StrokePriority::GridStroke,
},
LineSegment {
stroke: stroke.clone(),
offset: Abs::pt(1. + 2. + 4. + 8. + 16.),
length: Abs::pt(32.),
priority: StrokePriority::GridStroke,
},
],
// interrupted every time by colspans
vec![],
vec![LineSegment {
stroke,
offset: Abs::pt(0.),
length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.),
priority: StrokePriority::GridStroke,
}],
];
for (x, expected_splits) in expected_vline_splits.iter().enumerate() {
let tracks = rows.iter().map(|row| (row.y, row.height));
assert_eq!(
expected_splits,
&generate_line_segments(&grid, tracks, x, &[], vline_stroke_at_row)
.collect::<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<_>>()
);
}
}