Table cell x and y fields [More Flexible Tables Pt.2b] (#3050)

This commit is contained in:
PgBiel 2024-01-17 12:52:28 -03:00 committed by GitHub
parent 7cb257a1ac
commit 21585e03cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 784 additions and 70 deletions

View File

@ -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::engine::Engine;
use crate::foundations::{ use crate::foundations::{
Array, CastInfo, Content, FromValue, Func, IntoValue, Reflect, Resolve, Smart, 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. /// Represents a cell in CellGrid, to be laid out by GridLayouter.
#[derive(Clone)]
pub struct Cell { pub struct Cell {
/// The cell's body. /// The cell's body.
pub body: Content, pub body: Content,
@ -123,6 +128,15 @@ pub trait ResolvableCell {
inset: Sides<Rel<Length>>, inset: Sides<Rel<Length>>,
styles: StyleChain, styles: StyleChain,
) -> Cell; ) -> 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. /// 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 } Self { cols, rows, cells, has_gutter, is_rtl }
} }
/// Resolves all cells in the grid before creating it. /// Resolves and positions all cells in the grid before creating it.
/// Allows them to keep track of their final properties and adjust their /// Allows them to keep track of their final properties and positions
/// fields accordingly. /// and adjust their fields accordingly.
/// Cells must implement Clone as they will be owned. Additionally, they /// 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 /// must implement Default in order to fill positions in the grid which
/// empty cells, if it is not completely filled. /// weren't explicitly specified by the user with empty cells.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn resolve<T: ResolvableCell + Clone + Default>( pub fn resolve<T: ResolvableCell + Clone + Default>(
tracks: Axes<&[Sizing]>, tracks: Axes<&[Sizing]>,
@ -216,38 +230,129 @@ impl CellGrid {
inset: Sides<Rel<Length>>, inset: Sides<Rel<Length>>,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
span: Span,
) -> SourceResult<Self> { ) -> SourceResult<Self> {
// Number of content columns: Always at least one. // Number of content columns: Always at least one.
let c = tracks.x.len().max(1); let c = tracks.x.len().max(1);
// If not all columns in the last row have cells, we will add empty // We can't just use the cell's index in the 'cells' vector to
// cells and complete the row so that those positions are susceptible // determine its automatic position, since cells could have arbitrary
// to show rules and receive grid styling. // positions, so the position of a cell in 'cells' can differ from its
// We apply '% c' twice so that 'cells_remaining' is zero when // final position in 'resolved_cells' (see below).
// the last row is already filled (then 'cell_count % c' would be zero). // Therefore, we use a counter, 'auto_index', to determine the position
let cell_count = cells.len(); // of the next cell with (x: auto, y: auto). It is only stepped when
let cells_remaining = (c - cell_count % c) % c; // a cell with (x: auto, y: auto), usually the vast majority, is found.
let cells = cells let mut auto_index = 0;
.iter()
.cloned()
.chain(std::iter::repeat_with(T::default).take(cells_remaining))
.enumerate()
.map(|(i, cell)| {
let x = i % c;
let y = i / c;
Ok(cell.resolve_cell( // 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, x,
y, y,
&fill.resolve(engine, x, y)?, &fill.resolve(engine, x, y)?,
align.resolve(engine, x, y)?, align.resolve(engine, x, y)?,
inset, inset,
styles, styles,
)) );
})
.collect::<SourceResult<Vec<_>>>()?;
Ok(Self::new(tracks, gutter, cells, 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)| {
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<Cell>>>()?;
Ok(Self::new(tracks, gutter, resolved_cells, styles))
} }
/// Get the content of the cell in column `x` and row `y`. /// 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. /// Performs grid layout.
pub struct GridLayouter<'a> { pub struct GridLayouter<'a> {
/// The grid of cells. /// The grid of cells.

View File

@ -4,18 +4,19 @@ pub use self::layout::{Cell, CellGrid, Celled, GridLayouter, ResolvableCell};
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use ecow::eco_format;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use crate::diag::{SourceResult, StrResult}; use crate::diag::{SourceResult, StrResult, Trace, Tracepoint};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, scope, Array, Content, Fold, NativeElement, Packed, Show, Smart, cast, elem, scope, Array, Content, Fold, Packed, Show, Smart, StyleChain, Value,
StyleChain, Value,
}; };
use crate::layout::{ use crate::layout::{
Abs, AlignElem, Alignment, Axes, Fragment, Layout, Length, Regions, Rel, Sides, Abs, AlignElem, Alignment, Axes, Fragment, Layout, Length, Regions, Rel, Sides,
Sizing, Sizing,
}; };
use crate::syntax::Span;
use crate::visualize::{Paint, Stroke}; use crate::visualize::{Paint, Stroke};
/// Arranges content in a grid. /// 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 /// appearance options to depend on a cell's position (column and row), you may
/// specify a function to `fill` or `align` of the form /// specify a function to `fill` or `align` of the form
/// `(column, row) => value`. You may also use a show rule on /// `(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 /// # Examples
/// The example below demonstrates the different track sizing options. /// The example below demonstrates the different track sizing options.
@ -97,6 +99,61 @@ use crate::visualize::{Paint, Stroke};
/// ..range(25).map(str) /// ..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)] #[elem(scope, Layout)]
pub struct GridElem { pub struct GridElem {
/// The column sizes. /// The column sizes.
@ -213,7 +270,7 @@ pub struct GridElem {
/// ///
/// The cells are populated in row-major order. /// The cells are populated in row-major order.
#[variadic] #[variadic]
pub children: Vec<GridCell>, pub children: Vec<Packed<GridCell>>,
} }
#[scope] #[scope]
@ -241,6 +298,8 @@ impl Layout for Packed<GridElem> {
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); 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()); 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( let grid = CellGrid::resolve(
tracks, tracks,
gutter, gutter,
@ -250,7 +309,9 @@ impl Layout for Packed<GridElem> {
inset, inset,
engine, engine,
styles, styles,
)?; self.span(),
)
.trace(engine.world, tracepoint, self.span())?;
let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span()); let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
@ -290,12 +351,84 @@ cast! {
/// [G], grid.cell(inset: 0pt)[H] /// [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)] #[elem(name = "cell", title = "Grid Cell", Show)]
pub struct GridCell { pub struct GridCell {
/// The cell's body. /// The cell's body.
#[required] #[required]
body: Content, 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. /// The cell's fill override.
fill: Smart<Option<Paint>>, fill: Smart<Option<Paint>>,
@ -311,39 +444,54 @@ cast! {
v: Content => v.into(), v: Content => v.into(),
} }
impl Default for GridCell { impl Default for Packed<GridCell> {
fn default() -> Self { 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( fn resolve_cell(
mut self, mut self,
_: usize, x: usize,
_: usize, y: usize,
fill: &Option<Paint>, fill: &Option<Paint>,
align: Smart<Alignment>, align: Smart<Alignment>,
inset: Sides<Rel<Length>>, inset: Sides<Rel<Length>>,
styles: StyleChain, styles: StyleChain,
) -> Cell { ) -> Cell {
let fill = self.fill(styles).unwrap_or_else(|| fill.clone()); let cell = &mut *self;
self.push_fill(Smart::Custom(fill.clone())); let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
self.push_align(match align { 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(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 // Don't fold if the grid is using outer alignment. Use the
// cell's alignment instead (which, in the end, will fold with // cell's alignment instead (which, in the end, will fold with
// the outer alignment when it is effectively displayed). // the outer alignment when it is effectively displayed).
Smart::Auto => self.align(styles), Smart::Auto => cell.align(styles),
}); });
self.push_inset(Smart::Custom( cell.push_inset(Smart::Custom(
self.inset(styles).map_or(inset, |inner| inner.fold(inset)).map(Some), cell.inset(styles).map_or(inset, |inner| inner.fold(inset)).map(Some),
)); ));
Cell { body: self.pack(), fill } 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> { impl Show for Packed<GridCell> {

View File

@ -240,8 +240,13 @@ impl Show for Packed<BibliographyElem> {
if references.iter().any(|(prefix, _)| prefix.is_some()) { if references.iter().any(|(prefix, _)| prefix.is_some()) {
let mut cells = vec![]; let mut cells = vec![];
for (prefix, reference) in references { for (prefix, reference) in references {
cells.push(GridCell::new(prefix.clone().unwrap_or_default())); cells.push(
cells.push(GridCell::new(reference.clone())); 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()); seq.push(VElem::new(row_gutter).with_weakness(3).pack());
@ -945,7 +950,10 @@ impl ElemRenderer<'_> {
if let Some(prefix) = suf_prefix { if let Some(prefix) = suf_prefix {
const COLUMN_GUTTER: Em = Em::new(0.65); const COLUMN_GUTTER: Em = Em::new(0.65);
content = GridElem::new(vec![GridCell::new(prefix), GridCell::new(content)]) 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_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
.pack() .pack()

View File

@ -1,13 +1,16 @@
use crate::diag::SourceResult; use ecow::eco_format;
use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ 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::{ use crate::layout::{
show_grid_cell, Abs, Alignment, Axes, Cell, CellGrid, Celled, Fragment, GridLayouter, show_grid_cell, Abs, Alignment, Axes, Cell, CellGrid, Celled, Fragment, GridLayouter,
Layout, Length, Regions, Rel, ResolvableCell, Sides, TrackSizings, Layout, Length, Regions, Rel, ResolvableCell, Sides, TrackSizings,
}; };
use crate::model::Figurable; use crate::model::Figurable;
use crate::syntax::Span;
use crate::text::{Lang, LocalName, Region}; use crate::text::{Lang, LocalName, Region};
use crate::visualize::{Paint, Stroke}; use crate::visualize::{Paint, Stroke};
@ -29,6 +32,8 @@ use crate::visualize::{Paint, Stroke};
/// [figure]($figure). /// [figure]($figure).
/// ///
/// # Example /// # Example
///
/// The example below demonstrates some of the most common table options.
/// ```example /// ```example
/// #table( /// #table(
/// columns: (1fr, auto, auto), /// columns: (1fr, auto, auto),
@ -47,6 +52,40 @@ use crate::visualize::{Paint, Stroke};
/// [$a$: edge length] /// [$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)] #[elem(scope, Layout, LocalName, Figurable)]
pub struct TableElem { pub struct TableElem {
/// The column sizes. See the [grid documentation]($grid) for more /// The column sizes. See the [grid documentation]($grid) for more
@ -157,7 +196,7 @@ pub struct TableElem {
/// The contents of the table cells. /// The contents of the table cells.
#[variadic] #[variadic]
pub children: Vec<TableCell>, pub children: Vec<Packed<TableCell>>,
} }
#[scope] #[scope]
@ -185,6 +224,8 @@ impl Layout for Packed<TableElem> {
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); 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()); 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( let grid = CellGrid::resolve(
tracks, tracks,
gutter, gutter,
@ -194,7 +235,9 @@ impl Layout for Packed<TableElem> {
inset, inset,
engine, engine,
styles, styles,
)?; self.span(),
)
.trace(engine.world, tracepoint, self.span())?;
let layouter = GridLayouter::new(&grid, &stroke, regions, styles, 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] /// [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)] #[elem(name = "cell", title = "Table Cell", Show)]
pub struct TableCell { pub struct TableCell {
/// The cell's body. /// The cell's body.
#[required] #[required]
body: Content, 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. /// The cell's fill override.
fill: Smart<Option<Paint>>, fill: Smart<Option<Paint>>,
@ -280,39 +359,54 @@ cast! {
v: Content => v.into(), v: Content => v.into(),
} }
impl Default for TableCell { impl Default for Packed<TableCell> {
fn default() -> Self { 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( fn resolve_cell(
mut self, mut self,
_: usize, x: usize,
_: usize, y: usize,
fill: &Option<Paint>, fill: &Option<Paint>,
align: Smart<Alignment>, align: Smart<Alignment>,
inset: Sides<Rel<Length>>, inset: Sides<Rel<Length>>,
styles: StyleChain, styles: StyleChain,
) -> Cell { ) -> Cell {
let fill = self.fill(styles).unwrap_or_else(|| fill.clone()); let cell = &mut *self;
self.push_fill(Smart::Custom(fill.clone())); let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
self.push_align(match align { 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(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 // Don't fold if the table is using outer alignment. Use the
// cell's alignment instead (which, in the end, will fold with // cell's alignment instead (which, in the end, will fold with
// the outer alignment when it is effectively displayed). // the outer alignment when it is effectively displayed).
Smart::Auto => self.align(styles), Smart::Auto => cell.align(styles),
}); });
self.push_inset(Smart::Custom( cell.push_inset(Smart::Custom(
self.inset(styles).map_or(inset, |inner| inner.fold(inset)).map(Some), cell.inset(styles).map_or(inset, |inner| inner.fold(inset)).map(Some),
)); ));
Cell { body: self.pack(), fill } 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> { impl Show for Packed<TableCell> {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 44 KiB

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

View File

@ -105,3 +105,25 @@
[Sweet], [Italics] [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]
)
}

View 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],
)

View File

@ -100,3 +100,25 @@
[John], [Dog] [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]
)
}