summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/layout/grid.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-10-27 19:04:55 +0100
committerGitHub <noreply@github.com>2024-10-27 18:04:55 +0000
commitbe7cfc85d08c545abfac08098b7b33b4bd71f37e (patch)
treef4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-library/src/layout/grid.rs
parentb8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff)
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-library/src/layout/grid.rs')
-rw-r--r--crates/typst-library/src/layout/grid.rs924
1 files changed, 924 insertions, 0 deletions
diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst-library/src/layout/grid.rs
new file mode 100644
index 00000000..2e1e9abc
--- /dev/null
+++ b/crates/typst-library/src/layout/grid.rs
@@ -0,0 +1,924 @@
+use std::num::NonZeroUsize;
+use std::sync::Arc;
+
+use comemo::Track;
+use smallvec::{smallvec, SmallVec};
+use typst_utils::NonZeroExt;
+
+use crate::diag::{bail, At, HintedStrResult, HintedString, SourceResult};
+use crate::engine::Engine;
+use crate::foundations::{
+ cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func,
+ IntoValue, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, Value,
+};
+use crate::layout::{
+ Alignment, BlockElem, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing,
+};
+use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine};
+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.
+///
+/// While the grid and table elements work very similarly, they are intended for
+/// different use cases and carry different semantics. The grid element is
+/// intended for presentational and layout purposes, while the
+/// [`{table}`]($table) element is intended for, in broad terms, presenting
+/// multiple related data points. In the future, Typst will annotate its output
+/// such that screenreaders will announce content in `table` as tabular while a
+/// grid's content will be announced no different than multiple content blocks
+/// in the document flow. Set and show rules on one of these elements do not
+/// affect the other.
+///
+/// A grid's sizing 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
+/// width, 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. It also
+/// shows how you can use [`grid.cell`]($grid.cell) to make an individual cell
+/// span two grid tracks.
+///
+/// ```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],
+/// grid.cell(
+/// colspan: 2,
+/// image("tiger.jpg", width: 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)
+/// )
+/// ```
+///
+/// # Styling the grid
+/// The grid's appearance can be customized through different parameters. These
+/// are the most important ones:
+///
+/// - [`fill`]($grid.fill) to give all cells a background
+/// - [`align`]($grid.align) to change how cells are aligned
+/// - [`inset`]($grid.inset) to optionally add internal padding to each cell
+/// - [`stroke`]($grid.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. Likewise, you can override
+/// individual grid lines with the [`grid.hline`]($grid.hline) and
+/// [`grid.vline`]($grid.vline) elements.
+///
+/// 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 or the examples
+/// below for more information.
+///
+/// Locating most of your styling in set and show rules is recommended, as it
+/// keeps the grid's or table's actual usages clean and easy to read. It also
+/// allows you to easily change the grid's appearance in one place.
+///
+/// ## Stroke styling precedence
+/// There are three ways to set the stroke of a grid cell: through
+/// [`{grid.cell}`'s `stroke` field]($grid.cell.stroke), by using
+/// [`{grid.hline}`]($grid.hline) and [`{grid.vline}`]($grid.vline), or by
+/// setting the [`{grid}`'s `stroke` field]($grid.stroke). When multiple of
+/// these settings are present and conflict, the `hline` and `vline` settings
+/// take the highest precedence, followed by the `cell` settings, and finally
+/// the `grid` settings.
+///
+/// Furthermore, strokes of a repeated grid header or footer will take
+/// precedence over regular cell strokes.
+#[elem(scope, Show)]
+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 and columns.
+ ///
+ /// If there are more gutters than defined sizes, the last gutter is
+ /// repeated.
+ ///
+ /// This is a shorthand to set `column-gutter` and `row-gutter` to the same
+ /// value.
+ #[external]
+ pub gutter: TrackSizings,
+
+ /// The gaps between columns.
+ #[parse(
+ let gutter = args.named("gutter")?;
+ args.named("column-gutter")?.or_else(|| gutter.clone())
+ )]
+ #[borrowed]
+ pub column_gutter: TrackSizings,
+
+ /// The gaps between rows.
+ #[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
+ /// receives the cells' column and row indices, starting from zero. This can
+ /// be used to implement striped grids.
+ ///
+ /// ```example
+ /// #grid(
+ /// fill: (x, y) =>
+ /// if calc.even(x + y) { luma(230) }
+ /// else { white },
+ /// align: center + horizon,
+ /// columns: 4,
+ /// inset: 2pt,
+ /// [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 receives the cells' column and row indices, starting from
+ /// zero. If set to `{auto}`, the outer alignment is used.
+ ///
+ /// You can find an example for this argument at the
+ /// [`table.align`]($table.align) parameter.
+ #[borrowed]
+ pub align: Celled<Smart<Alignment>>,
+
+ /// How to [stroke]($stroke) the cells.
+ ///
+ /// Grids have no strokes by default, which can be changed by setting this
+ /// option to the desired stroke.
+ ///
+ /// If it is necessary to place lines which can cross spacing between cells
+ /// produced by the `gutter` option, or to override the stroke between
+ /// multiple specific cells, consider specifying one or more of
+ /// [`grid.hline`]($grid.hline) and [`grid.vline`]($grid.vline) alongside
+ /// your grid cells.
+ ///
+ /// ```example
+ /// #set page(height: 13em, width: 26em)
+ ///
+ /// #let cv(..jobs) = grid(
+ /// columns: 2,
+ /// inset: 5pt,
+ /// stroke: (x, y) => if x == 0 and y > 0 {
+ /// (right: (
+ /// paint: luma(180),
+ /// thickness: 1.5pt,
+ /// dash: "dotted"
+ /// ))
+ /// },
+ /// grid.header(grid.cell(colspan: 2)[
+ /// *Professional Experience*
+ /// #box(width: 1fr, line(length: 100%, stroke: luma(180)))
+ /// ]),
+ /// ..{
+ /// let last = none
+ /// for job in jobs.pos() {
+ /// (
+ /// if job.year != last [*#job.year*],
+ /// [
+ /// *#job.company* - #job.role _(#job.timeframe)_ \
+ /// #job.details
+ /// ]
+ /// )
+ /// last = job.year
+ /// }
+ /// }
+ /// )
+ ///
+ /// #cv(
+ /// (
+ /// year: 2012,
+ /// company: [Pear Seed & Co.],
+ /// role: [Lead Engineer],
+ /// timeframe: [Jul - Dec],
+ /// details: [
+ /// - Raised engineers from 3x to 10x
+ /// - Did a great job
+ /// ],
+ /// ),
+ /// (
+ /// year: 2012,
+ /// company: [Mega Corp.],
+ /// role: [VP of Sales],
+ /// timeframe: [Mar - Jun],
+ /// details: [- Closed tons of customers],
+ /// ),
+ /// (
+ /// year: 2013,
+ /// company: [Tiny Co.],
+ /// role: [CEO],
+ /// timeframe: [Jan - Dec],
+ /// details: [- Delivered 4x more shareholder value],
+ /// ),
+ /// (
+ /// year: 2014,
+ /// company: [Glorbocorp Ltd],
+ /// role: [CTO],
+ /// timeframe: [Jan - Mar],
+ /// details: [- Drove containerization forward],
+ /// ),
+ /// )
+ /// ```
+ #[resolve]
+ #[fold]
+ pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>,
+
+ /// How much to pad the cells' content.
+ ///
+ /// You can find an example for this argument at the
+ /// [`table.inset`]($table.inset) parameter.
+ #[fold]
+ pub inset: Celled<Sides<Option<Rel<Length>>>>,
+
+ /// The contents of the grid cells, plus any extra grid lines specified
+ /// with the [`grid.hline`]($grid.hline) and [`grid.vline`]($grid.vline)
+ /// elements.
+ ///
+ /// The cells are populated in row-major order.
+ #[variadic]
+ pub children: Vec<GridChild>,
+}
+
+#[scope]
+impl GridElem {
+ #[elem]
+ type GridCell;
+
+ #[elem]
+ type GridHLine;
+
+ #[elem]
+ type GridVLine;
+
+ #[elem]
+ type GridHeader;
+
+ #[elem]
+ type GridFooter;
+}
+
+impl Show for Packed<GridElem> {
+ fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_grid)
+ .pack()
+ .spanned(self.span()))
+ }
+}
+
+/// 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::<HintedStrResult<_>>()?),
+}
+
+/// Any child of a grid element.
+#[derive(Debug, PartialEq, Clone, Hash)]
+pub enum GridChild {
+ Header(Packed<GridHeader>),
+ Footer(Packed<GridFooter>),
+ Item(GridItem),
+}
+
+cast! {
+ GridChild,
+ self => match self {
+ Self::Header(header) => header.into_value(),
+ Self::Footer(footer) => footer.into_value(),
+ Self::Item(item) => item.into_value(),
+ },
+ v: Content => {
+ v.try_into()?
+ },
+}
+
+impl TryFrom<Content> for GridChild {
+ type Error = HintedString;
+ fn try_from(value: Content) -> HintedStrResult<Self> {
+ if value.is::<TableHeader>() {
+ bail!(
+ "cannot use `table.header` as a grid header";
+ hint: "use `grid.header` instead"
+ )
+ }
+ if value.is::<TableFooter>() {
+ bail!(
+ "cannot use `table.footer` as a grid footer";
+ hint: "use `grid.footer` instead"
+ )
+ }
+
+ value
+ .into_packed::<GridHeader>()
+ .map(Self::Header)
+ .or_else(|value| value.into_packed::<GridFooter>().map(Self::Footer))
+ .or_else(|value| GridItem::try_from(value).map(Self::Item))
+ }
+}
+
+/// A grid item, which is the basic unit of grid specification.
+#[derive(Debug, PartialEq, Clone, Hash)]
+pub enum GridItem {
+ HLine(Packed<GridHLine>),
+ VLine(Packed<GridVLine>),
+ Cell(Packed<GridCell>),
+}
+
+cast! {
+ GridItem,
+ self => match self {
+ Self::HLine(hline) => hline.into_value(),
+ Self::VLine(vline) => vline.into_value(),
+ Self::Cell(cell) => cell.into_value(),
+ },
+ v: Content => {
+ v.try_into()?
+ }
+}
+
+impl TryFrom<Content> for GridItem {
+ type Error = HintedString;
+ fn try_from(value: Content) -> HintedStrResult<Self> {
+ if value.is::<GridHeader>() {
+ bail!("cannot place a grid header within another header or footer");
+ }
+ if value.is::<TableHeader>() {
+ bail!("cannot place a table header within another header or footer");
+ }
+ if value.is::<GridFooter>() {
+ bail!("cannot place a grid footer within another footer or header");
+ }
+ if value.is::<TableFooter>() {
+ bail!("cannot place a table footer within another footer or header");
+ }
+ if value.is::<TableCell>() {
+ bail!(
+ "cannot use `table.cell` as a grid cell";
+ hint: "use `grid.cell` instead"
+ );
+ }
+ if value.is::<TableHLine>() {
+ bail!(
+ "cannot use `table.hline` as a grid line";
+ hint: "use `grid.hline` instead"
+ );
+ }
+ if value.is::<TableVLine>() {
+ bail!(
+ "cannot use `table.vline` as a grid line";
+ hint: "use `grid.vline` instead"
+ );
+ }
+
+ Ok(value
+ .into_packed::<GridHLine>()
+ .map(Self::HLine)
+ .or_else(|value| value.into_packed::<GridVLine>().map(Self::VLine))
+ .or_else(|value| value.into_packed::<GridCell>().map(Self::Cell))
+ .unwrap_or_else(|value| {
+ let span = value.span();
+ Self::Cell(Packed::new(GridCell::new(value)).spanned(span))
+ }))
+ }
+}
+
+/// A repeatable grid header.
+///
+/// If `repeat` is set to `true`, the header will be repeated across pages. For
+/// an example, refer to the [`table.header`]($table.header) element and the
+/// [`grid.stroke`]($grid.stroke) parameter.
+#[elem(name = "header", title = "Grid Header")]
+pub struct GridHeader {
+ /// Whether this header should be repeated across pages.
+ #[default(true)]
+ pub repeat: bool,
+
+ /// The cells and lines within the header.
+ #[variadic]
+ pub children: Vec<GridItem>,
+}
+
+/// A repeatable grid footer.
+///
+/// Just like the [`grid.header`]($grid.header) element, the footer can repeat
+/// itself on every page of the table.
+///
+/// No other grid cells may be placed after the footer.
+#[elem(name = "footer", title = "Grid Footer")]
+pub struct GridFooter {
+ /// Whether this footer should be repeated across pages.
+ #[default(true)]
+ pub repeat: bool,
+
+ /// The cells and lines within the footer.
+ #[variadic]
+ pub children: Vec<GridItem>,
+}
+
+/// A horizontal line in the grid.
+///
+/// Overrides any per-cell stroke, including stroke specified through the grid's
+/// `stroke` field. Can cross spacing between cells created through the grid's
+/// `column-gutter` option.
+///
+/// An example for this function can be found at the
+/// [`table.hline`]($table.hline) element.
+#[elem(name = "hline", title = "Grid Horizontal Line")]
+pub struct GridHLine {
+ /// The row above which the horizontal line is placed (zero-indexed).
+ /// If the `position` field is set to `{bottom}`, the line is placed below
+ /// the row with the given index instead (see that field's docs for
+ /// details).
+ ///
+ /// Specifying `{auto}` causes the line to be placed at the row below the
+ /// last automatically positioned cell (that is, cell without coordinate
+ /// overrides) before the line among the grid's children. If there is no
+ /// such cell before the line, it is placed at the top of the grid (row 0).
+ /// Note that specifying for this option exactly the total amount of rows
+ /// in the grid causes this horizontal line to override the bottom border
+ /// of the grid, while a value of 0 overrides the top border.
+ pub y: Smart<usize>,
+
+ /// The column at which the horizontal line starts (zero-indexed, inclusive).
+ pub start: usize,
+
+ /// The column before which the horizontal line ends (zero-indexed,
+ /// exclusive).
+ /// Therefore, the horizontal line will be drawn up to and across column
+ /// `end - 1`.
+ ///
+ /// A value equal to `{none}` or to the amount of columns causes it to
+ /// extend all the way towards the end of the grid.
+ pub end: Option<NonZeroUsize>,
+
+ /// The line's stroke.
+ ///
+ /// Specifying `{none}` removes any lines previously placed across this
+ /// line's range, including hlines or per-cell stroke below it.
+ #[resolve]
+ #[fold]
+ #[default(Some(Arc::new(Stroke::default())))]
+ pub stroke: Option<Arc<Stroke>>,
+
+ /// The position at which the line is placed, given its row (`y`) - either
+ /// `{top}` to draw above it or `{bottom}` to draw below it.
+ ///
+ /// This setting is only relevant when row gutter is enabled (and
+ /// shouldn't be used otherwise - prefer just increasing the `y` field by
+ /// one instead), since then the position below a row becomes different
+ /// from the position above the next row due to the spacing between both.
+ #[default(OuterVAlignment::Top)]
+ pub position: OuterVAlignment,
+}
+
+/// A vertical line in the grid.
+///
+/// Overrides any per-cell stroke, including stroke specified through the
+/// grid's `stroke` field. Can cross spacing between cells created through
+/// the grid's `row-gutter` option.
+#[elem(name = "vline", title = "Grid Vertical Line")]
+pub struct GridVLine {
+ /// The column before which the horizontal line is placed (zero-indexed).
+ /// If the `position` field is set to `{end}`, the line is placed after the
+ /// column with the given index instead (see that field's docs for
+ /// details).
+ ///
+ /// Specifying `{auto}` causes the line to be placed at the column after
+ /// the last automatically positioned cell (that is, cell without
+ /// coordinate overrides) before the line among the grid's children. If
+ /// there is no such cell before the line, it is placed before the grid's
+ /// first column (column 0).
+ /// Note that specifying for this option exactly the total amount of
+ /// columns in the grid causes this vertical line to override the end
+ /// border of the grid (right in LTR, left in RTL), while a value of 0
+ /// overrides the start border (left in LTR, right in RTL).
+ pub x: Smart<usize>,
+
+ /// The row at which the vertical line starts (zero-indexed, inclusive).
+ pub start: usize,
+
+ /// The row on top of which the vertical line ends (zero-indexed,
+ /// exclusive).
+ /// Therefore, the vertical line will be drawn up to and across row
+ /// `end - 1`.
+ ///
+ /// A value equal to `{none}` or to the amount of rows causes it to extend
+ /// all the way towards the bottom of the grid.
+ pub end: Option<NonZeroUsize>,
+
+ /// The line's stroke.
+ ///
+ /// Specifying `{none}` removes any lines previously placed across this
+ /// line's range, including vlines or per-cell stroke below it.
+ #[resolve]
+ #[fold]
+ #[default(Some(Arc::new(Stroke::default())))]
+ pub stroke: Option<Arc<Stroke>>,
+
+ /// The position at which the line is placed, given its column (`x`) -
+ /// either `{start}` to draw before it or `{end}` to draw after it.
+ ///
+ /// The values `{left}` and `{right}` are also accepted, but discouraged as
+ /// they cause your grid to be inconsistent between left-to-right and
+ /// right-to-left documents.
+ ///
+ /// This setting is only relevant when column gutter is enabled (and
+ /// shouldn't be used otherwise - prefer just increasing the `x` field by
+ /// one instead), since then the position after a column becomes different
+ /// from the position before the next column due to the spacing between
+ /// both.
+ #[default(OuterHAlignment::Start)]
+ pub position: OuterHAlignment,
+}
+
+/// A cell in the grid. You can use this function in the argument list of a grid
+/// to override grid style properties for an individual cell or manually
+/// positioning it within the grid. You can also use this function in show rules
+/// to apply certain styles to multiple cells at once.
+///
+/// For example, you can override the position and stroke for a single cell:
+///
+/// ```example
+/// >>> #set page(width: auto)
+/// >>> #set text(15pt, font: "Noto Sans Symbols 2", bottom-edge: -.2em)
+/// <<< #set text(15pt, font: "Noto Sans Symbols 2")
+/// #show regex("[♚-♟︎]"): set text(fill: rgb("21212A"))
+/// #show regex("[♔-♙]"): set text(fill: rgb("111015"))
+///
+/// #grid(
+/// fill: (x, y) => rgb(
+/// if calc.odd(x + y) { "7F8396" }
+/// else { "EFF0F3" }
+/// ),
+/// columns: (1em,) * 8,
+/// rows: 1em,
+/// align: center + horizon,
+///
+/// [♖], [♘], [♗], [♕], [♔], [♗], [♘], [♖],
+/// [♙], [♙], [♙], [♙], [], [♙], [♙], [♙],
+/// grid.cell(
+/// x: 4, y: 3,
+/// stroke: blue.transparentize(60%)
+/// )[♙],
+///
+/// ..(grid.cell(y: 6)[♟],) * 8,
+/// ..([♜], [♞], [♝], [♛], [♚], [♝], [♞], [♜])
+/// .map(grid.cell.with(y: 7)),
+/// )
+/// ```
+///
+/// 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.
+/// Refer to the examples of the [`table.cell`]($table.cell) element to learn
+/// more about this.
+#[elem(name = "cell", title = "Grid Cell", Show)]
+pub struct GridCell {
+ /// The cell's body.
+ #[required]
+ pub 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
+ /// #let circ(c) = circle(
+ /// fill: c, width: 5mm
+ /// )
+ ///
+ /// #grid(
+ /// columns: 4,
+ /// rows: 7mm,
+ /// stroke: .5pt + blue,
+ /// align: center + horizon,
+ /// inset: 1mm,
+ ///
+ /// grid.cell(x: 2, y: 2, circ(aqua)),
+ /// circ(yellow),
+ /// grid.cell(x: 3, circ(green)),
+ /// circ(black),
+ /// )
+ /// ```
+ pub 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
+ /// #let tri(c) = polygon.regular(
+ /// fill: c,
+ /// size: 5mm,
+ /// vertices: 3,
+ /// )
+ ///
+ /// #grid(
+ /// columns: 2,
+ /// stroke: blue,
+ /// inset: 1mm,
+ ///
+ /// tri(black),
+ /// grid.cell(y: 1, tri(teal)),
+ /// grid.cell(y: 1, tri(red)),
+ /// grid.cell(y: 2, tri(orange))
+ /// )
+ /// ```
+ pub y: Smart<usize>,
+
+ /// The amount of columns spanned by this cell.
+ #[default(NonZeroUsize::ONE)]
+ pub colspan: NonZeroUsize,
+
+ /// The amount of rows spanned by this cell.
+ #[default(NonZeroUsize::ONE)]
+ pub rowspan: NonZeroUsize,
+
+ /// The cell's [fill]($grid.fill) override.
+ pub fill: Smart<Option<Paint>>,
+
+ /// The cell's [alignment]($grid.align) override.
+ pub align: Smart<Alignment>,
+
+ /// The cell's [inset]($grid.inset) override.
+ pub inset: Smart<Sides<Option<Rel<Length>>>>,
+
+ /// The cell's [stroke]($grid.stroke) override.
+ #[resolve]
+ #[fold]
+ pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
+
+ /// Whether rows spanned by this cell can be placed in different pages.
+ /// When equal to `{auto}`, a cell spanning only fixed-size rows is
+ /// unbreakable, while a cell spanning at least one `{auto}`-sized row is
+ /// breakable.
+ pub breakable: Smart<bool>,
+}
+
+cast! {
+ GridCell,
+ v: Content => v.into(),
+}
+
+impl Show for Packed<GridCell> {
+ fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles))
+ }
+}
+
+impl Default for Packed<GridCell> {
+ fn default() -> Self {
+ Packed::new(GridCell::new(Content::default()))
+ }
+}
+
+impl From<Content> for GridCell {
+ fn from(value: Content) -> Self {
+ #[allow(clippy::unwrap_or_default)]
+ value.unpack::<Self>().unwrap_or_else(Self::new)
+ }
+}
+
+/// Function with common code to display a grid cell or table cell.
+pub(crate) fn show_grid_cell(
+ mut body: Content,
+ inset: Smart<Sides<Option<Rel<Length>>>>,
+ align: Smart<Alignment>,
+) -> 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.aligned(alignment);
+ }
+
+ Ok(body)
+}
+
+/// A value that can be configured per cell.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub enum Celled<T> {
+ /// A bare value, the same for all cells.
+ Value(T),
+ /// A closure mapping from cell coordinates to a value.
+ Func(Func),
+ /// An array of alignment values corresponding to each column.
+ Array(Vec<T>),
+}
+
+impl<T: Default + Clone + FromValue> Celled<T> {
+ /// Resolve the value based on the cell position.
+ pub fn resolve(
+ &self,
+ engine: &mut Engine,
+ styles: StyleChain,
+ x: usize,
+ y: usize,
+ ) -> SourceResult<T> {
+ Ok(match self {
+ Self::Value(value) => value.clone(),
+ Self::Func(func) => func
+ .call(engine, Context::new(None, Some(styles)).track(), [x, y])?
+ .cast()
+ .at(func.span())?,
+ Self::Array(array) => x
+ .checked_rem(array.len())
+ .and_then(|i| array.get(i))
+ .cloned()
+ .unwrap_or_default(),
+ })
+ }
+}
+
+impl<T: Default> Default for Celled<T> {
+ fn default() -> Self {
+ Self::Value(T::default())
+ }
+}
+
+impl<T: Reflect> Reflect for Celled<T> {
+ fn input() -> CastInfo {
+ T::input() + Array::input() + Func::input()
+ }
+
+ fn output() -> CastInfo {
+ T::output() + Array::output() + Func::output()
+ }
+
+ fn castable(value: &Value) -> bool {
+ Array::castable(value) || Func::castable(value) || T::castable(value)
+ }
+}
+
+impl<T: IntoValue> IntoValue for Celled<T> {
+ fn into_value(self) -> Value {
+ match self {
+ Self::Value(value) => value.into_value(),
+ Self::Func(func) => func.into_value(),
+ Self::Array(arr) => arr.into_value(),
+ }
+ }
+}
+
+impl<T: FromValue> FromValue for Celled<T> {
+ fn from_value(value: Value) -> HintedStrResult<Self> {
+ match value {
+ Value::Func(v) => Ok(Self::Func(v)),
+ Value::Array(array) => Ok(Self::Array(
+ array.into_iter().map(T::from_value).collect::<HintedStrResult<_>>()?,
+ )),
+ v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)),
+ v => Err(Self::error(&v)),
+ }
+ }
+}
+
+impl<T: Fold> Fold for Celled<T> {
+ fn fold(self, outer: Self) -> Self {
+ match (self, outer) {
+ (Self::Value(inner), Self::Value(outer)) => Self::Value(inner.fold(outer)),
+ (self_, _) => self_,
+ }
+ }
+}
+
+impl<T: Resolve> Resolve for Celled<T> {
+ type Output = ResolvedCelled<T>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ match self {
+ Self::Value(value) => ResolvedCelled(Celled::Value(value.resolve(styles))),
+ Self::Func(func) => ResolvedCelled(Celled::Func(func)),
+ Self::Array(values) => ResolvedCelled(Celled::Array(
+ values.into_iter().map(|value| value.resolve(styles)).collect(),
+ )),
+ }
+ }
+}
+
+/// The result of resolving a Celled's value according to styles.
+/// Holds resolved values which depend on each grid cell's position.
+/// When it is a closure, however, it is only resolved when the closure is
+/// called.
+#[derive(Default, Clone)]
+pub struct ResolvedCelled<T: Resolve>(Celled<T::Output>);
+
+impl<T> ResolvedCelled<T>
+where
+ T: FromValue + Resolve,
+ <T as Resolve>::Output: Default + Clone,
+{
+ /// Resolve the value based on the cell position.
+ pub fn resolve(
+ &self,
+ engine: &mut Engine,
+ styles: StyleChain,
+ x: usize,
+ y: usize,
+ ) -> SourceResult<T::Output> {
+ Ok(match &self.0 {
+ Celled::Value(value) => value.clone(),
+ Celled::Func(func) => func
+ .call(engine, Context::new(None, Some(styles)).track(), [x, y])?
+ .cast::<T>()
+ .at(func.span())?
+ .resolve(styles),
+ Celled::Array(array) => x
+ .checked_rem(array.len())
+ .and_then(|i| array.get(i))
+ .cloned()
+ .unwrap_or_default(),
+ })
+ }
+}