mirror of
https://github.com/typst/typst
synced 2025-05-15 09:35:28 +08:00
1519 lines
63 KiB
Rust
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<_>>()
|
|
);
|
|
}
|
|
}
|