typst_library/model/table.rs
1use std::num::NonZeroUsize;
2use std::sync::Arc;
3
4use typst_utils::NonZeroExt;
5
6use crate::diag::{bail, HintedStrResult, HintedString, SourceResult};
7use crate::engine::Engine;
8use crate::foundations::{
9 cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain,
10 TargetElem,
11};
12use crate::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag};
13use crate::introspection::Locator;
14use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
15use crate::layout::{
16 show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine,
17 GridHeader, GridVLine, Length, OuterHAlignment, OuterVAlignment, Rel, Sides,
18 TrackSizings,
19};
20use crate::model::Figurable;
21use crate::text::LocalName;
22use crate::visualize::{Paint, Stroke};
23
24/// A table of items.
25///
26/// Tables are used to arrange content in cells. Cells can contain arbitrary
27/// content, including multiple paragraphs and are specified in row-major order.
28/// For a hands-on explanation of all the ways you can use and customize tables
29/// in Typst, check out the [table guide]($guides/table-guide).
30///
31/// Because tables are just grids with different defaults for some cell
32/// properties (notably `stroke` and `inset`), refer to the [grid
33/// documentation]($grid) for more information on how to size the table tracks
34/// and specify the cell appearance properties.
35///
36/// If you are unsure whether you should be using a table or a grid, consider
37/// whether the content you are arranging semantically belongs together as a set
38/// of related data points or similar or whether you are just want to enhance
39/// your presentation by arranging unrelated content in a grid. In the former
40/// case, a table is the right choice, while in the latter case, a grid is more
41/// appropriate. Furthermore, Typst will annotate its output in the future such
42/// that screenreaders will announce content in `table` as tabular while a
43/// grid's content will be announced no different than multiple content blocks
44/// in the document flow.
45///
46/// Note that, to override a particular cell's properties or apply show rules on
47/// table cells, you can use the [`table.cell`]($table.cell) element. See its
48/// documentation for more information.
49///
50/// Although the `table` and the `grid` share most properties, set and show
51/// rules on one of them do not affect the other.
52///
53/// To give a table a caption and make it [referenceable]($ref), put it into a
54/// [figure].
55///
56/// # Example
57///
58/// The example below demonstrates some of the most common table options.
59/// ```example
60/// #table(
61/// columns: (1fr, auto, auto),
62/// inset: 10pt,
63/// align: horizon,
64/// table.header(
65/// [], [*Volume*], [*Parameters*],
66/// ),
67/// image("cylinder.svg"),
68/// $ pi h (D^2 - d^2) / 4 $,
69/// [
70/// $h$: height \
71/// $D$: outer radius \
72/// $d$: inner radius
73/// ],
74/// image("tetrahedron.svg"),
75/// $ sqrt(2) / 12 a^3 $,
76/// [$a$: edge length]
77/// )
78/// ```
79///
80/// Much like with grids, you can use [`table.cell`]($table.cell) to customize
81/// the appearance and the position of each cell.
82///
83/// ```example
84/// >>> #set page(width: auto)
85/// >>> #set text(font: "IBM Plex Sans")
86/// >>> #let gray = rgb("#565565")
87/// >>>
88/// #set table(
89/// stroke: none,
90/// gutter: 0.2em,
91/// fill: (x, y) =>
92/// if x == 0 or y == 0 { gray },
93/// inset: (right: 1.5em),
94/// )
95///
96/// #show table.cell: it => {
97/// if it.x == 0 or it.y == 0 {
98/// set text(white)
99/// strong(it)
100/// } else if it.body == [] {
101/// // Replace empty cells with 'N/A'
102/// pad(..it.inset)[_N/A_]
103/// } else {
104/// it
105/// }
106/// }
107///
108/// #let a = table.cell(
109/// fill: green.lighten(60%),
110/// )[A]
111/// #let b = table.cell(
112/// fill: aqua.lighten(60%),
113/// )[B]
114///
115/// #table(
116/// columns: 4,
117/// [], [Exam 1], [Exam 2], [Exam 3],
118///
119/// [John], [], a, [],
120/// [Mary], [], a, a,
121/// [Robert], b, a, b,
122/// )
123/// ```
124#[elem(scope, Show, LocalName, Figurable)]
125pub struct TableElem {
126 /// The column sizes. See the [grid documentation]($grid) for more
127 /// information on track sizing.
128 #[borrowed]
129 pub columns: TrackSizings,
130
131 /// The row sizes. See the [grid documentation]($grid) for more information
132 /// on track sizing.
133 #[borrowed]
134 pub rows: TrackSizings,
135
136 /// The gaps between rows and columns. This is a shorthand for setting
137 /// `column-gutter` and `row-gutter` to the same value. See the [grid
138 /// documentation]($grid) for more information on gutters.
139 #[external]
140 pub gutter: TrackSizings,
141
142 /// The gaps between columns. Takes precedence over `gutter`. See the
143 /// [grid documentation]($grid) for more information on gutters.
144 #[borrowed]
145 #[parse(
146 let gutter = args.named("gutter")?;
147 args.named("column-gutter")?.or_else(|| gutter.clone())
148 )]
149 pub column_gutter: TrackSizings,
150
151 /// The gaps between rows. Takes precedence over `gutter`. See the
152 /// [grid documentation]($grid) for more information on gutters.
153 #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
154 #[borrowed]
155 pub row_gutter: TrackSizings,
156
157 /// How to fill the cells.
158 ///
159 /// This can be a color or a function that returns a color. The function
160 /// receives the cells' column and row indices, starting from zero. This can
161 /// be used to implement striped tables.
162 ///
163 /// ```example
164 /// #table(
165 /// fill: (x, _) =>
166 /// if calc.odd(x) { luma(240) }
167 /// else { white },
168 /// align: (x, y) =>
169 /// if y == 0 { center }
170 /// else if x == 0 { left }
171 /// else { right },
172 /// columns: 4,
173 /// [], [*Q1*], [*Q2*], [*Q3*],
174 /// [Revenue:], [1000 €], [2000 €], [3000 €],
175 /// [Expenses:], [500 €], [1000 €], [1500 €],
176 /// [Profit:], [500 €], [1000 €], [1500 €],
177 /// )
178 /// ```
179 #[borrowed]
180 pub fill: Celled<Option<Paint>>,
181
182 /// How to align the cells' content.
183 ///
184 /// This can either be a single alignment, an array of alignments
185 /// (corresponding to each column) or a function that returns an alignment.
186 /// The function receives the cells' column and row indices, starting from
187 /// zero. If set to `{auto}`, the outer alignment is used.
188 ///
189 /// ```example
190 /// #table(
191 /// columns: 3,
192 /// align: (left, center, right),
193 /// [Hello], [Hello], [Hello],
194 /// [A], [B], [C],
195 /// )
196 /// ```
197 #[borrowed]
198 pub align: Celled<Smart<Alignment>>,
199
200 /// How to [stroke] the cells.
201 ///
202 /// Strokes can be disabled by setting this to `{none}`.
203 ///
204 /// If it is necessary to place lines which can cross spacing between cells
205 /// produced by the `gutter` option, or to override the stroke between
206 /// multiple specific cells, consider specifying one or more of
207 /// [`table.hline`]($table.hline) and [`table.vline`]($table.vline)
208 /// alongside your table cells.
209 ///
210 /// See the [grid documentation]($grid.stroke) for more information on
211 /// strokes.
212 #[resolve]
213 #[fold]
214 #[default(Celled::Value(Sides::splat(Some(Some(Arc::new(Stroke::default()))))))]
215 pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>,
216
217 /// How much to pad the cells' content.
218 ///
219 /// ```example
220 /// #table(
221 /// inset: 10pt,
222 /// [Hello],
223 /// [World],
224 /// )
225 ///
226 /// #table(
227 /// columns: 2,
228 /// inset: (
229 /// x: 20pt,
230 /// y: 10pt,
231 /// ),
232 /// [Hello],
233 /// [World],
234 /// )
235 /// ```
236 #[fold]
237 #[default(Celled::Value(Sides::splat(Some(Abs::pt(5.0).into()))))]
238 pub inset: Celled<Sides<Option<Rel<Length>>>>,
239
240 /// The contents of the table cells, plus any extra table lines specified
241 /// with the [`table.hline`]($table.hline) and
242 /// [`table.vline`]($table.vline) elements.
243 #[variadic]
244 pub children: Vec<TableChild>,
245}
246
247#[scope]
248impl TableElem {
249 #[elem]
250 type TableCell;
251
252 #[elem]
253 type TableHLine;
254
255 #[elem]
256 type TableVLine;
257
258 #[elem]
259 type TableHeader;
260
261 #[elem]
262 type TableFooter;
263}
264
265fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
266 let cell = cell.body.clone();
267 let Some(cell) = cell.to_packed::<TableCell>() else { return cell };
268 let mut attrs = HtmlAttrs::default();
269 let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string());
270 if let Some(colspan) = span(cell.colspan(styles)) {
271 attrs.push(attr::colspan, colspan);
272 }
273 if let Some(rowspan) = span(cell.rowspan(styles)) {
274 attrs.push(attr::rowspan, rowspan);
275 }
276 HtmlElem::new(tag)
277 .with_body(Some(cell.body.clone()))
278 .with_attrs(attrs)
279 .pack()
280 .spanned(cell.span())
281}
282
283fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
284 let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack();
285 let mut rows: Vec<_> = grid.entries.chunks(grid.cols.len()).collect();
286
287 let tr = |tag, row: &[Entry]| {
288 let row = row
289 .iter()
290 .flat_map(|entry| entry.as_cell())
291 .map(|cell| show_cell_html(tag, cell, styles));
292 elem(tag::tr, Content::sequence(row))
293 };
294
295 let footer = grid.footer.map(|ft| {
296 let rows = rows.drain(ft.unwrap().start..);
297 elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row))))
298 });
299 let header = grid.header.map(|hd| {
300 let rows = rows.drain(..hd.unwrap().end);
301 elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))
302 });
303
304 let mut body = Content::sequence(rows.into_iter().map(|row| tr(tag::td, row)));
305 if header.is_some() || footer.is_some() {
306 body = elem(tag::tbody, body);
307 }
308
309 let content = header.into_iter().chain(core::iter::once(body)).chain(footer);
310 elem(tag::table, Content::sequence(content))
311}
312
313impl Show for Packed<TableElem> {
314 fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
315 Ok(if TargetElem::target_in(styles).is_html() {
316 // TODO: This is a hack, it is not clear whether the locator is actually used by HTML.
317 // How can we find out whether locator is actually used?
318 let locator = Locator::root();
319 show_cellgrid_html(table_to_cellgrid(self, engine, locator, styles)?, styles)
320 } else {
321 BlockElem::multi_layouter(self.clone(), engine.routines.layout_table).pack()
322 }
323 .spanned(self.span()))
324 }
325}
326
327impl LocalName for Packed<TableElem> {
328 const KEY: &'static str = "table";
329}
330
331impl Figurable for Packed<TableElem> {}
332
333/// Any child of a table element.
334#[derive(Debug, PartialEq, Clone, Hash)]
335pub enum TableChild {
336 Header(Packed<TableHeader>),
337 Footer(Packed<TableFooter>),
338 Item(TableItem),
339}
340
341cast! {
342 TableChild,
343 self => match self {
344 Self::Header(header) => header.into_value(),
345 Self::Footer(footer) => footer.into_value(),
346 Self::Item(item) => item.into_value(),
347 },
348 v: Content => {
349 v.try_into()?
350 },
351}
352
353impl TryFrom<Content> for TableChild {
354 type Error = HintedString;
355
356 fn try_from(value: Content) -> HintedStrResult<Self> {
357 if value.is::<GridHeader>() {
358 bail!(
359 "cannot use `grid.header` as a table header";
360 hint: "use `table.header` instead"
361 )
362 }
363 if value.is::<GridFooter>() {
364 bail!(
365 "cannot use `grid.footer` as a table footer";
366 hint: "use `table.footer` instead"
367 )
368 }
369
370 value
371 .into_packed::<TableHeader>()
372 .map(Self::Header)
373 .or_else(|value| value.into_packed::<TableFooter>().map(Self::Footer))
374 .or_else(|value| TableItem::try_from(value).map(Self::Item))
375 }
376}
377
378/// A table item, which is the basic unit of table specification.
379#[derive(Debug, PartialEq, Clone, Hash)]
380pub enum TableItem {
381 HLine(Packed<TableHLine>),
382 VLine(Packed<TableVLine>),
383 Cell(Packed<TableCell>),
384}
385
386cast! {
387 TableItem,
388 self => match self {
389 Self::HLine(hline) => hline.into_value(),
390 Self::VLine(vline) => vline.into_value(),
391 Self::Cell(cell) => cell.into_value(),
392 },
393 v: Content => {
394 v.try_into()?
395 },
396}
397
398impl TryFrom<Content> for TableItem {
399 type Error = HintedString;
400
401 fn try_from(value: Content) -> HintedStrResult<Self> {
402 if value.is::<GridHeader>() {
403 bail!("cannot place a grid header within another header or footer");
404 }
405 if value.is::<TableHeader>() {
406 bail!("cannot place a table header within another header or footer");
407 }
408 if value.is::<GridFooter>() {
409 bail!("cannot place a grid footer within another footer or header");
410 }
411 if value.is::<TableFooter>() {
412 bail!("cannot place a table footer within another footer or header");
413 }
414 if value.is::<GridCell>() {
415 bail!(
416 "cannot use `grid.cell` as a table cell";
417 hint: "use `table.cell` instead"
418 );
419 }
420 if value.is::<GridHLine>() {
421 bail!(
422 "cannot use `grid.hline` as a table line";
423 hint: "use `table.hline` instead"
424 );
425 }
426 if value.is::<GridVLine>() {
427 bail!(
428 "cannot use `grid.vline` as a table line";
429 hint: "use `table.vline` instead"
430 );
431 }
432
433 Ok(value
434 .into_packed::<TableHLine>()
435 .map(Self::HLine)
436 .or_else(|value| value.into_packed::<TableVLine>().map(Self::VLine))
437 .or_else(|value| value.into_packed::<TableCell>().map(Self::Cell))
438 .unwrap_or_else(|value| {
439 let span = value.span();
440 Self::Cell(Packed::new(TableCell::new(value)).spanned(span))
441 }))
442 }
443}
444
445/// A repeatable table header.
446///
447/// You should wrap your tables' heading rows in this function even if you do not
448/// plan to wrap your table across pages because Typst will use this function to
449/// attach accessibility metadata to tables in the future and ensure universal
450/// access to your document.
451///
452/// You can use the `repeat` parameter to control whether your table's header
453/// will be repeated across pages.
454///
455/// ```example
456/// #set page(height: 11.5em)
457/// #set table(
458/// fill: (x, y) =>
459/// if x == 0 or y == 0 {
460/// gray.lighten(40%)
461/// },
462/// align: right,
463/// )
464///
465/// #show table.cell.where(x: 0): strong
466/// #show table.cell.where(y: 0): strong
467///
468/// #table(
469/// columns: 4,
470/// table.header(
471/// [], [Blue chip],
472/// [Fresh IPO], [Penny st'k],
473/// ),
474/// table.cell(
475/// rowspan: 6,
476/// align: horizon,
477/// rotate(-90deg, reflow: true)[
478/// *USD / day*
479/// ],
480/// ),
481/// [0.20], [104], [5],
482/// [3.17], [108], [4],
483/// [1.59], [84], [1],
484/// [0.26], [98], [15],
485/// [0.01], [195], [4],
486/// [7.34], [57], [2],
487/// )
488/// ```
489#[elem(name = "header", title = "Table Header")]
490pub struct TableHeader {
491 /// Whether this header should be repeated across pages.
492 #[default(true)]
493 pub repeat: bool,
494
495 /// The cells and lines within the header.
496 #[variadic]
497 pub children: Vec<TableItem>,
498}
499
500/// A repeatable table footer.
501///
502/// Just like the [`table.header`]($table.header) element, the footer can repeat
503/// itself on every page of the table. This is useful for improving legibility
504/// by adding the column labels in both the header and footer of a large table,
505/// totals, or other information that should be visible on every page.
506///
507/// No other table cells may be placed after the footer.
508#[elem(name = "footer", title = "Table Footer")]
509pub struct TableFooter {
510 /// Whether this footer should be repeated across pages.
511 #[default(true)]
512 pub repeat: bool,
513
514 /// The cells and lines within the footer.
515 #[variadic]
516 pub children: Vec<TableItem>,
517}
518
519/// A horizontal line in the table.
520///
521/// Overrides any per-cell stroke, including stroke specified through the
522/// table's `stroke` field. Can cross spacing between cells created through the
523/// table's [`column-gutter`]($table.column-gutter) option.
524///
525/// Use this function instead of the table's `stroke` field if you want to
526/// manually place a horizontal line at a specific position in a single table.
527/// Consider using [table's `stroke`]($table.stroke) field or [`table.cell`'s
528/// `stroke`]($table.cell.stroke) field instead if the line you want to place is
529/// part of all your tables' designs.
530///
531/// ```example
532/// #set table.hline(stroke: .6pt)
533///
534/// #table(
535/// stroke: none,
536/// columns: (auto, 1fr),
537/// [09:00], [Badge pick up],
538/// [09:45], [Opening Keynote],
539/// [10:30], [Talk: Typst's Future],
540/// [11:15], [Session: Good PRs],
541/// table.hline(start: 1),
542/// [Noon], [_Lunch break_],
543/// table.hline(start: 1),
544/// [14:00], [Talk: Tracked Layout],
545/// [15:00], [Talk: Automations],
546/// [16:00], [Workshop: Tables],
547/// table.hline(),
548/// [19:00], [Day 1 Attendee Mixer],
549/// )
550/// ```
551#[elem(name = "hline", title = "Table Horizontal Line")]
552pub struct TableHLine {
553 /// The row above which the horizontal line is placed (zero-indexed).
554 /// Functions identically to the `y` field in [`grid.hline`]($grid.hline.y).
555 pub y: Smart<usize>,
556
557 /// The column at which the horizontal line starts (zero-indexed, inclusive).
558 pub start: usize,
559
560 /// The column before which the horizontal line ends (zero-indexed,
561 /// exclusive).
562 pub end: Option<NonZeroUsize>,
563
564 /// The line's stroke.
565 ///
566 /// Specifying `{none}` removes any lines previously placed across this
567 /// line's range, including hlines or per-cell stroke below it.
568 #[resolve]
569 #[fold]
570 #[default(Some(Arc::new(Stroke::default())))]
571 pub stroke: Option<Arc<Stroke>>,
572
573 /// The position at which the line is placed, given its row (`y`) - either
574 /// `{top}` to draw above it or `{bottom}` to draw below it.
575 ///
576 /// This setting is only relevant when row gutter is enabled (and
577 /// shouldn't be used otherwise - prefer just increasing the `y` field by
578 /// one instead), since then the position below a row becomes different
579 /// from the position above the next row due to the spacing between both.
580 #[default(OuterVAlignment::Top)]
581 pub position: OuterVAlignment,
582}
583
584/// A vertical line in the table. See the docs for [`grid.vline`]($grid.vline)
585/// for more information regarding how to use this element's fields.
586///
587/// Overrides any per-cell stroke, including stroke specified through the
588/// table's `stroke` field. Can cross spacing between cells created through the
589/// table's [`row-gutter`]($table.row-gutter) option.
590///
591/// Similar to [`table.hline`]($table.hline), use this function if you want to
592/// manually place a vertical line at a specific position in a single table and
593/// use the [table's `stroke`]($table.stroke) field or [`table.cell`'s
594/// `stroke`]($table.cell.stroke) field instead if the line you want to place is
595/// part of all your tables' designs.
596#[elem(name = "vline", title = "Table Vertical Line")]
597pub struct TableVLine {
598 /// The column before which the horizontal line is placed (zero-indexed).
599 /// Functions identically to the `x` field in [`grid.vline`]($grid.vline).
600 pub x: Smart<usize>,
601
602 /// The row at which the vertical line starts (zero-indexed, inclusive).
603 pub start: usize,
604
605 /// The row on top of which the vertical line ends (zero-indexed,
606 /// exclusive).
607 pub end: Option<NonZeroUsize>,
608
609 /// The line's stroke.
610 ///
611 /// Specifying `{none}` removes any lines previously placed across this
612 /// line's range, including vlines or per-cell stroke below it.
613 #[resolve]
614 #[fold]
615 #[default(Some(Arc::new(Stroke::default())))]
616 pub stroke: Option<Arc<Stroke>>,
617
618 /// The position at which the line is placed, given its column (`x`) -
619 /// either `{start}` to draw before it or `{end}` to draw after it.
620 ///
621 /// The values `{left}` and `{right}` are also accepted, but discouraged as
622 /// they cause your table to be inconsistent between left-to-right and
623 /// right-to-left documents.
624 ///
625 /// This setting is only relevant when column gutter is enabled (and
626 /// shouldn't be used otherwise - prefer just increasing the `x` field by
627 /// one instead), since then the position after a column becomes different
628 /// from the position before the next column due to the spacing between
629 /// both.
630 #[default(OuterHAlignment::Start)]
631 pub position: OuterHAlignment,
632}
633
634/// A cell in the table. Use this to position a cell manually or to apply
635/// styling. To do the latter, you can either use the function to override the
636/// properties for a particular cell, or use it in show rules to apply certain
637/// styles to multiple cells at once.
638///
639/// Perhaps the most important use case of `{table.cell}` is to make a cell span
640/// multiple columns and/or rows with the `colspan` and `rowspan` fields.
641///
642/// ```example
643/// >>> #set page(width: auto)
644/// #show table.cell.where(y: 0): strong
645/// #set table(
646/// stroke: (x, y) => if y == 0 {
647/// (bottom: 0.7pt + black)
648/// },
649/// align: (x, y) => (
650/// if x > 0 { center }
651/// else { left }
652/// )
653/// )
654///
655/// #table(
656/// columns: 3,
657/// table.header(
658/// [Substance],
659/// [Subcritical °C],
660/// [Supercritical °C],
661/// ),
662/// [Hydrochloric Acid],
663/// [12.0], [92.1],
664/// [Sodium Myreth Sulfate],
665/// [16.6], [104],
666/// [Potassium Hydroxide],
667/// table.cell(colspan: 2)[24.7],
668/// )
669/// ```
670///
671/// For example, you can override the fill, alignment or inset for a single
672/// cell:
673///
674/// ```example
675/// >>> #set page(width: auto)
676/// // You can also import those.
677/// #import table: cell, header
678///
679/// #table(
680/// columns: 2,
681/// align: center,
682/// header(
683/// [*Trip progress*],
684/// [*Itinerary*],
685/// ),
686/// cell(
687/// align: right,
688/// fill: fuchsia.lighten(80%),
689/// [🚗],
690/// ),
691/// [Get in, folks!],
692/// [🚗], [Eat curbside hotdog],
693/// cell(align: left)[🌴🚗],
694/// cell(
695/// inset: 0.06em,
696/// text(1.62em)[🛖🌅🌊],
697/// ),
698/// )
699/// ```
700///
701/// You may also apply a show rule on `table.cell` to style all cells at once.
702/// Combined with selectors, this allows you to apply styles based on a cell's
703/// position:
704///
705/// ```example
706/// #show table.cell.where(x: 0): strong
707///
708/// #table(
709/// columns: 3,
710/// gutter: 3pt,
711/// [Name], [Age], [Strength],
712/// [Hannes], [36], [Grace],
713/// [Irma], [50], [Resourcefulness],
714/// [Vikram], [49], [Perseverance],
715/// )
716/// ```
717#[elem(name = "cell", title = "Table Cell", Show)]
718pub struct TableCell {
719 /// The cell's body.
720 #[required]
721 pub body: Content,
722
723 /// The cell's column (zero-indexed).
724 /// Functions identically to the `x` field in [`grid.cell`]($grid.cell).
725 pub x: Smart<usize>,
726
727 /// The cell's row (zero-indexed).
728 /// Functions identically to the `y` field in [`grid.cell`]($grid.cell).
729 pub y: Smart<usize>,
730
731 /// The amount of columns spanned by this cell.
732 #[default(NonZeroUsize::ONE)]
733 pub colspan: NonZeroUsize,
734
735 /// The amount of rows spanned by this cell.
736 #[default(NonZeroUsize::ONE)]
737 pub rowspan: NonZeroUsize,
738
739 /// The cell's [fill]($table.fill) override.
740 pub fill: Smart<Option<Paint>>,
741
742 /// The cell's [alignment]($table.align) override.
743 pub align: Smart<Alignment>,
744
745 /// The cell's [inset]($table.inset) override.
746 pub inset: Smart<Sides<Option<Rel<Length>>>>,
747
748 /// The cell's [stroke]($table.stroke) override.
749 #[resolve]
750 #[fold]
751 pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
752
753 /// Whether rows spanned by this cell can be placed in different pages.
754 /// When equal to `{auto}`, a cell spanning only fixed-size rows is
755 /// unbreakable, while a cell spanning at least one `{auto}`-sized row is
756 /// breakable.
757 pub breakable: Smart<bool>,
758}
759
760cast! {
761 TableCell,
762 v: Content => v.into(),
763}
764
765impl Show for Packed<TableCell> {
766 fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
767 show_grid_cell(self.body.clone(), self.inset(styles), self.align(styles))
768 }
769}
770
771impl Default for Packed<TableCell> {
772 fn default() -> Self {
773 Packed::new(TableCell::new(Content::default()))
774 }
775}
776
777impl From<Content> for TableCell {
778 fn from(value: Content) -> Self {
779 #[allow(clippy::unwrap_or_default)]
780 value.unpack::<Self>().unwrap_or_else(Self::new)
781 }
782}