Merging cells: Rowspans [More Flexible Tables Pt.3b] (#3501)
@ -1,9 +1,9 @@
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::layout::CellGrid;
|
||||
use super::layout::{CellGrid, RowPiece};
|
||||
use crate::foundations::{AlternativeFold, Fold};
|
||||
use crate::layout::{Abs, Axes};
|
||||
use crate::layout::Abs;
|
||||
use crate::visualize::Stroke;
|
||||
|
||||
/// Represents an explicit grid line (horizontal or vertical) specified by the
|
||||
@ -67,7 +67,7 @@ pub(super) enum StrokePriority {
|
||||
}
|
||||
|
||||
/// Data for a particular line segment in the grid as generated by
|
||||
/// 'generate_line_segments'.
|
||||
/// `generate_line_segments`.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) struct LineSegment {
|
||||
/// The stroke with which to draw this segment.
|
||||
@ -100,7 +100,7 @@ pub(super) struct LineSegment {
|
||||
/// 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
|
||||
/// 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.
|
||||
@ -115,13 +115,13 @@ pub(super) struct LineSegment {
|
||||
///
|
||||
/// 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
|
||||
/// vertical lines, for instance, `tracks` would describe the rows in the
|
||||
/// current region, as pairs (row index, row height).
|
||||
pub(super) fn generate_line_segments<'grid, F, I>(
|
||||
pub(super) fn generate_line_segments<'grid, F, I, L>(
|
||||
grid: &'grid CellGrid,
|
||||
tracks: I,
|
||||
index: usize,
|
||||
lines: &'grid [Line],
|
||||
lines: L,
|
||||
is_max_index: bool,
|
||||
line_stroke_at_track: F,
|
||||
) -> impl Iterator<Item = LineSegment> + 'grid
|
||||
@ -135,6 +135,8 @@ where
|
||||
+ 'grid,
|
||||
I: IntoIterator<Item = (usize, Abs)>,
|
||||
I::IntoIter: 'grid,
|
||||
L: IntoIterator<Item = &'grid Line>,
|
||||
L::IntoIter: Clone + 'grid,
|
||||
{
|
||||
// The segment currently being drawn.
|
||||
//
|
||||
@ -162,7 +164,7 @@ where
|
||||
// Note that the maximum index is always an odd number when there's gutter,
|
||||
// so we must check for it to ensure we don't give it the same treatment as
|
||||
// a line before a gutter track.
|
||||
let expected_line_position = if grid.has_gutter && index % 2 == 1 && !is_max_index {
|
||||
let expected_line_position = if grid.is_gutter_track(index) && !is_max_index {
|
||||
LinePosition::After
|
||||
} else {
|
||||
LinePosition::Before
|
||||
@ -194,6 +196,7 @@ where
|
||||
// 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
|
||||
@ -205,7 +208,7 @@ where
|
||||
// strokes of each user-specified line (with priority to the
|
||||
// user-specified line specified last).
|
||||
let mut line_strokes = lines
|
||||
.iter()
|
||||
.clone()
|
||||
.filter(|line| {
|
||||
line.position == expected_line_position
|
||||
&& line
|
||||
@ -332,45 +335,48 @@ pub(super) fn vline_stroke_at_row(
|
||||
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() {
|
||||
// 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), this means it would overlap with the vline,
|
||||
// so the vline must not be drawn at this row.
|
||||
let first_adjacent_cell = if grid.has_gutter {
|
||||
// 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.
|
||||
(x + x % 2, y + y % 2)
|
||||
} else {
|
||||
(x, y)
|
||||
};
|
||||
let Axes { x: parent_x, y: parent_y } = grid
|
||||
.parent_cell_position(first_adjacent_cell.0, first_adjacent_cell.1)
|
||||
.unwrap();
|
||||
|
||||
if parent_x < x && parent_y <= y {
|
||||
// There is a colspan cell going through this vline's position,
|
||||
// so don't draw it here.
|
||||
return None;
|
||||
// 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| grid.parent_cell(left_x, y))
|
||||
.map(|left_cell| {
|
||||
.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() {
|
||||
grid.parent_cell(x, y)
|
||||
.map(|right_cell| {
|
||||
// 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))
|
||||
@ -416,6 +422,12 @@ pub(super) fn vline_stroke_at_row(
|
||||
/// 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
|
||||
@ -428,58 +440,105 @@ pub(super) fn vline_stroke_at_row(
|
||||
///
|
||||
/// 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(super) 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)> {
|
||||
// There are no rowspans yet, so no need to add a check here. The line will
|
||||
// always be drawn, if it has a stroke.
|
||||
let cell_x = if grid.has_gutter {
|
||||
// Skip the gutter column this hline is in.
|
||||
// This is because positions above and below it, even if gutter, could
|
||||
// be part of a colspan, so we have to check the following cell.
|
||||
// However, this is only valid if we're not in a gutter row.
|
||||
x + x % 2
|
||||
} else {
|
||||
x
|
||||
};
|
||||
// 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);
|
||||
|
||||
let (top_cell_stroke, top_cell_prioritized) = y
|
||||
.checked_sub(1)
|
||||
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.parent_cell_position(cell_x, top_y)
|
||||
grid.effective_parent_cell_position(x, top_y)
|
||||
})
|
||||
.filter(|Axes { x: parent_x, .. }| {
|
||||
// Only use the stroke of the cell above us but one column to the
|
||||
// right if it is merged with a cell before this line's column.
|
||||
// If the position above us is a simple non-merged cell, or the
|
||||
// parent of a colspan, this will also evaluate to true.
|
||||
parent_x <= &x
|
||||
})
|
||||
.map(|Axes { x: parent_x, y: parent_y }| {
|
||||
let top_cell = grid.cell(parent_x, parent_y).unwrap();
|
||||
(top_cell.stroke.bottom.clone(), top_cell.stroke_overridden.bottom)
|
||||
.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));
|
||||
|
||||
let (bottom_cell_stroke, bottom_cell_prioritized) = if y < grid.rows.len() {
|
||||
// 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.parent_cell_position(cell_x, y)
|
||||
.filter(|Axes { x: parent_x, .. }| {
|
||||
// Only use the stroke of the cell below us but one column to the
|
||||
// right if it is merged with a cell before this line's column.
|
||||
// If the position below us is a simple non-merged cell, or the
|
||||
// parent of a colspan, this will also evaluate to true.
|
||||
parent_x <= &x
|
||||
})
|
||||
.map(|Axes { x: parent_x, y: parent_y }| {
|
||||
let bottom_cell = grid.cell(parent_x, parent_y).unwrap();
|
||||
(bottom_cell.stroke.top.clone(), bottom_cell.stroke_overridden.top)
|
||||
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 {
|
||||
@ -496,11 +555,17 @@ pub(super) fn hline_stroke_at_column(
|
||||
};
|
||||
|
||||
let (prioritized_cell_stroke, deprioritized_cell_stroke) =
|
||||
if top_cell_prioritized && !bottom_cell_prioritized {
|
||||
if !use_bottom_border_stroke
|
||||
&& (use_top_border_stroke || 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.
|
||||
(bottom_cell_stroke, top_cell_stroke)
|
||||
};
|
||||
|
||||
@ -524,7 +589,7 @@ mod test {
|
||||
use super::super::layout::{Entry, RowPiece};
|
||||
use super::*;
|
||||
use crate::foundations::Content;
|
||||
use crate::layout::{Cell, Sides, Sizing};
|
||||
use crate::layout::{Axes, Cell, Sides, Sizing};
|
||||
use crate::util::NonZeroExt;
|
||||
|
||||
fn sample_cell() -> Cell {
|
||||
@ -532,43 +597,47 @@ mod test {
|
||||
body: Content::default(),
|
||||
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(colspan: usize) -> Cell {
|
||||
fn cell_with_colspan_rowspan(colspan: usize, rowspan: usize) -> Cell {
|
||||
Cell {
|
||||
body: Content::default(),
|
||||
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(gutters: bool) -> CellGrid {
|
||||
fn sample_grid_for_vlines(gutters: bool) -> CellGrid {
|
||||
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(2)),
|
||||
Entry::Cell(cell_with_colspan_rowspan(2, 1)),
|
||||
Entry::Merged { parent: 2 },
|
||||
// row 1
|
||||
Entry::Cell(sample_cell()),
|
||||
Entry::Cell(cell_with_colspan(3)),
|
||||
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(2)),
|
||||
Entry::Cell(cell_with_colspan_rowspan(2, 1)),
|
||||
Entry::Merged { parent: 10 },
|
||||
// row 3
|
||||
Entry::Cell(sample_cell()),
|
||||
Entry::Cell(cell_with_colspan(3)),
|
||||
Entry::Cell(cell_with_colspan_rowspan(3, 2)),
|
||||
Entry::Merged { parent: 13 },
|
||||
Entry::Merged { parent: 13 },
|
||||
// row 4
|
||||
@ -579,7 +648,7 @@ mod test {
|
||||
// row 5
|
||||
Entry::Cell(sample_cell()),
|
||||
Entry::Cell(sample_cell()),
|
||||
Entry::Cell(cell_with_colspan(2)),
|
||||
Entry::Cell(cell_with_colspan_rowspan(2, 1)),
|
||||
Entry::Merged { parent: 22 },
|
||||
];
|
||||
CellGrid::new_internal(
|
||||
@ -598,7 +667,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_vline_splitting_without_gutter() {
|
||||
let stroke = Arc::new(Stroke::default());
|
||||
let grid = sample_grid(false);
|
||||
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 },
|
||||
@ -670,7 +739,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_vline_splitting_with_gutter_and_per_cell_stroke() {
|
||||
let stroke = Arc::new(Stroke::default());
|
||||
let grid = sample_grid(true);
|
||||
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 },
|
||||
@ -694,16 +763,11 @@ mod test {
|
||||
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.),
|
||||
priority: StrokePriority::GridStroke,
|
||||
},
|
||||
LineSegment {
|
||||
stroke: stroke.clone(),
|
||||
offset: Abs::pt(1. + 2. + 4. + 8.),
|
||||
length: Abs::pt(16.),
|
||||
length: Abs::pt(4. + 8. + 16.),
|
||||
priority: StrokePriority::GridStroke,
|
||||
},
|
||||
LineSegment {
|
||||
@ -735,16 +799,11 @@ mod test {
|
||||
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.),
|
||||
priority: StrokePriority::GridStroke,
|
||||
},
|
||||
LineSegment {
|
||||
stroke: stroke.clone(),
|
||||
offset: Abs::pt(1. + 2. + 4. + 8.),
|
||||
length: Abs::pt(16.),
|
||||
length: Abs::pt(4. + 8. + 16.),
|
||||
priority: StrokePriority::GridStroke,
|
||||
},
|
||||
LineSegment {
|
||||
@ -787,16 +846,11 @@ mod test {
|
||||
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.),
|
||||
priority: StrokePriority::GridStroke,
|
||||
},
|
||||
LineSegment {
|
||||
stroke: stroke.clone(),
|
||||
offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128.),
|
||||
length: Abs::pt(256.),
|
||||
length: Abs::pt(64. + 128. + 256.),
|
||||
priority: StrokePriority::GridStroke,
|
||||
},
|
||||
LineSegment {
|
||||
@ -880,16 +934,11 @@ mod test {
|
||||
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.),
|
||||
priority: StrokePriority::GridStroke,
|
||||
},
|
||||
LineSegment {
|
||||
stroke: stroke.clone(),
|
||||
offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128.),
|
||||
length: Abs::pt(256.),
|
||||
length: Abs::pt(64. + 128. + 256.),
|
||||
priority: StrokePriority::GridStroke,
|
||||
},
|
||||
LineSegment {
|
||||
@ -922,7 +971,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_vline_splitting_with_gutter_and_explicit_vlines() {
|
||||
let stroke = Arc::new(Stroke::default());
|
||||
let grid = sample_grid(true);
|
||||
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 },
|
||||
@ -1102,4 +1151,409 @@ mod test {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_grid_for_hlines(gutters: bool) -> CellGrid {
|
||||
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![],
|
||||
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,
|
||||
&[],
|
||||
y == grid.rows.len(),
|
||||
|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
|
||||
},
|
||||
],
|
||||
y == grid.rows.len(),
|
||||
|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,
|
||||
&[],
|
||||
4 == grid.rows.len(),
|
||||
|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<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
mod layout;
|
||||
mod lines;
|
||||
mod rowspans;
|
||||
|
||||
pub use self::layout::{Cell, CellGrid, Celled, GridItem, GridLayouter, ResolvableCell};
|
||||
pub use self::lines::LinePosition;
|
||||
@ -644,6 +645,10 @@ pub struct GridCell {
|
||||
#[default(NonZeroUsize::ONE)]
|
||||
pub colspan: NonZeroUsize,
|
||||
|
||||
/// The amount of rows spanned by this cell.
|
||||
#[default(NonZeroUsize::ONE)]
|
||||
pub rowspan: NonZeroUsize,
|
||||
|
||||
/// The cell's fill override.
|
||||
pub fill: Smart<Option<Paint>>,
|
||||
|
||||
@ -657,6 +662,12 @@ pub struct GridCell {
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
|
||||
|
||||
/// Whether rows spanned by this cell can be placed in different pages.
|
||||
/// When equal to `{auto}`, a cell spanning only fixed-size rows is
|
||||
/// unbreakable, while a cell spanning at least one `{auto}`-sized row is
|
||||
/// breakable.
|
||||
pub breakable: Smart<bool>,
|
||||
}
|
||||
|
||||
cast! {
|
||||
@ -679,10 +690,13 @@ impl ResolvableCell for Packed<GridCell> {
|
||||
align: Smart<Alignment>,
|
||||
inset: Sides<Option<Rel<Length>>>,
|
||||
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
|
||||
breakable: bool,
|
||||
styles: StyleChain,
|
||||
) -> Cell {
|
||||
let cell = &mut *self;
|
||||
let colspan = cell.colspan(styles);
|
||||
let rowspan = cell.rowspan(styles);
|
||||
let breakable = cell.breakable(styles).unwrap_or(breakable);
|
||||
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
|
||||
|
||||
let cell_stroke = cell.stroke(styles);
|
||||
@ -727,12 +741,15 @@ impl ResolvableCell for Packed<GridCell> {
|
||||
}))
|
||||
}),
|
||||
);
|
||||
cell.push_breakable(Smart::Custom(breakable));
|
||||
Cell {
|
||||
body: self.pack(),
|
||||
fill,
|
||||
colspan,
|
||||
rowspan,
|
||||
stroke,
|
||||
stroke_overridden,
|
||||
breakable,
|
||||
}
|
||||
}
|
||||
|
||||
@ -748,6 +765,10 @@ impl ResolvableCell for Packed<GridCell> {
|
||||
(**self).colspan(styles)
|
||||
}
|
||||
|
||||
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
|
||||
(**self).rowspan(styles)
|
||||
}
|
||||
|
||||
fn span(&self) -> Span {
|
||||
Packed::span(self)
|
||||
}
|
||||
|
864
crates/typst/src/layout/grid/rowspans.rs
Normal file
@ -0,0 +1,864 @@
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::Resolve;
|
||||
use crate::layout::{
|
||||
Abs, Axes, Cell, Frame, GridLayouter, LayoutMultiple, Point, Regions, Size, Sizing,
|
||||
};
|
||||
use crate::util::MaybeReverseIter;
|
||||
|
||||
use super::layout::{points, Row};
|
||||
|
||||
/// All information needed to layout a single rowspan.
|
||||
pub(super) struct Rowspan {
|
||||
// First column of this rowspan.
|
||||
pub(super) x: usize,
|
||||
// First row of this rowspan.
|
||||
pub(super) y: usize,
|
||||
// Amount of rows spanned by the cell at (x, y).
|
||||
pub(super) rowspan: usize,
|
||||
/// The horizontal offset of this rowspan in all regions.
|
||||
pub(super) dx: Abs,
|
||||
/// The vertical offset of this rowspan in the first region.
|
||||
pub(super) dy: Abs,
|
||||
/// The index of the first region this rowspan appears in.
|
||||
pub(super) first_region: usize,
|
||||
/// The full height in the first region this rowspan appears in, for
|
||||
/// relative sizing.
|
||||
pub(super) region_full: Abs,
|
||||
/// The vertical space available for this rowspan in each region.
|
||||
pub(super) heights: Vec<Abs>,
|
||||
}
|
||||
|
||||
/// The output of the simulation of an unbreakable row group.
|
||||
#[derive(Default)]
|
||||
pub(super) struct UnbreakableRowGroup {
|
||||
/// The rows in this group of unbreakable rows.
|
||||
/// Includes their indices and their predicted heights.
|
||||
pub(super) rows: Vec<(usize, Abs)>,
|
||||
/// The total height of this row group.
|
||||
pub(super) height: Abs,
|
||||
}
|
||||
|
||||
/// Data used to measure a cell in an auto row.
|
||||
pub(super) struct CellMeasurementData<'layouter> {
|
||||
/// The available width for the cell across all regions.
|
||||
pub(super) width: Abs,
|
||||
/// The available height for the cell in its first region.
|
||||
pub(super) height: Abs,
|
||||
/// The backlog of heights available for the cell in later regions.
|
||||
/// When this is `None`, the `custom_backlog` field should be used instead.
|
||||
pub(super) backlog: Option<&'layouter [Abs]>,
|
||||
/// If the backlog needs to be built from scratch instead of reusing the
|
||||
/// one at the current region, which is the case of a multi-region rowspan
|
||||
/// (needs to join its backlog of already laid out heights with the current
|
||||
/// backlog), then this vector will store the new backlog.
|
||||
pub(super) custom_backlog: Vec<Abs>,
|
||||
/// The full height of the first region of the cell.
|
||||
pub(super) full: Abs,
|
||||
/// The total height of previous rows spanned by the cell in the current
|
||||
/// region (so far).
|
||||
pub(super) height_in_this_region: Abs,
|
||||
/// The amount of previous regions spanned by the cell.
|
||||
/// They are skipped for measurement purposes.
|
||||
pub(super) frames_in_previous_regions: usize,
|
||||
}
|
||||
|
||||
impl<'a> GridLayouter<'a> {
|
||||
/// Layout a rowspan over the already finished regions, plus the current
|
||||
/// region, if it wasn't finished yet (because we're being called from
|
||||
/// `finish_region`, but note that this function is also called once after
|
||||
/// all regions are finished, in which case `current_region` is `None`).
|
||||
///
|
||||
/// We need to do this only once we already know the heights of all
|
||||
/// spanned rows, which is only possible after laying out the last row
|
||||
/// spanned by the rowspan (or some row immediately after the last one).
|
||||
pub(super) fn layout_rowspan(
|
||||
&mut self,
|
||||
rowspan_data: Rowspan,
|
||||
current_region: Option<&mut Frame>,
|
||||
engine: &mut Engine,
|
||||
) -> SourceResult<()> {
|
||||
let Rowspan {
|
||||
x, y, dx, dy, first_region, region_full, heights, ..
|
||||
} = rowspan_data;
|
||||
let [first_height, backlog @ ..] = heights.as_slice() else {
|
||||
// Nothing to layout.
|
||||
return Ok(());
|
||||
};
|
||||
let first_column = self.rcols[x];
|
||||
let cell = self.grid.cell(x, y).unwrap();
|
||||
let width = self.cell_spanned_width(cell, x);
|
||||
let dx = if self.is_rtl { dx - width + first_column } else { dx };
|
||||
|
||||
// Prepare regions.
|
||||
let size = Size::new(width, *first_height);
|
||||
let mut pod = Regions::one(size, Axes::splat(true));
|
||||
pod.full = region_full;
|
||||
pod.backlog = backlog;
|
||||
|
||||
// Push the layouted frames directly into the finished frames.
|
||||
// At first, we draw the rowspan starting at its expected offset
|
||||
// in the first region.
|
||||
let mut pos = Point::new(dx, dy);
|
||||
let fragment = cell.layout(engine, self.styles, pod)?;
|
||||
for (finished, frame) in self
|
||||
.finished
|
||||
.iter_mut()
|
||||
.chain(current_region.into_iter())
|
||||
.skip(first_region)
|
||||
.zip(fragment)
|
||||
{
|
||||
finished.push_frame(pos, frame);
|
||||
|
||||
// From the second region onwards, the rowspan's continuation
|
||||
// starts at the very top.
|
||||
pos.y = Abs::zero();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if a row contains the beginning of one or more rowspan cells.
|
||||
/// If so, adds them to the rowspans vector.
|
||||
pub(super) fn check_for_rowspans(&mut self, y: usize) {
|
||||
// We will compute the horizontal offset of each rowspan in advance.
|
||||
// For that reason, we must reverse the column order when using RTL.
|
||||
let offsets = points(self.rcols.iter().copied().rev_if(self.is_rtl));
|
||||
for (x, dx) in (0..self.rcols.len()).rev_if(self.is_rtl).zip(offsets) {
|
||||
let Some(cell) = self.grid.cell(x, y) else {
|
||||
continue;
|
||||
};
|
||||
let rowspan = self.grid.effective_rowspan_of_cell(cell);
|
||||
if rowspan > 1 {
|
||||
// Rowspan detected. We will lay it out later.
|
||||
self.rowspans.push(Rowspan {
|
||||
x,
|
||||
y,
|
||||
rowspan,
|
||||
dx,
|
||||
// The four fields below will be updated in 'finish_region'.
|
||||
dy: Abs::zero(),
|
||||
first_region: usize::MAX,
|
||||
region_full: Abs::zero(),
|
||||
heights: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the upcoming rows will be grouped together under an
|
||||
/// unbreakable row group, and, if so, advances regions until there is
|
||||
/// enough space for them. This can be needed, for example, if there's an
|
||||
/// unbreakable rowspan crossing those rows.
|
||||
pub(super) fn check_for_unbreakable_rows(
|
||||
&mut self,
|
||||
current_row: usize,
|
||||
engine: &mut Engine,
|
||||
) -> SourceResult<()> {
|
||||
if self.unbreakable_rows_left == 0 {
|
||||
let row_group =
|
||||
self.simulate_unbreakable_row_group(current_row, &self.regions, engine)?;
|
||||
|
||||
// Skip to fitting region.
|
||||
while !self.regions.size.y.fits(row_group.height) && !self.regions.in_last() {
|
||||
self.finish_region(engine)?;
|
||||
}
|
||||
self.unbreakable_rows_left = row_group.rows.len();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Simulates a group of unbreakable rows, starting with the index of the
|
||||
/// first row in the group. Keeps adding rows to the group until none have
|
||||
/// unbreakable cells in common.
|
||||
///
|
||||
/// This is used to figure out how much height the next unbreakable row
|
||||
/// group (if any) needs.
|
||||
pub(super) fn simulate_unbreakable_row_group(
|
||||
&self,
|
||||
first_row: usize,
|
||||
regions: &Regions<'_>,
|
||||
engine: &mut Engine,
|
||||
) -> SourceResult<UnbreakableRowGroup> {
|
||||
let mut row_group = UnbreakableRowGroup::default();
|
||||
let mut unbreakable_rows_left = 0;
|
||||
for (y, row) in self.grid.rows.iter().enumerate().skip(first_row) {
|
||||
let additional_unbreakable_rows = self.check_for_unbreakable_cells(y);
|
||||
unbreakable_rows_left =
|
||||
unbreakable_rows_left.max(additional_unbreakable_rows);
|
||||
if unbreakable_rows_left == 0 {
|
||||
// This check is in case the first row does not have any
|
||||
// unbreakable cells. Therefore, no unbreakable row group
|
||||
// is formed.
|
||||
break;
|
||||
}
|
||||
let height = match row {
|
||||
Sizing::Rel(v) => v.resolve(self.styles).relative_to(regions.base().y),
|
||||
|
||||
// No need to pass the regions to the auto row, since
|
||||
// unbreakable auto rows are always measured with infinite
|
||||
// height, ignore backlog, and do not invoke the rowspan
|
||||
// simulation procedure at all.
|
||||
Sizing::Auto => self
|
||||
.measure_auto_row(
|
||||
engine,
|
||||
y,
|
||||
false,
|
||||
unbreakable_rows_left,
|
||||
Some(&row_group),
|
||||
)?
|
||||
.unwrap()
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or_else(Abs::zero),
|
||||
// Fractional rows don't matter when calculating the space
|
||||
// needed for unbreakable rows
|
||||
Sizing::Fr(_) => Abs::zero(),
|
||||
};
|
||||
row_group.height += height;
|
||||
row_group.rows.push((y, height));
|
||||
unbreakable_rows_left -= 1;
|
||||
if unbreakable_rows_left == 0 {
|
||||
// This second check is necessary so we can tell distinct
|
||||
// but consecutive unbreakable row groups apart. If the
|
||||
// unbreakable row group ended at this row, we stop before
|
||||
// checking the next one.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(row_group)
|
||||
}
|
||||
|
||||
/// Checks if one or more of the cells at the given row are unbreakable.
|
||||
/// If so, returns the largest rowspan among the unbreakable cells;
|
||||
/// the spanned rows must, as a result, be laid out in the same region.
|
||||
pub(super) fn check_for_unbreakable_cells(&self, y: usize) -> usize {
|
||||
(0..self.grid.cols.len())
|
||||
.filter_map(|x| self.grid.cell(x, y))
|
||||
.filter(|cell| !cell.breakable)
|
||||
.map(|cell| self.grid.effective_rowspan_of_cell(cell))
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Used by `measure_auto_row` to gather data needed to measure the cell.
|
||||
pub(super) fn prepare_auto_row_cell_measurement(
|
||||
&self,
|
||||
parent: Axes<usize>,
|
||||
cell: &Cell,
|
||||
breakable: bool,
|
||||
row_group_data: Option<&UnbreakableRowGroup>,
|
||||
) -> CellMeasurementData<'_> {
|
||||
let rowspan = self.grid.effective_rowspan_of_cell(cell);
|
||||
|
||||
// This variable is used to construct a custom backlog if the cell
|
||||
// is a rowspan. When measuring, we join the heights from previous
|
||||
// regions to the current backlog to form the rowspan's expected
|
||||
// backlog.
|
||||
let mut rowspan_backlog: Vec<Abs> = vec![];
|
||||
|
||||
// Each declaration, from top to bottom:
|
||||
// 1. The height available to the cell in the first region.
|
||||
// Usually, this will just be the size remaining in the current
|
||||
// region.
|
||||
// 2. The backlog of upcoming region heights to specify as
|
||||
// available to the cell.
|
||||
// 3. The full height of the first region of the cell.
|
||||
// 4. The total height of the cell covered by previously spanned
|
||||
// rows in this region. This is used by rowspans to be able to tell
|
||||
// how much the auto row needs to expand.
|
||||
// 5. The amount of frames laid out by this cell in previous
|
||||
// regions. When the cell isn't a rowspan, this is always zero.
|
||||
// These frames are skipped after measuring.
|
||||
let (height, backlog, full, height_in_this_region, frames_in_previous_regions);
|
||||
if rowspan == 1 {
|
||||
// Not a rowspan, so the cell only occupies this row. Therefore:
|
||||
// 1. When we measure the cell below, use the available height
|
||||
// remaining in the region as the height it has available.
|
||||
// However, if the auto row is unbreakable, measure with infinite
|
||||
// height instead to see how much content expands.
|
||||
// 2. Also use the region's backlog when measuring.
|
||||
// 3. Use the same full region height.
|
||||
// 4. No height occupied by this cell in this region so far.
|
||||
// 5. Yes, this cell started in this region.
|
||||
height = if breakable { self.regions.size.y } else { Abs::inf() };
|
||||
backlog = Some(self.regions.backlog);
|
||||
full = if breakable { self.regions.full } else { Abs::inf() };
|
||||
height_in_this_region = Abs::zero();
|
||||
frames_in_previous_regions = 0;
|
||||
} else {
|
||||
// Height of the rowspan covered by spanned rows in the current
|
||||
// region.
|
||||
let laid_out_height: Abs = self
|
||||
.lrows
|
||||
.iter()
|
||||
.filter_map(|row| match row {
|
||||
Row::Frame(frame, y, _)
|
||||
if (parent.y..parent.y + rowspan).contains(y) =>
|
||||
{
|
||||
Some(frame.height())
|
||||
}
|
||||
// Either we have a row outside of the rowspan, or a
|
||||
// fractional row, whose size we can't really guess.
|
||||
_ => None,
|
||||
})
|
||||
.sum();
|
||||
|
||||
// If we're currently simulating an unbreakable row group, also
|
||||
// consider the height of previously spanned rows which are in
|
||||
// the row group but not yet laid out.
|
||||
let unbreakable_height: Abs = row_group_data
|
||||
.into_iter()
|
||||
.flat_map(|row_group| &row_group.rows)
|
||||
.filter(|(y, _)| (parent.y..parent.y + rowspan).contains(y))
|
||||
.map(|(_, height)| height)
|
||||
.sum();
|
||||
|
||||
height_in_this_region = laid_out_height + unbreakable_height;
|
||||
|
||||
// Ensure we will measure the rowspan with the correct heights.
|
||||
// For that, we will gather the total height spanned by this
|
||||
// rowspan in previous regions.
|
||||
if let Some((rowspan_full, [rowspan_height, rowspan_other_heights @ ..])) =
|
||||
self.rowspans
|
||||
.iter()
|
||||
.find(|data| data.x == parent.x && data.y == parent.y)
|
||||
.map(|data| (data.region_full, &*data.heights))
|
||||
{
|
||||
// The rowspan started in a previous region (as it already
|
||||
// has at least one region height).
|
||||
// Therefore, its initial height will be the height in its
|
||||
// first spanned region, and the backlog will be the
|
||||
// remaining heights, plus the current region's size, plus
|
||||
// the current backlog.
|
||||
frames_in_previous_regions = rowspan_other_heights.len() + 1;
|
||||
|
||||
let heights_up_to_current_region = rowspan_other_heights
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(std::iter::once(if breakable {
|
||||
self.initial.y
|
||||
} else {
|
||||
// When measuring unbreakable auto rows, infinite
|
||||
// height is available for content to expand.
|
||||
Abs::inf()
|
||||
}));
|
||||
|
||||
rowspan_backlog = if breakable {
|
||||
// This auto row is breakable. Therefore, join the
|
||||
// rowspan's already laid out heights with the current
|
||||
// region's height and current backlog to ensure a good
|
||||
// level of accuracy in the measurements.
|
||||
heights_up_to_current_region
|
||||
.chain(self.regions.backlog.iter().copied())
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
// No extra backlog if this is an unbreakable auto row.
|
||||
// Ensure, when measuring, that the rowspan can be laid
|
||||
// out through all spanned rows which were already laid
|
||||
// out so far, but don't go further than this region.
|
||||
heights_up_to_current_region.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
height = *rowspan_height;
|
||||
backlog = None;
|
||||
full = rowspan_full;
|
||||
} else {
|
||||
// The rowspan started in the current region, as its vector
|
||||
// of heights in regions is currently empty.
|
||||
// Therefore, the initial height it has available will be
|
||||
// the current available size, plus the size spanned in
|
||||
// previous rows in this region (and/or unbreakable row
|
||||
// group, if it's being simulated).
|
||||
// The backlog and full will be that of the current region.
|
||||
// However, use infinite height instead if we're measuring an
|
||||
// unbreakable auto row.
|
||||
height = if breakable {
|
||||
height_in_this_region + self.regions.size.y
|
||||
} else {
|
||||
Abs::inf()
|
||||
};
|
||||
backlog = Some(self.regions.backlog);
|
||||
full = if breakable { self.regions.full } else { Abs::inf() };
|
||||
frames_in_previous_regions = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let width = self.cell_spanned_width(cell, parent.x);
|
||||
CellMeasurementData {
|
||||
width,
|
||||
height,
|
||||
backlog,
|
||||
custom_backlog: rowspan_backlog,
|
||||
full,
|
||||
height_in_this_region,
|
||||
frames_in_previous_regions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Used in `measure_auto_row` to prepare a rowspan's `sizes` vector.
|
||||
/// Returns `true` if we'll need to run a simulation to more accurately
|
||||
/// expand the auto row based on the rowspan's demanded size, or `false`
|
||||
/// otherwise.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn prepare_rowspan_sizes(
|
||||
&self,
|
||||
auto_row_y: usize,
|
||||
sizes: &mut Vec<Abs>,
|
||||
cell: &Cell,
|
||||
parent_y: usize,
|
||||
rowspan: usize,
|
||||
unbreakable_rows_left: usize,
|
||||
measurement_data: &CellMeasurementData<'_>,
|
||||
) -> bool {
|
||||
if sizes.len() <= 1
|
||||
&& sizes.first().map_or(true, |&first_frame_size| {
|
||||
first_frame_size <= measurement_data.height_in_this_region
|
||||
})
|
||||
{
|
||||
// Ignore a rowspan fully covered by rows in previous
|
||||
// regions and/or in the current region.
|
||||
sizes.clear();
|
||||
return false;
|
||||
}
|
||||
if let Some(first_frame_size) = sizes.first_mut() {
|
||||
// Subtract already covered height from the size requested
|
||||
// by this rowspan to the auto row in the first region.
|
||||
*first_frame_size = (*first_frame_size
|
||||
- measurement_data.height_in_this_region)
|
||||
.max(Abs::zero());
|
||||
}
|
||||
|
||||
let last_spanned_row = parent_y + rowspan - 1;
|
||||
|
||||
// When the rowspan is unbreakable, or all of its upcoming
|
||||
// spanned rows are in the same unbreakable row group, its
|
||||
// spanned gutter will certainly be in the same region as all
|
||||
// of its other spanned rows, thus gutters won't be removed,
|
||||
// and we can safely reduce how much the auto row expands by
|
||||
// without using simulation.
|
||||
let is_effectively_unbreakable_rowspan =
|
||||
!cell.breakable || auto_row_y + unbreakable_rows_left > last_spanned_row;
|
||||
|
||||
// If the rowspan doesn't end at this row and the grid has
|
||||
// gutter, we will need to run a simulation to find out how
|
||||
// much to expand this row by later. This is because gutters
|
||||
// spanned by this rowspan might be removed if they appear
|
||||
// around a pagebreak, so the auto row might have to expand a
|
||||
// bit more to compensate for the missing gutter height.
|
||||
// However, unbreakable rowspans aren't affected by that
|
||||
// problem.
|
||||
if auto_row_y != last_spanned_row
|
||||
&& !sizes.is_empty()
|
||||
&& self.grid.has_gutter
|
||||
&& !is_effectively_unbreakable_rowspan
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// We can only predict the resolved size of upcoming fixed-size
|
||||
// rows, but not fractional rows. In the future, we might be
|
||||
// able to simulate and circumvent the problem with fractional
|
||||
// rows. Relative rows are currently always measured relative
|
||||
// to the first region as well.
|
||||
// We can ignore auto rows since this is the last spanned auto
|
||||
// row.
|
||||
let will_be_covered_height: Abs = self
|
||||
.grid
|
||||
.rows
|
||||
.iter()
|
||||
.skip(auto_row_y + 1)
|
||||
.take(last_spanned_row - auto_row_y)
|
||||
.map(|row| match row {
|
||||
Sizing::Rel(v) => {
|
||||
v.resolve(self.styles).relative_to(self.regions.base().y)
|
||||
}
|
||||
_ => Abs::zero(),
|
||||
})
|
||||
.sum();
|
||||
|
||||
// Remove or reduce the sizes of the rowspan at the current or future
|
||||
// regions where it will already be covered by further rows spanned by
|
||||
// it.
|
||||
subtract_end_sizes(sizes, will_be_covered_height);
|
||||
|
||||
// No need to run a simulation for this rowspan.
|
||||
false
|
||||
}
|
||||
|
||||
/// Performs a simulation to predict by how much height the last spanned
|
||||
/// auto row will have to expand, given the current sizes of the auto row
|
||||
/// in each region and the pending rowspans' data (parent Y, rowspan amount
|
||||
/// and vector of requested sizes).
|
||||
pub(super) fn simulate_and_measure_rowspans_in_auto_row(
|
||||
&self,
|
||||
y: usize,
|
||||
resolved: &mut Vec<Abs>,
|
||||
pending_rowspans: &[(usize, usize, Vec<Abs>)],
|
||||
unbreakable_rows_left: usize,
|
||||
row_group_data: Option<&UnbreakableRowGroup>,
|
||||
engine: &mut Engine,
|
||||
) -> SourceResult<()> {
|
||||
// To begin our simulation, we have to unify the sizes demanded by
|
||||
// each rowspan into one simple vector of sizes, as if they were
|
||||
// all a single rowspan. These sizes will be appended to
|
||||
// 'resolved' once we finish our simulation.
|
||||
let mut simulated_sizes: Vec<Abs> = vec![];
|
||||
let last_resolved_size = resolved.last().copied();
|
||||
let mut max_spanned_row = y;
|
||||
for (parent_y, rowspan, sizes) in pending_rowspans {
|
||||
let mut sizes = sizes.iter();
|
||||
for (target, size) in resolved.iter_mut().zip(&mut sizes) {
|
||||
// First, we update the already resolved sizes as required
|
||||
// by this rowspan. No need to simulate this since the auto row
|
||||
// will already expand throughout already resolved regions.
|
||||
// Our simulation, therefore, won't otherwise change already
|
||||
// resolved sizes, other than, perhaps, the last one (at the
|
||||
// last currently resolved region, at which we can expand).
|
||||
target.set_max(*size);
|
||||
}
|
||||
for (simulated_target, rowspan_size) in
|
||||
simulated_sizes.iter_mut().zip(&mut sizes)
|
||||
{
|
||||
// The remaining sizes are exclusive to rowspans, since
|
||||
// other cells in this row didn't require as many regions.
|
||||
// We will perform a simulation to see how much of these sizes
|
||||
// does the auto row actually need to expand by, and how much
|
||||
// is already covered by upcoming rows spanned by the rowspans.
|
||||
simulated_target.set_max(*rowspan_size);
|
||||
}
|
||||
simulated_sizes.extend(sizes);
|
||||
max_spanned_row = max_spanned_row.max(parent_y + rowspan - 1);
|
||||
}
|
||||
if simulated_sizes.is_empty() && resolved.last() == last_resolved_size.as_ref() {
|
||||
// The rowspans already fit in the already resolved sizes.
|
||||
// No need for simulation.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// We will be updating the last resolved size (expanding the auto
|
||||
// row) as needed. Therefore, consider it as part of the simulation.
|
||||
// At the end, we push it back.
|
||||
if let Some(modified_last_resolved_size) = resolved.pop() {
|
||||
simulated_sizes.insert(0, modified_last_resolved_size);
|
||||
}
|
||||
|
||||
// Prepare regions for simulation.
|
||||
// If we're currently inside an unbreakable row group simulation,
|
||||
// subtract the current row group height from the available space
|
||||
// when simulating rowspans in said group.
|
||||
let mut simulated_regions = self.regions;
|
||||
simulated_regions.size.y -=
|
||||
row_group_data.map_or(Abs::zero(), |row_group| row_group.height);
|
||||
|
||||
for _ in 0..resolved.len() {
|
||||
// Ensure we start at the region where we will expand the auto
|
||||
// row.
|
||||
// Note that we won't accidentally call '.next()' once more than
|
||||
// desired (we won't skip the last resolved frame, where we will
|
||||
// expand) because we popped the last resolved size from the
|
||||
// resolved vector, above.
|
||||
simulated_regions.next();
|
||||
}
|
||||
if let Some(original_last_resolved_size) = last_resolved_size {
|
||||
// We're now at the (current) last region of this auto row.
|
||||
// Consider resolved height as already taken space.
|
||||
simulated_regions.size.y -= original_last_resolved_size;
|
||||
}
|
||||
|
||||
// Now we run the simulation to check how much the auto row needs to
|
||||
// grow to ensure that rowspans have the height they need.
|
||||
let simulations_stabilized = self.run_rowspan_simulation(
|
||||
y,
|
||||
max_spanned_row,
|
||||
simulated_regions,
|
||||
&mut simulated_sizes,
|
||||
engine,
|
||||
last_resolved_size,
|
||||
unbreakable_rows_left,
|
||||
)?;
|
||||
|
||||
if !simulations_stabilized {
|
||||
// If the simulation didn't stabilize above, we will just pretend
|
||||
// all gutters were removed, as a best effort. That means the auto
|
||||
// row will expand more than it normally should, but there isn't
|
||||
// much we can do.
|
||||
let will_be_covered_height = self
|
||||
.grid
|
||||
.rows
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(y + 1)
|
||||
.take(max_spanned_row - y)
|
||||
.filter(|(y, _)| !self.grid.is_gutter_track(*y))
|
||||
.map(|(_, row)| match row {
|
||||
Sizing::Rel(v) => {
|
||||
v.resolve(self.styles).relative_to(self.regions.base().y)
|
||||
}
|
||||
_ => Abs::zero(),
|
||||
})
|
||||
.sum();
|
||||
|
||||
subtract_end_sizes(&mut simulated_sizes, will_be_covered_height);
|
||||
}
|
||||
|
||||
resolved.extend(simulated_sizes);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Performs a simulation of laying out multiple rowspans (consolidated
|
||||
/// into a single vector of simulated sizes) ending in a certain auto row
|
||||
/// in order to find out how much the auto row will need to expand to cover
|
||||
/// the rowspans' requested sizes, considering how much size has been
|
||||
/// covered by other rows and by gutter between rows.
|
||||
///
|
||||
/// For example, for a rowspan cell containing a block of 8pt of height
|
||||
/// spanning rows (1pt, auto, 0.5pt, 0.5pt), with a gutter of 1pt between
|
||||
/// each row, we have that the rows it spans provide 1pt + 0.5pt + 0.5pt
|
||||
/// = 2pt of height, plus 1pt + 1pt + 1pt = 3pt of gutter, with a total of
|
||||
/// 2pt + 3pt = 5pt of height already covered by fixed-size rows and
|
||||
/// gutters. This means that the auto row must (under normal conditions)
|
||||
/// expand by 3pt (8pt - 5pt) so that the rowspan has enough height across
|
||||
/// rows to fully draw its contents.
|
||||
///
|
||||
/// However, it's possible that the last row is sent to the next page to
|
||||
/// respect a pagebreak, and then the 1pt gutter before it disappears. This
|
||||
/// would lead to our rowspan having a height of 7pt available if we fail
|
||||
/// to predict this situation when measuring the auto row.
|
||||
///
|
||||
/// The algorithm below will, thus, attempt to simulate the layout of each
|
||||
/// spanned row, considering the space available in the current page and in
|
||||
/// upcoming pages (through the region backlog), in order to predict which
|
||||
/// rows will be sent to a new page and thus have their preceding gutter
|
||||
/// spacing removed (meaning the auto row has to grow a bit more). After
|
||||
/// simulating, we subtract the total height spanned by upcoming rows and
|
||||
/// gutter from the total rowspan height - this will be how much our auto
|
||||
/// row has to expand. We then simulate again to check if, if the auto row
|
||||
/// expanded by that amount, that would prompt the auto row to need to
|
||||
/// expand even more, because expanding the auto row might cause some other
|
||||
/// larger gutter spacing to disappear (leading to the rowspan having less
|
||||
/// space available instead of more); if so, we update the amount to expand
|
||||
/// and run the simulation again. Otherwise (if it should expand by the
|
||||
/// same amount, meaning we predicted correctly, or by less, meaning the
|
||||
/// auto row will be a bit larger than it should be, but that's a
|
||||
/// compromise we're willing to accept), we conclude the simulation
|
||||
/// (consider it stabilized) and return the result.
|
||||
///
|
||||
/// Tries up to 5 times. If two consecutive simulations stabilize, then
|
||||
/// we subtract the predicted expansion height ('amount_to_grow') from the
|
||||
/// total height requested by rowspans (the 'requested_rowspan_height') to
|
||||
/// obtain how much height is covered by upcoming rows, according to our
|
||||
/// simulation, and the result of that operation is used to reduce or
|
||||
/// remove heights from the end of the vector of simulated sizes, such that
|
||||
/// the remaining heights are exactly how much the auto row should expand
|
||||
/// by. Then, we return `true`.
|
||||
///
|
||||
/// If the simulations don't stabilize (they return 5 different and
|
||||
/// successively larger values), aborts and returns `false`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn run_rowspan_simulation(
|
||||
&self,
|
||||
y: usize,
|
||||
max_spanned_row: usize,
|
||||
mut simulated_regions: Regions<'_>,
|
||||
simulated_sizes: &mut Vec<Abs>,
|
||||
engine: &mut Engine,
|
||||
last_resolved_size: Option<Abs>,
|
||||
unbreakable_rows_left: usize,
|
||||
) -> SourceResult<bool> {
|
||||
// The max amount this row can expand will be the total size requested
|
||||
// by rowspans which was not yet resolved. It is worth noting that,
|
||||
// earlier, we pushed the last resolved size to 'simulated_sizes' as
|
||||
// row expansion starts with it, so it's possible a rowspan requested
|
||||
// to extend that size (we will see, through the simulation, if that's
|
||||
// needed); however, we must subtract that resolved size from the total
|
||||
// sum of sizes, as it was already resolved and thus the auto row will
|
||||
// already grow by at least that much in the last resolved region (we
|
||||
// would grow by the same size twice otherwise).
|
||||
let requested_rowspan_height =
|
||||
simulated_sizes.iter().sum::<Abs>() - last_resolved_size.unwrap_or_default();
|
||||
|
||||
// The amount the row will effectively grow by, according to the latest
|
||||
// simulation.
|
||||
let mut amount_to_grow = Abs::zero();
|
||||
|
||||
// Try to simulate up to 5 times. If it doesn't stabilize at a value
|
||||
// which, when used and combined with upcoming spanned rows, covers all
|
||||
// of the requested rowspan height, we give up.
|
||||
for _attempt in 0..5 {
|
||||
let mut regions = simulated_regions;
|
||||
let mut total_spanned_height = Abs::zero();
|
||||
let mut unbreakable_rows_left = unbreakable_rows_left;
|
||||
|
||||
// Height of the latest spanned gutter row.
|
||||
// Zero if it was removed.
|
||||
let mut latest_spanned_gutter_height = Abs::zero();
|
||||
let spanned_rows = &self.grid.rows[y + 1..=max_spanned_row];
|
||||
for (offset, row) in spanned_rows.iter().enumerate() {
|
||||
if (total_spanned_height + amount_to_grow).fits(requested_rowspan_height)
|
||||
{
|
||||
// Stop the simulation, as the combination of upcoming
|
||||
// spanned rows (so far) and the current amount the auto
|
||||
// row expands by has already fully covered the height the
|
||||
// rowspans need.
|
||||
break;
|
||||
}
|
||||
let spanned_y = y + 1 + offset;
|
||||
let is_gutter = self.grid.is_gutter_track(spanned_y);
|
||||
|
||||
if unbreakable_rows_left == 0 {
|
||||
// Simulate unbreakable row groups, and skip regions until
|
||||
// they fit. There is no risk of infinite recursion, as
|
||||
// no auto rows participate in the simulation, so the
|
||||
// unbreakable row group simulator won't recursively call
|
||||
// 'measure_auto_row' or (consequently) this function.
|
||||
let row_group =
|
||||
self.simulate_unbreakable_row_group(spanned_y, ®ions, engine)?;
|
||||
while !regions.size.y.fits(row_group.height) && !regions.in_last() {
|
||||
total_spanned_height -= latest_spanned_gutter_height;
|
||||
latest_spanned_gutter_height = Abs::zero();
|
||||
regions.next();
|
||||
}
|
||||
|
||||
unbreakable_rows_left = row_group.rows.len();
|
||||
}
|
||||
|
||||
match row {
|
||||
// Fixed-size spanned rows are what we are interested in.
|
||||
// They contribute a fixed amount of height to our rowspan.
|
||||
Sizing::Rel(v) => {
|
||||
let height = v.resolve(self.styles).relative_to(regions.base().y);
|
||||
total_spanned_height += height;
|
||||
if is_gutter {
|
||||
latest_spanned_gutter_height = height;
|
||||
}
|
||||
|
||||
let mut skipped_region = false;
|
||||
while unbreakable_rows_left == 0
|
||||
&& !regions.size.y.fits(height)
|
||||
&& !regions.in_last()
|
||||
{
|
||||
// A row was pushed to the next region. Therefore,
|
||||
// the immediately preceding gutter row is removed.
|
||||
total_spanned_height -= latest_spanned_gutter_height;
|
||||
latest_spanned_gutter_height = Abs::zero();
|
||||
skipped_region = true;
|
||||
regions.next();
|
||||
}
|
||||
|
||||
if !skipped_region || !is_gutter {
|
||||
// No gutter at the top of a new region, so don't
|
||||
// account for it if we just skipped a region.
|
||||
regions.size.y -= height;
|
||||
}
|
||||
}
|
||||
Sizing::Auto => {
|
||||
// We only simulate for rowspans which end at the
|
||||
// current auto row. Therefore, there won't be any
|
||||
// further auto rows.
|
||||
unreachable!();
|
||||
}
|
||||
// For now, we ignore fractional rows on simulation.
|
||||
Sizing::Fr(_) if is_gutter => {
|
||||
latest_spanned_gutter_height = Abs::zero();
|
||||
}
|
||||
Sizing::Fr(_) => {}
|
||||
}
|
||||
|
||||
unbreakable_rows_left = unbreakable_rows_left.saturating_sub(1);
|
||||
}
|
||||
|
||||
// If the total height spanned by upcoming spanned rows plus the
|
||||
// current amount we predict the auto row will have to grow (from
|
||||
// the previous iteration) are larger than the size requested by
|
||||
// rowspans, this means the auto row will grow enough in order to
|
||||
// cover the requested rowspan height, so we stop the simulation.
|
||||
//
|
||||
// If that's not yet the case, we will simulate again and make the
|
||||
// auto row grow even more, and do so until either the auto row has
|
||||
// grown enough, or we tried to do so over 5 times.
|
||||
//
|
||||
// A flaw of this approach is that we consider rowspans' content to
|
||||
// be contiguous. That is, we treat rowspans' requested heights as
|
||||
// a simple number, instead of properly using the vector of
|
||||
// requested heights in each region. This can lead to some
|
||||
// weirdness when using multi-page rowspans with content that
|
||||
// reacts to the amount of space available, including paragraphs.
|
||||
// However, this is probably the best we can do for now.
|
||||
if (total_spanned_height + amount_to_grow).fits(requested_rowspan_height) {
|
||||
// Reduce sizes by the amount to be covered by upcoming spanned
|
||||
// rows, which is equivalent to the amount that we don't grow.
|
||||
// We reduce from the end as that's where the spanned rows will
|
||||
// cover. The remaining sizes will all be covered by the auto
|
||||
// row instead (which will grow by those sizes).
|
||||
subtract_end_sizes(
|
||||
simulated_sizes,
|
||||
requested_rowspan_height - amount_to_grow,
|
||||
);
|
||||
|
||||
if let Some(last_resolved_size) = last_resolved_size {
|
||||
// Ensure the first simulated size is at least as large as
|
||||
// the last resolved size (its initial value). As it was
|
||||
// already resolved before, we must not reduce below the
|
||||
// resolved size to avoid problems with non-rowspan cells.
|
||||
if let Some(first_simulated_size) = simulated_sizes.first_mut() {
|
||||
first_simulated_size.set_max(last_resolved_size);
|
||||
} else {
|
||||
simulated_sizes.push(last_resolved_size);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// For the next simulation, we will test if the auto row can grow
|
||||
// by precisely how much rowspan height is not covered by upcoming
|
||||
// spanned rows, according to the current simulation.
|
||||
// We know that the new amount to grow is larger (and thus the
|
||||
// auto row only expands between each simulation), because we
|
||||
// checked above if
|
||||
// 'total_spanned_height + (now old_)amount_to_grow >= requested_rowspan_height',
|
||||
// which was false, so it holds that
|
||||
// 'total_spanned_height + old_amount_to_grow < requested_rowspan_height'
|
||||
// Thus,
|
||||
// 'old_amount_to_grow < requested_rowspan_height - total_spanned_height'
|
||||
// Therefore, by definition, 'old_amount_to_grow < amount_to_grow'.
|
||||
let old_amount_to_grow = std::mem::replace(
|
||||
&mut amount_to_grow,
|
||||
requested_rowspan_height - total_spanned_height,
|
||||
);
|
||||
|
||||
// We advance the 'regions' variable accordingly, so that, in the
|
||||
// next simulation, we consider already grown space as final.
|
||||
// That is, we effectively simulate how rows would be placed if the
|
||||
// auto row grew by precisely the new value of 'amount_to_grow'.
|
||||
let mut extra_amount_to_grow = amount_to_grow - old_amount_to_grow;
|
||||
while extra_amount_to_grow > Abs::zero()
|
||||
&& simulated_regions.size.y < extra_amount_to_grow
|
||||
{
|
||||
extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero());
|
||||
simulated_regions.next();
|
||||
}
|
||||
simulated_regions.size.y -= extra_amount_to_grow;
|
||||
}
|
||||
|
||||
// Simulation didn't succeed in 5 attempts.
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Subtracts some size from the end of a vector of sizes.
|
||||
/// For example, subtracting 5pt from \[2pt, 1pt, 3pt\] will result in \[1pt\].
|
||||
fn subtract_end_sizes(sizes: &mut Vec<Abs>, mut subtract: Abs) {
|
||||
while subtract > Abs::zero() && sizes.last().is_some_and(|&size| size <= subtract) {
|
||||
subtract -= sizes.pop().unwrap();
|
||||
}
|
||||
if subtract > Abs::zero() {
|
||||
if let Some(last_size) = sizes.last_mut() {
|
||||
*last_size -= subtract;
|
||||
}
|
||||
}
|
||||
}
|
@ -535,6 +535,10 @@ pub struct TableCell {
|
||||
#[default(NonZeroUsize::ONE)]
|
||||
pub colspan: NonZeroUsize,
|
||||
|
||||
/// The amount of rows spanned by this cell.
|
||||
#[default(NonZeroUsize::ONE)]
|
||||
rowspan: NonZeroUsize,
|
||||
|
||||
/// The cell's alignment override.
|
||||
pub align: Smart<Alignment>,
|
||||
|
||||
@ -545,6 +549,12 @@ pub struct TableCell {
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
|
||||
|
||||
/// Whether rows spanned by this cell can be placed in different pages.
|
||||
/// When equal to `{auto}`, a cell spanning only fixed-size rows is
|
||||
/// unbreakable, while a cell spanning at least one `{auto}`-sized row is
|
||||
/// breakable.
|
||||
pub breakable: Smart<bool>,
|
||||
}
|
||||
|
||||
cast! {
|
||||
@ -567,10 +577,13 @@ impl ResolvableCell for Packed<TableCell> {
|
||||
align: Smart<Alignment>,
|
||||
inset: Sides<Option<Rel<Length>>>,
|
||||
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
|
||||
breakable: bool,
|
||||
styles: StyleChain,
|
||||
) -> Cell {
|
||||
let cell = &mut *self;
|
||||
let colspan = cell.colspan(styles);
|
||||
let rowspan = cell.rowspan(styles);
|
||||
let breakable = cell.breakable(styles).unwrap_or(breakable);
|
||||
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
|
||||
|
||||
let cell_stroke = cell.stroke(styles);
|
||||
@ -615,12 +628,15 @@ impl ResolvableCell for Packed<TableCell> {
|
||||
}))
|
||||
}),
|
||||
);
|
||||
cell.push_breakable(Smart::Custom(breakable));
|
||||
Cell {
|
||||
body: self.pack(),
|
||||
fill,
|
||||
colspan,
|
||||
rowspan,
|
||||
stroke,
|
||||
stroke_overridden,
|
||||
breakable,
|
||||
}
|
||||
}
|
||||
|
||||
@ -632,10 +648,14 @@ impl ResolvableCell for Packed<TableCell> {
|
||||
(**self).y(styles)
|
||||
}
|
||||
|
||||
fn colspan(&self, styles: StyleChain) -> std::num::NonZeroUsize {
|
||||
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
|
||||
(**self).colspan(styles)
|
||||
}
|
||||
|
||||
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
|
||||
(**self).rowspan(styles)
|
||||
}
|
||||
|
||||
fn span(&self) -> Span {
|
||||
Packed::span(self)
|
||||
}
|
||||
|
BIN
tests/ref/bugs/grid-4.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
tests/ref/layout/grid-rowspan-basic.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
tests/ref/layout/grid-rowspan-split-1.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
tests/ref/layout/grid-rowspan-split-2.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
tests/ref/layout/grid-rowspan-split-3.png
Normal file
After Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 54 KiB |
17
tests/typ/bugs/grid-4.typ
Normal file
@ -0,0 +1,17 @@
|
||||
// Ensure gutter rows at the top or bottom of a region are skipped.
|
||||
|
||||
---
|
||||
#set page(height: 10em)
|
||||
|
||||
#table(
|
||||
row-gutter: 1.5em,
|
||||
inset: 0pt,
|
||||
rows: (1fr, auto),
|
||||
[a],
|
||||
[],
|
||||
[],
|
||||
[f],
|
||||
[e\ e],
|
||||
[],
|
||||
[a]
|
||||
)
|
@ -81,7 +81,7 @@
|
||||
|
||||
---
|
||||
// Error: 4:8-4:32 cell would span a previously placed cell at column 2, row 0
|
||||
// Hint: 4:8-4:32 try specifying your cells in a different order or reducing the cell's colspan
|
||||
// Hint: 4:8-4:32 try specifying your cells in a different order or reducing the cell's rowspan or colspan
|
||||
#grid(
|
||||
columns: 3,
|
||||
grid.cell(x: 2, y: 0)[x],
|
||||
|
@ -221,3 +221,11 @@
|
||||
fill: (x, y) => if calc.odd(x + y) { red.lighten(50%) } else { green },
|
||||
table.cell(x: 2, y: 6148914691236517206)[a],
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 3:3-3:45 cell would span an exceedingly large position
|
||||
// Hint: 3:3-3:45 try reducing the cell's rowspan or colspan
|
||||
#grid(
|
||||
columns: 500,
|
||||
grid.cell(rowspan: 6148914691236517206)[a]
|
||||
)
|
||||
|
211
tests/typ/layout/grid-rowspan-basic.typ
Normal file
@ -0,0 +1,211 @@
|
||||
#grid(
|
||||
columns: 4,
|
||||
fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
|
||||
inset: 5pt,
|
||||
align: center,
|
||||
grid.cell(rowspan: 2, fill: orange)[*Left*],
|
||||
[Right A], [Right A], [Right A],
|
||||
[Right B], grid.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
|
||||
[Left A], [Left A],
|
||||
[Left B], [Left B], grid.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
|
||||
)
|
||||
|
||||
#table(
|
||||
columns: 4,
|
||||
fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
|
||||
inset: 5pt,
|
||||
align: center,
|
||||
table.cell(rowspan: 2, fill: orange)[*Left*],
|
||||
[Right A], [Right A], [Right A],
|
||||
[Right B], table.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
|
||||
[Left A], [Left A],
|
||||
[Left B], [Left B], table.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
|
||||
)
|
||||
|
||||
---
|
||||
#grid(
|
||||
columns: 4,
|
||||
fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
|
||||
inset: 5pt,
|
||||
align: center,
|
||||
gutter: 3pt,
|
||||
grid.cell(rowspan: 2, fill: orange)[*Left*],
|
||||
[Right A], [Right A], [Right A],
|
||||
[Right B], grid.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
|
||||
[Left A], [Left A],
|
||||
[Left B], [Left B], grid.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
|
||||
)
|
||||
|
||||
#table(
|
||||
columns: 4,
|
||||
fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
|
||||
inset: 5pt,
|
||||
align: center,
|
||||
gutter: 3pt,
|
||||
table.cell(rowspan: 2, fill: orange)[*Left*],
|
||||
[Right A], [Right A], [Right A],
|
||||
[Right B], table.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
|
||||
[Left A], [Left A],
|
||||
[Left B], [Left B], table.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
|
||||
)
|
||||
|
||||
---
|
||||
// Fixed-size rows
|
||||
#set page(height: 10em)
|
||||
#grid(
|
||||
columns: 2,
|
||||
rows: 1.5em,
|
||||
fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
|
||||
grid.cell(rowspan: 3)[R1], [b],
|
||||
[c],
|
||||
[d],
|
||||
[e], [f],
|
||||
grid.cell(rowspan: 5)[R2], [h],
|
||||
[i],
|
||||
[j],
|
||||
[k],
|
||||
[l],
|
||||
[m], [n]
|
||||
)
|
||||
|
||||
---
|
||||
// Cell coordinate tests
|
||||
#set page(height: 10em)
|
||||
#show table.cell: it => [(#it.x, #it.y)]
|
||||
#table(
|
||||
columns: 3,
|
||||
fill: red,
|
||||
[a], [b], table.cell(rowspan: 2)[c],
|
||||
table.cell(colspan: 2)[d],
|
||||
table.cell(colspan: 3, rowspan: 10)[a],
|
||||
table.cell(colspan: 2)[b],
|
||||
)
|
||||
#table(
|
||||
columns: 3,
|
||||
gutter: 3pt,
|
||||
fill: red,
|
||||
[a], [b], table.cell(rowspan: 2)[c],
|
||||
table.cell(colspan: 2)[d],
|
||||
table.cell(colspan: 3, rowspan: 9)[a],
|
||||
table.cell(colspan: 2)[b],
|
||||
)
|
||||
|
||||
---
|
||||
// Auto row expansion
|
||||
#set page(height: 10em)
|
||||
#grid(
|
||||
columns: (1em, 1em),
|
||||
rows: (0.5em, 0.5em, auto),
|
||||
fill: orange,
|
||||
gutter: 3pt,
|
||||
grid.cell(rowspan: 4, [x x x x] + place(bottom)[*Bot*]),
|
||||
[a],
|
||||
[b],
|
||||
[c],
|
||||
[d]
|
||||
)
|
||||
|
||||
---
|
||||
// Excessive rowspan (no gutter)
|
||||
#set page(height: 10em)
|
||||
#table(
|
||||
columns: 4,
|
||||
fill: red,
|
||||
[a], [b], table.cell(rowspan: 2)[c], [d],
|
||||
table.cell(colspan: 2, stroke: (bottom: aqua + 2pt))[e], table.cell(stroke: (bottom: aqua))[f],
|
||||
table.cell(colspan: 2, rowspan: 10)[R1], table.cell(colspan: 2, rowspan: 10)[R2],
|
||||
[b],
|
||||
)
|
||||
|
||||
---
|
||||
// Excessive rowspan (with gutter)
|
||||
#set page(height: 10em)
|
||||
#table(
|
||||
columns: 4,
|
||||
gutter: 3pt,
|
||||
fill: red,
|
||||
[a], [b], table.cell(rowspan: 2)[c], [d],
|
||||
table.cell(colspan: 2, stroke: (bottom: aqua + 2pt))[e], table.cell(stroke: (bottom: aqua))[f],
|
||||
table.cell(colspan: 2, rowspan: 10)[R1], table.cell(colspan: 2, rowspan: 10)[R2],
|
||||
[b],
|
||||
)
|
||||
|
||||
---
|
||||
// Fractional rows
|
||||
// They cause the auto row to expand more than needed.
|
||||
#set page(height: 10em)
|
||||
#grid(
|
||||
fill: red,
|
||||
gutter: 3pt,
|
||||
columns: 3,
|
||||
rows: (1em, auto, 1fr),
|
||||
[a], [b], grid.cell(rowspan: 3, block(height: 4em, width: 1em, fill: orange)),
|
||||
[c], [d],
|
||||
[e], [f]
|
||||
)
|
||||
|
||||
---
|
||||
// Fractional rows
|
||||
#set page(height: 10em)
|
||||
#grid(
|
||||
fill: red,
|
||||
gutter: 3pt,
|
||||
columns: 3,
|
||||
rows: (1fr, auto, 1em),
|
||||
[a], [b], grid.cell(rowspan: 3, block(height: 4em, width: 1em, fill: orange)),
|
||||
[c], [d],
|
||||
[e], [f]
|
||||
)
|
||||
|
||||
---
|
||||
// Cell order
|
||||
#let count = counter("count")
|
||||
#show grid.cell: it => {
|
||||
count.step()
|
||||
count.display()
|
||||
}
|
||||
|
||||
#grid(
|
||||
columns: (2em,) * 3,
|
||||
stroke: aqua,
|
||||
rows: 1.2em,
|
||||
fill: (x, y) => if calc.odd(x + y) { red } else { orange },
|
||||
[a], grid.cell(rowspan: 2)[b], grid.cell(rowspan: 2)[c],
|
||||
[d],
|
||||
grid.cell(rowspan: 2)[f], [g], [h],
|
||||
[i], [j],
|
||||
[k], [l], [m],
|
||||
grid.cell(rowspan: 2)[n], [o], [p],
|
||||
[q], [r],
|
||||
[s], [t], [u]
|
||||
)
|
||||
|
||||
---
|
||||
#table(
|
||||
columns: 3,
|
||||
rows: (auto, auto, auto, 2em),
|
||||
gutter: 3pt,
|
||||
table.cell(rowspan: 4)[a \ b\ c\ d\ e], [c], [d],
|
||||
[e], table.cell(breakable: false, rowspan: 2)[f],
|
||||
[g]
|
||||
)
|
||||
|
||||
---
|
||||
// Test cell breakability
|
||||
#show grid.cell: it => {
|
||||
assert.eq(it.breakable, (it.x, it.y) != (0, 6) and (it.y in (2, 5, 6) or (it.x, it.y) in ((0, 1), (2, 3), (1, 7))))
|
||||
it.breakable
|
||||
}
|
||||
#grid(
|
||||
columns: 3,
|
||||
rows: (6pt, 1fr, auto, 1%, 1em, auto, auto, 0.2in),
|
||||
row-gutter: (0pt, 0pt, 0pt, auto),
|
||||
[a], [b], [c],
|
||||
grid.cell(rowspan: 3)[d], [e], [f],
|
||||
[g], [h],
|
||||
[i], grid.cell(rowspan: 2)[j],
|
||||
[k],
|
||||
grid.cell(y: 5)[l],
|
||||
grid.cell(y: 6, breakable: false)[m], grid.cell(y: 6, breakable: true)[n],
|
||||
grid.cell(y: 7, breakable: false)[o], grid.cell(y: 7, breakable: true)[p], grid.cell(y: 7, breakable: auto)[q]
|
||||
)
|
89
tests/typ/layout/grid-rowspan-split-1.typ
Normal file
@ -0,0 +1,89 @@
|
||||
// Rowspan split tests
|
||||
|
||||
---
|
||||
#set page(height: 10em)
|
||||
#table(
|
||||
columns: 2,
|
||||
rows: (auto, auto, 3em),
|
||||
fill: red,
|
||||
[a], table.cell(rowspan: 3, block(width: 50%, height: 10em, fill: orange) + place(bottom)[*ZD*]),
|
||||
[e],
|
||||
[f]
|
||||
)
|
||||
|
||||
---
|
||||
#set page(height: 10em)
|
||||
#table(
|
||||
columns: 2,
|
||||
rows: (auto, auto, 3em),
|
||||
row-gutter: 1em,
|
||||
fill: red,
|
||||
[a], table.cell(rowspan: 3, block(width: 50%, height: 10em, fill: orange) + place(bottom)[*ZD*]),
|
||||
[e],
|
||||
[f]
|
||||
)
|
||||
|
||||
---
|
||||
#set page(height: 5em)
|
||||
#table(
|
||||
columns: 2,
|
||||
fill: red,
|
||||
inset: 0pt,
|
||||
table.cell(fill: orange, rowspan: 10, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]),
|
||||
..([y],) * 10,
|
||||
[a], [b],
|
||||
)
|
||||
|
||||
---
|
||||
#set page(height: 5em)
|
||||
#table(
|
||||
columns: 2,
|
||||
fill: red,
|
||||
inset: 0pt,
|
||||
gutter: 2pt,
|
||||
table.cell(fill: orange, rowspan: 10, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]),
|
||||
..([y],) * 10,
|
||||
[a], [b],
|
||||
)
|
||||
|
||||
---
|
||||
#set page(height: 5em)
|
||||
#table(
|
||||
columns: 2,
|
||||
fill: red,
|
||||
inset: 0pt,
|
||||
table.cell(fill: orange, rowspan: 10, breakable: false, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]),
|
||||
..([y],) * 10,
|
||||
[a], [b],
|
||||
)
|
||||
|
||||
---
|
||||
#set page(height: 5em)
|
||||
#table(
|
||||
columns: 2,
|
||||
fill: red,
|
||||
inset: 0pt,
|
||||
gutter: 2pt,
|
||||
table.cell(fill: orange, rowspan: 10, breakable: false, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]),
|
||||
..([y],) * 10,
|
||||
[a], [b],
|
||||
)
|
||||
|
||||
---
|
||||
#set page(height: 5em)
|
||||
#grid(
|
||||
columns: 2,
|
||||
stroke: red,
|
||||
inset: 5pt,
|
||||
grid.cell(rowspan: 5)[a\ b\ c\ d\ e]
|
||||
)
|
||||
|
||||
---
|
||||
#set page(height: 5em)
|
||||
#table(
|
||||
columns: 2,
|
||||
gutter: 3pt,
|
||||
stroke: red,
|
||||
inset: 5pt,
|
||||
table.cell(rowspan: 5)[a\ b\ c\ d\ e]
|
||||
)
|
37
tests/typ/layout/grid-rowspan-split-2.typ
Normal file
@ -0,0 +1,37 @@
|
||||
// Rowspan split without ending at the auto row
|
||||
|
||||
---
|
||||
#set page(height: 6em)
|
||||
#table(
|
||||
rows: (4em,) * 7 + (auto,) + (4em,) * 7,
|
||||
columns: 2,
|
||||
column-gutter: 1em,
|
||||
row-gutter: (1em, 2em) * 4,
|
||||
fill: (x, y) => if calc.odd(x + y) { orange.lighten(20%) } else { red },
|
||||
table.cell(rowspan: 15, [a \ ] * 15),
|
||||
[] * 15
|
||||
)
|
||||
|
||||
---
|
||||
#set page(height: 6em)
|
||||
#table(
|
||||
rows: (4em,) * 7 + (auto,) + (4em,) * 7,
|
||||
columns: 2,
|
||||
column-gutter: 1em,
|
||||
row-gutter: (1em, 2em) * 4,
|
||||
fill: (x, y) => if calc.odd(x + y) { green } else { green.darken(40%) },
|
||||
table.cell(rowspan: 15, block(fill: blue, width: 2em, height: 4em * 14 + 3em)),
|
||||
[] * 15
|
||||
)
|
||||
|
||||
---
|
||||
#set page(height: 6em)
|
||||
#table(
|
||||
rows: (3em,) * 15,
|
||||
columns: 2,
|
||||
column-gutter: 1em,
|
||||
row-gutter: (1em, 2em) * 4,
|
||||
fill: (x, y) => if calc.odd(x + y) { aqua } else { blue },
|
||||
table.cell(breakable: true, rowspan: 15, [a \ ] * 15),
|
||||
[] * 15
|
||||
)
|
108
tests/typ/layout/grid-rowspan-split-3.typ
Normal file
@ -0,0 +1,108 @@
|
||||
// Some splitting corner cases
|
||||
|
||||
---
|
||||
// Inside the larger rowspan's range, there's an unbreakable rowspan and a
|
||||
// breakable rowspan. This should work normally.
|
||||
// The auto row will also expand ignoring the last fractional row.
|
||||
#set page(height: 10em)
|
||||
#table(
|
||||
gutter: 0.5em,
|
||||
columns: 2,
|
||||
rows: (2em,) * 10 + (auto, auto, 2em, 1fr),
|
||||
fill: (_, y) => if calc.even(y) { aqua } else { blue },
|
||||
table.cell(rowspan: 14, block(width: 2em, height: 2em * 10 + 2em + 5em, fill: red)[]),
|
||||
..([a],) * 5,
|
||||
table.cell(rowspan: 3)[a\ b],
|
||||
table.cell(rowspan: 5, [a\ b\ c\ d\ e\ f\ g\ h]),
|
||||
[z]
|
||||
)
|
||||
|
||||
---
|
||||
// Inset moving to next region bug
|
||||
#set page(width: 10cm, height: 2.5cm, margin: 0.5cm)
|
||||
#set text(size: 11pt)
|
||||
#table(
|
||||
columns: (1fr, 1fr, 1fr),
|
||||
[A],
|
||||
[B],
|
||||
[C],
|
||||
[D],
|
||||
table.cell(rowspan: 2, lorem(4)),
|
||||
[E],
|
||||
[F],
|
||||
[G],
|
||||
)
|
||||
|
||||
---
|
||||
// Second lorem must be sent to the next page, too big
|
||||
#set page(width: 10cm, height: 9cm, margin: 1cm)
|
||||
#set text(size: 11pt)
|
||||
#table(
|
||||
columns: (1fr, 1fr, 1fr),
|
||||
align: center,
|
||||
rows: (4cm, auto),
|
||||
[A], [B], [C],
|
||||
table.cell(rowspan: 4, breakable: false, lorem(10)),
|
||||
[D],
|
||||
table.cell(rowspan: 2, breakable: false, lorem(20)),
|
||||
[E],
|
||||
)
|
||||
|
||||
---
|
||||
// Auto row must expand properly in both cases
|
||||
#set text(10pt)
|
||||
#show table.cell: it => if it.x == 0 { it } else { layout(size => size.height) }
|
||||
#table(
|
||||
columns: 2,
|
||||
rows: (1em, auto, 2em, 3em, 4em),
|
||||
gutter: 3pt,
|
||||
table.cell(rowspan: 5, block(fill: orange, height: 15em)[a]),
|
||||
[b],
|
||||
[c],
|
||||
[d],
|
||||
[e],
|
||||
[f]
|
||||
)
|
||||
|
||||
#table(
|
||||
columns: 2,
|
||||
rows: (1em, auto, 2em, 3em, 4em),
|
||||
gutter: 3pt,
|
||||
table.cell(rowspan: 5, breakable: false, block(fill: orange, height: 15em)[a]),
|
||||
[b],
|
||||
[c],
|
||||
[d],
|
||||
[e],
|
||||
[f]
|
||||
)
|
||||
|
||||
---
|
||||
// Expanding on unbreakable auto row
|
||||
#set page(height: 7em, margin: (bottom: 2em))
|
||||
#grid(
|
||||
columns: 2,
|
||||
rows: (1em, 1em, auto, 1em, 1em, 1em),
|
||||
fill: (x, y) => if x == 0 { aqua } else { blue },
|
||||
stroke: black,
|
||||
gutter: 2pt,
|
||||
grid.cell(rowspan: 5, block(height: 10em)[a]),
|
||||
[a],
|
||||
[b],
|
||||
grid.cell(breakable: false, v(3em) + [c]),
|
||||
[d],
|
||||
[e],
|
||||
[f], [g]
|
||||
)
|
||||
|
||||
---
|
||||
#show table.cell.where(x: 0): strong
|
||||
#show table.cell.where(y: 0): strong
|
||||
#set page(height: 13em)
|
||||
#let lets-repeat(thing, n) = ((thing + colbreak(),) * (calc.max(0, n - 1)) + (thing,)).join()
|
||||
#table(
|
||||
columns: 4,
|
||||
fill: (x, y) => if x == 0 or y == 0 { gray },
|
||||
[], [Test 1], [Test 2], [Test 3],
|
||||
table.cell(rowspan: 15, align: horizon, lets-repeat((rotate(-90deg, reflow: true)[*All Tests*]), 3)),
|
||||
..([123], [456], [789]) * 15
|
||||
)
|
@ -137,3 +137,44 @@
|
||||
#grid(
|
||||
[a], grid.vline(position: left)
|
||||
)
|
||||
|
||||
---
|
||||
#set text(dir: rtl)
|
||||
|
||||
#grid(
|
||||
columns: 4,
|
||||
fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
|
||||
inset: 5pt,
|
||||
align: center,
|
||||
grid.cell(rowspan: 2, fill: orange)[*Left*],
|
||||
[Right A], [Right A], [Right A],
|
||||
[Right B], grid.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
|
||||
[Left A], [Left A],
|
||||
[Left B], [Left B], grid.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
|
||||
)
|
||||
|
||||
#table(
|
||||
columns: 4,
|
||||
fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
|
||||
inset: 5pt,
|
||||
align: center,
|
||||
gutter: 3pt,
|
||||
table.cell(rowspan: 2, fill: orange)[*Left*],
|
||||
[Right A], [Right A], [Right A],
|
||||
[Right B], table.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
|
||||
[Left A], [Left A],
|
||||
[Left B], [Left B], table.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
|
||||
)
|
||||
|
||||
---
|
||||
#set page(height: 10em)
|
||||
#set text(dir: rtl)
|
||||
#table(
|
||||
columns: 2,
|
||||
rows: (auto, auto, 3em),
|
||||
row-gutter: 1em,
|
||||
fill: red,
|
||||
[a], table.cell(rowspan: 3, block(width: 50%, height: 10em, fill: orange) + place(bottom)[*ZD*]),
|
||||
[e],
|
||||
[f]
|
||||
)
|
||||
|
@ -274,6 +274,20 @@
|
||||
table.hline(position: bottom)
|
||||
)
|
||||
|
||||
---
|
||||
// Test partial border line overrides
|
||||
#set page(width: auto, height: 7em, margin: (bottom: 1em))
|
||||
#table(
|
||||
columns: 4,
|
||||
stroke: (x, y) => if y == 0 or y == 4 { orange } else { aqua },
|
||||
table.hline(stroke: blue, start: 1, end: 2), table.cell(stroke: red, v(3em)), table.cell(stroke: blue)[b], table.cell(stroke: green)[c], [M],
|
||||
[a], [b], [c], [M],
|
||||
[d], [e], [f], [M],
|
||||
[g], [h], [i], [M],
|
||||
table.cell(stroke: red)[a], table.cell(stroke: blue)[b], table.cell(stroke: green)[c], [M],
|
||||
table.hline(stroke: blue, start: 1, end: 2),
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 8:3-8:32 cannot place horizontal line at the 'bottom' position of the bottom border (y = 2)
|
||||
// Hint: 8:3-8:32 set the line's position to 'top' or place it at a smaller 'y' index
|
||||
|