use typst::eval::{CastInfo, Reflect}; use crate::layout::{AlignElem, GridLayouter, TrackSizings}; use crate::meta::{Figurable, LocalName}; use crate::prelude::*; /// A table of items. /// /// Tables are used to arrange content in cells. Cells can contain arbitrary /// content, including multiple paragraphs and are specified in row-major order. /// Because tables are just grids with configurable cell properties, refer to /// the [grid documentation]($grid) for more information on how to size the /// table tracks. /// /// To give a table a caption and make it [referenceable]($ref), put it into a /// [figure]($figure). /// /// # Example /// ```example /// #table( /// columns: (1fr, auto, auto), /// inset: 10pt, /// align: horizon, /// [], [*Area*], [*Parameters*], /// image("cylinder.svg"), /// $ pi h (D^2 - d^2) / 4 $, /// [ /// $h$: height \ /// $D$: outer radius \ /// $d$: inner radius /// ], /// image("tetrahedron.svg"), /// $ sqrt(2) / 12 a^3 $, /// [$a$: edge length] /// ) /// ``` #[elem(Layout, LocalName, Figurable)] pub struct TableElem { /// The column sizes. See the [grid documentation]($grid) for more /// information on track sizing. pub columns: TrackSizings, /// The row sizes. See the [grid documentation]($grid) for more information /// on track sizing. pub rows: TrackSizings, /// The gaps between rows & columns. See the [grid documentation]($grid) for /// more information on gutters. #[external] pub gutter: TrackSizings, /// The gaps between columns. Takes precedence over `gutter`. See the /// [grid documentation]($grid) for more information on gutters. #[parse( let gutter = args.named("gutter")?; args.named("column-gutter")?.or_else(|| gutter.clone()) )] pub column_gutter: TrackSizings, /// The gaps between rows. Takes precedence over `gutter`. See the /// [grid documentation]($grid) for more information on gutters. #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] pub row_gutter: TrackSizings, /// How to fill the cells. /// /// This can be a color or a function that returns a color. The function is /// passed the cells' column and row index, starting at zero. This can be /// used to implement striped tables. /// /// ```example /// #table( /// fill: (col, _) => if calc.odd(col) { luma(240) } else { white }, /// align: (col, row) => /// if row == 0 { center } /// else if col == 0 { left } /// else { right }, /// columns: 4, /// [], [*Q1*], [*Q2*], [*Q3*], /// [Revenue:], [1000 €], [2000 €], [3000 €], /// [Expenses:], [500 €], [1000 €], [1500 €], /// [Profit:], [500 €], [1000 €], [1500 €], /// ) /// ``` pub fill: Celled>, /// How to align the cells' content. /// /// This can either be a single alignment, an array of alignments /// (corresponding to each column) or a function that returns an alignment. /// The function is passed the cells' column and row index, starting at zero. /// If set to `{auto}`, the outer alignment is used. /// /// ```example /// #table( /// columns: 3, /// align: (x, y) => (left, center, right).at(x), /// [Hello], [Hello], [Hello], /// [A], [B], [C], /// ) /// ``` pub align: Celled>, /// How to [stroke]($stroke) the cells. /// /// Strokes can be disabled by setting this to `{none}`. /// /// _Note:_ Richer stroke customization for individual cells is not yet /// implemented, but will be in the future. In the meantime, you can use the /// third-party [tablex library](https://github.com/PgBiel/typst-tablex/). #[resolve] #[fold] #[default(Some(Stroke::default()))] pub stroke: Option, /// How much to pad the cells' content. #[default(Abs::pt(5.0).into())] pub inset: Rel, /// The contents of the table cells. #[variadic] pub children: Vec, } impl Layout for TableElem { #[tracing::instrument(name = "TableElem::layout", skip_all)] fn layout( &self, vt: &mut Vt, styles: StyleChain, regions: Regions, ) -> SourceResult { let inset = self.inset(styles); let align = self.align(styles); let tracks = Axes::new(self.columns(styles).0, self.rows(styles).0); let gutter = Axes::new(self.column_gutter(styles).0, self.row_gutter(styles).0); let cols = tracks.x.len().max(1); let cells: Vec<_> = self .children() .into_iter() .enumerate() .map(|(i, child)| { let mut child = child.padded(Sides::splat(inset)); let x = i % cols; let y = i / cols; if let Smart::Custom(alignment) = align.resolve(vt, x, y)? { child = child.styled(AlignElem::set_alignment(alignment)); } Ok(child) }) .collect::>()?; let fill = self.fill(styles); let stroke = self.stroke(styles).map(Stroke::unwrap_or_default); // Prepare grid layout by unifying content and gutter tracks. let layouter = GridLayouter::new( tracks.as_deref(), gutter.as_deref(), &cells, regions, styles, ); // Measure the columns and layout the grid row-by-row. let mut layout = layouter.layout(vt)?; // Add lines and backgrounds. for (frame, rows) in layout.fragment.iter_mut().zip(&layout.rows) { if layout.cols.is_empty() || rows.is_empty() { continue; } // Render table lines. if let Some(stroke) = &stroke { let thickness = stroke.thickness; let half = thickness / 2.0; // Render horizontal lines. for offset in points(rows.iter().map(|piece| piece.height)) { let target = Point::with_x(frame.width() + thickness); let hline = Geometry::Line(target).stroked(stroke.clone()); frame.prepend( Point::new(-half, offset), FrameItem::Shape(hline, self.span()), ); } // Render vertical lines. for offset in points(layout.cols.iter().copied()) { let target = Point::with_y(frame.height() + thickness); let vline = Geometry::Line(target).stroked(stroke.clone()); frame.prepend( Point::new(offset, -half), FrameItem::Shape(vline, self.span()), ); } } // Render cell backgrounds. let mut dx = Abs::zero(); for (x, &col) in layout.cols.iter().enumerate() { let mut dy = Abs::zero(); for row in rows { if let Some(fill) = fill.resolve(vt, x, row.y)? { let pos = Point::new(dx, dy); let size = Size::new(col, row.height); let rect = Geometry::Rect(size).filled(fill); frame.prepend(pos, FrameItem::Shape(rect, self.span())); } dy += row.height; } dx += col; } } Ok(layout.fragment) } } /// Turn an iterator of extents into an iterator of offsets before, in between, /// and after the extents, e.g. [10mm, 5mm] -> [0mm, 10mm, 15mm]. fn points(extents: impl IntoIterator) -> impl Iterator { let mut offset = Abs::zero(); std::iter::once(Abs::zero()).chain(extents).map(move |extent| { offset += extent; offset }) } /// A value that can be configured per cell. #[derive(Debug, Clone, PartialEq, Hash)] pub enum Celled { /// A bare value, the same for all cells. Value(T), /// A closure mapping from cell coordinates to a value. Func(Func), /// An array of alignment values corresponding to each column. Array(Vec), } impl Celled { /// Resolve the value based on the cell position. pub fn resolve(&self, vt: &mut Vt, x: usize, y: usize) -> SourceResult { Ok(match self { Self::Value(value) => value.clone(), Self::Func(func) => func.call_vt(vt, [x, y])?.cast().at(func.span())?, Self::Array(array) => x .checked_rem(array.len()) .and_then(|i| array.get(i)) .cloned() .unwrap_or_default(), }) } } impl Default for Celled { fn default() -> Self { Self::Value(T::default()) } } impl Reflect for Celled { fn input() -> CastInfo { T::input() + Array::input() + Func::input() } fn output() -> CastInfo { T::output() + Array::output() + Func::output() } fn castable(value: &Value) -> bool { Array::castable(value) || Func::castable(value) || T::castable(value) } } impl IntoValue for Celled { fn into_value(self) -> Value { match self { Self::Value(value) => value.into_value(), Self::Func(func) => func.into_value(), Self::Array(arr) => arr.into_value(), } } } impl FromValue for Celled { fn from_value(value: Value) -> StrResult { match value { Value::Func(v) => Ok(Self::Func(v)), Value::Array(array) => Ok(Self::Array( array.into_iter().map(T::from_value).collect::>()?, )), v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), v => Err(Self::error(&v)), } } } impl LocalName for TableElem { fn local_name(&self, lang: Lang, _: Option) -> &'static str { match lang { Lang::ALBANIAN => "Tabel", Lang::ARABIC => "جدول", Lang::BOKMÅL => "Tabell", Lang::CHINESE => "表", Lang::CZECH => "Tabulka", Lang::DANISH => "Tabel", Lang::DUTCH => "Tabel", Lang::FILIPINO => "Talaan", Lang::FINNISH => "Taulukko", Lang::FRENCH => "Tableau", Lang::GERMAN => "Tabelle", Lang::HUNGARIAN => "Táblázat", Lang::ITALIAN => "Tabella", Lang::NYNORSK => "Tabell", Lang::POLISH => "Tabela", Lang::PORTUGUESE => "Tabela", Lang::ROMANIAN => "Tabelul", Lang::RUSSIAN => "Таблица", Lang::SLOVENIAN => "Tabela", Lang::SPANISH => "Tabla", Lang::SWEDISH => "Tabell", Lang::TURKISH => "Tablo", Lang::UKRAINIAN => "Таблиця", Lang::VIETNAMESE => "Bảng", Lang::JAPANESE => "表", Lang::ENGLISH | _ => "Table", } } } impl Figurable for TableElem {}