Merging cells: Colspans [More Flexible Tables Pt.3a] (#3239)
This commit is contained in:
parent
310a89cbd8
commit
cd71741532
@ -1,3 +1,5 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use ecow::eco_format;
|
||||
|
||||
use crate::diag::{
|
||||
@ -14,7 +16,7 @@ use crate::layout::{
|
||||
};
|
||||
use crate::syntax::Span;
|
||||
use crate::text::TextElem;
|
||||
use crate::util::Numeric;
|
||||
use crate::util::{MaybeReverseIter, NonZeroExt, Numeric};
|
||||
use crate::visualize::{FixedStroke, Geometry, Paint};
|
||||
|
||||
/// A value that can be configured per cell.
|
||||
@ -93,12 +95,14 @@ pub struct Cell {
|
||||
pub body: Content,
|
||||
/// The cell's fill.
|
||||
pub fill: Option<Paint>,
|
||||
/// The amount of columns spanned by the cell.
|
||||
pub colspan: NonZeroUsize,
|
||||
}
|
||||
|
||||
impl From<Content> for Cell {
|
||||
/// Create a simple cell given its body.
|
||||
fn from(body: Content) -> Self {
|
||||
Self { body, fill: None }
|
||||
Self { body, fill: None, colspan: NonZeroUsize::ONE }
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,6 +117,28 @@ impl LayoutMultiple for Cell {
|
||||
}
|
||||
}
|
||||
|
||||
/// A grid entry.
|
||||
#[derive(Clone)]
|
||||
enum Entry {
|
||||
/// An entry which holds a cell.
|
||||
Cell(Cell),
|
||||
/// An entry which is merged with another cell.
|
||||
Merged {
|
||||
/// The index of the cell this entry is merged with.
|
||||
parent: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
/// Obtains the cell inside this entry, if this is not a merged cell.
|
||||
fn as_cell(&self) -> Option<&Cell> {
|
||||
match self {
|
||||
Self::Cell(cell) => Some(cell),
|
||||
Self::Merged { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for cell-like elements which are aware of their final properties in
|
||||
/// the table, and may have property overrides.
|
||||
pub trait ResolvableCell {
|
||||
@ -135,6 +161,9 @@ pub trait ResolvableCell {
|
||||
/// Returns this cell's row override.
|
||||
fn y(&self, styles: StyleChain) -> Smart<usize>;
|
||||
|
||||
/// The amount of columns spanned by this cell.
|
||||
fn colspan(&self, styles: StyleChain) -> NonZeroUsize;
|
||||
|
||||
/// The cell's span, for errors.
|
||||
fn span(&self) -> Span;
|
||||
}
|
||||
@ -142,76 +171,24 @@ pub trait ResolvableCell {
|
||||
/// A grid of cells, including the columns, rows, and cell data.
|
||||
pub struct CellGrid {
|
||||
/// The grid cells.
|
||||
cells: Vec<Cell>,
|
||||
entries: Vec<Entry>,
|
||||
/// The column tracks including gutter tracks.
|
||||
cols: Vec<Sizing>,
|
||||
/// The row tracks including gutter tracks.
|
||||
rows: Vec<Sizing>,
|
||||
/// Whether this grid has gutters.
|
||||
has_gutter: bool,
|
||||
/// Whether this is an RTL grid.
|
||||
is_rtl: bool,
|
||||
}
|
||||
|
||||
impl CellGrid {
|
||||
/// Generates the cell grid, given the tracks and resolved cells.
|
||||
/// Generates the cell grid, given the tracks and cells.
|
||||
pub fn new(
|
||||
tracks: Axes<&[Sizing]>,
|
||||
gutter: Axes<&[Sizing]>,
|
||||
cells: Vec<Cell>,
|
||||
styles: StyleChain,
|
||||
cells: impl IntoIterator<Item = Cell>,
|
||||
) -> Self {
|
||||
let mut cols = vec![];
|
||||
let mut rows = vec![];
|
||||
|
||||
// Number of content columns: Always at least one.
|
||||
let c = tracks.x.len().max(1);
|
||||
|
||||
// Number of content rows: At least as many as given, but also at least
|
||||
// as many as needed to place each item.
|
||||
let r = {
|
||||
let len = cells.len();
|
||||
let given = tracks.y.len();
|
||||
let needed = len / c + (len % c).clamp(0, 1);
|
||||
given.max(needed)
|
||||
};
|
||||
|
||||
let has_gutter = gutter.any(|tracks| !tracks.is_empty());
|
||||
let auto = Sizing::Auto;
|
||||
let zero = Sizing::Rel(Rel::zero());
|
||||
let get_or = |tracks: &[_], idx, default| {
|
||||
tracks.get(idx).or(tracks.last()).copied().unwrap_or(default)
|
||||
};
|
||||
|
||||
// Collect content and gutter columns.
|
||||
for x in 0..c {
|
||||
cols.push(get_or(tracks.x, x, auto));
|
||||
if has_gutter {
|
||||
cols.push(get_or(gutter.x, x, zero));
|
||||
}
|
||||
}
|
||||
|
||||
// Collect content and gutter rows.
|
||||
for y in 0..r {
|
||||
rows.push(get_or(tracks.y, y, auto));
|
||||
if has_gutter {
|
||||
rows.push(get_or(gutter.y, y, zero));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove superfluous gutter tracks.
|
||||
if has_gutter {
|
||||
cols.pop();
|
||||
rows.pop();
|
||||
}
|
||||
|
||||
// Reverse for RTL.
|
||||
let is_rtl = TextElem::dir_in(styles) == Dir::RTL;
|
||||
if is_rtl {
|
||||
cols.reverse();
|
||||
}
|
||||
|
||||
Self { cols, rows, cells, has_gutter, is_rtl }
|
||||
let entries = cells.into_iter().map(Entry::Cell).collect();
|
||||
Self::new_internal(tracks, gutter, entries)
|
||||
}
|
||||
|
||||
/// Resolves and positions all cells in the grid before creating it.
|
||||
@ -257,7 +234,7 @@ impl CellGrid {
|
||||
let Some(cell_count) = cells.len().checked_add((c - cells.len() % c) % c) else {
|
||||
bail!(span, "too many cells were given")
|
||||
};
|
||||
let mut resolved_cells: Vec<Option<Cell>> = Vec::with_capacity(cell_count);
|
||||
let mut resolved_cells: Vec<Option<Entry>> = Vec::with_capacity(cell_count);
|
||||
for cell in cells.iter().cloned() {
|
||||
let cell_span = cell.span();
|
||||
// Let's calculate the cell's final position based on its
|
||||
@ -270,6 +247,23 @@ impl CellGrid {
|
||||
};
|
||||
let x = resolved_index % c;
|
||||
let y = resolved_index / c;
|
||||
let colspan = cell.colspan(styles).get();
|
||||
|
||||
if colspan > c - x {
|
||||
bail!(
|
||||
cell_span,
|
||||
"cell's colspan would cause it to exceed the available column(s)";
|
||||
hint: "try placing the cell in another position or reducing its colspan"
|
||||
)
|
||||
}
|
||||
|
||||
let Some(largest_index) = resolved_index.checked_add(colspan - 1) else {
|
||||
bail!(
|
||||
cell_span,
|
||||
"cell would span an exceedingly large position";
|
||||
hint: "try reducing the cell's colspan"
|
||||
)
|
||||
};
|
||||
|
||||
// Let's resolve the cell so it can determine its own fields
|
||||
// based on its final position.
|
||||
@ -282,7 +276,7 @@ impl CellGrid {
|
||||
styles,
|
||||
);
|
||||
|
||||
if resolved_index >= resolved_cells.len() {
|
||||
if largest_index >= resolved_cells.len() {
|
||||
// Ensure the length of the vector of resolved cells is always
|
||||
// a multiple of 'c' by pushing full rows every time. Here, we
|
||||
// add enough absent positions (later converted to empty cells)
|
||||
@ -292,7 +286,7 @@ impl CellGrid {
|
||||
// eventually susceptible to show rules and receive grid
|
||||
// styling, as they will be resolved as empty cells in a second
|
||||
// loop below.
|
||||
let Some(new_len) = resolved_index
|
||||
let Some(new_len) = largest_index
|
||||
.checked_add(1)
|
||||
.and_then(|new_len| new_len.checked_add((c - new_len % c) % c))
|
||||
else {
|
||||
@ -322,11 +316,30 @@ impl CellGrid {
|
||||
);
|
||||
}
|
||||
|
||||
*slot = Some(cell);
|
||||
*slot = Some(Entry::Cell(cell));
|
||||
|
||||
// Now, if the cell spans more than one column, we fill the spanned
|
||||
// positions in the grid with Entry::Merged pointing to the
|
||||
// original cell as its parent.
|
||||
for (offset, slot) in resolved_cells[resolved_index..][..colspan]
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.skip(1)
|
||||
{
|
||||
if slot.is_some() {
|
||||
let spanned_x = x + offset;
|
||||
bail!(
|
||||
cell_span,
|
||||
"cell would span a previously placed cell at column {spanned_x}, row {y}";
|
||||
hint: "try specifying your cells in a different order or reducing the cell's colspan"
|
||||
)
|
||||
}
|
||||
*slot = Some(Entry::Merged { parent: resolved_index });
|
||||
}
|
||||
}
|
||||
|
||||
// Replace absent entries by resolved empty cells, and produce a vector
|
||||
// of 'Cell' from 'Option<Cell>' (final step).
|
||||
// of 'Entry' from 'Option<Entry>' (final step).
|
||||
let resolved_cells = resolved_cells
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
@ -347,40 +360,119 @@ impl CellGrid {
|
||||
inset,
|
||||
styles,
|
||||
);
|
||||
Ok(new_cell)
|
||||
Ok(Entry::Cell(new_cell))
|
||||
}
|
||||
})
|
||||
.collect::<SourceResult<Vec<Cell>>>()?;
|
||||
.collect::<SourceResult<Vec<Entry>>>()?;
|
||||
|
||||
Ok(Self::new(tracks, gutter, resolved_cells, styles))
|
||||
Ok(Self::new_internal(tracks, gutter, resolved_cells))
|
||||
}
|
||||
|
||||
/// Get the content of the cell in column `x` and row `y`.
|
||||
/// Generates the cell grid, given the tracks and resolved entries.
|
||||
fn new_internal(
|
||||
tracks: Axes<&[Sizing]>,
|
||||
gutter: Axes<&[Sizing]>,
|
||||
entries: Vec<Entry>,
|
||||
) -> Self {
|
||||
let mut cols = vec![];
|
||||
let mut rows = vec![];
|
||||
|
||||
// Number of content columns: Always at least one.
|
||||
let c = tracks.x.len().max(1);
|
||||
|
||||
// Number of content rows: At least as many as given, but also at least
|
||||
// as many as needed to place each item.
|
||||
let r = {
|
||||
let len = entries.len();
|
||||
let given = tracks.y.len();
|
||||
let needed = len / c + (len % c).clamp(0, 1);
|
||||
given.max(needed)
|
||||
};
|
||||
|
||||
let has_gutter = gutter.any(|tracks| !tracks.is_empty());
|
||||
let auto = Sizing::Auto;
|
||||
let zero = Sizing::Rel(Rel::zero());
|
||||
let get_or = |tracks: &[_], idx, default| {
|
||||
tracks.get(idx).or(tracks.last()).copied().unwrap_or(default)
|
||||
};
|
||||
|
||||
// Collect content and gutter columns.
|
||||
for x in 0..c {
|
||||
cols.push(get_or(tracks.x, x, auto));
|
||||
if has_gutter {
|
||||
cols.push(get_or(gutter.x, x, zero));
|
||||
}
|
||||
}
|
||||
|
||||
// Collect content and gutter rows.
|
||||
for y in 0..r {
|
||||
rows.push(get_or(tracks.y, y, auto));
|
||||
if has_gutter {
|
||||
rows.push(get_or(gutter.y, y, zero));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove superfluous gutter tracks.
|
||||
if has_gutter {
|
||||
cols.pop();
|
||||
rows.pop();
|
||||
}
|
||||
|
||||
Self { cols, rows, entries, has_gutter }
|
||||
}
|
||||
|
||||
/// Get the grid entry in column `x` and row `y`.
|
||||
///
|
||||
/// Returns `None` if it's a gutter cell.
|
||||
#[track_caller]
|
||||
fn cell(&self, mut x: usize, y: usize) -> Option<&Cell> {
|
||||
fn entry(&self, x: usize, y: usize) -> Option<&Entry> {
|
||||
assert!(x < self.cols.len());
|
||||
assert!(y < self.rows.len());
|
||||
|
||||
// Columns are reorder, but the cell slice is not.
|
||||
if self.is_rtl {
|
||||
x = self.cols.len() - 1 - x;
|
||||
}
|
||||
|
||||
if self.has_gutter {
|
||||
// Even columns and rows are children, odd ones are gutter.
|
||||
if x % 2 == 0 && y % 2 == 0 {
|
||||
let c = 1 + self.cols.len() / 2;
|
||||
self.cells.get((y / 2) * c + x / 2)
|
||||
self.entries.get((y / 2) * c + x / 2)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let c = self.cols.len();
|
||||
self.cells.get(y * c + x)
|
||||
self.entries.get(y * c + x)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the content of the cell in column `x` and row `y`.
|
||||
///
|
||||
/// Returns `None` if it's a gutter cell or merged position.
|
||||
#[track_caller]
|
||||
fn cell(&self, x: usize, y: usize) -> Option<&Cell> {
|
||||
self.entry(x, y).and_then(Entry::as_cell)
|
||||
}
|
||||
|
||||
/// Returns the position of the parent cell of the grid entry at the given
|
||||
/// position. It is guaranteed to have a non-gutter, non-merged cell at
|
||||
/// the returned position, due to how the grid is built.
|
||||
/// If the entry at the given position is a cell, returns the given
|
||||
/// position.
|
||||
/// If it is a merged cell, returns the parent cell's position.
|
||||
/// If it is a gutter cell, returns None.
|
||||
#[track_caller]
|
||||
fn parent_cell_position(&self, x: usize, y: usize) -> Option<Axes<usize>> {
|
||||
self.entry(x, y).map(|entry| match entry {
|
||||
Entry::Cell(_) => Axes::new(x, y),
|
||||
Entry::Merged { parent } => {
|
||||
let c = if self.has_gutter {
|
||||
1 + self.cols.len() / 2
|
||||
} else {
|
||||
self.cols.len()
|
||||
};
|
||||
let factor = if self.has_gutter { 2 } else { 1 };
|
||||
Axes::new(factor * (*parent % c), factor * (*parent / c))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a cell's requested x and y, the vector with the resolved cell
|
||||
@ -390,7 +482,7 @@ impl CellGrid {
|
||||
fn resolve_cell_position(
|
||||
cell_x: Smart<usize>,
|
||||
cell_y: Smart<usize>,
|
||||
resolved_cells: &[Option<Cell>],
|
||||
resolved_cells: &[Option<Entry>],
|
||||
auto_index: &mut usize,
|
||||
columns: usize,
|
||||
) -> HintedStrResult<usize> {
|
||||
@ -497,6 +589,8 @@ pub struct GridLayouter<'a> {
|
||||
initial: Size,
|
||||
/// Frames for finished regions.
|
||||
finished: Vec<Frame>,
|
||||
/// Whether this is an RTL grid.
|
||||
is_rtl: bool,
|
||||
/// The span of the grid element.
|
||||
span: Span,
|
||||
}
|
||||
@ -547,6 +641,7 @@ impl<'a> GridLayouter<'a> {
|
||||
lrows: vec![],
|
||||
initial: regions.size,
|
||||
finished: vec![],
|
||||
is_rtl: TextElem::dir_in(styles) == Dir::RTL,
|
||||
span,
|
||||
}
|
||||
}
|
||||
@ -570,14 +665,14 @@ impl<'a> GridLayouter<'a> {
|
||||
}
|
||||
|
||||
self.finish_region(engine)?;
|
||||
self.render_fills_strokes()?;
|
||||
|
||||
Ok(Fragment::frames(self.finished))
|
||||
self.render_fills_strokes()
|
||||
}
|
||||
|
||||
/// Add lines and backgrounds.
|
||||
fn render_fills_strokes(&mut self) -> SourceResult<()> {
|
||||
for (frame, rows) in self.finished.iter_mut().zip(&self.rrows) {
|
||||
fn render_fills_strokes(mut self) -> SourceResult<Fragment> {
|
||||
let mut finished = std::mem::take(&mut self.finished);
|
||||
for (frame, rows) in finished.iter_mut().zip(&self.rrows) {
|
||||
if self.rcols.is_empty() || rows.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@ -598,28 +693,51 @@ impl<'a> GridLayouter<'a> {
|
||||
}
|
||||
|
||||
// Render vertical lines.
|
||||
for offset in points(self.rcols.iter().copied()) {
|
||||
let target = Point::with_y(frame.height() + thickness);
|
||||
let vline = Geometry::Line(target).stroked(stroke.clone());
|
||||
frame.prepend(
|
||||
Point::new(offset, -half),
|
||||
FrameItem::Shape(vline, self.span),
|
||||
);
|
||||
for (x, dx) in points(self.rcols.iter().copied()).enumerate() {
|
||||
let dx = if self.is_rtl { self.width - dx } else { dx };
|
||||
// We want each vline to span the entire table (start
|
||||
// at y = 0, end after all rows).
|
||||
// We use 'split_vline' to split the vline such that it
|
||||
// is not drawn above colspans.
|
||||
for (dy, length) in
|
||||
split_vline(self.grid, rows, x, 0, self.grid.rows.len())
|
||||
{
|
||||
let target = Point::with_y(length + thickness);
|
||||
let vline = Geometry::Line(target).stroked(stroke.clone());
|
||||
frame.prepend(
|
||||
Point::new(dx, dy - half),
|
||||
FrameItem::Shape(vline, self.span),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render cell backgrounds.
|
||||
// Reverse with RTL so that later columns start first.
|
||||
let mut dx = Abs::zero();
|
||||
for (x, &col) in self.rcols.iter().enumerate() {
|
||||
for (x, &col) in self.rcols.iter().enumerate().rev_if(self.is_rtl) {
|
||||
let mut dy = Abs::zero();
|
||||
for row in rows {
|
||||
let fill =
|
||||
self.grid.cell(x, row.y).and_then(|cell| cell.fill.clone());
|
||||
if let Some(fill) = fill {
|
||||
let pos = Point::new(dx, dy);
|
||||
let size = Size::new(col, row.height);
|
||||
let rect = Geometry::Rect(size).filled(fill);
|
||||
frame.prepend(pos, FrameItem::Shape(rect, self.span));
|
||||
if let Some(cell) = self.grid.cell(x, row.y) {
|
||||
let fill = cell.fill.clone();
|
||||
if let Some(fill) = fill {
|
||||
let width = self.cell_spanned_width(x, cell.colspan.get());
|
||||
// In the grid, cell colspans expand to the right,
|
||||
// so we're at the leftmost (lowest 'x') column
|
||||
// spanned by the cell. However, in RTL, cells
|
||||
// expand to the left. Therefore, without the
|
||||
// offset below, cell fills would start at the
|
||||
// rightmost visual position of a cell and extend
|
||||
// over to unrelated columns to the right in RTL.
|
||||
// We avoid this by ensuring the fill starts at the
|
||||
// very left of the cell, even with colspan > 1.
|
||||
let offset =
|
||||
if self.is_rtl { -width + col } else { Abs::zero() };
|
||||
let pos = Point::new(dx + offset, dy);
|
||||
let size = Size::new(width, row.height);
|
||||
let rect = Geometry::Rect(size).filled(fill);
|
||||
frame.prepend(pos, FrameItem::Shape(rect, self.span));
|
||||
}
|
||||
}
|
||||
dy += row.height;
|
||||
}
|
||||
@ -627,7 +745,7 @@ impl<'a> GridLayouter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(Fragment::frames(finished))
|
||||
}
|
||||
|
||||
/// Determine all column sizes.
|
||||
@ -675,6 +793,16 @@ impl<'a> GridLayouter<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Total width spanned by the cell (among resolved columns).
|
||||
/// Includes spanned gutter columns.
|
||||
fn cell_spanned_width(&self, x: usize, colspan: usize) -> Abs {
|
||||
self.rcols
|
||||
.iter()
|
||||
.skip(x)
|
||||
.take(if self.grid.has_gutter { 2 * colspan - 1 } else { colspan })
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Measure the size that is available to auto columns.
|
||||
fn measure_auto_columns(
|
||||
&mut self,
|
||||
@ -683,6 +811,14 @@ impl<'a> GridLayouter<'a> {
|
||||
) -> SourceResult<(Abs, usize)> {
|
||||
let mut auto = Abs::zero();
|
||||
let mut count = 0;
|
||||
let all_frac_cols = self
|
||||
.grid
|
||||
.cols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, col)| col.is_fractional())
|
||||
.map(|(x, _)| x)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Determine size of auto columns by laying out all cells in those
|
||||
// columns, measuring them and finding the largest one.
|
||||
@ -693,21 +829,82 @@ impl<'a> GridLayouter<'a> {
|
||||
|
||||
let mut resolved = Abs::zero();
|
||||
for y in 0..self.grid.rows.len() {
|
||||
if let Some(cell) = self.grid.cell(x, y) {
|
||||
// For relative rows, we can already resolve the correct
|
||||
// base and for auto and fr we could only guess anyway.
|
||||
let height = match self.grid.rows[y] {
|
||||
Sizing::Rel(v) => {
|
||||
v.resolve(self.styles).relative_to(self.regions.base().y)
|
||||
}
|
||||
_ => self.regions.base().y,
|
||||
};
|
||||
// We get the parent cell in case this is a merged position.
|
||||
let Some(Axes { x: parent_x, y: parent_y }) =
|
||||
self.grid.parent_cell_position(x, y)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let cell = self.grid.cell(parent_x, parent_y).unwrap();
|
||||
let colspan = cell.colspan.get();
|
||||
if colspan > 1 {
|
||||
let last_spanned_auto_col = self
|
||||
.grid
|
||||
.cols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(parent_x)
|
||||
.take(if self.grid.has_gutter {
|
||||
2 * colspan - 1
|
||||
} else {
|
||||
colspan
|
||||
})
|
||||
.rev()
|
||||
.find(|(_, col)| **col == Sizing::Auto)
|
||||
.map(|(x, _)| x);
|
||||
|
||||
let size = Size::new(available, height);
|
||||
let pod = Regions::one(size, Axes::splat(false));
|
||||
let frame = cell.measure(engine, self.styles, pod)?.into_frame();
|
||||
resolved.set_max(frame.width());
|
||||
if last_spanned_auto_col != Some(x) {
|
||||
// A colspan only affects the size of the last spanned
|
||||
// auto column.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if colspan > 1
|
||||
&& self.regions.size.x.is_finite()
|
||||
&& !all_frac_cols.is_empty()
|
||||
&& all_frac_cols
|
||||
.iter()
|
||||
.all(|x| (parent_x..parent_x + colspan).contains(x))
|
||||
{
|
||||
// Additionally, as a heuristic, a colspan won't affect the
|
||||
// size of auto columns if it already spans all fractional
|
||||
// columns, since those would already expand to provide all
|
||||
// remaining available after auto column sizing to that
|
||||
// cell. However, this heuristic is only valid in finite
|
||||
// regions (pages without 'auto' width), since otherwise
|
||||
// the fractional columns don't expand at all.
|
||||
continue;
|
||||
}
|
||||
|
||||
// For relative rows, we can already resolve the correct
|
||||
// base and for auto and fr we could only guess anyway.
|
||||
let height = match self.grid.rows[y] {
|
||||
Sizing::Rel(v) => {
|
||||
v.resolve(self.styles).relative_to(self.regions.base().y)
|
||||
}
|
||||
_ => self.regions.base().y,
|
||||
};
|
||||
// Don't expand this auto column more than the cell actually
|
||||
// needs. To do this, we check how much the other, previously
|
||||
// resolved columns provide to the cell in terms of width
|
||||
// (if it is a colspan), and subtract this from its expected
|
||||
// width when comparing with other cells in this column. Note
|
||||
// that, since this is the last auto column spanned by this
|
||||
// cell, all other auto columns will already have been resolved
|
||||
// and will be considered.
|
||||
// Only fractional columns will be excluded from this
|
||||
// calculation, which can lead to auto columns being expanded
|
||||
// unnecessarily when cells span both a fractional column and
|
||||
// an auto column. One mitigation for this is the heuristic
|
||||
// used above to not expand the last auto column spanned by a
|
||||
// cell if it spans all fractional columns in a finite region.
|
||||
let already_covered_width = self.cell_spanned_width(parent_x, colspan);
|
||||
|
||||
let size = Size::new(available, height);
|
||||
let pod = Regions::one(size, Axes::splat(false));
|
||||
let frame = cell.measure(engine, self.styles, pod)?.into_frame();
|
||||
resolved.set_max(frame.width() - already_covered_width);
|
||||
}
|
||||
|
||||
self.rcols[x] = resolved;
|
||||
@ -824,10 +1021,10 @@ impl<'a> GridLayouter<'a> {
|
||||
) -> SourceResult<Option<Vec<Abs>>> {
|
||||
let mut resolved: Vec<Abs> = vec![];
|
||||
|
||||
for (x, &rcol) in self.rcols.iter().enumerate() {
|
||||
for x in 0..self.rcols.len() {
|
||||
if let Some(cell) = self.grid.cell(x, y) {
|
||||
let mut pod = self.regions;
|
||||
pod.size.x = rcol;
|
||||
pod.size.x = self.cell_spanned_width(x, cell.colspan.get());
|
||||
|
||||
let frames = cell.measure(engine, self.styles, pod)?.into_frames();
|
||||
|
||||
@ -897,14 +1094,29 @@ impl<'a> GridLayouter<'a> {
|
||||
let mut output = Frame::soft(Size::new(self.width, height));
|
||||
let mut pos = Point::zero();
|
||||
|
||||
for (x, &rcol) in self.rcols.iter().enumerate() {
|
||||
// Reverse the column order when using RTL.
|
||||
for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) {
|
||||
if let Some(cell) = self.grid.cell(x, y) {
|
||||
let size = Size::new(rcol, height);
|
||||
let width = self.cell_spanned_width(x, cell.colspan.get());
|
||||
let size = Size::new(width, height);
|
||||
let mut pod = Regions::one(size, Axes::splat(true));
|
||||
if self.grid.rows[y] == Sizing::Auto {
|
||||
pod.full = self.regions.full;
|
||||
}
|
||||
let frame = cell.layout(engine, self.styles, pod)?.into_frame();
|
||||
let mut frame = cell.layout(engine, self.styles, pod)?.into_frame();
|
||||
if self.is_rtl {
|
||||
// In the grid, cell colspans expand to the right,
|
||||
// so we're at the leftmost (lowest 'x') column
|
||||
// spanned by the cell. However, in RTL, cells
|
||||
// expand to the left. Therefore, without the
|
||||
// offset below, the cell's contents would be laid out
|
||||
// starting at its rightmost visual position and extend
|
||||
// over to unrelated cells to its right in RTL.
|
||||
// We avoid this by ensuring the rendered cell starts at
|
||||
// the very left of the cell, even with colspan > 1.
|
||||
let offset = Point::with_x(-width + rcol);
|
||||
frame.translate(offset);
|
||||
}
|
||||
output.push_frame(pos, frame);
|
||||
}
|
||||
|
||||
@ -935,13 +1147,18 @@ impl<'a> GridLayouter<'a> {
|
||||
|
||||
// Layout the row.
|
||||
let mut pos = Point::zero();
|
||||
for (x, &rcol) in self.rcols.iter().enumerate() {
|
||||
for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) {
|
||||
if let Some(cell) = self.grid.cell(x, y) {
|
||||
pod.size.x = rcol;
|
||||
let width = self.cell_spanned_width(x, cell.colspan.get());
|
||||
pod.size.x = width;
|
||||
|
||||
// Push the layouted frames into the individual output frames.
|
||||
let fragment = cell.layout(engine, self.styles, pod)?;
|
||||
for (output, frame) in outputs.iter_mut().zip(fragment) {
|
||||
for (output, mut frame) in outputs.iter_mut().zip(fragment) {
|
||||
if self.is_rtl {
|
||||
let offset = Point::with_x(-width + rcol);
|
||||
frame.translate(offset);
|
||||
}
|
||||
output.push_frame(pos, frame);
|
||||
}
|
||||
}
|
||||
@ -1017,3 +1234,287 @@ fn points(extents: impl IntoIterator<Item = Abs>) -> impl Iterator<Item = Abs> {
|
||||
offset
|
||||
})
|
||||
}
|
||||
|
||||
/// Given the 'x' of the column right after the vline (or cols.len() at the
|
||||
/// border) and its start..end range of rows, alongside the rows for the
|
||||
/// current region, splits the vline into contiguous parts to draw, including
|
||||
/// the height of the vline in each part. This will go through each row and
|
||||
/// interrupt the current vline to be drawn when a colspan is detected, or the
|
||||
/// end of the row range (or of the region) is reached.
|
||||
/// The idea is to not draw vlines over colspans.
|
||||
/// This will return the start offsets and lengths of each final segment of
|
||||
/// this vline. The offsets are relative to the top of the first row.
|
||||
/// Note that this assumes that rows are sorted according to ascending 'y'.
|
||||
fn split_vline(
|
||||
grid: &CellGrid,
|
||||
rows: &[RowPiece],
|
||||
x: usize,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) -> impl IntoIterator<Item = (Abs, Abs)> {
|
||||
// Each segment of this vline that should be drawn.
|
||||
// The last element in the vector below is the currently drawn segment.
|
||||
// That is, the last segment will be expanded until interrupted.
|
||||
let mut drawn_vlines = vec![];
|
||||
// Whether the latest vline segment is complete, because we hit a row we
|
||||
// should skip while drawing the vline. Starts at true so we push
|
||||
// the first segment to the vector.
|
||||
let mut interrupted = true;
|
||||
// How far down from the first row have we gone so far.
|
||||
// Used to determine the positions at which to draw each segment.
|
||||
let mut offset = Abs::zero();
|
||||
|
||||
// We start drawing at the first suitable row, and keep going down
|
||||
// (increasing y) expanding the last segment until we hit a row on top of
|
||||
// which we shouldn't draw, which is skipped, leading to the creation of a
|
||||
// new vline segment later if a suitable row is found, restarting the
|
||||
// cycle.
|
||||
for row in rows.iter().take_while(|row| row.y < end) {
|
||||
if should_draw_vline_at_row(grid, x, row.y, start, end) {
|
||||
if interrupted {
|
||||
// Last segment was interrupted by a colspan, or there are no
|
||||
// segments yet.
|
||||
// Create a new segment to draw. We start spanning this row.
|
||||
drawn_vlines.push((offset, row.height));
|
||||
interrupted = false;
|
||||
} else {
|
||||
// Extend the current segment so it covers at least this row
|
||||
// as well.
|
||||
// The vector can't be empty if interrupted is false.
|
||||
let current_segment = drawn_vlines.last_mut().unwrap();
|
||||
current_segment.1 += row.height;
|
||||
}
|
||||
} else {
|
||||
interrupted = true;
|
||||
}
|
||||
offset += row.height;
|
||||
}
|
||||
|
||||
drawn_vlines
|
||||
}
|
||||
|
||||
/// Returns 'true' if the vline right before column 'x', given its start..end
|
||||
/// range of rows, should be drawn when going through row 'y'.
|
||||
/// That only occurs if the row is within its start..end range, and if it
|
||||
/// wouldn't go through a colspan.
|
||||
fn should_draw_vline_at_row(
|
||||
grid: &CellGrid,
|
||||
x: usize,
|
||||
y: usize,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) -> bool {
|
||||
if !(start..end).contains(&y) {
|
||||
// Row is out of range for this line
|
||||
return false;
|
||||
}
|
||||
if x == 0 || x == grid.cols.len() {
|
||||
// Border vline. Always drawn.
|
||||
return true;
|
||||
}
|
||||
// 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();
|
||||
|
||||
parent_x >= x || parent_y > y
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
fn sample_cell() -> Cell {
|
||||
Cell {
|
||||
body: Content::default(),
|
||||
fill: None,
|
||||
colspan: NonZeroUsize::ONE,
|
||||
}
|
||||
}
|
||||
|
||||
fn cell_with_colspan(colspan: usize) -> Cell {
|
||||
Cell {
|
||||
body: Content::default(),
|
||||
fill: None,
|
||||
colspan: NonZeroUsize::try_from(colspan).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_grid(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::Merged { parent: 2 },
|
||||
// row 1
|
||||
Entry::Cell(sample_cell()),
|
||||
Entry::Cell(cell_with_colspan(3)),
|
||||
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::Merged { parent: 10 },
|
||||
// row 3
|
||||
Entry::Cell(sample_cell()),
|
||||
Entry::Cell(cell_with_colspan(3)),
|
||||
Entry::Merged { parent: 13 },
|
||||
Entry::Merged { parent: 13 },
|
||||
// row 4
|
||||
Entry::Cell(sample_cell()),
|
||||
Entry::Merged { parent: 13 },
|
||||
Entry::Merged { parent: 13 },
|
||||
Entry::Merged { parent: 13 },
|
||||
// row 5
|
||||
Entry::Cell(sample_cell()),
|
||||
Entry::Cell(sample_cell()),
|
||||
Entry::Cell(cell_with_colspan(2)),
|
||||
Entry::Merged { parent: 22 },
|
||||
];
|
||||
CellGrid::new_internal(
|
||||
Axes::with_x(&[Sizing::Auto; COLS]),
|
||||
if gutters {
|
||||
Axes::new(&[Sizing::Auto; COLS - 1], &[Sizing::Auto; ROWS - 1])
|
||||
} else {
|
||||
Axes::default()
|
||||
},
|
||||
entries,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vline_splitting_without_gutter() {
|
||||
let grid = sample_grid(false);
|
||||
let rows = &[
|
||||
RowPiece { height: Abs::pt(1.0), y: 0 },
|
||||
RowPiece { height: Abs::pt(2.0), y: 1 },
|
||||
RowPiece { height: Abs::pt(4.0), y: 2 },
|
||||
RowPiece { height: Abs::pt(8.0), y: 3 },
|
||||
RowPiece { height: Abs::pt(16.0), y: 4 },
|
||||
RowPiece { height: Abs::pt(32.0), y: 5 },
|
||||
];
|
||||
let expected_vline_splits = &[
|
||||
vec![(Abs::pt(0.), Abs::pt(1. + 2. + 4. + 8. + 16. + 32.))],
|
||||
vec![(Abs::pt(0.), Abs::pt(1. + 2. + 4. + 8. + 16. + 32.))],
|
||||
// interrupted a few times by colspans
|
||||
vec![
|
||||
(Abs::pt(0.), Abs::pt(1.)),
|
||||
(Abs::pt(1. + 2.), Abs::pt(4.)),
|
||||
(Abs::pt(1. + 2. + 4. + 8. + 16.), Abs::pt(32.)),
|
||||
],
|
||||
// interrupted every time by colspans
|
||||
vec![],
|
||||
vec![(Abs::pt(0.), Abs::pt(1. + 2. + 4. + 8. + 16. + 32.))],
|
||||
];
|
||||
for (x, expected_splits) in expected_vline_splits.iter().enumerate() {
|
||||
assert_eq!(
|
||||
expected_splits,
|
||||
&split_vline(&grid, rows, x, 0, 6).into_iter().collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vline_splitting_with_gutter() {
|
||||
let grid = sample_grid(true);
|
||||
let rows = &[
|
||||
RowPiece { height: Abs::pt(1.0), y: 0 },
|
||||
RowPiece { height: Abs::pt(2.0), y: 1 },
|
||||
RowPiece { height: Abs::pt(4.0), y: 2 },
|
||||
RowPiece { height: Abs::pt(8.0), y: 3 },
|
||||
RowPiece { height: Abs::pt(16.0), y: 4 },
|
||||
RowPiece { height: Abs::pt(32.0), y: 5 },
|
||||
RowPiece { height: Abs::pt(64.0), y: 6 },
|
||||
RowPiece { height: Abs::pt(128.0), y: 7 },
|
||||
RowPiece { height: Abs::pt(256.0), y: 8 },
|
||||
RowPiece { height: Abs::pt(512.0), y: 9 },
|
||||
RowPiece { height: Abs::pt(1024.0), y: 10 },
|
||||
];
|
||||
let expected_vline_splits = &[
|
||||
// left border
|
||||
vec![(
|
||||
Abs::pt(0.),
|
||||
Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024.),
|
||||
)],
|
||||
// gutter line below
|
||||
vec![(
|
||||
Abs::pt(0.),
|
||||
Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024.),
|
||||
)],
|
||||
vec![(
|
||||
Abs::pt(0.),
|
||||
Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024.),
|
||||
)],
|
||||
// gutter line below
|
||||
// the two lines below are interrupted multiple times by colspans
|
||||
vec![
|
||||
(Abs::pt(0.), Abs::pt(1. + 2.)),
|
||||
(Abs::pt(1. + 2. + 4.), Abs::pt(8. + 16. + 32.)),
|
||||
(
|
||||
Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.),
|
||||
Abs::pt(512. + 1024.),
|
||||
),
|
||||
],
|
||||
vec![
|
||||
(Abs::pt(0.), Abs::pt(1. + 2.)),
|
||||
(Abs::pt(1. + 2. + 4.), Abs::pt(8. + 16. + 32.)),
|
||||
(
|
||||
Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.),
|
||||
Abs::pt(512. + 1024.),
|
||||
),
|
||||
],
|
||||
// gutter line below
|
||||
// the two lines below can only cross certain gutter rows, because
|
||||
// all non-gutter cells in the following column are merged with
|
||||
// cells from the previous column.
|
||||
vec![
|
||||
(Abs::pt(1.), Abs::pt(2.)),
|
||||
(Abs::pt(1. + 2. + 4.), Abs::pt(8.)),
|
||||
(Abs::pt(1. + 2. + 4. + 8. + 16.), Abs::pt(32.)),
|
||||
(
|
||||
Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.),
|
||||
Abs::pt(512.),
|
||||
),
|
||||
],
|
||||
vec![
|
||||
(Abs::pt(1.), Abs::pt(2.)),
|
||||
(Abs::pt(1. + 2. + 4.), Abs::pt(8.)),
|
||||
(Abs::pt(1. + 2. + 4. + 8. + 16.), Abs::pt(32.)),
|
||||
(
|
||||
Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.),
|
||||
Abs::pt(512.),
|
||||
),
|
||||
],
|
||||
// right border
|
||||
vec![(
|
||||
Abs::pt(0.),
|
||||
Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024.),
|
||||
)],
|
||||
];
|
||||
for (x, expected_splits) in expected_vline_splits.iter().enumerate() {
|
||||
assert_eq!(
|
||||
expected_splits,
|
||||
&split_vline(&grid, rows, x, 0, 11).into_iter().collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ use crate::layout::{
|
||||
Sides, Sizing,
|
||||
};
|
||||
use crate::syntax::Span;
|
||||
use crate::util::NonZeroExt;
|
||||
use crate::visualize::{Paint, Stroke};
|
||||
|
||||
/// Arranges content in a grid.
|
||||
@ -429,6 +430,10 @@ pub struct GridCell {
|
||||
/// ```
|
||||
y: Smart<usize>,
|
||||
|
||||
/// The amount of columns spanned by this cell.
|
||||
#[default(NonZeroUsize::ONE)]
|
||||
colspan: NonZeroUsize,
|
||||
|
||||
/// The cell's fill override.
|
||||
fill: Smart<Option<Paint>>,
|
||||
|
||||
@ -461,6 +466,7 @@ impl ResolvableCell for Packed<GridCell> {
|
||||
styles: StyleChain,
|
||||
) -> Cell {
|
||||
let cell = &mut *self;
|
||||
let colspan = cell.colspan(styles);
|
||||
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
|
||||
cell.push_x(Smart::Custom(x));
|
||||
cell.push_y(Smart::Custom(y));
|
||||
@ -478,7 +484,7 @@ impl ResolvableCell for Packed<GridCell> {
|
||||
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)).map(Some),
|
||||
));
|
||||
|
||||
Cell { body: self.pack(), fill }
|
||||
Cell { body: self.pack(), fill, colspan }
|
||||
}
|
||||
|
||||
fn x(&self, styles: StyleChain) -> Smart<usize> {
|
||||
@ -489,6 +495,10 @@ impl ResolvableCell for Packed<GridCell> {
|
||||
(**self).y(styles)
|
||||
}
|
||||
|
||||
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
|
||||
(**self).colspan(styles)
|
||||
}
|
||||
|
||||
fn span(&self) -> Span {
|
||||
Packed::span(self)
|
||||
}
|
||||
|
@ -278,7 +278,6 @@ impl LayoutMultiple for Packed<EnumElem> {
|
||||
]),
|
||||
Axes::with_y(&[gutter.into()]),
|
||||
cells,
|
||||
styles,
|
||||
);
|
||||
let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
|
||||
|
||||
|
@ -176,7 +176,6 @@ impl LayoutMultiple for Packed<ListElem> {
|
||||
]),
|
||||
Axes::with_y(&[gutter.into()]),
|
||||
cells,
|
||||
styles,
|
||||
);
|
||||
let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use ecow::eco_format;
|
||||
|
||||
use crate::diag::{SourceResult, Trace, Tracepoint};
|
||||
@ -12,6 +14,7 @@ use crate::layout::{
|
||||
use crate::model::Figurable;
|
||||
use crate::syntax::Span;
|
||||
use crate::text::{Lang, LocalName, Region};
|
||||
use crate::util::NonZeroExt;
|
||||
use crate::visualize::{Paint, Stroke};
|
||||
|
||||
/// A table of items.
|
||||
@ -346,6 +349,10 @@ pub struct TableCell {
|
||||
/// The cell's fill override.
|
||||
fill: Smart<Option<Paint>>,
|
||||
|
||||
/// The amount of columns spanned by this cell.
|
||||
#[default(NonZeroUsize::ONE)]
|
||||
colspan: NonZeroUsize,
|
||||
|
||||
/// The cell's alignment override.
|
||||
align: Smart<Alignment>,
|
||||
|
||||
@ -375,6 +382,7 @@ impl ResolvableCell for Packed<TableCell> {
|
||||
styles: StyleChain,
|
||||
) -> Cell {
|
||||
let cell = &mut *self;
|
||||
let colspan = cell.colspan(styles);
|
||||
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
|
||||
cell.push_x(Smart::Custom(x));
|
||||
cell.push_y(Smart::Custom(y));
|
||||
@ -392,7 +400,7 @@ impl ResolvableCell for Packed<TableCell> {
|
||||
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)).map(Some),
|
||||
));
|
||||
|
||||
Cell { body: self.pack(), fill }
|
||||
Cell { body: self.pack(), fill, colspan }
|
||||
}
|
||||
|
||||
fn x(&self, styles: StyleChain) -> Smart<usize> {
|
||||
@ -403,6 +411,10 @@ impl ResolvableCell for Packed<TableCell> {
|
||||
(**self).y(styles)
|
||||
}
|
||||
|
||||
fn colspan(&self, styles: StyleChain) -> std::num::NonZeroUsize {
|
||||
(**self).colspan(styles)
|
||||
}
|
||||
|
||||
fn span(&self) -> Span {
|
||||
Packed::span(self)
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ pub use self::scalar::Scalar;
|
||||
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::hash::Hash;
|
||||
use std::iter::{Chain, Flatten, Rev};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::{Add, Deref, Div, Mul, Neg, Sub};
|
||||
use std::sync::Arc;
|
||||
@ -116,6 +117,34 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter for reversing iterators conditionally.
|
||||
pub trait MaybeReverseIter {
|
||||
type RevIfIter;
|
||||
|
||||
/// Reverse this iterator (apply .rev()) based on some condition.
|
||||
fn rev_if(self, condition: bool) -> Self::RevIfIter
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl<I: Iterator + DoubleEndedIterator> MaybeReverseIter for I {
|
||||
type RevIfIter =
|
||||
Chain<Flatten<std::option::IntoIter<I>>, Flatten<std::option::IntoIter<Rev<I>>>>;
|
||||
|
||||
fn rev_if(self, condition: bool) -> Self::RevIfIter
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let (maybe_self_iter, maybe_rev_iter) =
|
||||
if condition { (None, Some(self.rev())) } else { (Some(self), None) };
|
||||
|
||||
maybe_self_iter
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(maybe_rev_iter.into_iter().flatten())
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the [`Option`]-wrapped L is same to R.
|
||||
pub fn option_eq<L, R>(left: Option<L>, other: R) -> bool
|
||||
where
|
||||
|
BIN
tests/ref/layout/grid-colspan.png
Normal file
BIN
tests/ref/layout/grid-colspan.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 89 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 40 KiB |
141
tests/typ/layout/grid-colspan.typ
Normal file
141
tests/typ/layout/grid-colspan.typ
Normal file
@ -0,0 +1,141 @@
|
||||
#grid(
|
||||
columns: 4,
|
||||
fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
|
||||
inset: 5pt,
|
||||
align: center,
|
||||
grid.cell(colspan: 4)[*Full Header*],
|
||||
grid.cell(colspan: 2, fill: orange)[*Half*],
|
||||
grid.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
|
||||
[*A*], [*B*], [*C*], [*D*],
|
||||
[1], [2], [3], [4],
|
||||
[5], grid.cell(colspan: 3, fill: orange.darken(10%))[6],
|
||||
grid.cell(colspan: 2, fill: orange)[7], [8], [9],
|
||||
[10], grid.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
|
||||
)
|
||||
|
||||
#table(
|
||||
columns: 4,
|
||||
fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
|
||||
inset: 5pt,
|
||||
align: center,
|
||||
table.cell(colspan: 4)[*Full Header*],
|
||||
table.cell(colspan: 2, fill: orange)[*Half*],
|
||||
table.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
|
||||
[*A*], [*B*], [*C*], [*D*],
|
||||
[1], [2], [3], [4],
|
||||
[5], table.cell(colspan: 3, fill: orange.darken(10%))[6],
|
||||
table.cell(colspan: 2, fill: orange)[7], [8], [9],
|
||||
[10], table.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
|
||||
)
|
||||
|
||||
---
|
||||
#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(colspan: 4)[*Full Header*],
|
||||
grid.cell(colspan: 2, fill: orange)[*Half*],
|
||||
grid.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
|
||||
[*A*], [*B*], [*C*], [*D*],
|
||||
[1], [2], [3], [4],
|
||||
[5], grid.cell(colspan: 3, fill: orange.darken(10%))[6],
|
||||
grid.cell(colspan: 2, fill: orange)[7], [8], [9],
|
||||
[10], grid.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
|
||||
)
|
||||
|
||||
#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(colspan: 4)[*Full Header*],
|
||||
table.cell(colspan: 2, fill: orange)[*Half*],
|
||||
table.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
|
||||
[*A*], [*B*], [*C*], [*D*],
|
||||
[1], [2], [3], [4],
|
||||
[5], table.cell(colspan: 3, fill: orange.darken(10%))[6],
|
||||
table.cell(colspan: 2, fill: orange)[7], [8], [9],
|
||||
[10], table.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
|
||||
)
|
||||
|
||||
---
|
||||
#set page(width: 300pt)
|
||||
#table(
|
||||
columns: (2em, 2em, auto, auto),
|
||||
stroke: 5pt,
|
||||
[A], [B], [C], [D],
|
||||
table.cell(colspan: 4, lorem(20)),
|
||||
[A], table.cell(colspan: 2)[BCBCBCBC], [D]
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 3:8-3:32 cell's colspan would cause it to exceed the available column(s)
|
||||
// Hint: 3:8-3:32 try placing the cell in another position or reducing its colspan
|
||||
#grid(
|
||||
columns: 3,
|
||||
[a], grid.cell(colspan: 3)[b]
|
||||
)
|
||||
|
||||
---
|
||||
// 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
|
||||
#grid(
|
||||
columns: 3,
|
||||
grid.cell(x: 2, y: 0)[x],
|
||||
[a], grid.cell(colspan: 2)[b]
|
||||
)
|
||||
|
||||
---
|
||||
// Colspan over all fractional columns shouldn't expand auto columns on finite pages
|
||||
#table(
|
||||
columns: (1fr, 1fr, auto),
|
||||
[A], [B], [C],
|
||||
[D], [E], [F]
|
||||
)
|
||||
#table(
|
||||
columns: (1fr, 1fr, auto),
|
||||
table.cell(colspan: 3, lorem(8)),
|
||||
[A], [B], [C],
|
||||
[D], [E], [F]
|
||||
)
|
||||
|
||||
---
|
||||
// Colspan over only some fractional columns will not trigger the heuristic, and
|
||||
// the auto column will expand more than it should. The table looks off, as a result.
|
||||
#table(
|
||||
columns: (1fr, 1fr, auto),
|
||||
[], table.cell(colspan: 2, lorem(8)),
|
||||
[A], [B], [C],
|
||||
[D], [E], [F]
|
||||
)
|
||||
|
||||
---
|
||||
// On infinite pages, colspan over all fractional columns SHOULD expand auto columns
|
||||
#set page(width: auto)
|
||||
#table(
|
||||
columns: (1fr, 1fr, auto),
|
||||
[A], [B], [C],
|
||||
[D], [E], [F]
|
||||
)
|
||||
#table(
|
||||
columns: (1fr, 1fr, auto),
|
||||
table.cell(colspan: 3, lorem(8)),
|
||||
[A], [B], [C],
|
||||
[D], [E], [F]
|
||||
)
|
||||
|
||||
---
|
||||
// Test multiple regions
|
||||
#set page(height: 5em)
|
||||
#grid(
|
||||
stroke: red,
|
||||
fill: aqua,
|
||||
columns: 4,
|
||||
[a], [b], [c], [d],
|
||||
[a], grid.cell(colspan: 2)[e, f, g, h, i], [f],
|
||||
[e], [g], grid.cell(colspan: 2)[eee\ e\ e\ e],
|
||||
grid.cell(colspan: 4)[eeee e e e]
|
||||
)
|
@ -7,3 +7,84 @@
|
||||
---
|
||||
#set text(dir: rtl)
|
||||
#table(columns: 2)[A][B][C][D]
|
||||
|
||||
---
|
||||
// Test interaction between RTL and colspans
|
||||
#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(colspan: 4)[*Full Header*],
|
||||
grid.cell(colspan: 2, fill: orange)[*Half*],
|
||||
grid.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
|
||||
[*A*], [*B*], [*C*], [*D*],
|
||||
[1], [2], [3], [4],
|
||||
[5], grid.cell(colspan: 3, fill: orange.darken(10%))[6],
|
||||
grid.cell(colspan: 2, fill: orange)[7], [8], [9],
|
||||
[10], grid.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
|
||||
)
|
||||
|
||||
#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(colspan: 4)[*Full Header*],
|
||||
grid.cell(colspan: 2, fill: orange)[*Half*],
|
||||
grid.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
|
||||
[*A*], [*B*], [*C*], [*D*],
|
||||
[1], [2], [3], [4],
|
||||
[5], grid.cell(colspan: 3, fill: orange.darken(10%))[6],
|
||||
grid.cell(colspan: 2, fill: orange)[7], [8], [9],
|
||||
[10], grid.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
|
||||
)
|
||||
|
||||
---
|
||||
#set text(dir: rtl)
|
||||
#table(
|
||||
columns: 4,
|
||||
fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
|
||||
inset: 5pt,
|
||||
align: center,
|
||||
table.cell(colspan: 4)[*Full Header*],
|
||||
table.cell(colspan: 2, fill: orange)[*Half*],
|
||||
table.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
|
||||
[*A*], [*B*], [*C*], [*D*],
|
||||
[1], [2], [3], [4],
|
||||
[5], table.cell(colspan: 3, fill: orange.darken(10%))[6],
|
||||
table.cell(colspan: 2, fill: orange)[7], [8], [9],
|
||||
[10], table.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
|
||||
)
|
||||
|
||||
#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(colspan: 4)[*Full Header*],
|
||||
table.cell(colspan: 2, fill: orange)[*Half*],
|
||||
table.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
|
||||
[*A*], [*B*], [*C*], [*D*],
|
||||
[1], [2], [3], [4],
|
||||
[5], table.cell(colspan: 3, fill: orange.darken(10%))[6],
|
||||
table.cell(colspan: 2, fill: orange)[7], [8], [9],
|
||||
[10], table.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
|
||||
)
|
||||
|
||||
---
|
||||
// Test multiple regions
|
||||
#set page(height: 5em)
|
||||
#set text(dir: rtl)
|
||||
#grid(
|
||||
stroke: red,
|
||||
fill: aqua,
|
||||
columns: 4,
|
||||
[a], [b], [c], [d],
|
||||
[a], grid.cell(colspan: 2)[e, f, g, h, i], [f],
|
||||
[e], [g], grid.cell(colspan: 2)[eee\ e\ e\ e],
|
||||
grid.cell(colspan: 4)[eeee e e e]
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user