diff --git a/crates/typst/src/layout/grid/layout.rs b/crates/typst/src/layout/grid/layout.rs index a7e259d01..b2490d1ea 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 FromValue for Celled { } /// 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>, styles: StyleChain, ) -> Cell; + + /// Returns this cell's column override. + fn x(&self, styles: StyleChain) -> Smart; + + /// Returns this cell's row override. + fn y(&self, styles: StyleChain) -> Smart; + + /// 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( tracks: Axes<&[Sizing]>, @@ -216,38 +230,129 @@ impl CellGrid { inset: Sides>, engine: &mut Engine, styles: StyleChain, + span: Span, ) -> SourceResult { // 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> = 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' (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::>>()?; + .collect::>>()?; - 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, + cell_y: Smart, + resolved_cells: &[Option], + auto_index: &mut usize, + columns: usize, +) -> HintedStrResult { + // 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 ab758e984..6ed9cce58 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, + pub children: Vec>, } #[scope] @@ -241,6 +298,8 @@ impl Layout for Packed { 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 { 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, + + /// 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, + /// The cell's fill override. fill: Smart>, @@ -311,39 +444,54 @@ cast! { v: Content => v.into(), } -impl Default for GridCell { +impl Default for Packed { fn default() -> Self { - Self::new(Content::default()) + Packed::new(GridCell::new(Content::default())) } } -impl ResolvableCell for GridCell { +impl ResolvableCell for Packed { fn resolve_cell( mut self, - _: usize, - _: usize, + x: usize, + y: usize, fill: &Option, align: Smart, inset: Sides>, 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 { + (**self).x(styles) + } + + fn y(&self, styles: StyleChain) -> Smart { + (**self).y(styles) + } + + fn span(&self) -> Span { + Packed::span(self) + } } impl Show for Packed { diff --git a/crates/typst/src/model/bibliography.rs b/crates/typst/src/model/bibliography.rs index 81caf9537..94681fe1d 100644 --- a/crates/typst/src/model/bibliography.rs +++ b/crates/typst/src/model/bibliography.rs @@ -240,8 +240,13 @@ impl Show for Packed { 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 ef0d3f917..413a375b4 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, + pub children: Vec>, } #[scope] @@ -185,6 +224,8 @@ impl Layout for Packed { 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 { 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 {} /// [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, + + /// The cell's row (zero-indexed). + /// Functions identically to the `y` field in [`grid.cell`]($grid.cell). + y: Smart, + /// The cell's fill override. fill: Smart>, @@ -280,39 +359,54 @@ cast! { v: Content => v.into(), } -impl Default for TableCell { +impl Default for Packed { fn default() -> Self { - Self::new(Content::default()) + Packed::new(TableCell::new(Content::default())) } } -impl ResolvableCell for TableCell { +impl ResolvableCell for Packed { fn resolve_cell( mut self, - _: usize, - _: usize, + x: usize, + y: usize, fill: &Option, align: Smart, inset: Sides>, 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 { + (**self).x(styles) + } + + fn y(&self, styles: StyleChain) -> Smart { + (**self).y(styles) + } + + fn span(&self) -> Span { + Packed::span(self) + } } impl Show for Packed { diff --git a/tests/ref/layout/grid-cell.png b/tests/ref/layout/grid-cell.png index fb6831236..07508b400 100644 Binary files a/tests/ref/layout/grid-cell.png and b/tests/ref/layout/grid-cell.png differ diff --git a/tests/ref/layout/grid-positioning.png b/tests/ref/layout/grid-positioning.png new file mode 100644 index 000000000..5d60c8b71 Binary files /dev/null and b/tests/ref/layout/grid-positioning.png differ diff --git a/tests/ref/layout/table-cell.png b/tests/ref/layout/table-cell.png index fa3d04ccb..8e91e6459 100644 Binary files a/tests/ref/layout/table-cell.png and b/tests/ref/layout/table-cell.png differ diff --git a/tests/typ/layout/grid-cell.typ b/tests/typ/layout/grid-cell.typ index ced16a97a..425d036c7 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 000000000..ca71cb372 --- /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 a4d3bba47..d79298aea 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] + ) +}