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]($func/grid) for more information on how to size the /// table tracks. /// /// To give a table a caption and make it [referenceable]($func/ref), put it /// into a [figure]($func/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] /// ) /// ``` /// /// Display: Table /// Category: layout #[element(Layout, LocalName, Figurable)] pub struct TableElem { /// Defines the column sizes. See the [grid documentation]($func/grid) for /// more information on track sizing. pub columns: TrackSizings, /// Defines the row sizes. See the [grid documentation]($func/grid) for more /// information on track sizing. pub rows: TrackSizings, /// Defines the gaps between rows & columns. See the [grid /// documentation]($func/grid) for more information on gutters. #[external] pub gutter: TrackSizings, /// Defines the gaps between columns. Takes precedence over `gutter`. See /// the [grid documentation]($func/grid) for more information on gutters. #[parse( let gutter = args.named("gutter")?; args.named("column-gutter")?.or_else(|| gutter.clone()) )] pub column_gutter: TrackSizings, /// Defines the gaps between rows. Takes precedence over `gutter`. See the /// [grid documentation]($func/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 cell's 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 cell's content. /// /// This can either be a single alignment or a function that returns an /// alignment. The function is passed the cell's 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 the cells. /// /// This can be a color, a stroke width, both, or `{none}` to disable /// the stroke. #[resolve] #[fold] #[default(Some(PartialStroke::default()))] pub stroke: Option, /// How much to pad the cells's content. /// /// The default value is `{5pt}`. #[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(PartialStroke::unwrap_or_default); // Prepare grid layout by unifying content and gutter tracks. let layouter = GridLayouter::new( vt, tracks.as_deref(), gutter.as_deref(), &cells, regions, styles, ); // Measure the columns and layout the grid row-by-row. let mut layout = layouter.layout()?; // Add lines and backgrounds. for (frame, rows) in layout.fragment.iter_mut().zip(&layout.rows) { // 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 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.into_iter()) .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), } 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, [Value::Int(x as i64), Value::Int(y as i64)])? .cast() .at(func.span())?, }) } } impl Default for Celled { fn default() -> Self { Self::Value(T::default()) } } impl Cast for Celled { fn is(value: &Value) -> bool { matches!(value, Value::Func(_)) || T::is(value) } fn cast(value: Value) -> StrResult { match value { Value::Func(v) => Ok(Self::Func(v)), v if T::is(&v) => Ok(Self::Value(T::cast(v)?)), v => ::error(v), } } fn describe() -> CastInfo { T::describe() + CastInfo::Type("function") } } impl> From> for Value { fn from(celled: Celled) -> Self { match celled { Celled::Value(value) => value.into(), Celled::Func(func) => func.into(), } } } impl LocalName for TableElem { fn local_name(&self, lang: Lang, _: Option) -> &'static str { match lang { Lang::ARABIC => "جدول", Lang::BOKMÅL => "Tabell", Lang::CHINESE => "表", Lang::CZECH => "Tabulka", Lang::FRENCH => "Tableau", Lang::GERMAN => "Tabelle", Lang::ITALIAN => "Tabella", Lang::NYNORSK => "Tabell", Lang::POLISH => "Tabela", Lang::PORTUGUESE => "Tabela", Lang::RUSSIAN => "Таблица", Lang::SLOVENIAN => "Tabela", Lang::SPANISH => "Tabla", Lang::UKRAINIAN => "Таблиця", Lang::VIETNAMESE => "Bảng", Lang::ENGLISH | _ => "Table", } } } impl Figurable for TableElem { fn priority(&self, _styles: StyleChain) -> isize { -1000 } }