pub mod resolve; use std::num::{NonZeroU32, 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, Packed, Reflect, Resolve, Smart, StyleChain, Value, }; use crate::introspection::Locatable; use crate::layout::{ Alignment, 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, Locatable)] 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. 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. 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()) )] pub column_gutter: TrackSizings, /// The gaps between rows. #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] 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], /// ) /// ``` pub fill: Celled>, /// 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. pub align: Celled>, /// 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], /// ), /// ) /// ``` #[fold] pub stroke: Celled>>>>, /// 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>>>, /// 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, } #[scope] impl GridElem { #[elem] type GridCell; #[elem] type GridHLine; #[elem] type GridVLine; #[elem] type GridHeader; #[elem] type GridFooter; } /// 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::>()?), } /// Any child of a grid element. #[derive(Debug, PartialEq, Clone, Hash)] pub enum GridChild { Header(Packed), Footer(Packed), 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 for GridChild { type Error = HintedString; fn try_from(value: Content) -> HintedStrResult { if value.is::() { bail!( "cannot use `table.header` as a grid header"; hint: "use `grid.header` instead" ) } if value.is::() { bail!( "cannot use `table.footer` as a grid footer"; hint: "use `grid.footer` instead" ) } value .into_packed::() .map(Self::Header) .or_else(|value| value.into_packed::().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), VLine(Packed), Cell(Packed), } 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 for GridItem { type Error = HintedString; fn try_from(value: Content) -> HintedStrResult { if value.is::() { bail!("cannot place a grid header within another header or footer"); } if value.is::() { bail!("cannot place a table header within another header or footer"); } if value.is::() { bail!("cannot place a grid footer within another footer or header"); } if value.is::() { bail!("cannot place a table footer within another footer or header"); } if value.is::() { bail!( "cannot use `table.cell` as a grid cell"; hint: "use `grid.cell` instead" ); } if value.is::() { bail!( "cannot use `table.hline` as a grid line"; hint: "use `grid.hline` instead" ); } if value.is::() { bail!( "cannot use `table.vline` as a grid line"; hint: "use `grid.vline` instead" ); } Ok(value .into_packed::() .map(Self::HLine) .or_else(|value| value.into_packed::().map(Self::VLine)) .or_else(|value| value.into_packed::().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 level of the header. Must not be zero. /// /// This allows repeating multiple headers at once. Headers with different /// levels can repeat together, as long as they have ascending levels. /// /// Notably, when a header with a lower level starts repeating, all higher /// or equal level headers stop repeating (they are "replaced" by the new /// header). #[default(NonZeroU32::ONE)] pub level: NonZeroU32, /// The cells and lines within the header. #[variadic] pub children: Vec, } /// 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, } /// 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, /// 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, /// The line's stroke. /// /// Specifying `{none}` removes any lines previously placed across this /// line's range, including hlines or per-cell stroke below it. #[fold] #[default(Some(Arc::new(Stroke::default())))] pub stroke: Option>, /// 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, /// 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, /// The line's stroke. /// /// Specifying `{none}` removes any lines previously placed across this /// line's range, including vlines or per-cell stroke below it. #[fold] #[default(Some(Arc::new(Stroke::default())))] pub stroke: Option>, /// 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", Locatable)] 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, /// 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, /// 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>, /// The cell's [alignment]($grid.align) override. pub align: Smart, /// The cell's [inset]($grid.inset) override. pub inset: Smart>>>, /// The cell's [stroke]($grid.stroke) override. #[fold] pub stroke: Sides>>>, /// 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, } cast! { GridCell, v: Content => v.into(), } impl Default for Packed { fn default() -> Self { Packed::new( // Explicitly set colspan and rowspan to ensure they won't be // overridden by set rules (default cells are created after // colspans and rowspans are processed in the resolver) GridCell::new(Content::default()) .with_colspan(NonZeroUsize::ONE) .with_rowspan(NonZeroUsize::ONE), ) } } impl From for GridCell { fn from(value: Content) -> Self { #[allow(clippy::unwrap_or_default)] value.unpack::().unwrap_or_else(Self::new) } } /// A value that can be configured per cell. #[derive(Debug, Clone, PartialEq, Hash)] pub enum Celled { /// 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), } impl Celled { /// Resolve the value based on the cell position. pub fn resolve( &self, engine: &mut Engine, styles: StyleChain, x: usize, y: usize, ) -> SourceResult { 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 Default for Celled { fn default() -> Self { Self::Value(T::default()) } } impl Reflect for Celled { 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 IntoValue for Celled { 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 FromValue for Celled { fn from_value(value: Value) -> HintedStrResult { match value { Value::Func(v) => Ok(Self::Func(v)), Value::Array(array) => Ok(Self::Array( array.into_iter().map(T::from_value).collect::>()?, )), v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), v => Err(Self::error(&v)), } } } impl Fold for Celled { fn fold(self, outer: Self) -> Self { match (self, outer) { (Self::Value(inner), Self::Value(outer)) => Self::Value(inner.fold(outer)), (self_, _) => self_, } } } impl Resolve for Celled { type Output = ResolvedCelled; 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(Celled); impl ResolvedCelled where T: FromValue + 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 { Ok(match &self.0 { Celled::Value(value) => value.clone(), Celled::Func(func) => func .call(engine, Context::new(None, Some(styles)).track(), [x, y])? .cast::() .at(func.span())? .resolve(styles), Celled::Array(array) => x .checked_rem(array.len()) .and_then(|i| array.get(i)) .cloned() .unwrap_or_default(), }) } }