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