typst_library/layout/grid/
mod.rs

1pub mod resolve;
2
3use std::num::{NonZeroU32, NonZeroUsize};
4use std::sync::Arc;
5
6use comemo::Track;
7use smallvec::{SmallVec, smallvec};
8use typst_utils::NonZeroExt;
9
10use crate::diag::{At, HintedStrResult, HintedString, SourceResult, bail};
11use crate::engine::Engine;
12use crate::foundations::{
13    Array, CastInfo, Content, Context, Fold, FromValue, Func, IntoValue, Packed, Reflect,
14    Resolve, Smart, StyleChain, Synthesize, Value, cast, elem, scope,
15};
16use crate::introspection::Tagged;
17use crate::layout::resolve::{CellGrid, grid_to_cellgrid};
18use crate::layout::{
19    Alignment, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing,
20};
21use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine};
22use crate::visualize::{Paint, Stroke};
23
24/// Arranges content in a grid.
25///
26/// The grid element allows you to arrange content in a grid. You can define the
27/// number of rows and columns, as well as the size of the gutters between them.
28/// There are multiple sizing modes for columns and rows that can be used to
29/// create complex layouts.
30///
31/// While the grid and table elements work very similarly, they are intended for
32/// different use cases and carry different semantics. The grid element is
33/// intended for presentational and layout purposes, while the [`table`] element
34/// is intended for, in broad terms, presenting multiple related data points.
35/// Set and show rules on one of these elements do not affect the other. Refer
36/// to the [Accessibility Section]($grid/#accessibility) to learn how grids and
37/// tables are presented to users of Assistive Technology (AT) like screen
38/// readers.
39///
40/// # Sizing the tracks { #track-size }
41///
42/// A grid's sizing is determined by the track sizes specified in the arguments.
43/// There are multiple sizing parameters: [`columns`]($grid.columns),
44/// [`rows`]($grid.rows) and [`gutter`]($grid.gutter).
45/// Because each of the sizing parameters accepts the same values, we will
46/// explain them just once, here. Each sizing argument accepts an array of
47/// individual track sizes. A track size is either:
48///
49/// - `{auto}`: The track will be sized to fit its contents. It will be at most
50///   as large as the remaining space. If there is more than one `{auto}` track
51///   width, and together they claim more than the available space, the `{auto}`
52///   tracks will fairly distribute the available space among themselves.
53///
54/// - A fixed or relative length (e.g. `{10pt}` or `{20% - 1cm}`): The track
55///   will be exactly of this size.
56///
57/// - A fractional length (e.g. `{1fr}`): Once all other tracks have been sized,
58///   the remaining space will be divided among the fractional tracks according
59///   to their fractions. For example, if there are two fractional tracks, each
60///   with a fraction of `{1fr}`, they will each take up half of the remaining
61///   space.
62///
63/// To specify a single track, the array can be omitted in favor of a single
64/// value. To specify multiple `{auto}` tracks, enter the number of tracks
65/// instead of an array. For example, `columns:` `{3}` is equivalent to
66/// `columns:` `{(auto, auto, auto)}`.
67///
68/// # Examples
69/// The example below demonstrates the different track sizing options. It also
70/// shows how you can use [`grid.cell`] to make an individual cell span two grid
71/// tracks.
72///
73/// ```example
74/// // We use `rect` to emphasize the
75/// // area of cells.
76/// #set rect(
77///   inset: 8pt,
78///   fill: rgb("e4e5ea"),
79///   width: 100%,
80/// )
81///
82/// #grid(
83///   columns: (60pt, 1fr, 2fr),
84///   rows: (auto, 60pt),
85///   gutter: 3pt,
86///   rect[Fixed width, auto height],
87///   rect[1/3 of the remains],
88///   rect[2/3 of the remains],
89///   rect(height: 100%)[Fixed height],
90///   grid.cell(
91///     colspan: 2,
92///     image("tiger.jpg", width: 100%),
93///   ),
94/// )
95/// ```
96///
97/// You can also [spread]($arguments/#spreading) an array of strings or content
98/// into a grid to populate its cells.
99///
100/// ```example
101/// #grid(
102///   columns: 5,
103///   gutter: 5pt,
104///   ..range(25).map(str)
105/// )
106/// ```
107///
108/// # Styling the grid { #styling }
109/// The grid and table elements work similarly. For a hands-on explanation,
110/// refer to the [Table Guide]($guides/tables/#fills); for a quick overview,
111/// continue reading.
112///
113/// The grid's appearance can be customized through different parameters. These
114/// are the most important ones:
115///
116/// - [`align`]($grid.align) to change how cells are aligned
117/// - [`inset`]($grid.inset) to optionally add internal padding to cells
118/// - [`fill`]($grid.fill) to give cells a background
119/// - [`stroke`]($grid.stroke) to optionally enable grid lines with a certain
120///   stroke
121///
122/// To meet different needs, there are various ways to set them.
123///
124/// If you need to override the above options for individual cells, you can use
125/// the [`grid.cell`] element. Likewise, you can override individual grid lines
126/// with the [`grid.hline`] and [`grid.vline`] elements.
127///
128/// To configure an overall style for a grid, you may instead specify the option
129/// in any of the following fashions:
130///
131/// - As a single value that applies to all cells.
132/// - As an array of values corresponding to each column. The array will be
133///   cycled if there are more columns than the array has items.
134/// - As a function in the form of `(x, y) => value`. It receives the cell's
135///   column and row indices (both starting from zero) and should return the
136///   value to apply to that cell.
137///
138/// ```example
139/// #grid(
140///   columns: 5,
141///
142///   // By a single value
143///   align: center,
144///   // By a single but more complicated value
145///   inset: (x: 2pt, y: 3pt),
146///   // By an array of values (cycling)
147///   fill: (rgb("#239dad50"), none),
148///   // By a function that returns a value
149///   stroke: (x, y) => if calc.rem(x + y, 3) == 0 { 0.5pt },
150///
151///   ..range(5 * 3).map(n => numbering("A", n + 1))
152/// )
153/// ```
154///
155/// On top of that, you may [apply styling rules]($styling) to [`grid`] and
156/// [`grid.cell`]. Especially, the [`x`]($grid.cell.x) and [`y`]($grid.cell.y)
157/// fields of `grid.cell` can be used in a [`where`]($function.where) selector,
158/// making it possible to style cells at specific columns or rows, or individual
159/// positions.
160///
161/// ## Stroke styling precedence
162/// As explained above, there are three ways to set the stroke of a grid cell:
163/// through [`{grid.cell}`'s `stroke` field]($grid.cell.stroke), by using
164/// [`{grid.hline}`]($grid.hline) and [`{grid.vline}`]($grid.vline), or by
165/// setting the [`{grid}`'s `stroke` field]($grid.stroke). When multiple of
166/// these settings are present and conflict, the `hline` and `vline` settings
167/// take the highest precedence, followed by the `cell` settings, and finally
168/// the `grid` settings.
169///
170/// Furthermore, strokes of a repeated grid header or footer will take
171/// precedence over regular cell strokes.
172///
173/// # Accessibility
174/// Grids do not carry any special semantics. Assistive Technology (AT) does not
175/// offer the ability to navigate two-dimensionally by cell in grids. If you
176/// want to present tabular data, use the [`table`] element instead.
177///
178/// AT will read the grid cells in their semantic order. Usually, this is the
179/// order in which you passed them to the grid. However, if you manually
180/// positioned them using [`grid.cell`'s `x` and `y` arguments]($grid.cell.x),
181/// cells will be read row by row, from left to right (in left-to-right
182/// documents). A cell will be read when its position is first reached.
183#[elem(scope, Synthesize, Tagged)]
184pub struct GridElem {
185    /// The column sizes.
186    ///
187    /// Either specify a track size array or provide an integer to create a grid
188    /// with that many `{auto}`-sized columns. Note that opposed to rows and
189    /// gutters, providing a single track size will only ever create a single
190    /// column.
191    ///
192    /// See the [track size section](#track-size) above for more details.
193    pub columns: TrackSizings,
194
195    /// The row sizes.
196    ///
197    /// If there are more cells than fit the defined rows, the last row is
198    /// repeated until there are no more cells.
199    ///
200    /// See the [track size section](#track-size) above for more details.
201    pub rows: TrackSizings,
202
203    /// The gaps between rows and columns. This is a shorthand to set
204    /// [`column-gutter`]($grid.column-gutter) and [`row-gutter`]($grid.row-gutter)
205    /// to the same value.
206    ///
207    /// If there are more gutters than defined sizes, the last gutter is
208    /// repeated.
209    ///
210    /// See the [track size section](#track-size) above for more details.
211    #[external]
212    pub gutter: TrackSizings,
213
214    /// The gaps between columns.
215    #[parse(
216        let gutter = args.named("gutter")?;
217        args.named("column-gutter")?.or_else(|| gutter.clone())
218    )]
219    pub column_gutter: TrackSizings,
220
221    /// The gaps between rows.
222    #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
223    pub row_gutter: TrackSizings,
224
225    /// How much to pad the cells' content.
226    ///
227    /// To specify a uniform inset for all cells, you can use a single length
228    /// for all sides, or a dictionary of lengths for individual sides. See the
229    /// [box's documentation]($box.inset) for more details.
230    ///
231    /// To specify varying inset for different cells, you can:
232    /// - use a single inset for all cells
233    /// - use an array of insets corresponding to each column
234    /// - use a function that maps a cell's position to its inset
235    ///
236    /// See the [styling section](#styling) above for more details.
237    ///
238    /// In addition, you can find an example at the [`table.inset`] parameter.
239    #[fold]
240    pub inset: Celled<Sides<Option<Rel<Length>>>>,
241
242    /// How to align the cells' content.
243    ///
244    /// If set to `{auto}`, the outer alignment is used.
245    ///
246    /// You can specify the alignment in any of the following fashions:
247    /// - use a single alignment for all cells
248    /// - use an array of alignments corresponding to each column
249    /// - use a function that maps a cell's position to its alignment
250    ///
251    /// See the [styling section](#styling) above for details.
252    ///
253    /// In addition, you can find an example at the [`table.align`] parameter.
254    pub align: Celled<Smart<Alignment>>,
255
256    /// How to fill the cells.
257    ///
258    /// This can be:
259    /// - a single color for all cells
260    /// - an array of colors corresponding to each column
261    /// - a function that maps a cell's position to its color
262    ///
263    /// Most notably, arrays and functions are useful for creating striped grids.
264    /// See the [styling section](#styling) above for more details.
265    ///
266    /// ```example
267    /// #grid(
268    ///   fill: (x, y) =>
269    ///     if calc.even(x + y) { luma(230) }
270    ///     else { white },
271    ///   align: center + horizon,
272    ///   columns: 4,
273    ///   inset: 2pt,
274    ///   [X], [O], [X], [O],
275    ///   [O], [X], [O], [X],
276    ///   [X], [O], [X], [O],
277    ///   [O], [X], [O], [X],
278    /// )
279    /// ```
280    pub fill: Celled<Option<Paint>>,
281
282    /// How to [stroke]($stroke) the cells.
283    ///
284    /// Grids have no strokes by default, which can be changed by setting this
285    /// option to the desired stroke.
286    ///
287    /// If it is necessary to place lines which can cross spacing between cells
288    /// produced by the [`gutter`]($grid.gutter) option, or to override the
289    /// stroke between multiple specific cells, consider specifying one or more
290    /// of [`grid.hline`] and [`grid.vline`] alongside your grid cells.
291    ///
292    /// To specify the same stroke for all cells, you can use a single [stroke]
293    /// for all sides, or a dictionary of [strokes]($stroke) for individual
294    /// sides. See the [rectangle's documentation]($rect.stroke) for more
295    /// details.
296    ///
297    /// To specify varying strokes for different cells, you can:
298    /// - use a single stroke for all cells
299    /// - use an array of strokes corresponding to each column
300    /// - use a function that maps a cell's position to its stroke
301    ///
302    /// See the [styling section](#styling) above for more details.
303    ///
304    /// ```example:"Passing a function to set a stroke based on position"
305    /// #set page(width: 420pt)
306    /// #set text(number-type: "old-style")
307    /// #show grid.cell.where(y: 0): set text(size: 1.3em)
308    ///
309    /// #grid(
310    ///   columns: (1fr, 2fr, 2fr),
311    ///   row-gutter: 1.5em,
312    ///   inset: (left: 0.5em),
313    ///   stroke: (x, y) => if x > 0 { (left: 0.5pt + gray) },
314    ///   align: horizon,
315    ///
316    ///   [Winter \ 2007 \ Season],
317    ///   [Aaron Copland \ *The Tender Land* \ January 2007],
318    ///   [Eric Satie \ *Gymnopedie 1, 2* \ February 2007],
319    ///
320    ///   [],
321    ///   [Jan 12 \ *Middlebury College \ Center for the Arts* \ 20:00],
322    ///   [Feb 2 \ *Johnson State College Dibden Center for the Arts* \ 19:30],
323    ///
324    ///   [],
325    ///   [Skip a week \ #text(0.8em)[_Prepare your exams!_]],
326    ///   [Feb 9 \ *Castleton State College \ Fine Arts Center* \ 19:30],
327    ///
328    ///   [],
329    ///   [Jan 26, 27 \ *Lyndon State College Alexander Twilight Theater* \ 20:00],
330    ///   [
331    ///     Feb 17 --- #smallcaps[Anniversary] \
332    ///     *Middlebury College \ Center for the Arts* \
333    ///     19:00 #text(0.7em)[(for a special guest)]
334    ///   ],
335    /// )
336    /// ```
337    ///
338    /// ```example:"Folding the stroke dictionary"
339    /// #set page(height: 13em, width: 26em)
340    ///
341    /// #let cv(..jobs) = grid(
342    ///   columns: 2,
343    ///   inset: 5pt,
344    ///   stroke: (x, y) => if x == 0 and y > 0 {
345    ///     (right: (
346    ///       paint: luma(180),
347    ///       thickness: 1.5pt,
348    ///       dash: "dotted",
349    ///     ))
350    ///   },
351    ///   grid.header(grid.cell(colspan: 2)[
352    ///     *Professional Experience*
353    ///     #box(width: 1fr, line(length: 100%, stroke: luma(180)))
354    ///   ]),
355    ///   ..{
356    ///     let last = none
357    ///     for job in jobs.pos() {
358    ///       (
359    ///         if job.year != last [*#job.year*],
360    ///         [
361    ///           *#job.company* - #job.role _(#job.timeframe)_ \
362    ///           #job.details
363    ///         ]
364    ///       )
365    ///       last = job.year
366    ///     }
367    ///   }
368    /// )
369    ///
370    /// #cv(
371    ///   (
372    ///     year: 2012,
373    ///     company: [Pear Seed & Co.],
374    ///     role: [Lead Engineer],
375    ///     timeframe: [Jul - Dec],
376    ///     details: [
377    ///       - Raised engineers from 3x to 10x
378    ///       - Did a great job
379    ///     ],
380    ///   ),
381    ///   (
382    ///     year: 2012,
383    ///     company: [Mega Corp.],
384    ///     role: [VP of Sales],
385    ///     timeframe: [Mar - Jun],
386    ///     details: [- Closed tons of customers],
387    ///   ),
388    ///   (
389    ///     year: 2013,
390    ///     company: [Tiny Co.],
391    ///     role: [CEO],
392    ///     timeframe: [Jan - Dec],
393    ///     details: [- Delivered 4x more shareholder value],
394    ///   ),
395    ///   (
396    ///     year: 2014,
397    ///     company: [Glorbocorp Ltd],
398    ///     role: [CTO],
399    ///     timeframe: [Jan - Mar],
400    ///     details: [- Drove containerization forward],
401    ///   ),
402    /// )
403    /// ```
404    #[fold]
405    pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>,
406
407    #[internal]
408    #[synthesized]
409    pub grid: Arc<CellGrid>,
410
411    /// The contents of the grid cells, plus any extra grid lines specified with
412    /// the [`grid.hline`] and [`grid.vline`] elements.
413    ///
414    /// The cells are populated in row-major order.
415    #[variadic]
416    pub children: Vec<GridChild>,
417}
418
419#[scope]
420impl GridElem {
421    #[elem]
422    type GridCell;
423
424    #[elem]
425    type GridHLine;
426
427    #[elem]
428    type GridVLine;
429
430    #[elem]
431    type GridHeader;
432
433    #[elem]
434    type GridFooter;
435}
436
437impl Synthesize for Packed<GridElem> {
438    fn synthesize(
439        &mut self,
440        engine: &mut Engine,
441        styles: StyleChain,
442    ) -> SourceResult<()> {
443        let grid = grid_to_cellgrid(self, engine, styles)?;
444        self.grid = Some(Arc::new(grid));
445        Ok(())
446    }
447}
448
449/// Track sizing definitions.
450#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
451pub struct TrackSizings(pub SmallVec<[Sizing; 4]>);
452
453cast! {
454    TrackSizings,
455    self => self.0.into_value(),
456    sizing: Sizing => Self(smallvec![sizing]),
457    count: NonZeroUsize => Self(smallvec![Sizing::Auto; count.get()]),
458    values: Array => Self(values.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?),
459}
460
461/// Any child of a grid element.
462#[derive(Debug, Clone, PartialEq, Hash)]
463pub enum GridChild {
464    Header(Packed<GridHeader>),
465    Footer(Packed<GridFooter>),
466    Item(GridItem),
467}
468
469cast! {
470    GridChild,
471    self => match self {
472        Self::Header(header) => header.into_value(),
473        Self::Footer(footer) => footer.into_value(),
474        Self::Item(item) => item.into_value(),
475    },
476    v: Content => {
477        v.try_into()?
478    },
479}
480
481impl TryFrom<Content> for GridChild {
482    type Error = HintedString;
483    fn try_from(value: Content) -> HintedStrResult<Self> {
484        if value.is::<TableHeader>() {
485            bail!(
486                "cannot use `table.header` as a grid header";
487                hint: "use `grid.header` instead"
488            )
489        }
490        if value.is::<TableFooter>() {
491            bail!(
492                "cannot use `table.footer` as a grid footer";
493                hint: "use `grid.footer` instead"
494            )
495        }
496
497        value
498            .into_packed::<GridHeader>()
499            .map(Self::Header)
500            .or_else(|value| value.into_packed::<GridFooter>().map(Self::Footer))
501            .or_else(|value| GridItem::try_from(value).map(Self::Item))
502    }
503}
504
505/// A grid item, which is the basic unit of grid specification.
506#[derive(Debug, Clone, PartialEq, Hash)]
507pub enum GridItem {
508    HLine(Packed<GridHLine>),
509    VLine(Packed<GridVLine>),
510    Cell(Packed<GridCell>),
511}
512
513cast! {
514    GridItem,
515    self => match self {
516        Self::HLine(hline) => hline.into_value(),
517        Self::VLine(vline) => vline.into_value(),
518        Self::Cell(cell) => cell.into_value(),
519    },
520    v: Content => {
521        v.try_into()?
522    }
523}
524
525impl TryFrom<Content> for GridItem {
526    type Error = HintedString;
527    fn try_from(value: Content) -> HintedStrResult<Self> {
528        if value.is::<GridHeader>() {
529            bail!("cannot place a grid header within another header or footer");
530        }
531        if value.is::<TableHeader>() {
532            bail!("cannot place a table header within another header or footer");
533        }
534        if value.is::<GridFooter>() {
535            bail!("cannot place a grid footer within another footer or header");
536        }
537        if value.is::<TableFooter>() {
538            bail!("cannot place a table footer within another footer or header");
539        }
540        if value.is::<TableCell>() {
541            bail!(
542                "cannot use `table.cell` as a grid cell";
543                hint: "use `grid.cell` instead"
544            );
545        }
546        if value.is::<TableHLine>() {
547            bail!(
548                "cannot use `table.hline` as a grid line";
549                hint: "use `grid.hline` instead"
550            );
551        }
552        if value.is::<TableVLine>() {
553            bail!(
554                "cannot use `table.vline` as a grid line";
555                hint: "use `grid.vline` instead"
556            );
557        }
558
559        Ok(value
560            .into_packed::<GridHLine>()
561            .map(Self::HLine)
562            .or_else(|value| value.into_packed::<GridVLine>().map(Self::VLine))
563            .or_else(|value| value.into_packed::<GridCell>().map(Self::Cell))
564            .unwrap_or_else(|value| {
565                let span = value.span();
566                Self::Cell(Packed::new(GridCell::new(value)).spanned(span))
567            }))
568    }
569}
570
571/// A repeatable grid header.
572///
573/// If `repeat` is set to `true`, the header will be repeated across pages. For
574/// an example, refer to the [`table.header`] element and the [`grid.stroke`]
575/// parameter.
576#[elem(name = "header", title = "Grid Header")]
577pub struct GridHeader {
578    /// Whether this header should be repeated across pages.
579    #[default(true)]
580    pub repeat: bool,
581
582    /// The level of the header. Must not be zero.
583    ///
584    /// This allows repeating multiple headers at once. Headers with different
585    /// levels can repeat together, as long as they have ascending levels.
586    ///
587    /// Notably, when a header with a lower level starts repeating, all higher
588    /// or equal level headers stop repeating (they are "replaced" by the new
589    /// header).
590    #[default(NonZeroU32::ONE)]
591    pub level: NonZeroU32,
592
593    /// The cells and lines within the header.
594    #[variadic]
595    pub children: Vec<GridItem>,
596}
597
598/// A repeatable grid footer.
599///
600/// Just like the [`grid.header`] element, the footer can repeat itself on every
601/// page of the table.
602///
603/// No other grid cells may be placed after the footer.
604#[elem(name = "footer", title = "Grid Footer")]
605pub struct GridFooter {
606    /// Whether this footer should be repeated across pages.
607    #[default(true)]
608    pub repeat: bool,
609
610    /// The cells and lines within the footer.
611    #[variadic]
612    pub children: Vec<GridItem>,
613}
614
615/// A horizontal line in the grid.
616///
617/// Overrides any per-cell stroke, including stroke specified through the grid's
618/// `stroke` field. Can cross spacing between cells created through the grid's
619/// `column-gutter` option.
620///
621/// An example for this function can be found at the [`table.hline`] element.
622#[elem(name = "hline", title = "Grid Horizontal Line")]
623pub struct GridHLine {
624    /// The row above which the horizontal line is placed (zero-indexed).
625    /// If the `position` field is set to `{bottom}`, the line is placed below
626    /// the row with the given index instead (see [`grid.hline.position`] for
627    /// details).
628    ///
629    /// Specifying `{auto}` causes the line to be placed at the row below the
630    /// last automatically positioned cell (that is, cell without coordinate
631    /// overrides) before the line among the grid's children. If there is no
632    /// such cell before the line, it is placed at the top of the grid (row 0).
633    /// Note that specifying for this option exactly the total amount of rows
634    /// in the grid causes this horizontal line to override the bottom border
635    /// of the grid, while a value of 0 overrides the top border.
636    pub y: Smart<usize>,
637
638    /// The column at which the horizontal line starts (zero-indexed, inclusive).
639    pub start: usize,
640
641    /// The column before which the horizontal line ends (zero-indexed,
642    /// exclusive).
643    /// Therefore, the horizontal line will be drawn up to and across column
644    /// `end - 1`.
645    ///
646    /// A value equal to `{none}` or to the amount of columns causes it to
647    /// extend all the way towards the end of the grid.
648    pub end: Option<NonZeroUsize>,
649
650    /// The line's stroke.
651    ///
652    /// Specifying `{none}` removes any lines previously placed across this
653    /// line's range, including hlines or per-cell stroke below it.
654    #[fold]
655    #[default(Some(Arc::new(Stroke::default())))]
656    pub stroke: Option<Arc<Stroke>>,
657
658    /// The position at which the line is placed, given its row (`y`) - either
659    /// `{top}` to draw above it or `{bottom}` to draw below it.
660    ///
661    /// This setting is only relevant when row gutter is enabled (and
662    /// shouldn't be used otherwise - prefer just increasing the `y` field by
663    /// one instead), since then the position below a row becomes different
664    /// from the position above the next row due to the spacing between both.
665    #[default(OuterVAlignment::Top)]
666    pub position: OuterVAlignment,
667}
668
669/// A vertical line in the grid.
670///
671/// Overrides any per-cell stroke, including stroke specified through the
672/// grid's `stroke` field. Can cross spacing between cells created through
673/// the grid's `row-gutter` option.
674#[elem(name = "vline", title = "Grid Vertical Line")]
675pub struct GridVLine {
676    /// The column before which the vertical line is placed (zero-indexed).
677    /// If the `position` field is set to `{end}`, the line is placed after the
678    /// column with the given index instead (see [`grid.vline.position`] for
679    /// details).
680    ///
681    /// Specifying `{auto}` causes the line to be placed at the column after
682    /// the last automatically positioned cell (that is, cell without
683    /// coordinate overrides) before the line among the grid's children. If
684    /// there is no such cell before the line, it is placed before the grid's
685    /// first column (column 0).
686    /// Note that specifying for this option exactly the total amount of
687    /// columns in the grid causes this vertical line to override the end
688    /// border of the grid (right in LTR, left in RTL), while a value of 0
689    /// overrides the start border (left in LTR, right in RTL).
690    pub x: Smart<usize>,
691
692    /// The row at which the vertical line starts (zero-indexed, inclusive).
693    pub start: usize,
694
695    /// The row on top of which the vertical line ends (zero-indexed,
696    /// exclusive).
697    /// Therefore, the vertical line will be drawn up to and across row
698    /// `end - 1`.
699    ///
700    /// A value equal to `{none}` or to the amount of rows causes it to extend
701    /// all the way towards the bottom of the grid.
702    pub end: Option<NonZeroUsize>,
703
704    /// The line's stroke.
705    ///
706    /// Specifying `{none}` removes any lines previously placed across this
707    /// line's range, including vlines or per-cell stroke below it.
708    #[fold]
709    #[default(Some(Arc::new(Stroke::default())))]
710    pub stroke: Option<Arc<Stroke>>,
711
712    /// The position at which the line is placed, given its column (`x`) -
713    /// either `{start}` to draw before it or `{end}` to draw after it.
714    ///
715    /// The values `{left}` and `{right}` are also accepted, but discouraged as
716    /// they cause your grid to be inconsistent between left-to-right and
717    /// right-to-left documents.
718    ///
719    /// This setting is only relevant when column gutter is enabled (and
720    /// shouldn't be used otherwise - prefer just increasing the `x` field by
721    /// one instead), since then the position after a column becomes different
722    /// from the position before the next column due to the spacing between
723    /// both.
724    #[default(OuterHAlignment::Start)]
725    pub position: OuterHAlignment,
726}
727
728/// A cell in the grid. You can use this function in the argument list of a grid
729/// to override grid style properties for an individual cell or manually
730/// positioning it within the grid. You can also use this function in show rules
731/// to apply certain styles to multiple cells at once.
732///
733/// For example, you can override the position and stroke for a single cell:
734///
735/// ```example
736/// >>> #set page(width: auto)
737/// >>> #set text(15pt, font: "Noto Sans Symbols 2", bottom-edge: -.2em)
738/// <<< #set text(15pt, font: "Noto Sans Symbols 2")
739/// #show regex("[♚-♟︎]"): set text(fill: rgb("21212A"))
740/// #show regex("[♔-♙]"): set text(fill: rgb("111015"))
741///
742/// #grid(
743///   fill: (x, y) => rgb(
744///     if calc.odd(x + y) { "7F8396" }
745///     else { "EFF0F3" }
746///   ),
747///   columns: (1em,) * 8,
748///   rows: 1em,
749///   align: center + horizon,
750///
751///   [♖], [♘], [♗], [♕], [♔], [♗], [♘], [♖],
752///   [♙], [♙], [♙], [♙], [],  [♙], [♙], [♙],
753///   grid.cell(
754///     x: 4, y: 3,
755///     stroke: blue.transparentize(60%)
756///   )[♙],
757///
758///   ..(grid.cell(y: 6)[♟],) * 8,
759///   ..([♜], [♞], [♝], [♛], [♚], [♝], [♞], [♜])
760///     .map(grid.cell.with(y: 7)),
761/// )
762/// ```
763///
764/// You may also apply a show rule on `grid.cell` to style all cells at once,
765/// which allows you, for example, to apply styles based on a cell's position.
766/// Refer to the examples of the [`table.cell`] element to learn more about
767/// this.
768#[elem(name = "cell", title = "Grid Cell")]
769pub struct GridCell {
770    /// The cell's body.
771    #[required]
772    pub body: Content,
773
774    /// The cell's column (zero-indexed).
775    /// This field may be used in show rules to style a cell depending on its
776    /// column.
777    ///
778    /// You may override this field to pick in which column the cell must
779    /// be placed. If no row (`y`) is chosen, the cell will be placed in the
780    /// first row (starting at row 0) with that column available (or a new row
781    /// if none). If both `x` and `y` are chosen, however, the cell will be
782    /// placed in that exact position. An error is raised if that position is
783    /// not available (thus, it is usually wise to specify cells with a custom
784    /// position before cells with automatic positions).
785    ///
786    /// ```example
787    /// #let circ(c) = circle(
788    ///     fill: c, width: 5mm
789    /// )
790    ///
791    /// #grid(
792    ///   columns: 4,
793    ///   rows: 7mm,
794    ///   stroke: .5pt + blue,
795    ///   align: center + horizon,
796    ///   inset: 1mm,
797    ///
798    ///   grid.cell(x: 2, y: 2, circ(aqua)),
799    ///   circ(yellow),
800    ///   grid.cell(x: 3, circ(green)),
801    ///   circ(black),
802    /// )
803    /// ```
804    pub x: Smart<usize>,
805
806    /// The cell's row (zero-indexed).
807    /// This field may be used in show rules to style a cell depending on its
808    /// row.
809    ///
810    /// You may override this field to pick in which row the cell must be
811    /// placed. If no column (`x`) is chosen, the cell will be placed in the
812    /// first column (starting at column 0) available in the chosen row. If all
813    /// columns in the chosen row are already occupied, an error is raised.
814    ///
815    /// ```example
816    /// #let tri(c) = polygon.regular(
817    ///   fill: c,
818    ///   size: 5mm,
819    ///   vertices: 3,
820    /// )
821    ///
822    /// #grid(
823    ///   columns: 2,
824    ///   stroke: blue,
825    ///   inset: 1mm,
826    ///
827    ///   tri(black),
828    ///   grid.cell(y: 1, tri(teal)),
829    ///   grid.cell(y: 1, tri(red)),
830    ///   grid.cell(y: 2, tri(orange))
831    /// )
832    /// ```
833    pub y: Smart<usize>,
834
835    /// The amount of columns spanned by this cell.
836    #[default(NonZeroUsize::ONE)]
837    pub colspan: NonZeroUsize,
838
839    /// The amount of rows spanned by this cell.
840    #[default(NonZeroUsize::ONE)]
841    pub rowspan: NonZeroUsize,
842
843    /// The cell's [inset]($grid.inset) override.
844    pub inset: Smart<Sides<Option<Rel<Length>>>>,
845
846    /// The cell's [alignment]($grid.align) override.
847    pub align: Smart<Alignment>,
848
849    /// The cell's [fill]($grid.fill) override.
850    pub fill: Smart<Option<Paint>>,
851
852    /// The cell's [stroke]($grid.stroke) override.
853    #[fold]
854    pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
855
856    #[internal]
857    #[parse(Some(false))]
858    pub is_repeated: bool,
859
860    /// Whether rows spanned by this cell can be placed in different pages.
861    /// When equal to `{auto}`, a cell spanning only fixed-size rows is
862    /// unbreakable, while a cell spanning at least one `{auto}`-sized row is
863    /// breakable.
864    pub breakable: Smart<bool>,
865}
866
867cast! {
868    GridCell,
869    v: Content => v.into(),
870}
871
872impl Default for Packed<GridCell> {
873    fn default() -> Self {
874        Packed::new(
875            // Explicitly set colspan and rowspan to ensure they won't be
876            // overridden by set rules (default cells are created after
877            // colspans and rowspans are processed in the resolver)
878            GridCell::new(Content::default())
879                .with_colspan(NonZeroUsize::ONE)
880                .with_rowspan(NonZeroUsize::ONE),
881        )
882    }
883}
884
885impl From<Content> for GridCell {
886    fn from(value: Content) -> Self {
887        #[allow(clippy::unwrap_or_default)]
888        value.unpack::<Self>().unwrap_or_else(Self::new)
889    }
890}
891
892/// A value that can be configured per cell.
893#[derive(Debug, Clone, PartialEq, Hash)]
894pub enum Celled<T> {
895    /// A bare value, the same for all cells.
896    Value(T),
897    /// A closure mapping from cell coordinates to a value.
898    Func(Func),
899    /// An array of values corresponding to each column. The array will be
900    /// cycled if there are more columns than the array has items.
901    Array(Vec<T>),
902}
903
904impl<T: Default + Clone + FromValue> Celled<T> {
905    /// Resolve the value based on the cell position.
906    pub fn resolve(
907        &self,
908        engine: &mut Engine,
909        styles: StyleChain,
910        x: usize,
911        y: usize,
912    ) -> SourceResult<T> {
913        Ok(match self {
914            Self::Value(value) => value.clone(),
915            Self::Func(func) => func
916                .call(engine, Context::new(None, Some(styles)).track(), [x, y])?
917                .cast()
918                .at(func.span())?,
919            Self::Array(array) => x
920                .checked_rem(array.len())
921                .and_then(|i| array.get(i))
922                .cloned()
923                .unwrap_or_default(),
924        })
925    }
926}
927
928impl<T: Default> Default for Celled<T> {
929    fn default() -> Self {
930        Self::Value(T::default())
931    }
932}
933
934impl<T: Reflect> Reflect for Celled<T> {
935    fn input() -> CastInfo {
936        T::input() + Array::input() + Func::input()
937    }
938
939    fn output() -> CastInfo {
940        T::output() + Array::output() + Func::output()
941    }
942
943    fn castable(value: &Value) -> bool {
944        Array::castable(value) || Func::castable(value) || T::castable(value)
945    }
946}
947
948impl<T: IntoValue> IntoValue for Celled<T> {
949    fn into_value(self) -> Value {
950        match self {
951            Self::Value(value) => value.into_value(),
952            Self::Func(func) => func.into_value(),
953            Self::Array(arr) => arr.into_value(),
954        }
955    }
956}
957
958impl<T: FromValue> FromValue for Celled<T> {
959    fn from_value(value: Value) -> HintedStrResult<Self> {
960        match value {
961            Value::Func(v) => Ok(Self::Func(v)),
962            Value::Array(array) => Ok(Self::Array(
963                array.into_iter().map(T::from_value).collect::<HintedStrResult<_>>()?,
964            )),
965            v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)),
966            v => Err(Self::error(&v)),
967        }
968    }
969}
970
971impl<T: Fold> Fold for Celled<T> {
972    fn fold(self, outer: Self) -> Self {
973        match (self, outer) {
974            (Self::Value(inner), Self::Value(outer)) => Self::Value(inner.fold(outer)),
975            (self_, _) => self_,
976        }
977    }
978}
979
980impl<T: Resolve> Resolve for Celled<T> {
981    type Output = ResolvedCelled<T>;
982
983    fn resolve(self, styles: StyleChain) -> Self::Output {
984        match self {
985            Self::Value(value) => ResolvedCelled(Celled::Value(value.resolve(styles))),
986            Self::Func(func) => ResolvedCelled(Celled::Func(func)),
987            Self::Array(values) => ResolvedCelled(Celled::Array(
988                values.into_iter().map(|value| value.resolve(styles)).collect(),
989            )),
990        }
991    }
992}
993
994/// The result of resolving a Celled's value according to styles.
995/// Holds resolved values which depend on each grid cell's position.
996/// When it is a closure, however, it is only resolved when the closure is
997/// called.
998#[derive(Default, Clone)]
999pub struct ResolvedCelled<T: Resolve>(Celled<T::Output>);
1000
1001impl<T> ResolvedCelled<T>
1002where
1003    T: FromValue + Resolve,
1004    <T as Resolve>::Output: Default + Clone,
1005{
1006    /// Resolve the value based on the cell position.
1007    pub fn resolve(
1008        &self,
1009        engine: &mut Engine,
1010        styles: StyleChain,
1011        x: usize,
1012        y: usize,
1013    ) -> SourceResult<T::Output> {
1014        Ok(match &self.0 {
1015            Celled::Value(value) => value.clone(),
1016            Celled::Func(func) => func
1017                .call(engine, Context::new(None, Some(styles)).track(), [x, y])?
1018                .cast::<T>()
1019                .at(func.span())?
1020                .resolve(styles),
1021            Celled::Array(array) => x
1022                .checked_rem(array.len())
1023                .and_then(|i| array.get(i))
1024                .cloned()
1025                .unwrap_or_default(),
1026        })
1027    }
1028}