summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst/src/foundations/auto.rs16
-rw-r--r--crates/typst/src/layout/grid/layout.rs (renamed from crates/typst/src/layout/grid.rs)621
-rw-r--r--crates/typst/src/layout/grid/mod.rs383
-rw-r--r--crates/typst/src/model/bibliography.rs8
-rw-r--r--crates/typst/src/model/enum.rs26
-rw-r--r--crates/typst/src/model/list.rs24
-rw-r--r--crates/typst/src/model/table.rs139
-rw-r--r--tests/ref/layout/grid-cell.pngbin0 -> 39178 bytes
-rw-r--r--tests/ref/layout/grid-styling.pngbin18429 -> 27200 bytes
-rw-r--r--tests/ref/layout/table-cell.pngbin0 -> 41893 bytes
-rw-r--r--tests/ref/layout/table.pngbin12226 -> 22515 bytes
-rw-r--r--tests/typ/layout/grid-cell.typ107
-rw-r--r--tests/typ/layout/grid-styling.typ45
-rw-r--r--tests/typ/layout/table-cell.typ102
-rw-r--r--tests/typ/layout/table.typ45
15 files changed, 1062 insertions, 454 deletions
diff --git a/crates/typst/src/foundations/auto.rs b/crates/typst/src/foundations/auto.rs
index dd36e814..5cb03f7f 100644
--- a/crates/typst/src/foundations/auto.rs
+++ b/crates/typst/src/foundations/auto.rs
@@ -128,8 +128,20 @@ impl<T> Smart<T> {
}
}
- /// Retusn `Auto` if `self` is `Auto`, otherwise calls the provided function onthe contained
- /// value and returns the result.
+ /// Keeps `self` if it contains a custom value, otherwise returns the
+ /// output of the given function.
+ pub fn or_else<F>(self, f: F) -> Self
+ where
+ F: FnOnce() -> Self,
+ {
+ match self {
+ Self::Custom(x) => Self::Custom(x),
+ Self::Auto => f(),
+ }
+ }
+
+ /// Returns `Auto` if `self` is `Auto`, otherwise calls the provided
+ /// function on the contained value and returns the result.
pub fn and_then<F, U>(self, f: F) -> Smart<U>
where
F: FnOnce(T) -> Smart<U>,
diff --git a/crates/typst/src/layout/grid.rs b/crates/typst/src/layout/grid/layout.rs
index 56f8190b..24d641a6 100644
--- a/crates/typst/src/layout/grid.rs
+++ b/crates/typst/src/layout/grid/layout.rs
@@ -1,282 +1,17 @@
-use std::num::NonZeroUsize;
-
-use smallvec::{smallvec, SmallVec};
-
use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
- cast, elem, Array, CastInfo, Content, FromValue, Func, IntoValue, NativeElement,
- Reflect, Resolve, Smart, StyleChain, Value,
+ Array, CastInfo, Content, FromValue, Func, IntoValue, Reflect, Resolve, Smart,
+ StyleChain, Value,
};
use crate::layout::{
- Abs, Align, AlignElem, Axes, Dir, Fr, Fragment, Frame, FrameItem, Layout, Length,
- Point, Regions, Rel, Sides, Size, Sizing,
+ Abs, Align, Axes, Dir, Fr, Fragment, Frame, FrameItem, Layout, Length, Point,
+ Regions, Rel, Sides, Size, Sizing,
};
use crate::syntax::Span;
use crate::text::TextElem;
use crate::util::Numeric;
-use crate::visualize::{FixedStroke, Geometry, Paint, Stroke};
-
-/// Arranges content in a grid.
-///
-/// The grid element allows you to arrange content in a grid. You can define the
-/// number of rows and columns, as well as the size of the gutters between them.
-/// There are multiple sizing modes for columns and rows that can be used to
-/// create complex layouts.
-///
-/// The sizing of the grid is determined by the track sizes specified in the
-/// arguments. Because each of the sizing parameters accepts the same values, we
-/// will explain them just once, here. Each sizing argument accepts an array of
-/// individual track sizes. A track size is either:
-///
-/// - `{auto}`: The track will be sized to fit its contents. It will be at most
-/// as large as the remaining space. If there is more than one `{auto}` track
-/// which, and together they claim more than the available space, the `{auto}`
-/// tracks will fairly distribute the available space among themselves.
-///
-/// - A fixed or relative length (e.g. `{10pt}` or `{20% - 1cm}`): The track
-/// will be exactly of this size.
-///
-/// - A fractional length (e.g. `{1fr}`): Once all other tracks have been sized,
-/// the remaining space will be divided among the fractional tracks according
-/// to their fractions. For example, if there are two fractional tracks, each
-/// with a fraction of `{1fr}`, they will each take up half of the remaining
-/// space.
-///
-/// To specify a single track, the array can be omitted in favor of a single
-/// value. To specify multiple `{auto}` tracks, enter the number of tracks
-/// instead of an array. For example, `columns:` `{3}` is equivalent to
-/// `columns:` `{(auto, auto, auto)}`.
-///
-/// # Examples
-/// The example below demonstrates the different track sizing options.
-///
-/// ```example
-/// // We use `rect` to emphasize the
-/// // area of cells.
-/// #set rect(
-/// inset: 8pt,
-/// fill: rgb("e4e5ea"),
-/// width: 100%,
-/// )
-///
-/// #grid(
-/// columns: (60pt, 1fr, 2fr),
-/// rows: (auto, 60pt),
-/// gutter: 3pt,
-/// rect[Fixed width, auto height],
-/// rect[1/3 of the remains],
-/// rect[2/3 of the remains],
-/// rect(height: 100%)[Fixed height],
-/// image("tiger.jpg", height: 100%),
-/// image("tiger.jpg", height: 100%),
-/// )
-/// ```
-///
-/// You can also [spread]($arguments/#spreading) an array of strings or content
-/// into a grid to populate its cells.
-///
-/// ```example
-/// #grid(
-/// columns: 5,
-/// gutter: 5pt,
-/// ..range(25).map(str)
-/// )
-/// ```
-#[elem(Layout)]
-pub struct GridElem {
- /// The column sizes.
- ///
- /// Either specify a track size array or provide an integer to create a grid
- /// with that many `{auto}`-sized columns. Note that opposed to rows and
- /// gutters, providing a single track size will only ever create a single
- /// column.
- #[borrowed]
- pub columns: TrackSizings,
-
- /// The row sizes.
- ///
- /// If there are more cells than fit the defined rows, the last row is
- /// repeated until there are no more cells.
- #[borrowed]
- pub rows: TrackSizings,
-
- /// The gaps between rows & columns.
- ///
- /// If there are more gutters than defined sizes, the last gutter is repeated.
- #[external]
- pub gutter: TrackSizings,
-
- /// The gaps between columns. Takes precedence over `gutter`.
- #[parse(
- let gutter = args.named("gutter")?;
- args.named("column-gutter")?.or_else(|| gutter.clone())
- )]
- #[borrowed]
- pub column_gutter: TrackSizings,
-
- /// The gaps between rows. Takes precedence over `gutter`.
- #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
- #[borrowed]
- pub row_gutter: TrackSizings,
-
- /// How to fill the cells.
- ///
- /// This can be a color or a function that returns a color. The function is
- /// passed the cells' column and row index, starting at zero. This can be
- /// used to implement striped grids.
- ///
- /// ```example
- /// #grid(
- /// fill: (col, row) => if calc.even(col + row) { luma(240) } else { white },
- /// align: center + horizon,
- /// columns: 4,
- /// [X], [O], [X], [O],
- /// [O], [X], [O], [X],
- /// [X], [O], [X], [O],
- /// [O], [X], [O], [X]
- /// )
- /// ```
- #[borrowed]
- pub fill: Celled<Option<Paint>>,
-
- /// How to align the cells' content.
- ///
- /// This can either be a single alignment, an array of alignments
- /// (corresponding to each column) or a function that returns an alignment.
- /// The function is passed the cells' column and row index, starting at zero.
- /// If set to `{auto}`, the outer alignment is used.
- ///
- /// ```example
- /// #grid(
- /// columns: 3,
- /// align: (x, y) => (left, center, right).at(x),
- /// [Hello], [Hello], [Hello],
- /// [A], [B], [C],
- /// )
- /// ```
- #[borrowed]
- pub align: Celled<Smart<Align>>,
-
- /// How to [stroke]($stroke) the cells.
- ///
- /// Grids have no strokes by default, which can be changed by setting this
- /// option to the desired stroke.
- ///
- /// _Note:_ Richer stroke customization for individual cells is not yet
- /// implemented, but will be in the future. In the meantime, you can use the
- /// third-party [tablex library](https://github.com/PgBiel/typst-tablex/).
- #[resolve]
- #[fold]
- pub stroke: Option<Stroke>,
-
- /// How much to pad the cells' content.
- ///
- /// ```example
- /// #grid(
- /// inset: 10pt,
- /// fill: (_, row) => (red, blue).at(row),
- /// [Hello],
- /// [World],
- /// )
- ///
- /// #grid(
- /// columns: 2,
- /// inset: (
- /// x: 20pt,
- /// y: 10pt,
- /// ),
- /// fill: (col, _) => (red, blue).at(col),
- /// [Hello],
- /// [World],
- /// )
- /// ```
- #[fold]
- #[default(Sides::splat(Abs::pt(0.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// The contents of the grid cells.
- ///
- /// The cells are populated in row-major order.
- #[variadic]
- pub children: Vec<Content>,
-}
-
-impl Layout for GridElem {
- #[typst_macros::time(name = "grid", span = self.span())]
- fn layout(
- &self,
- engine: &mut Engine,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let inset = self.inset(styles);
- let align = self.align(styles);
- let columns = self.columns(styles);
- let rows = self.rows(styles);
- let column_gutter = self.column_gutter(styles);
- let row_gutter = self.row_gutter(styles);
- let fill = self.fill(styles);
- let stroke = self.stroke(styles).map(Stroke::unwrap_or_default);
-
- 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 cells =
- apply_align_inset_to_cells(engine, &tracks, &self.children, align, inset)?;
-
- // Prepare grid layout by unifying content and gutter tracks.
- let layouter = GridLayouter::new(
- tracks,
- gutter,
- &cells,
- fill,
- &stroke,
- regions,
- styles,
- self.span(),
- );
-
- // Measure the columns and layout the grid row-by-row.
- Ok(layouter.layout(engine)?.fragment)
- }
-}
-
-pub fn apply_align_inset_to_cells(
- engine: &mut Engine,
- tracks: &Axes<&[Sizing]>,
- cells: &[Content],
- align: &Celled<Smart<Align>>,
- inset: Sides<Rel<Length>>,
-) -> SourceResult<Vec<Content>> {
- let cols = tracks.x.len().max(1);
- cells
- .iter()
- .enumerate()
- .map(|(i, child)| {
- let mut child = child.clone().padded(inset);
-
- let x = i % cols;
- let y = i / cols;
- if let Smart::Custom(alignment) = align.resolve(engine, x, y)? {
- child = child.styled(AlignElem::set_alignment(alignment));
- }
-
- Ok(child)
- })
- .collect()
-}
-
-/// Track sizing definitions.
-#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
-pub struct TrackSizings(pub SmallVec<[Sizing; 4]>);
-
-cast! {
- TrackSizings,
- self => self.0.into_value(),
- sizing: Sizing => Self(smallvec![sizing]),
- count: NonZeroUsize => Self(smallvec![Sizing::Auto; count.get()]),
- values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
-}
+use crate::visualize::{FixedStroke, Geometry, Paint};
/// A value that can be configured per cell.
#[derive(Debug, Clone, PartialEq, Hash)]
@@ -347,87 +82,70 @@ impl<T: FromValue> FromValue for Celled<T> {
}
}
-/// Performs grid layout.
-pub struct GridLayouter<'a> {
- /// The grid cells.
- cells: &'a [Content],
- /// Whether this is an RTL grid.
- is_rtl: bool,
- /// Whether this grid has gutters.
- has_gutter: bool,
- /// The column tracks including gutter tracks.
- cols: Vec<Sizing>,
- /// The row tracks including gutter tracks.
- rows: Vec<Sizing>,
- // How to fill the cells.
- #[allow(dead_code)]
- fill: &'a Celled<Option<Paint>>,
- // How to stroke the cells.
- #[allow(dead_code)]
- stroke: &'a Option<FixedStroke>,
- /// The regions to layout children into.
- regions: Regions<'a>,
- /// The inherited styles.
- styles: StyleChain<'a>,
- /// Resolved column sizes.
- rcols: Vec<Abs>,
- /// The sum of `rcols`.
- width: Abs,
- /// Resolve row sizes, by region.
- rrows: Vec<Vec<RowPiece>>,
- /// Rows in the current region.
- lrows: Vec<Row>,
- /// The initial size of the current region before we started subtracting.
- initial: Size,
- /// Frames for finished regions.
- finished: Vec<Frame>,
- /// The span of the grid element.
- span: Span,
+/// Represents a cell in CellGrid, to be laid out by GridLayouter.
+pub struct Cell {
+ /// The cell's body.
+ pub body: Content,
+ /// The cell's fill.
+ pub fill: Option<Paint>,
}
-/// The resulting sizes of columns and rows in a grid.
-#[derive(Debug)]
-pub struct GridLayout {
- /// The fragment.
- pub fragment: Fragment,
- /// The column widths.
- pub cols: Vec<Abs>,
- /// The heights of the resulting rows segments, by region.
- pub rows: Vec<Vec<RowPiece>>,
+impl From<Content> for Cell {
+ /// Create a simple cell given its body.
+ fn from(body: Content) -> Self {
+ Self { body, fill: None }
+ }
}
-/// Details about a resulting row piece.
-#[derive(Debug)]
-pub struct RowPiece {
- /// The height of the segment.
- pub height: Abs,
- /// The index of the row.
- pub y: usize,
+impl Layout for Cell {
+ fn layout(
+ &self,
+ engine: &mut Engine,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment> {
+ self.body.layout(engine, styles, regions)
+ }
}
-/// Produced by initial row layout, auto and relative rows are already finished,
-/// fractional rows not yet.
-enum Row {
- /// Finished row frame of auto or relative row with y index.
- Frame(Frame, usize),
- /// Fractional row with y index.
- Fr(Fr, usize),
+/// Used for cell-like elements which are aware of their final properties in
+/// the table, and may have property overrides.
+pub trait ResolvableCell {
+ /// Resolves the cell's fields, given its coordinates and default grid-wide
+ /// fill, align and inset properties.
+ /// Returns a final Cell.
+ fn resolve_cell(
+ self,
+ x: usize,
+ y: usize,
+ fill: &Option<Paint>,
+ align: Smart<Align>,
+ inset: Sides<Rel<Length>>,
+ styles: StyleChain,
+ ) -> Cell;
}
-impl<'a> GridLayouter<'a> {
- /// Create a new grid layouter.
- ///
- /// This prepares grid layout by unifying content and gutter tracks.
- #[allow(clippy::too_many_arguments)]
+/// A grid of cells, including the columns, rows, and cell data.
+pub struct CellGrid {
+ /// The grid cells.
+ cells: Vec<Cell>,
+ /// The column tracks including gutter tracks.
+ cols: Vec<Sizing>,
+ /// The row tracks including gutter tracks.
+ rows: Vec<Sizing>,
+ /// Whether this grid has gutters.
+ has_gutter: bool,
+ /// Whether this is an RTL grid.
+ is_rtl: bool,
+}
+
+impl CellGrid {
+ /// Generates the cell grid, given the tracks and resolved cells.
pub fn new(
tracks: Axes<&[Sizing]>,
gutter: Axes<&[Sizing]>,
- cells: &'a [Content],
- fill: &'a Celled<Option<Paint>>,
- stroke: &'a Option<FixedStroke>,
- regions: Regions<'a>,
- styles: StyleChain<'a>,
- span: Span,
+ cells: Vec<Cell>,
+ styles: StyleChain,
) -> Self {
let mut cols = vec![];
let mut rows = vec![];
@@ -479,22 +197,154 @@ impl<'a> GridLayouter<'a> {
cols.reverse();
}
+ 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.
+ /// 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.
+ #[allow(clippy::too_many_arguments)]
+ pub fn resolve<T: ResolvableCell + Clone + Default>(
+ tracks: Axes<&[Sizing]>,
+ gutter: Axes<&[Sizing]>,
+ cells: &[T],
+ fill: &Celled<Option<Paint>>,
+ align: &Celled<Smart<Align>>,
+ inset: Sides<Rel<Length>>,
+ engine: &mut Engine,
+ styles: StyleChain,
+ ) -> 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))
+ .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,
+ ))
+ })
+ .collect::<SourceResult<Vec<_>>>()?;
+
+ Ok(Self::new(tracks, gutter, cells, styles))
+ }
+
+ /// Get the content of the cell in column `x` and row `y`.
+ ///
+ /// Returns `None` if it's a gutter cell.
+ #[track_caller]
+ fn cell(&self, mut x: usize, y: usize) -> Option<&Cell> {
+ assert!(x < self.cols.len());
+ assert!(y < self.rows.len());
+
+ // Columns are reorder, but the cell slice is not.
+ if self.is_rtl {
+ x = self.cols.len() - 1 - x;
+ }
+
+ if self.has_gutter {
+ // Even columns and rows are children, odd ones are gutter.
+ if x % 2 == 0 && y % 2 == 0 {
+ let c = 1 + self.cols.len() / 2;
+ self.cells.get((y / 2) * c + x / 2)
+ } else {
+ None
+ }
+ } else {
+ let c = self.cols.len();
+ self.cells.get(y * c + x)
+ }
+ }
+}
+
+/// Performs grid layout.
+pub struct GridLayouter<'a> {
+ /// The grid of cells.
+ grid: &'a CellGrid,
+ // How to stroke the cells.
+ stroke: &'a Option<FixedStroke>,
+ /// The regions to layout children into.
+ regions: Regions<'a>,
+ /// The inherited styles.
+ styles: StyleChain<'a>,
+ /// Resolved column sizes.
+ rcols: Vec<Abs>,
+ /// The sum of `rcols`.
+ width: Abs,
+ /// Resolve row sizes, by region.
+ rrows: Vec<Vec<RowPiece>>,
+ /// Rows in the current region.
+ lrows: Vec<Row>,
+ /// The initial size of the current region before we started subtracting.
+ initial: Size,
+ /// Frames for finished regions.
+ finished: Vec<Frame>,
+ /// The span of the grid element.
+ span: Span,
+}
+
+/// Details about a resulting row piece.
+#[derive(Debug)]
+pub struct RowPiece {
+ /// The height of the segment.
+ pub height: Abs,
+ /// The index of the row.
+ pub y: usize,
+}
+
+/// Produced by initial row layout, auto and relative rows are already finished,
+/// fractional rows not yet.
+enum Row {
+ /// Finished row frame of auto or relative row with y index.
+ Frame(Frame, usize),
+ /// Fractional row with y index.
+ Fr(Fr, usize),
+}
+
+impl<'a> GridLayouter<'a> {
+ /// Create a new grid layouter.
+ ///
+ /// This prepares grid layout by unifying content and gutter tracks.
+ #[allow(clippy::too_many_arguments)]
+ pub fn new(
+ grid: &'a CellGrid,
+ stroke: &'a Option<FixedStroke>,
+ regions: Regions<'a>,
+ styles: StyleChain<'a>,
+ span: Span,
+ ) -> Self {
// We use these regions for auto row measurement. Since at that moment,
// columns are already sized, we can enable horizontal expansion.
let mut regions = regions;
regions.expand = Axes::new(true, false);
Self {
- cells,
- is_rtl,
- has_gutter,
- rows,
- fill,
+ grid,
stroke,
regions,
styles,
- rcols: vec![Abs::zero(); cols.len()],
- cols,
+ rcols: vec![Abs::zero(); grid.cols.len()],
width: Abs::zero(),
rrows: vec![],
lrows: vec![],
@@ -505,17 +355,17 @@ impl<'a> GridLayouter<'a> {
}
/// Determines the columns sizes and then layouts the grid row-by-row.
- pub fn layout(mut self, engine: &mut Engine) -> SourceResult<GridLayout> {
+ pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> {
self.measure_columns(engine)?;
- for y in 0..self.rows.len() {
+ for y in 0..self.grid.rows.len() {
// Skip to next region if current one is full, but only for content
// rows, not for gutter rows.
- if self.regions.is_full() && (!self.has_gutter || y % 2 == 0) {
+ if self.regions.is_full() && (!self.grid.has_gutter || y % 2 == 0) {
self.finish_region(engine)?;
}
- match self.rows[y] {
+ match self.grid.rows[y] {
Sizing::Auto => self.layout_auto_row(engine, y)?,
Sizing::Rel(v) => self.layout_relative_row(engine, v, y)?,
Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y)),
@@ -524,19 +374,13 @@ impl<'a> GridLayouter<'a> {
self.finish_region(engine)?;
- if self.stroke.is_some() || !matches!(self.fill, Celled::Value(None)) {
- self.render_fills_strokes(engine)?;
- }
+ self.render_fills_strokes()?;
- Ok(GridLayout {
- fragment: Fragment::frames(self.finished),
- cols: self.rcols,
- rows: self.rrows,
- })
+ Ok(Fragment::frames(self.finished))
}
/// Add lines and backgrounds.
- fn render_fills_strokes(&mut self, engine: &mut Engine) -> SourceResult<()> {
+ fn render_fills_strokes(&mut self) -> SourceResult<()> {
for (frame, rows) in self.finished.iter_mut().zip(&self.rrows) {
if self.rcols.is_empty() || rows.is_empty() {
continue;
@@ -573,7 +417,9 @@ impl<'a> GridLayouter<'a> {
for (x, &col) in self.rcols.iter().enumerate() {
let mut dy = Abs::zero();
for row in rows {
- if let Some(fill) = self.fill.resolve(engine, x, row.y)? {
+ let fill =
+ self.grid.cell(x, row.y).and_then(|cell| cell.fill.clone());
+ if let Some(fill) = fill {
let pos = Point::new(dx, dy);
let size = Size::new(col, row.height);
let rect = Geometry::Rect(size).filled(fill);
@@ -598,7 +444,7 @@ impl<'a> GridLayouter<'a> {
// Resolve the size of all relative columns and compute the sum of all
// fractional tracks.
- for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
+ for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) {
match col {
Sizing::Auto => {}
Sizing::Rel(v) => {
@@ -644,17 +490,17 @@ impl<'a> GridLayouter<'a> {
// Determine size of auto columns by laying out all cells in those
// columns, measuring them and finding the largest one.
- for (x, &col) in self.cols.iter().enumerate() {
+ for (x, &col) in self.grid.cols.iter().enumerate() {
if col != Sizing::Auto {
continue;
}
let mut resolved = Abs::zero();
- for y in 0..self.rows.len() {
- if let Some(cell) = self.cell(x, y) {
+ for y in 0..self.grid.rows.len() {
+ if let Some(cell) = self.grid.cell(x, y) {
// For relative rows, we can already resolve the correct
// base and for auto and fr we could only guess anyway.
- let height = match self.rows[y] {
+ let height = match self.grid.rows[y] {
Sizing::Rel(v) => {
v.resolve(self.styles).relative_to(self.regions.base().y)
}
@@ -682,7 +528,7 @@ impl<'a> GridLayouter<'a> {
return;
}
- for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
+ for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) {
if let Sizing::Fr(v) = col {
*rcol = v.share(fr, remaining);
}
@@ -703,7 +549,7 @@ impl<'a> GridLayouter<'a> {
last = fair;
fair = redistribute / (overlarge as f64);
- for (&col, &rcol) in self.cols.iter().zip(&self.rcols) {
+ for (&col, &rcol) in self.grid.cols.iter().zip(&self.rcols) {
// Remove an auto column if it is not overlarge (rcol <= fair),
// but also hasn't already been removed (rcol > last).
if col == Sizing::Auto && rcol <= fair && rcol > last {
@@ -715,7 +561,7 @@ impl<'a> GridLayouter<'a> {
}
// Redistribute space fairly among overlarge columns.
- for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
+ for (&col, rcol) in self.grid.cols.iter().zip(&mut self.rcols) {
if col == Sizing::Auto && *rcol > fair {
*rcol = fair;
}
@@ -783,7 +629,7 @@ impl<'a> GridLayouter<'a> {
let mut resolved: Vec<Abs> = vec![];
for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(cell) = self.cell(x, y) {
+ if let Some(cell) = self.grid.cell(x, y) {
let mut pod = self.regions;
pod.size.x = rcol;
@@ -831,7 +677,7 @@ impl<'a> GridLayouter<'a> {
self.finish_region(engine)?;
// Don't skip multiple regions for gutter and don't push a row.
- if self.has_gutter && y % 2 == 1 {
+ if self.grid.has_gutter && y % 2 == 1 {
return Ok(());
}
}
@@ -856,10 +702,10 @@ impl<'a> GridLayouter<'a> {
let mut pos = Point::zero();
for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(cell) = self.cell(x, y) {
+ if let Some(cell) = self.grid.cell(x, y) {
let size = Size::new(rcol, height);
let mut pod = Regions::one(size, Axes::splat(true));
- if self.rows[y] == Sizing::Auto {
+ if self.grid.rows[y] == Sizing::Auto {
pod.full = self.regions.full;
}
let frame = cell.layout(engine, self.styles, pod)?.into_frame();
@@ -894,7 +740,7 @@ impl<'a> GridLayouter<'a> {
// Layout the row.
let mut pos = Point::zero();
for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(cell) = self.cell(x, y) {
+ if let Some(cell) = self.grid.cell(x, y) {
pod.size.x = rcol;
// Push the layouted frames into the individual output frames.
@@ -964,33 +810,6 @@ impl<'a> GridLayouter<'a> {
Ok(())
}
-
- /// Get the content of the cell in column `x` and row `y`.
- ///
- /// Returns `None` if it's a gutter cell.
- #[track_caller]
- fn cell(&self, mut x: usize, y: usize) -> Option<&'a Content> {
- assert!(x < self.cols.len());
- assert!(y < self.rows.len());
-
- // Columns are reorder, but the cell slice is not.
- if self.is_rtl {
- x = self.cols.len() - 1 - x;
- }
-
- if self.has_gutter {
- // Even columns and rows are children, odd ones are gutter.
- if x % 2 == 0 && y % 2 == 0 {
- let c = 1 + self.cols.len() / 2;
- self.cells.get((y / 2) * c + x / 2)
- } else {
- None
- }
- } else {
- let c = self.cols.len();
- self.cells.get(y * c + x)
- }
- }
}
/// Turn an iterator of extents into an iterator of offsets before, in between,
diff --git a/crates/typst/src/layout/grid/mod.rs b/crates/typst/src/layout/grid/mod.rs
new file mode 100644
index 00000000..4d66fd4a
--- /dev/null
+++ b/crates/typst/src/layout/grid/mod.rs
@@ -0,0 +1,383 @@
+mod layout;
+
+pub use self::layout::{Cell, CellGrid, Celled, GridLayouter, ResolvableCell};
+
+use std::num::NonZeroUsize;
+
+use smallvec::{smallvec, SmallVec};
+
+use crate::diag::{SourceResult, StrResult};
+use crate::engine::Engine;
+use crate::foundations::{
+ cast, elem, scope, Array, Content, Fold, NativeElement, Show, Smart, StyleChain,
+ Value,
+};
+use crate::layout::{
+ Abs, Align, AlignElem, Axes, Fragment, Layout, Length, Regions, Rel, Sides, Sizing,
+};
+use crate::visualize::{Paint, Stroke};
+
+/// Arranges content in a grid.
+///
+/// The grid element allows you to arrange content in a grid. You can define the
+/// number of rows and columns, as well as the size of the gutters between them.
+/// There are multiple sizing modes for columns and rows that can be used to
+/// create complex layouts.
+///
+/// The sizing of the grid is determined by the track sizes specified in the
+/// arguments. Because each of the sizing parameters accepts the same values, we
+/// will explain them just once, here. Each sizing argument accepts an array of
+/// individual track sizes. A track size is either:
+///
+/// - `{auto}`: The track will be sized to fit its contents. It will be at most
+/// as large as the remaining space. If there is more than one `{auto}` track
+/// which, and together they claim more than the available space, the `{auto}`
+/// tracks will fairly distribute the available space among themselves.
+///
+/// - A fixed or relative length (e.g. `{10pt}` or `{20% - 1cm}`): The track
+/// will be exactly of this size.
+///
+/// - A fractional length (e.g. `{1fr}`): Once all other tracks have been sized,
+/// the remaining space will be divided among the fractional tracks according
+/// to their fractions. For example, if there are two fractional tracks, each
+/// with a fraction of `{1fr}`, they will each take up half of the remaining
+/// space.
+///
+/// To specify a single track, the array can be omitted in favor of a single
+/// value. To specify multiple `{auto}` tracks, enter the number of tracks
+/// instead of an array. For example, `columns:` `{3}` is equivalent to
+/// `columns:` `{(auto, auto, auto)}`.
+///
+/// # Styling the grid
+/// The grid's appearance can be customized through different parameters, such
+/// as `fill` to give all cells a background; `align` to change how cells are
+/// aligned; `inset` to optionally add internal padding to each cell; and
+/// `stroke` to optionally enable grid lines with a certain stroke.
+///
+/// If you need to override one of the above options for a single cell, you can
+/// use the [`grid.cell`]($grid.cell) element. Alternatively, if you need the
+/// 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.
+///
+/// # Examples
+/// The example below demonstrates the different track sizing options.
+///
+/// ```example
+/// // We use `rect` to emphasize the
+/// // area of cells.
+/// #set rect(
+/// inset: 8pt,
+/// fill: rgb("e4e5ea"),
+/// width: 100%,
+/// )
+///
+/// #grid(
+/// columns: (60pt, 1fr, 2fr),
+/// rows: (auto, 60pt),
+/// gutter: 3pt,
+/// rect[Fixed width, auto height],
+/// rect[1/3 of the remains],
+/// rect[2/3 of the remains],
+/// rect(height: 100%)[Fixed height],
+/// image("tiger.jpg", height: 100%),
+/// image("tiger.jpg", height: 100%),
+/// )
+/// ```
+///
+/// You can also [spread]($arguments/#spreading) an array of strings or content
+/// into a grid to populate its cells.
+///
+/// ```example
+/// #grid(
+/// columns: 5,
+/// gutter: 5pt,
+/// ..range(25).map(str)
+/// )
+/// ```
+#[elem(scope, Layout)]
+pub struct GridElem {
+ /// The column sizes.
+ ///
+ /// Either specify a track size array or provide an integer to create a grid
+ /// with that many `{auto}`-sized columns. Note that opposed to rows and
+ /// gutters, providing a single track size will only ever create a single
+ /// column.
+ #[borrowed]
+ pub columns: TrackSizings,
+
+ /// The row sizes.
+ ///
+ /// If there are more cells than fit the defined rows, the last row is
+ /// repeated until there are no more cells.
+ #[borrowed]
+ pub rows: TrackSizings,
+
+ /// The gaps between rows & columns.
+ ///
+ /// If there are more gutters than defined sizes, the last gutter is repeated.
+ #[external]
+ pub gutter: TrackSizings,
+
+ /// The gaps between columns. Takes precedence over `gutter`.
+ #[parse(
+ let gutter = args.named("gutter")?;
+ args.named("column-gutter")?.or_else(|| gutter.clone())
+ )]
+ #[borrowed]
+ pub column_gutter: TrackSizings,
+
+ /// The gaps between rows. Takes precedence over `gutter`.
+ #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
+ #[borrowed]
+ pub row_gutter: TrackSizings,
+
+ /// How to fill the cells.
+ ///
+ /// This can be a color or a function that returns a color. The function is
+ /// passed the cells' column and row index, starting at zero. This can be
+ /// used to implement striped grids.
+ ///
+ /// ```example
+ /// #grid(
+ /// fill: (col, row) => if calc.even(col + row) { luma(240) } else { white },
+ /// align: center + horizon,
+ /// columns: 4,
+ /// [X], [O], [X], [O],
+ /// [O], [X], [O], [X],
+ /// [X], [O], [X], [O],
+ /// [O], [X], [O], [X]
+ /// )
+ /// ```
+ #[borrowed]
+ pub fill: Celled<Option<Paint>>,
+
+ /// How to align the cells' content.
+ ///
+ /// This can either be a single alignment, an array of alignments
+ /// (corresponding to each column) or a function that returns an alignment.
+ /// The function is passed the cells' column and row index, starting at zero.
+ /// If set to `{auto}`, the outer alignment is used.
+ ///
+ /// ```example
+ /// #grid(
+ /// columns: 3,
+ /// align: (x, y) => (left, center, right).at(x),
+ /// [Hello], [Hello], [Hello],
+ /// [A], [B], [C],
+ /// )
+ /// ```
+ #[borrowed]
+ pub align: Celled<Smart<Align>>,
+
+ /// How to [stroke]($stroke) the cells.
+ ///
+ /// Grids have no strokes by default, which can be changed by setting this
+ /// option to the desired stroke.
+ ///
+ /// _Note:_ Richer stroke customization for individual cells is not yet
+ /// implemented, but will be in the future. In the meantime, you can use the
+ /// third-party [tablex library](https://github.com/PgBiel/typst-tablex/).
+ #[resolve]
+ #[fold]
+ pub stroke: Option<Stroke>,
+
+ /// How much to pad the cells' content.
+ ///
+ /// ```example
+ /// #grid(
+ /// inset: 10pt,
+ /// fill: (_, row) => (red, blue).at(row),
+ /// [Hello],
+ /// [World],
+ /// )
+ ///
+ /// #grid(
+ /// columns: 2,
+ /// inset: (
+ /// x: 20pt,
+ /// y: 10pt,
+ /// ),
+ /// fill: (col, _) => (red, blue).at(col),
+ /// [Hello],
+ /// [World],
+ /// )
+ /// ```
+ #[fold]
+ #[default(Sides::splat(Abs::pt(0.0).into()))]
+ pub inset: Sides<Option<Rel<Length>>>,
+
+ /// The contents of the grid cells.
+ ///
+ /// The cells are populated in row-major order.
+ #[variadic]
+ pub children: Vec<GridCell>,
+}
+
+#[scope]
+impl GridElem {
+ #[elem]
+ type GridCell;
+}
+
+impl Layout for GridElem {
+ #[typst_macros::time(name = "grid", span = self.span())]
+ fn layout(
+ &self,
+ engine: &mut Engine,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment> {
+ let inset = self.inset(styles);
+ let align = self.align(styles);
+ let columns = self.columns(styles);
+ let rows = self.rows(styles);
+ let column_gutter = self.column_gutter(styles);
+ let row_gutter = self.row_gutter(styles);
+ let fill = self.fill(styles);
+ let stroke = self.stroke(styles).map(Stroke::unwrap_or_default);
+
+ 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 grid = CellGrid::resolve(
+ tracks,
+ gutter,
+ self.children(),
+ fill,
+ align,
+ inset,
+ engine,
+ styles,
+ )?;
+
+ let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
+
+ // Measure the columns and layout the grid row-by-row.
+ layouter.layout(engine)
+ }
+}
+
+/// Track sizing definitions.
+#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
+pub struct TrackSizings(pub SmallVec<[Sizing; 4]>);
+
+cast! {
+ TrackSizings,
+ self => self.0.into_value(),
+ sizing: Sizing => Self(smallvec![sizing]),
+ count: NonZeroUsize => Self(smallvec![Sizing::Auto; count.get()]),
+ values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
+}
+
+/// A cell in the grid. Use this to either override grid properties for a
+/// particular cell, or in show rules to apply certain styles to multiple cells
+/// at once.
+///
+/// For example, you can override the fill, alignment or inset for a single
+/// cell:
+///
+/// ```example
+/// #grid(
+/// columns: 2,
+/// fill: red,
+/// align: left,
+/// inset: 5pt,
+/// [ABC], [ABC],
+/// grid.cell(fill: blue)[C], [D],
+/// grid.cell(align: center)[E], [F],
+/// [G], grid.cell(inset: 0pt)[H]
+/// )
+/// ```
+#[elem(name = "cell", title = "Grid Cell", Show)]
+pub struct GridCell {
+ /// The cell's body.
+ #[required]
+ body: Content,
+
+ /// The cell's fill override.
+ fill: Smart<Option<Paint>>,
+
+ /// The cell's alignment override.
+ align: Smart<Align>,
+
+ /// The cell's inset override.
+ inset: Smart<Sides<Option<Rel<Length>>>>,
+}
+
+cast! {
+ GridCell,
+ v: Content => v.into(),
+}
+
+impl Default for GridCell {
+ fn default() -> Self {
+ Self::new(Content::default())
+ }
+}
+
+impl ResolvableCell for GridCell {
+ fn resolve_cell(
+ mut self,
+ _: usize,
+ _: usize,
+ fill: &Option<Paint>,
+ align: Smart<Align>,
+ 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 {
+ Smart::Custom(align) => {
+ Smart::Custom(self.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),
+ });
+ self.push_inset(Smart::Custom(
+ self.inset(styles).map_or(inset, |inner| inner.fold(inset)).map(Some),
+ ));
+
+ Cell { body: self.pack(), fill }
+ }
+}
+
+impl Show for GridCell {
+ fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles))
+ }
+}
+
+impl From<Content> for GridCell {
+ fn from(value: Content) -> Self {
+ value
+ .to::<Self>()
+ .cloned()
+ .unwrap_or_else(|| Self::new(value.clone()))
+ }
+}
+
+/// Function with common code to display a grid cell or table cell.
+pub fn show_grid_cell(
+ mut body: Content,
+ inset: Smart<Sides<Option<Rel<Length>>>>,
+ align: Smart<Align>,
+) -> SourceResult<Content> {
+ let inset = inset.unwrap_or_default().map(Option::unwrap_or_default);
+
+ if inset != Sides::default() {
+ // Only pad if some inset is not 0pt.
+ // Avoids a bug where using .padded() in any way inside Show causes
+ // alignment in align(...) to break.
+ body = body.padded(inset);
+ }
+
+ if let Smart::Custom(alignment) = align {
+ body = body.styled(AlignElem::set_alignment(alignment));
+ }
+
+ Ok(body)
+}
diff --git a/crates/typst/src/model/bibliography.rs b/crates/typst/src/model/bibliography.rs
index fa35936b..62e6d986 100644
--- a/crates/typst/src/model/bibliography.rs
+++ b/crates/typst/src/model/bibliography.rs
@@ -29,7 +29,7 @@ use crate::foundations::{
};
use crate::introspection::{Introspector, Locatable, Location};
use crate::layout::{
- BlockElem, Em, GridElem, HElem, PadElem, Sizing, TrackSizings, VElem,
+ BlockElem, Em, GridCell, GridElem, HElem, PadElem, Sizing, TrackSizings, VElem,
};
use crate::model::{
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
@@ -239,8 +239,8 @@ impl Show for BibliographyElem {
if references.iter().any(|(prefix, _)| prefix.is_some()) {
let mut cells = vec![];
for (prefix, reference) in references {
- cells.push(prefix.clone().unwrap_or_default());
- cells.push(reference.clone());
+ cells.push(GridCell::new(prefix.clone().unwrap_or_default()));
+ cells.push(GridCell::new(reference.clone()));
}
seq.push(VElem::new(row_gutter).with_weakness(3).pack());
@@ -947,7 +947,7 @@ impl ElemRenderer<'_> {
if let Some(prefix) = suf_prefix {
const COLUMN_GUTTER: Em = Em::new(0.65);
- content = GridElem::new(vec![prefix, content])
+ content = GridElem::new(vec![GridCell::new(prefix), GridCell::new(content)])
.spanned(self.span)
.with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
diff --git a/crates/typst/src/model/enum.rs b/crates/typst/src/model/enum.rs
index bb44f438..7f3ffae1 100644
--- a/crates/typst/src/model/enum.rs
+++ b/crates/typst/src/model/enum.rs
@@ -6,8 +6,8 @@ use crate::foundations::{
cast, elem, scope, Array, Content, Fold, NativeElement, Smart, StyleChain,
};
use crate::layout::{
- Align, Axes, BlockElem, Celled, Em, Fragment, GridLayouter, HAlign, Layout, Length,
- Regions, Sizing, Spacing, VAlign,
+ Align, Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlign, Layout,
+ Length, Regions, Sizing, Spacing, VAlign,
};
use crate::model::{Numbering, NumberingPattern, ParElem};
use crate::text::TextElem;
@@ -259,16 +259,17 @@ impl Layout for EnumElem {
let resolved =
resolved.aligned(number_align).styled(TextElem::set_overhang(false));
- cells.push(Content::empty());
- cells.push(resolved);
- cells.push(Content::empty());
- cells.push(item.body().clone().styled(Self::set_parents(Parent(number))));
+ cells.push(Cell::from(Content::empty()));
+ cells.push(Cell::from(resolved));
+ cells.push(Cell::from(Content::empty()));
+ cells.push(Cell::from(
+ item.body().clone().styled(Self::set_parents(Parent(number))),
+ ));
number = number.saturating_add(1);
}
- let fill = Celled::Value(None);
let stroke = None;
- let layouter = GridLayouter::new(
+ let grid = CellGrid::new(
Axes::with_x(&[
Sizing::Rel(indent.into()),
Sizing::Auto,
@@ -276,15 +277,12 @@ impl Layout for EnumElem {
Sizing::Auto,
]),
Axes::with_y(&[gutter.into()]),
- &cells,
- &fill,
- &stroke,
- regions,
+ cells,
styles,
- self.span(),
);
+ let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
- Ok(layouter.layout(engine)?.fragment)
+ layouter.layout(engine)
}
}
diff --git a/crates/typst/src/model/list.rs b/crates/typst/src/model/list.rs
index 48913fc7..520657bb 100644
--- a/crates/typst/src/model/list.rs
+++ b/crates/typst/src/model/list.rs
@@ -5,8 +5,8 @@ use crate::foundations::{
Value,
};
use crate::layout::{
- Axes, BlockElem, Celled, Em, Fragment, GridLayouter, HAlign, Layout, Length, Regions,
- Sizing, Spacing, VAlign,
+ Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlign, Layout, Length,
+ Regions, Sizing, Spacing, VAlign,
};
use crate::model::ParElem;
use crate::text::TextElem;
@@ -160,15 +160,14 @@ impl Layout for ListElem {
let mut cells = vec![];
for item in self.children() {
- cells.push(Content::empty());
- cells.push(marker.clone());
- cells.push(Content::empty());
- cells.push(item.body().clone().styled(Self::set_depth(Depth)));
+ cells.push(Cell::from(Content::empty()));
+ cells.push(Cell::from(marker.clone()));
+ cells.push(Cell::from(Content::empty()));
+ cells.push(Cell::from(item.body().clone().styled(Self::set_depth(Depth))));
}
- let fill = Celled::Value(None);
let stroke = None;
- let layouter = GridLayouter::new(
+ let grid = CellGrid::new(
Axes::with_x(&[
Sizing::Rel(indent.into()),
Sizing::Auto,
@@ -176,15 +175,12 @@ impl Layout for ListElem {
Sizing::Auto,
]),
Axes::with_y(&[gutter.into()]),
- &cells,
- &fill,
- &stroke,
- regions,
+ cells,
styles,
- self.span(),
);
+ let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
- Ok(layouter.layout(engine)?.fragment)
+ layouter.layout(engine)
}
}
diff --git a/crates/typst/src/model/table.rs b/crates/typst/src/model/table.rs
index 33920445..169289aa 100644
--- a/crates/typst/src/model/table.rs
+++ b/crates/typst/src/model/table.rs
@@ -1,9 +1,11 @@
use crate::diag::SourceResult;
use crate::engine::Engine;
-use crate::foundations::{elem, Content, NativeElement, Smart, StyleChain};
+use crate::foundations::{
+ cast, elem, scope, Content, Fold, NativeElement, Show, Smart, StyleChain,
+};
use crate::layout::{
- apply_align_inset_to_cells, Abs, Align, Axes, Celled, Fragment, GridLayouter, Layout,
- Length, Regions, Rel, Sides, TrackSizings,
+ show_grid_cell, Abs, Align, Axes, Cell, CellGrid, Celled, Fragment, GridLayouter,
+ Layout, Length, Regions, Rel, ResolvableCell, Sides, TrackSizings,
};
use crate::model::Figurable;
use crate::text::{Lang, LocalName, Region};
@@ -13,9 +15,15 @@ use crate::visualize::{Paint, Stroke};
///
/// Tables are used to arrange content in cells. Cells can contain arbitrary
/// content, including multiple paragraphs and are specified in row-major order.
-/// Because tables are just grids with configurable cell properties, refer to
-/// the [grid documentation]($grid) for more information on how to size the
-/// table tracks.
+/// Because tables are just grids with different defaults for some cell
+/// properties (notably `stroke` and `inset`), refer to the
+/// [grid documentation]($grid) for more information on how to size the table
+/// tracks and specify the cell appearance properties.
+///
+/// Note that, to override a particular cell's properties or apply show rules
+/// on table cells, you can use the [`table.cell`]($table.cell) element (but
+/// not `grid.cell`, which is exclusive to grids). See its documentation for
+/// more information.
///
/// To give a table a caption and make it [referenceable]($ref), put it into a
/// [figure]($figure).
@@ -39,7 +47,7 @@ use crate::visualize::{Paint, Stroke};
/// [$a$: edge length]
/// )
/// ```
-#[elem(Layout, LocalName, Figurable)]
+#[elem(scope, Layout, LocalName, Figurable)]
pub struct TableElem {
/// The column sizes. See the [grid documentation]($grid) for more
/// information on track sizing.
@@ -149,7 +157,13 @@ pub struct TableElem {
/// The contents of the table cells.
#[variadic]
- pub children: Vec<Content>,
+ pub children: Vec<TableCell>,
+}
+
+#[scope]
+impl TableElem {
+ #[elem]
+ type TableCell;
}
impl Layout for TableElem {
@@ -171,22 +185,20 @@ impl Layout for 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());
- let cells =
- apply_align_inset_to_cells(engine, &tracks, self.children(), align, inset)?;
-
- // Prepare grid layout by unifying content and gutter tracks.
- let layouter = GridLayouter::new(
+ let grid = CellGrid::resolve(
tracks,
gutter,
- &cells,
+ self.children(),
fill,
- &stroke,
- regions,
+ align,
+ inset,
+ engine,
styles,
- self.span(),
- );
+ )?;
- Ok(layouter.layout(engine)?.fragment)
+ let layouter = GridLayouter::new(&grid, &stroke, regions, styles, self.span());
+
+ layouter.layout(engine)
}
}
@@ -227,3 +239,92 @@ impl LocalName for TableElem {
}
impl Figurable for TableElem {}
+
+/// A cell in the table. Use this to either override table properties for a
+/// particular cell, or in show rules to apply certain styles to multiple cells
+/// at once.
+///
+/// For example, you can override the fill, alignment or inset for a single
+/// cell:
+///
+/// ```example
+/// #table(
+/// columns: 2,
+/// fill: green,
+/// align: right,
+/// [*Name*], [*Data*],
+/// table.cell(fill: blue)[J.], [Organizer],
+/// table.cell(align: center)[K.], [Leader],
+/// [M.], table.cell(inset: 0pt)[Player]
+/// )
+/// ```
+#[elem(name = "cell", title = "Table Cell", Show)]
+pub struct TableCell {
+ /// The cell's body.
+ #[required]
+ body: Content,
+
+ /// The cell's fill override.
+ fill: Smart<Option<Paint>>,
+
+ /// The cell's alignment override.
+ align: Smart<Align>,
+
+ /// The cell's inset override.
+ inset: Smart<Sides<Option<Rel<Length>>>>,
+}
+
+cast! {
+ TableCell,
+ v: Content => v.into(),
+}
+
+impl Default for TableCell {
+ fn default() -> Self {
+ Self::new(Content::default())
+ }
+}
+
+impl ResolvableCell for TableCell {
+ fn resolve_cell(
+ mut self,
+ _: usize,
+ _: usize,
+ fill: &Option<Paint>,
+ align: Smart<Align>,
+ 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 {
+ Smart::Custom(align) => {
+ Smart::Custom(self.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),
+ });
+ self.push_inset(Smart::Custom(
+ self.inset(styles).map_or(inset, |inner| inner.fold(inset)).map(Some),
+ ));
+
+ Cell { body: self.pack(), fill }
+ }
+}
+
+impl Show for TableCell {
+ fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles))
+ }
+}
+
+impl From<Content> for TableCell {
+ fn from(value: Content) -> Self {
+ value
+ .to::<Self>()
+ .cloned()
+ .unwrap_or_else(|| Self::new(value.clone()))
+ }
+}
diff --git a/tests/ref/layout/grid-cell.png b/tests/ref/layout/grid-cell.png
new file mode 100644
index 00000000..fb683123
--- /dev/null
+++ b/tests/ref/layout/grid-cell.png
Binary files differ
diff --git a/tests/ref/layout/grid-styling.png b/tests/ref/layout/grid-styling.png
index ae5c0519..c626d2e7 100644
--- a/tests/ref/layout/grid-styling.png
+++ b/tests/ref/layout/grid-styling.png
Binary files differ
diff --git a/tests/ref/layout/table-cell.png b/tests/ref/layout/table-cell.png
new file mode 100644
index 00000000..fa3d04cc
--- /dev/null
+++ b/tests/ref/layout/table-cell.png
Binary files differ
diff --git a/tests/ref/layout/table.png b/tests/ref/layout/table.png
index fe4554d9..b6b31eb1 100644
--- a/tests/ref/layout/table.png
+++ b/tests/ref/layout/table.png
Binary files differ
diff --git a/tests/typ/layout/grid-cell.typ b/tests/typ/layout/grid-cell.typ
new file mode 100644
index 00000000..ced16a97
--- /dev/null
+++ b/tests/typ/layout/grid-cell.typ
@@ -0,0 +1,107 @@
+// Test basic styling using the grid.cell element.
+
+---
+// Cell override
+#grid(
+ align: left,
+ fill: red,
+ stroke: blue,
+ inset: 5pt,
+ columns: 2,
+ [AAAAA], [BBBBB],
+ [A], [B],
+ grid.cell(align: right)[C], [D],
+ align(right)[E], [F],
+ align(horizon)[G], [A\ A\ A],
+ grid.cell(align: horizon)[G2], [A\ A\ A],
+ grid.cell(inset: 0pt)[I], [F],
+ [H], grid.cell(fill: blue)[J]
+)
+
+---
+// Cell show rule
+#show grid.cell: it => [Zz]
+
+#grid(
+ align: left,
+ fill: red,
+ stroke: blue,
+ inset: 5pt,
+ columns: 2,
+ [AAAAA], [BBBBB],
+ [A], [B],
+ grid.cell(align: right)[C], [D],
+ align(right)[E], [F],
+ align(horizon)[G], [A\ A\ A]
+)
+
+---
+#show grid.cell: it => (it.align, it.fill)
+#grid(
+ align: left,
+ row-gutter: 5pt,
+ [A],
+ grid.cell(align: right)[B],
+ grid.cell(fill: aqua)[B],
+)
+
+---
+// Cell set rules
+#set grid.cell(align: center)
+#show grid.cell: it => (it.align, it.fill, it.inset)
+#set grid.cell(inset: 20pt)
+#grid(
+ align: left,
+ row-gutter: 5pt,
+ [A],
+ grid.cell(align: right)[B],
+ grid.cell(fill: aqua)[B],
+)
+
+---
+// Test folding per-cell properties (align and inset)
+#grid(
+ columns: (1fr, 1fr),
+ rows: (2.5em, auto),
+ align: right,
+ inset: 5pt,
+ fill: (x, y) => (green, aqua).at(calc.rem(x + y, 2)),
+ [Top], grid.cell(align: bottom)[Bot],
+ grid.cell(inset: (bottom: 0pt))[Bot], grid.cell(inset: (bottom: 0pt))[Bot]
+)
+
+---
+// Test overriding outside alignment
+#set align(bottom + right)
+#grid(
+ columns: (1fr, 1fr),
+ rows: 2em,
+ align: auto,
+ fill: green,
+ [BR], [BR],
+ grid.cell(align: left, fill: aqua)[BL], grid.cell(align: top, fill: red.lighten(50%))[TR]
+)
+
+---
+// First doc example
+#grid(
+ columns: 2,
+ fill: red,
+ align: left,
+ inset: 5pt,
+ [ABC], [ABC],
+ grid.cell(fill: blue)[C], [D],
+ grid.cell(align: center)[E], [F],
+ [G], grid.cell(inset: 0pt)[H]
+)
+
+---
+#{
+ show grid.cell: emph
+ grid(
+ columns: 2,
+ gutter: 3pt,
+ [Hello], [World],
+ [Sweet], [Italics]
+ )
+}
diff --git a/tests/typ/layout/grid-styling.typ b/tests/typ/layout/grid-styling.typ
index 577e15c4..e076d0c4 100644
--- a/tests/typ/layout/grid-styling.typ
+++ b/tests/typ/layout/grid-styling.typ
@@ -87,3 +87,48 @@ a
[A],
[B],
)
+
+---
+// Test interaction with gutters.
+#grid(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#grid(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ row-gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#grid(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ column-gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#grid(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
diff --git a/tests/typ/layout/table-cell.typ b/tests/typ/layout/table-cell.typ
new file mode 100644
index 00000000..a4d3bba4
--- /dev/null
+++ b/tests/typ/layout/table-cell.typ
@@ -0,0 +1,102 @@
+// Test basic styling using the table.cell element.
+
+---
+// Cell override
+#table(
+ align: left,
+ fill: red,
+ stroke: blue,
+ columns: 2,
+ [AAAAA], [BBBBB],
+ [A], [B],
+ table.cell(align: right)[C], [D],
+ align(right)[E], [F],
+ align(horizon)[G], [A\ A\ A],
+ table.cell(align: horizon)[G2], [A\ A\ A],
+ table.cell(inset: 0pt)[I], [F],
+ [H], table.cell(fill: blue)[J]
+)
+
+---
+// Cell show rule
+#show table.cell: it => [Zz]
+
+#table(
+ align: left,
+ fill: red,
+ stroke: blue,
+ columns: 2,
+ [AAAAA], [BBBBB],
+ [A], [B],
+ table.cell(align: right)[C], [D],
+ align(right)[E], [F],
+ align(horizon)[G], [A\ A\ A]
+)
+
+---
+#show table.cell: it => (it.align, it.fill)
+#table(
+ align: left,
+ row-gutter: 5pt,
+ [A],
+ table.cell(align: right)[B],
+ table.cell(fill: aqua)[B],
+)
+
+---
+// Cell set rules
+#set table.cell(align: center)
+#show table.cell: it => (it.align, it.fill, it.inset)
+#set table.cell(inset: 20pt)
+#table(
+ align: left,
+ row-gutter: 5pt,
+ [A],
+ table.cell(align: right)[B],
+ table.cell(fill: aqua)[B],
+)
+
+---
+// Test folding per-cell properties (align and inset)
+#table(
+ columns: (1fr, 1fr),
+ rows: (2.5em, auto),
+ align: right,
+ fill: (x, y) => (green, aqua).at(calc.rem(x + y, 2)),
+ [Top], table.cell(align: bottom)[Bot],
+ table.cell(inset: (bottom: 0pt))[Bot], table.cell(inset: (bottom: 0pt))[Bot]
+)
+
+---
+// Test overriding outside alignment
+#set align(bottom + right)
+#table(
+ columns: (1fr, 1fr),
+ rows: 2em,
+ align: auto,
+ fill: green,
+ [BR], [BR],
+ table.cell(align: left, fill: aqua)[BL], table.cell(align: top, fill: red.lighten(50%))[TR]
+)
+
+---
+// First doc example
+#table(
+ columns: 2,
+ fill: green,
+ align: right,
+ [*Name*], [*Data*],
+ table.cell(fill: blue)[J.], [Organizer],
+ table.cell(align: center)[K.], [Leader],
+ [M.], table.cell(inset: 0pt)[Player]
+)
+
+---
+#{
+ show table.cell: emph
+ table(
+ columns: 2,
+ [Person], [Animal],
+ [John], [Dog]
+ )
+}
diff --git a/tests/typ/layout/table.typ b/tests/typ/layout/table.typ
index 529f2720..1b250aa4 100644
--- a/tests/typ/layout/table.typ
+++ b/tests/typ/layout/table.typ
@@ -62,6 +62,51 @@
)
---
+// Test interaction with gutters.
+#table(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#table(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ row-gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#table(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ column-gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#table(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+---
// Ref: false
#table()