mirror of
https://github.com/typst/typst
synced 2025-05-18 11:05:28 +08:00
Table cell x
and y
fields [More Flexible Tables Pt.2b] (#3050)
This commit is contained in:
parent
7cb257a1ac
commit
21585e03cf
@ -1,4 +1,8 @@
|
||||
use crate::diag::{bail, At, SourceResult, StrResult};
|
||||
use ecow::eco_format;
|
||||
|
||||
use crate::diag::{
|
||||
bail, At, Hint, HintedStrResult, HintedString, SourceResult, StrResult,
|
||||
};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
Array, CastInfo, Content, FromValue, Func, IntoValue, Reflect, Resolve, Smart,
|
||||
@ -83,6 +87,7 @@ impl<T: FromValue> FromValue for Celled<T> {
|
||||
}
|
||||
|
||||
/// Represents a cell in CellGrid, to be laid out by GridLayouter.
|
||||
#[derive(Clone)]
|
||||
pub struct Cell {
|
||||
/// The cell's body.
|
||||
pub body: Content,
|
||||
@ -123,6 +128,15 @@ pub trait ResolvableCell {
|
||||
inset: Sides<Rel<Length>>,
|
||||
styles: StyleChain,
|
||||
) -> Cell;
|
||||
|
||||
/// Returns this cell's column override.
|
||||
fn x(&self, styles: StyleChain) -> Smart<usize>;
|
||||
|
||||
/// Returns this cell's row override.
|
||||
fn y(&self, styles: StyleChain) -> Smart<usize>;
|
||||
|
||||
/// The cell's span, for errors.
|
||||
fn span(&self) -> Span;
|
||||
}
|
||||
|
||||
/// A grid of cells, including the columns, rows, and cell data.
|
||||
@ -200,12 +214,12 @@ impl CellGrid {
|
||||
Self { cols, rows, cells, has_gutter, is_rtl }
|
||||
}
|
||||
|
||||
/// Resolves all cells in the grid before creating it.
|
||||
/// Allows them to keep track of their final properties and adjust their
|
||||
/// fields accordingly.
|
||||
/// Resolves and positions all cells in the grid before creating it.
|
||||
/// Allows them to keep track of their final properties and positions
|
||||
/// and adjust their fields accordingly.
|
||||
/// Cells must implement Clone as they will be owned. Additionally, they
|
||||
/// must implement Default in order to fill the last row of the grid with
|
||||
/// empty cells, if it is not completely filled.
|
||||
/// must implement Default in order to fill positions in the grid which
|
||||
/// weren't explicitly specified by the user with empty cells.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn resolve<T: ResolvableCell + Clone + Default>(
|
||||
tracks: Axes<&[Sizing]>,
|
||||
@ -216,38 +230,129 @@ impl CellGrid {
|
||||
inset: Sides<Rel<Length>>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
span: Span,
|
||||
) -> SourceResult<Self> {
|
||||
// Number of content columns: Always at least one.
|
||||
let c = tracks.x.len().max(1);
|
||||
|
||||
// If not all columns in the last row have cells, we will add empty
|
||||
// cells and complete the row so that those positions are susceptible
|
||||
// to show rules and receive grid styling.
|
||||
// We apply '% c' twice so that 'cells_remaining' is zero when
|
||||
// the last row is already filled (then 'cell_count % c' would be zero).
|
||||
let cell_count = cells.len();
|
||||
let cells_remaining = (c - cell_count % c) % c;
|
||||
let cells = cells
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(std::iter::repeat_with(T::default).take(cells_remaining))
|
||||
// We can't just use the cell's index in the 'cells' vector to
|
||||
// determine its automatic position, since cells could have arbitrary
|
||||
// positions, so the position of a cell in 'cells' can differ from its
|
||||
// final position in 'resolved_cells' (see below).
|
||||
// Therefore, we use a counter, 'auto_index', to determine the position
|
||||
// of the next cell with (x: auto, y: auto). It is only stepped when
|
||||
// a cell with (x: auto, y: auto), usually the vast majority, is found.
|
||||
let mut auto_index = 0;
|
||||
|
||||
// We have to rebuild the grid to account for arbitrary positions.
|
||||
// Create at least 'cells.len()' positions, since there will be at
|
||||
// least 'cells.len()' cells, even though some of them might be placed
|
||||
// in arbitrary positions and thus cause the grid to expand.
|
||||
// Additionally, make sure we allocate up to the next multiple of 'c',
|
||||
// since each row will have 'c' cells, even if the last few cells
|
||||
// weren't explicitly specified by the user.
|
||||
// We apply '% c' twice so that the amount of cells potentially missing
|
||||
// is zero when 'cells.len()' is already a multiple of 'c' (thus
|
||||
// 'cells.len() % c' would be zero).
|
||||
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);
|
||||
for cell in cells.iter().cloned() {
|
||||
let cell_span = cell.span();
|
||||
// Let's calculate the cell's final position based on its
|
||||
// requested position.
|
||||
let resolved_index = {
|
||||
let cell_x = cell.x(styles);
|
||||
let cell_y = cell.y(styles);
|
||||
resolve_cell_position(cell_x, cell_y, &resolved_cells, &mut auto_index, c)
|
||||
.at(cell_span)?
|
||||
};
|
||||
let x = resolved_index % c;
|
||||
let y = resolved_index / c;
|
||||
|
||||
// Let's resolve the cell so it can determine its own fields
|
||||
// based on its final position.
|
||||
let cell = cell.resolve_cell(
|
||||
x,
|
||||
y,
|
||||
&fill.resolve(engine, x, y)?,
|
||||
align.resolve(engine, x, y)?,
|
||||
inset,
|
||||
styles,
|
||||
);
|
||||
|
||||
if resolved_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)
|
||||
// to ensure the last row in the new vector length is
|
||||
// completely filled. This is necessary so that those
|
||||
// positions, even if not explicitly used at the end, are
|
||||
// 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
|
||||
.checked_add(1)
|
||||
.and_then(|new_len| new_len.checked_add((c - new_len % c) % c))
|
||||
else {
|
||||
bail!(cell_span, "cell position too large")
|
||||
};
|
||||
|
||||
// Here, the cell needs to be placed in a position which
|
||||
// doesn't exist yet in the grid (out of bounds). We will add
|
||||
// enough absent positions for this to be possible. They must
|
||||
// be absent as no cells actually occupy them (they can be
|
||||
// overridden later); however, if no cells occupy them as we
|
||||
// finish building the grid, then such positions will be
|
||||
// replaced by empty cells.
|
||||
resolved_cells.resize(new_len, None);
|
||||
}
|
||||
|
||||
// The vector is large enough to contain the cell, so we can just
|
||||
// index it directly to access the position it will be placed in.
|
||||
// However, we still need to ensure we won't try to place a cell
|
||||
// where there already is one.
|
||||
let slot = &mut resolved_cells[resolved_index];
|
||||
if slot.is_some() {
|
||||
bail!(
|
||||
cell_span,
|
||||
"attempted to place a second cell at column {x}, row {y}";
|
||||
hint: "try specifying your cells in a different order"
|
||||
);
|
||||
}
|
||||
|
||||
*slot = Some(cell);
|
||||
}
|
||||
|
||||
// Replace absent entries by resolved empty cells, and produce a vector
|
||||
// of 'Cell' from 'Option<Cell>' (final step).
|
||||
let resolved_cells = resolved_cells
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, cell)| {
|
||||
let x = i % c;
|
||||
let y = i / c;
|
||||
if let Some(cell) = cell {
|
||||
Ok(cell)
|
||||
} else {
|
||||
let x = i % c;
|
||||
let y = i / c;
|
||||
|
||||
Ok(cell.resolve_cell(
|
||||
x,
|
||||
y,
|
||||
&fill.resolve(engine, x, y)?,
|
||||
align.resolve(engine, x, y)?,
|
||||
inset,
|
||||
styles,
|
||||
))
|
||||
// Ensure all absent entries are affected by show rules and
|
||||
// grid styling by turning them into resolved empty cells.
|
||||
let new_cell = T::default().resolve_cell(
|
||||
x,
|
||||
y,
|
||||
&fill.resolve(engine, x, y)?,
|
||||
align.resolve(engine, x, y)?,
|
||||
inset,
|
||||
styles,
|
||||
);
|
||||
Ok(new_cell)
|
||||
}
|
||||
})
|
||||
.collect::<SourceResult<Vec<_>>>()?;
|
||||
.collect::<SourceResult<Vec<Cell>>>()?;
|
||||
|
||||
Ok(Self::new(tracks, gutter, cells, styles))
|
||||
Ok(Self::new(tracks, gutter, resolved_cells, styles))
|
||||
}
|
||||
|
||||
/// Get the content of the cell in column `x` and row `y`.
|
||||
@ -278,6 +383,98 @@ impl CellGrid {
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a cell's requested x and y, the vector with the resolved cell
|
||||
/// positions, the `auto_index` counter (determines the position of the next
|
||||
/// `(auto, auto)` cell) and the amount of columns in the grid, returns the
|
||||
/// final index of this cell in the vector of resolved cells.
|
||||
fn resolve_cell_position(
|
||||
cell_x: Smart<usize>,
|
||||
cell_y: Smart<usize>,
|
||||
resolved_cells: &[Option<Cell>],
|
||||
auto_index: &mut usize,
|
||||
columns: usize,
|
||||
) -> HintedStrResult<usize> {
|
||||
// Translates a (x, y) position to the equivalent index in the final cell vector.
|
||||
// Errors if the position would be too large.
|
||||
let cell_index = |x, y: usize| {
|
||||
y.checked_mul(columns)
|
||||
.and_then(|row_index| row_index.checked_add(x))
|
||||
.ok_or_else(|| HintedString::from(eco_format!("cell position too large")))
|
||||
};
|
||||
match (cell_x, cell_y) {
|
||||
// Fully automatic cell positioning. The cell did not
|
||||
// request a coordinate.
|
||||
(Smart::Auto, Smart::Auto) => {
|
||||
// Let's find the first available position starting from the
|
||||
// automatic position counter, searching in row-major order.
|
||||
let mut resolved_index = *auto_index;
|
||||
while let Some(Some(_)) = resolved_cells.get(resolved_index) {
|
||||
// Skip any non-absent cell positions (`Some(None)`) to
|
||||
// determine where this cell will be placed. An out of bounds
|
||||
// position (thus `None`) is also a valid new position (only
|
||||
// requires expanding the vector).
|
||||
resolved_index += 1;
|
||||
}
|
||||
|
||||
// Ensure the next cell with automatic position will be
|
||||
// placed after this one (maybe not immediately after).
|
||||
*auto_index = resolved_index + 1;
|
||||
|
||||
Ok(resolved_index)
|
||||
}
|
||||
// Cell has chosen at least its column.
|
||||
(Smart::Custom(cell_x), cell_y) => {
|
||||
if cell_x >= columns {
|
||||
return Err(HintedString::from(eco_format!(
|
||||
"cell could not be placed at invalid column {cell_x}"
|
||||
)));
|
||||
}
|
||||
if let Smart::Custom(cell_y) = cell_y {
|
||||
// Cell has chosen its exact position.
|
||||
cell_index(cell_x, cell_y)
|
||||
} else {
|
||||
// Cell has only chosen its column.
|
||||
// Let's find the first row which has that column available.
|
||||
let mut resolved_y = 0;
|
||||
while let Some(Some(_)) =
|
||||
resolved_cells.get(cell_index(cell_x, resolved_y)?)
|
||||
{
|
||||
// Try each row until either we reach an absent position
|
||||
// (`Some(None)`) or an out of bounds position (`None`),
|
||||
// in which case we'd create a new row to place this cell in.
|
||||
resolved_y += 1;
|
||||
}
|
||||
cell_index(cell_x, resolved_y)
|
||||
}
|
||||
}
|
||||
// Cell has only chosen its row, not its column.
|
||||
(Smart::Auto, Smart::Custom(cell_y)) => {
|
||||
// Let's find the first column which has that row available.
|
||||
let first_row_pos = cell_index(0, cell_y)?;
|
||||
let last_row_pos = first_row_pos
|
||||
.checked_add(columns)
|
||||
.ok_or_else(|| eco_format!("cell position too large"))?;
|
||||
|
||||
(first_row_pos..last_row_pos)
|
||||
.find(|possible_index| {
|
||||
// Much like in the previous cases, we skip any occupied
|
||||
// positions until we either reach an absent position
|
||||
// (`Some(None)`) or an out of bounds position (`None`),
|
||||
// in which case we can just expand the vector enough to
|
||||
// place this cell. In either case, we found an available
|
||||
// position.
|
||||
!matches!(resolved_cells.get(*possible_index), Some(Some(_)))
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
eco_format!(
|
||||
"cell could not be placed in row {cell_y} because it was full"
|
||||
)
|
||||
})
|
||||
.hint("try specifying your cells in a different order")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs grid layout.
|
||||
pub struct GridLayouter<'a> {
|
||||
/// The grid of cells.
|
||||
|
@ -4,18 +4,19 @@ pub use self::layout::{Cell, CellGrid, Celled, GridLayouter, ResolvableCell};
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use ecow::eco_format;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
use crate::diag::{SourceResult, StrResult};
|
||||
use crate::diag::{SourceResult, StrResult, Trace, Tracepoint};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, scope, Array, Content, Fold, NativeElement, Packed, Show, Smart,
|
||||
StyleChain, Value,
|
||||
cast, elem, scope, Array, Content, Fold, Packed, Show, Smart, StyleChain, Value,
|
||||
};
|
||||
use crate::layout::{
|
||||
Abs, AlignElem, Alignment, Axes, Fragment, Layout, Length, Regions, Rel, Sides,
|
||||
Sizing,
|
||||
};
|
||||
use crate::syntax::Span;
|
||||
use crate::visualize::{Paint, Stroke};
|
||||
|
||||
/// Arranges content in a grid.
|
||||
@ -60,7 +61,8 @@ use crate::visualize::{Paint, Stroke};
|
||||
/// appearance options to depend on a cell's position (column and row), you may
|
||||
/// specify a function to `fill` or `align` of the form
|
||||
/// `(column, row) => value`. You may also use a show rule on
|
||||
/// [`grid.cell`]($grid.cell) - see that element's examples for more information.
|
||||
/// [`grid.cell`]($grid.cell) - see that element's examples or the examples
|
||||
/// below for more information.
|
||||
///
|
||||
/// # Examples
|
||||
/// The example below demonstrates the different track sizing options.
|
||||
@ -97,6 +99,61 @@ use crate::visualize::{Paint, Stroke};
|
||||
/// ..range(25).map(str)
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Additionally, you can use [`grid.cell`]($grid.cell) in various ways to
|
||||
/// not only style each cell based on its position and other fields, but also
|
||||
/// to determine the cell's preferential position in the table.
|
||||
///
|
||||
/// ```example
|
||||
/// #set page(width: auto)
|
||||
/// #show grid.cell: it => {
|
||||
/// if it.y == 0 {
|
||||
/// // The first row's text must be white and bold.
|
||||
/// set text(white)
|
||||
/// strong(it)
|
||||
/// } else {
|
||||
/// // For the second row and beyond, we will show the day number for each
|
||||
/// // cell.
|
||||
///
|
||||
/// // In general, a cell's index is given by cell.x + columns * cell.y.
|
||||
/// // Days start in the second grid row, so we subtract 1 row.
|
||||
/// // But the first day is day 1, not day 0, so we add 1.
|
||||
/// let day = it.x + 7 * (it.y - 1) + 1
|
||||
/// if day <= 31 {
|
||||
/// // Place the day's number at the top left of the cell.
|
||||
/// // Only if the day is valid for this month (not 32 or higher).
|
||||
/// place(top + left, dx: 2pt, dy: 2pt, text(8pt, red.darken(40%))[#day])
|
||||
/// }
|
||||
/// it
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #grid(
|
||||
/// fill: (x, y) => if y == 0 { gray.darken(50%) },
|
||||
/// columns: (30pt,) * 7,
|
||||
/// rows: (auto, 30pt),
|
||||
/// // Events will be written at the bottom of each day square.
|
||||
/// align: bottom,
|
||||
/// inset: 5pt,
|
||||
/// stroke: (thickness: 0.5pt, dash: "densely-dotted"),
|
||||
///
|
||||
/// [Sun], [Mon], [Tue], [Wed], [Thu], [Fri], [Sat],
|
||||
///
|
||||
/// // This event will occur on the first Friday (sixth column).
|
||||
/// grid.cell(x: 5, fill: yellow.darken(10%))[Call],
|
||||
///
|
||||
/// // This event will occur every Monday (second column).
|
||||
/// // We have to repeat it 5 times so it occurs every week.
|
||||
/// ..(grid.cell(x: 1, fill: red.lighten(50%))[Meet],) * 5,
|
||||
///
|
||||
/// // This event will occur at day 19.
|
||||
/// grid.cell(x: 4, y: 3, fill: orange.lighten(25%))[Talk],
|
||||
///
|
||||
/// // These events will occur at the second week, where available.
|
||||
/// grid.cell(y: 2, fill: aqua)[Chat],
|
||||
/// grid.cell(y: 2, fill: aqua)[Walk],
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(scope, Layout)]
|
||||
pub struct GridElem {
|
||||
/// The column sizes.
|
||||
@ -213,7 +270,7 @@ pub struct GridElem {
|
||||
///
|
||||
/// The cells are populated in row-major order.
|
||||
#[variadic]
|
||||
pub children: Vec<GridCell>,
|
||||
pub children: Vec<Packed<GridCell>>,
|
||||
}
|
||||
|
||||
#[scope]
|
||||
@ -241,6 +298,8 @@ impl Layout for Packed<GridElem> {
|
||||
|
||||
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
|
||||
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
|
||||
// Use trace to link back to the grid when a specific cell errors
|
||||
let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
|
||||
let grid = CellGrid::resolve(
|
||||
tracks,
|
||||
gutter,
|
||||
@ -250,7 +309,9 @@ impl Layout for Packed<GridElem> {
|
||||
inset,
|
||||
engine,
|
||||
styles,
|
||||
)?;
|
||||
self.span(),
|
||||
)
|
||||
.trace(engine.world, tracepoint, self.span())?;
|
||||
|
||||
let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
|
||||
|
||||
@ -290,12 +351,84 @@ cast! {
|
||||
/// [G], grid.cell(inset: 0pt)[H]
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// You may also apply a show rule on `grid.cell` to style all cells at once,
|
||||
/// which allows you, for example, to apply styles based on a cell's position:
|
||||
///
|
||||
/// ```example
|
||||
/// #show grid.cell: it => {
|
||||
/// if it.y == 0 {
|
||||
/// // First row is bold
|
||||
/// strong(it)
|
||||
/// } else if it.x == 1 {
|
||||
/// // Second column is italicized
|
||||
/// // (except at the first row)
|
||||
/// emph(it)
|
||||
/// } else {
|
||||
/// // Remaining cells aren't changed
|
||||
/// it
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #grid(
|
||||
/// columns: 3,
|
||||
/// gutter: 3pt,
|
||||
/// [Name], [Age], [Info],
|
||||
/// [John], [52], [Nice],
|
||||
/// [Mary], [50], [Cool],
|
||||
/// [Jake], [49], [Epic]
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(name = "cell", title = "Grid Cell", Show)]
|
||||
pub struct GridCell {
|
||||
/// The cell's body.
|
||||
#[required]
|
||||
body: Content,
|
||||
|
||||
/// The cell's column (zero-indexed).
|
||||
/// This field may be used in show rules to style a cell depending on its
|
||||
/// column.
|
||||
///
|
||||
/// You may override this field to pick in which column the cell must
|
||||
/// be placed. If no row (`y`) is chosen, the cell will be placed in the
|
||||
/// first row (starting at row 0) with that column available (or a new row
|
||||
/// if none). If both `x` and `y` are chosen, however, the cell will be
|
||||
/// placed in that exact position. An error is raised if that position is
|
||||
/// not available (thus, it is usually wise to specify cells with a custom
|
||||
/// position before cells with automatic positions).
|
||||
///
|
||||
/// ```example
|
||||
/// #grid(
|
||||
/// columns: 4,
|
||||
/// rows: 2.5em,
|
||||
/// fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
|
||||
/// align: center + horizon,
|
||||
/// inset: 3pt,
|
||||
/// grid.cell(x: 2, y: 2)[3],
|
||||
/// [1], grid.cell(x: 3)[4], [2],
|
||||
/// )
|
||||
/// ```
|
||||
x: Smart<usize>,
|
||||
|
||||
/// The cell's row (zero-indexed).
|
||||
/// This field may be used in show rules to style a cell depending on its
|
||||
/// row.
|
||||
///
|
||||
/// You may override this field to pick in which row the cell must be
|
||||
/// placed. If no column (`x`) is chosen, the cell will be placed in the
|
||||
/// first column (starting at column 0) available in the chosen row. If all
|
||||
/// columns in the chosen row are already occupied, an error is raised.
|
||||
///
|
||||
/// ```example
|
||||
/// #grid(
|
||||
/// columns: 2,
|
||||
/// fill: (x, y) => if calc.odd(x + y) { gray.lighten(40%) },
|
||||
/// inset: 1pt,
|
||||
/// [A], grid.cell(y: 1)[B], grid.cell(y: 1)[C], grid.cell(y: 2)[D]
|
||||
/// )
|
||||
/// ```
|
||||
y: Smart<usize>,
|
||||
|
||||
/// The cell's fill override.
|
||||
fill: Smart<Option<Paint>>,
|
||||
|
||||
@ -311,39 +444,54 @@ cast! {
|
||||
v: Content => v.into(),
|
||||
}
|
||||
|
||||
impl Default for GridCell {
|
||||
impl Default for Packed<GridCell> {
|
||||
fn default() -> Self {
|
||||
Self::new(Content::default())
|
||||
Packed::new(GridCell::new(Content::default()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolvableCell for GridCell {
|
||||
impl ResolvableCell for Packed<GridCell> {
|
||||
fn resolve_cell(
|
||||
mut self,
|
||||
_: usize,
|
||||
_: usize,
|
||||
x: usize,
|
||||
y: usize,
|
||||
fill: &Option<Paint>,
|
||||
align: Smart<Alignment>,
|
||||
inset: Sides<Rel<Length>>,
|
||||
styles: StyleChain,
|
||||
) -> Cell {
|
||||
let fill = self.fill(styles).unwrap_or_else(|| fill.clone());
|
||||
self.push_fill(Smart::Custom(fill.clone()));
|
||||
self.push_align(match align {
|
||||
let cell = &mut *self;
|
||||
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
|
||||
cell.push_x(Smart::Custom(x));
|
||||
cell.push_y(Smart::Custom(y));
|
||||
cell.push_fill(Smart::Custom(fill.clone()));
|
||||
cell.push_align(match align {
|
||||
Smart::Custom(align) => {
|
||||
Smart::Custom(self.align(styles).map_or(align, |inner| inner.fold(align)))
|
||||
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
|
||||
}
|
||||
// Don't fold if the grid is using outer alignment. Use the
|
||||
// cell's alignment instead (which, in the end, will fold with
|
||||
// the outer alignment when it is effectively displayed).
|
||||
Smart::Auto => self.align(styles),
|
||||
Smart::Auto => cell.align(styles),
|
||||
});
|
||||
self.push_inset(Smart::Custom(
|
||||
self.inset(styles).map_or(inset, |inner| inner.fold(inset)).map(Some),
|
||||
cell.push_inset(Smart::Custom(
|
||||
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)).map(Some),
|
||||
));
|
||||
|
||||
Cell { body: self.pack(), fill }
|
||||
}
|
||||
|
||||
fn x(&self, styles: StyleChain) -> Smart<usize> {
|
||||
(**self).x(styles)
|
||||
}
|
||||
|
||||
fn y(&self, styles: StyleChain) -> Smart<usize> {
|
||||
(**self).y(styles)
|
||||
}
|
||||
|
||||
fn span(&self) -> Span {
|
||||
Packed::span(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Show for Packed<GridCell> {
|
||||
|
@ -240,8 +240,13 @@ impl Show for Packed<BibliographyElem> {
|
||||
if references.iter().any(|(prefix, _)| prefix.is_some()) {
|
||||
let mut cells = vec![];
|
||||
for (prefix, reference) in references {
|
||||
cells.push(GridCell::new(prefix.clone().unwrap_or_default()));
|
||||
cells.push(GridCell::new(reference.clone()));
|
||||
cells.push(
|
||||
Packed::new(GridCell::new(prefix.clone().unwrap_or_default()))
|
||||
.spanned(span),
|
||||
);
|
||||
cells.push(
|
||||
Packed::new(GridCell::new(reference.clone())).spanned(span),
|
||||
);
|
||||
}
|
||||
|
||||
seq.push(VElem::new(row_gutter).with_weakness(3).pack());
|
||||
@ -945,11 +950,14 @@ impl ElemRenderer<'_> {
|
||||
|
||||
if let Some(prefix) = suf_prefix {
|
||||
const COLUMN_GUTTER: Em = Em::new(0.65);
|
||||
content = GridElem::new(vec![GridCell::new(prefix), GridCell::new(content)])
|
||||
.with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
|
||||
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
|
||||
.pack()
|
||||
.spanned(self.span);
|
||||
content = GridElem::new(vec![
|
||||
Packed::new(GridCell::new(prefix)).spanned(self.span),
|
||||
Packed::new(GridCell::new(content)).spanned(self.span),
|
||||
])
|
||||
.with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
|
||||
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
|
||||
.pack()
|
||||
.spanned(self.span);
|
||||
}
|
||||
|
||||
match elem.display {
|
||||
|
@ -1,13 +1,16 @@
|
||||
use crate::diag::SourceResult;
|
||||
use ecow::eco_format;
|
||||
|
||||
use crate::diag::{SourceResult, Trace, Tracepoint};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, scope, Content, Fold, NativeElement, Packed, Show, Smart, StyleChain,
|
||||
cast, elem, scope, Content, Fold, Packed, Show, Smart, StyleChain,
|
||||
};
|
||||
use crate::layout::{
|
||||
show_grid_cell, Abs, Alignment, Axes, Cell, CellGrid, Celled, Fragment, GridLayouter,
|
||||
Layout, Length, Regions, Rel, ResolvableCell, Sides, TrackSizings,
|
||||
};
|
||||
use crate::model::Figurable;
|
||||
use crate::syntax::Span;
|
||||
use crate::text::{Lang, LocalName, Region};
|
||||
use crate::visualize::{Paint, Stroke};
|
||||
|
||||
@ -29,6 +32,8 @@ use crate::visualize::{Paint, Stroke};
|
||||
/// [figure]($figure).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// The example below demonstrates some of the most common table options.
|
||||
/// ```example
|
||||
/// #table(
|
||||
/// columns: (1fr, auto, auto),
|
||||
@ -47,6 +52,40 @@ use crate::visualize::{Paint, Stroke};
|
||||
/// [$a$: edge length]
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Much like with grids, you can use [`table.cell`]($table.cell) to customize
|
||||
/// the appearance and the position of each cell.
|
||||
///
|
||||
/// ```example
|
||||
/// #set page(width: auto)
|
||||
/// #show table.cell: it => {
|
||||
/// if it.x == 0 or it.y == 0 {
|
||||
/// set text(white)
|
||||
/// strong(it)
|
||||
/// } else if it.body == [] {
|
||||
/// // Replace empty cells with 'N/A'
|
||||
/// pad(rest: it.inset)[_N/A_]
|
||||
/// } else {
|
||||
/// it
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #table(
|
||||
/// fill: (x, y) => if x == 0 or y == 0 { gray.darken(50%) },
|
||||
/// columns: 4,
|
||||
/// [], [Exam 1], [Exam 2], [Exam 3],
|
||||
/// ..([John], [Mary], [Jake], [Robert]).map(table.cell.with(x: 0)),
|
||||
///
|
||||
/// // Mary got grade A on Exam 3.
|
||||
/// table.cell(x: 3, y: 2, fill: green)[A],
|
||||
///
|
||||
/// // Everyone got grade A on Exam 2.
|
||||
/// ..(table.cell(x: 2, fill: green)[A],) * 4,
|
||||
///
|
||||
/// // Robert got grade B on other exams.
|
||||
/// ..(table.cell(y: 4, fill: aqua)[B],) * 2,
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(scope, Layout, LocalName, Figurable)]
|
||||
pub struct TableElem {
|
||||
/// The column sizes. See the [grid documentation]($grid) for more
|
||||
@ -157,7 +196,7 @@ pub struct TableElem {
|
||||
|
||||
/// The contents of the table cells.
|
||||
#[variadic]
|
||||
pub children: Vec<TableCell>,
|
||||
pub children: Vec<Packed<TableCell>>,
|
||||
}
|
||||
|
||||
#[scope]
|
||||
@ -185,6 +224,8 @@ impl Layout for Packed<TableElem> {
|
||||
|
||||
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
|
||||
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
|
||||
// Use trace to link back to the table when a specific cell errors
|
||||
let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
|
||||
let grid = CellGrid::resolve(
|
||||
tracks,
|
||||
gutter,
|
||||
@ -194,7 +235,9 @@ impl Layout for Packed<TableElem> {
|
||||
inset,
|
||||
engine,
|
||||
styles,
|
||||
)?;
|
||||
self.span(),
|
||||
)
|
||||
.trace(engine.world, tracepoint, self.span())?;
|
||||
|
||||
let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
|
||||
|
||||
@ -259,12 +302,48 @@ impl Figurable for Packed<TableElem> {}
|
||||
/// [M.], table.cell(inset: 0pt)[Player]
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// You may also apply a show rule on `table.cell` to style all cells at once,
|
||||
/// which allows you, for example, to apply styles based on a cell's position:
|
||||
///
|
||||
/// ```example
|
||||
/// #show table.cell: it => {
|
||||
/// if it.y == 0 {
|
||||
/// // First row is bold
|
||||
/// strong(it)
|
||||
/// } else if it.x == 1 {
|
||||
/// // Second column is italicized
|
||||
/// // (except at the first row)
|
||||
/// emph(it)
|
||||
/// } else {
|
||||
/// // Remaining cells aren't changed
|
||||
/// it
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #table(
|
||||
/// columns: 3,
|
||||
/// gutter: 3pt,
|
||||
/// [Name], [Age], [Info],
|
||||
/// [John], [52], [Nice],
|
||||
/// [Mary], [50], [Cool],
|
||||
/// [Jake], [49], [Epic]
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(name = "cell", title = "Table Cell", Show)]
|
||||
pub struct TableCell {
|
||||
/// The cell's body.
|
||||
#[required]
|
||||
body: Content,
|
||||
|
||||
/// The cell's column (zero-indexed).
|
||||
/// Functions identically to the `x` field in [`grid.cell`]($grid.cell).
|
||||
x: Smart<usize>,
|
||||
|
||||
/// The cell's row (zero-indexed).
|
||||
/// Functions identically to the `y` field in [`grid.cell`]($grid.cell).
|
||||
y: Smart<usize>,
|
||||
|
||||
/// The cell's fill override.
|
||||
fill: Smart<Option<Paint>>,
|
||||
|
||||
@ -280,39 +359,54 @@ cast! {
|
||||
v: Content => v.into(),
|
||||
}
|
||||
|
||||
impl Default for TableCell {
|
||||
impl Default for Packed<TableCell> {
|
||||
fn default() -> Self {
|
||||
Self::new(Content::default())
|
||||
Packed::new(TableCell::new(Content::default()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolvableCell for TableCell {
|
||||
impl ResolvableCell for Packed<TableCell> {
|
||||
fn resolve_cell(
|
||||
mut self,
|
||||
_: usize,
|
||||
_: usize,
|
||||
x: usize,
|
||||
y: usize,
|
||||
fill: &Option<Paint>,
|
||||
align: Smart<Alignment>,
|
||||
inset: Sides<Rel<Length>>,
|
||||
styles: StyleChain,
|
||||
) -> Cell {
|
||||
let fill = self.fill(styles).unwrap_or_else(|| fill.clone());
|
||||
self.push_fill(Smart::Custom(fill.clone()));
|
||||
self.push_align(match align {
|
||||
let cell = &mut *self;
|
||||
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
|
||||
cell.push_x(Smart::Custom(x));
|
||||
cell.push_y(Smart::Custom(y));
|
||||
cell.push_fill(Smart::Custom(fill.clone()));
|
||||
cell.push_align(match align {
|
||||
Smart::Custom(align) => {
|
||||
Smart::Custom(self.align(styles).map_or(align, |inner| inner.fold(align)))
|
||||
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
|
||||
}
|
||||
// Don't fold if the table is using outer alignment. Use the
|
||||
// cell's alignment instead (which, in the end, will fold with
|
||||
// the outer alignment when it is effectively displayed).
|
||||
Smart::Auto => self.align(styles),
|
||||
Smart::Auto => cell.align(styles),
|
||||
});
|
||||
self.push_inset(Smart::Custom(
|
||||
self.inset(styles).map_or(inset, |inner| inner.fold(inset)).map(Some),
|
||||
cell.push_inset(Smart::Custom(
|
||||
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)).map(Some),
|
||||
));
|
||||
|
||||
Cell { body: self.pack(), fill }
|
||||
}
|
||||
|
||||
fn x(&self, styles: StyleChain) -> Smart<usize> {
|
||||
(**self).x(styles)
|
||||
}
|
||||
|
||||
fn y(&self, styles: StyleChain) -> Smart<usize> {
|
||||
(**self).y(styles)
|
||||
}
|
||||
|
||||
fn span(&self) -> Span {
|
||||
Packed::span(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Show for Packed<TableCell> {
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 44 KiB |
BIN
tests/ref/layout/grid-positioning.png
Normal file
BIN
tests/ref/layout/grid-positioning.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
Binary file not shown.
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 48 KiB |
@ -105,3 +105,25 @@
|
||||
[Sweet], [Italics]
|
||||
)
|
||||
}
|
||||
|
||||
---
|
||||
// Style based on position
|
||||
#{
|
||||
show grid.cell: it => {
|
||||
if it.y == 0 {
|
||||
strong(it)
|
||||
} else if it.x == 1 {
|
||||
emph(it)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
grid(
|
||||
columns: 3,
|
||||
gutter: 3pt,
|
||||
[Name], [Age], [Info],
|
||||
[John], [52], [Nice],
|
||||
[Mary], [50], [Cool],
|
||||
[Jake], [49], [Epic]
|
||||
)
|
||||
}
|
||||
|
223
tests/typ/layout/grid-positioning.typ
Normal file
223
tests/typ/layout/grid-positioning.typ
Normal file
@ -0,0 +1,223 @@
|
||||
// Test cell positioning in grids.
|
||||
|
||||
---
|
||||
#{
|
||||
show grid.cell: it => (it.x, it.y)
|
||||
grid(
|
||||
columns: 2,
|
||||
inset: 5pt,
|
||||
fill: aqua,
|
||||
gutter: 3pt,
|
||||
[Hello], [World],
|
||||
[Sweet], [Home]
|
||||
)
|
||||
}
|
||||
#{
|
||||
show table.cell: it => pad(rest: it.inset)[#(it.x, it.y)]
|
||||
table(
|
||||
columns: 2,
|
||||
gutter: 3pt,
|
||||
[Hello], [World],
|
||||
[Sweet], [Home]
|
||||
)
|
||||
}
|
||||
|
||||
---
|
||||
// Positioning cells in a different order than they appear
|
||||
#grid(
|
||||
columns: 2,
|
||||
[A], [B],
|
||||
grid.cell(x: 1, y: 2)[C], grid.cell(x: 0, y: 2)[D],
|
||||
grid.cell(x: 1, y: 1)[E], grid.cell(x: 0, y: 1)[F],
|
||||
)
|
||||
|
||||
---
|
||||
// Creating more rows by positioning out of bounds
|
||||
#grid(
|
||||
columns: 3,
|
||||
rows: 1.5em,
|
||||
inset: 5pt,
|
||||
fill: (x, y) => if (x, y) == (0, 0) { blue } else if (x, y) == (2, 3) { red } else { green },
|
||||
[A],
|
||||
grid.cell(x: 2, y: 3)[B]
|
||||
)
|
||||
|
||||
#table(
|
||||
columns: (3em, 1em, 3em),
|
||||
rows: 1.5em,
|
||||
inset: (top: 0pt, bottom: 0pt, rest: 5pt),
|
||||
fill: (x, y) => if (x, y) == (0, 0) { blue } else if (x, y) == (2, 3) { red } else { green },
|
||||
align: (x, y) => (left, center, right).at(x),
|
||||
[A],
|
||||
table.cell(x: 2, y: 3)[B]
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 3:3-3:42 attempted to place a second cell at column 0, row 0
|
||||
// Hint: 3:3-3:42 try specifying your cells in a different order
|
||||
#grid(
|
||||
[A],
|
||||
grid.cell(x: 0, y: 0)[This shall error]
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 3:3-3:43 attempted to place a second cell at column 0, row 0
|
||||
// Hint: 3:3-3:43 try specifying your cells in a different order
|
||||
#table(
|
||||
[A],
|
||||
table.cell(x: 0, y: 0)[This shall error]
|
||||
)
|
||||
|
||||
---
|
||||
// Automatic position cell skips custom position cell
|
||||
#grid(
|
||||
grid.cell(x: 0, y: 0)[This shall not error],
|
||||
[A]
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 4:3-4:36 cell could not be placed at invalid column 2
|
||||
#grid(
|
||||
columns: 2,
|
||||
[A],
|
||||
grid.cell(x: 2)[This shall error]
|
||||
)
|
||||
|
||||
---
|
||||
// Partial positioning
|
||||
#grid(
|
||||
columns: 3,
|
||||
rows: 1.5em,
|
||||
inset: 5pt,
|
||||
fill: aqua,
|
||||
[A], grid.cell(y: 1, fill: green)[B], [C], grid.cell(x: auto, y: 1, fill: green)[D], [E],
|
||||
grid.cell(y: 2, fill: green)[F], grid.cell(x: 0, fill: orange)[G], grid.cell(x: 0, y: auto, fill: orange)[H],
|
||||
grid.cell(x: 1, fill: orange)[I]
|
||||
)
|
||||
|
||||
#table(
|
||||
columns: 3,
|
||||
rows: 1.5em,
|
||||
inset: 5pt,
|
||||
fill: aqua,
|
||||
[A], table.cell(y: 1, fill: green)[B], [C], table.cell(x: auto, y: 1, fill: green)[D], [E],
|
||||
table.cell(y: 2, fill: green)[F], table.cell(x: 0, fill: orange)[G], table.cell(x: 0, y: auto, fill: orange)[H],
|
||||
table.cell(x: 1, fill: orange)[I]
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 4:3-4:21 cell could not be placed in row 0 because it was full
|
||||
// Hint: 4:3-4:21 try specifying your cells in a different order
|
||||
#grid(
|
||||
columns: 2,
|
||||
[A], [B],
|
||||
grid.cell(y: 0)[C]
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 4:3-4:22 cell could not be placed in row 0 because it was full
|
||||
// Hint: 4:3-4:22 try specifying your cells in a different order
|
||||
#table(
|
||||
columns: 2,
|
||||
[A], [B],
|
||||
table.cell(y: 0)[C]
|
||||
)
|
||||
|
||||
---
|
||||
// Doc example 1
|
||||
#set page(width: auto)
|
||||
#show grid.cell: it => {
|
||||
if it.y == 0 {
|
||||
set text(white)
|
||||
strong(it)
|
||||
} else {
|
||||
// For the second row and beyond, we will write the day number for each
|
||||
// cell.
|
||||
|
||||
// In general, a cell's index is given by cell.x + columns * cell.y.
|
||||
// Days start in the second grid row, so we subtract 1 row.
|
||||
// But the first day is day 1, not day 0, so we add 1.
|
||||
let day = it.x + 7 * (it.y - 1) + 1
|
||||
if day <= 31 {
|
||||
// Place the day's number at the top left of the cell.
|
||||
// Only if the day is valid for this month (not 32 or higher).
|
||||
place(top + left, dx: 2pt, dy: 2pt, text(8pt, red.darken(40%))[#day])
|
||||
}
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
#grid(
|
||||
fill: (x, y) => if y == 0 { gray.darken(50%) },
|
||||
columns: (30pt,) * 7,
|
||||
rows: (auto, 30pt),
|
||||
// Events will be written at the bottom of each day square.
|
||||
align: bottom,
|
||||
inset: 5pt,
|
||||
stroke: (thickness: 0.5pt, dash: "densely-dotted"),
|
||||
|
||||
[Sun], [Mon], [Tue], [Wed], [Thu], [Fri], [Sat],
|
||||
|
||||
// This event will occur on the first Friday (sixth column).
|
||||
grid.cell(x: 5, fill: yellow.darken(10%))[Call],
|
||||
|
||||
// This event will occur every Monday (second column).
|
||||
// We have to repeat it 5 times so it occurs every week.
|
||||
..(grid.cell(x: 1, fill: red.lighten(50%))[Meet],) * 5,
|
||||
|
||||
// This event will occur at day 19.
|
||||
grid.cell(x: 4, y: 3, fill: orange.lighten(25%))[Talk],
|
||||
|
||||
// These events will occur at the second week, where available.
|
||||
grid.cell(y: 2, fill: aqua)[Chat],
|
||||
grid.cell(y: 2, fill: aqua)[Walk],
|
||||
)
|
||||
|
||||
---
|
||||
// Doc example 2
|
||||
#set page(width: auto)
|
||||
#show table.cell: it => {
|
||||
if it.x == 0 or it.y == 0 {
|
||||
set text(white)
|
||||
strong(it)
|
||||
} else if it.body == [] {
|
||||
// Replace empty cells with 'N/A'
|
||||
pad(rest: it.inset)[_N/A_]
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
#table(
|
||||
fill: (x, y) => if x == 0 or y == 0 { gray.darken(50%) },
|
||||
columns: 4,
|
||||
[], [Exam 1], [Exam 2], [Exam 3],
|
||||
..([John], [Mary], [Jake], [Robert]).map(table.cell.with(x: 0)),
|
||||
|
||||
// Mary got grade A on Exam 3.
|
||||
table.cell(x: 3, y: 2, fill: green)[A],
|
||||
|
||||
// Everyone got grade A on Exam 2.
|
||||
..(table.cell(x: 2, fill: green)[A],) * 4,
|
||||
|
||||
// Robert got grade B on other exams.
|
||||
..(table.cell(y: 4, fill: aqua)[B],) * 2,
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 5:3-5:39 cell position too large
|
||||
#grid(
|
||||
columns: 3,
|
||||
rows: 2em,
|
||||
fill: (x, y) => if calc.odd(x + y) { red.lighten(50%) } else { green },
|
||||
grid.cell(y: 6148914691236517206)[a],
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 5:3-5:46 cell position too large
|
||||
#table(
|
||||
columns: 3,
|
||||
rows: 2em,
|
||||
fill: (x, y) => if calc.odd(x + y) { red.lighten(50%) } else { green },
|
||||
table.cell(x: 2, y: 6148914691236517206)[a],
|
||||
)
|
@ -100,3 +100,25 @@
|
||||
[John], [Dog]
|
||||
)
|
||||
}
|
||||
|
||||
---
|
||||
// Style based on position
|
||||
#{
|
||||
show table.cell: it => {
|
||||
if it.y == 0 {
|
||||
strong(it)
|
||||
} else if it.x == 1 {
|
||||
emph(it)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
table(
|
||||
columns: 3,
|
||||
gutter: 3pt,
|
||||
[Name], [Age], [Info],
|
||||
[John], [52], [Nice],
|
||||
[Mary], [50], [Cool],
|
||||
[Jake], [49], [Epic]
|
||||
)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user