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