summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPgBiel <9021226+PgBiel@users.noreply.github.com>2024-01-17 12:52:28 -0300
committerGitHub <noreply@github.com>2024-01-17 15:52:28 +0000
commit21585e03cfcc47ad283c162e4a2959ea5f8fbd6f (patch)
treefa1440df6f99af38c17a460183bb75e0c0873989
parent7cb257a1accdf0af99b7210a2db53ea562764e15 (diff)
Table cell `x` and `y` fields [More Flexible Tables Pt.2b] (#3050)
-rw-r--r--crates/typst/src/layout/grid/layout.rs257
-rw-r--r--crates/typst/src/layout/grid/mod.rs184
-rw-r--r--crates/typst/src/model/bibliography.rs22
-rw-r--r--crates/typst/src/model/table.rs126
-rw-r--r--tests/ref/layout/grid-cell.pngbin39178 -> 45433 bytes
-rw-r--r--tests/ref/layout/grid-positioning.pngbin0 -> 55048 bytes
-rw-r--r--tests/ref/layout/table-cell.pngbin41893 -> 49018 bytes
-rw-r--r--tests/typ/layout/grid-cell.typ22
-rw-r--r--tests/typ/layout/grid-positioning.typ223
-rw-r--r--tests/typ/layout/table-cell.typ22
10 files changed, 785 insertions, 71 deletions
diff --git a/crates/typst/src/layout/grid/layout.rs b/crates/typst/src/layout/grid/layout.rs
index a7e259d0..b2490d1e 100644
--- a/crates/typst/src/layout/grid/layout.rs
+++ b/crates/typst/src/layout/grid/layout.rs
@@ -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;
-
- Ok(cell.resolve_cell(
- x,
- y,
- &fill.resolve(engine, x, y)?,
- align.resolve(engine, x, y)?,
- inset,
- styles,
- ))
+ if let Some(cell) = cell {
+ Ok(cell)
+ } else {
+ let x = i % c;
+ let y = i / c;
+
+ // 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.
diff --git a/crates/typst/src/layout/grid/mod.rs b/crates/typst/src/layout/grid/mod.rs
index ab758e98..6ed9cce5 100644
--- a/crates/typst/src/layout/grid/mod.rs
+++ b/crates/typst/src/layout/grid/mod.rs
@@ -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> {
diff --git a/crates/typst/src/model/bibliography.rs b/crates/typst/src/model/bibliography.rs
index 81caf953..94681fe1 100644
--- a/crates/typst/src/model/bibliography.rs
+++ b/crates/typst/src/model/bibliography.rs
@@ -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 {
diff --git a/crates/typst/src/model/table.rs b/crates/typst/src/model/table.rs
index ef0d3f91..413a375b 100644
--- a/crates/typst/src/model/table.rs
+++ b/crates/typst/src/model/table.rs
@@ -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> {
diff --git a/tests/ref/layout/grid-cell.png b/tests/ref/layout/grid-cell.png
index fb683123..07508b40 100644
--- a/tests/ref/layout/grid-cell.png
+++ b/tests/ref/layout/grid-cell.png
Binary files differ
diff --git a/tests/ref/layout/grid-positioning.png b/tests/ref/layout/grid-positioning.png
new file mode 100644
index 00000000..5d60c8b7
--- /dev/null
+++ b/tests/ref/layout/grid-positioning.png
Binary files differ
diff --git a/tests/ref/layout/table-cell.png b/tests/ref/layout/table-cell.png
index fa3d04cc..8e91e645 100644
--- a/tests/ref/layout/table-cell.png
+++ b/tests/ref/layout/table-cell.png
Binary files differ
diff --git a/tests/typ/layout/grid-cell.typ b/tests/typ/layout/grid-cell.typ
index ced16a97..425d036c 100644
--- a/tests/typ/layout/grid-cell.typ
+++ b/tests/typ/layout/grid-cell.typ
@@ -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]
+ )
+}
diff --git a/tests/typ/layout/grid-positioning.typ b/tests/typ/layout/grid-positioning.typ
new file mode 100644
index 00000000..ca71cb37
--- /dev/null
+++ b/tests/typ/layout/grid-positioning.typ
@@ -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],
+)
diff --git a/tests/typ/layout/table-cell.typ b/tests/typ/layout/table-cell.typ
index a4d3bba4..d79298ae 100644
--- a/tests/typ/layout/table-cell.typ
+++ b/tests/typ/layout/table-cell.typ
@@ -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]
+ )
+}