use crate::layout::{AlignNode, GridLayouter, Sizing, TrackSizings}; use crate::prelude::*; /// # Table /// 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. /// /// ## 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] /// ) /// ``` /// /// ## Parameters /// - cells: `Content` (positional, variadic) /// The contents of the table cells. /// /// - rows: `TrackSizings` (named) /// Defines the row sizes. /// See the [grid documentation]($func/grid) for more information on track /// sizing. /// /// - columns: `TrackSizings` (named) /// Defines the column sizes. /// See the [grid documentation]($func/grid) for more information on track /// sizing. /// /// - gutter: `TrackSizings` (named) /// Defines the gaps between rows & columns. /// See the [grid documentation]($func/grid) for more information on gutters. /// /// - column-gutter: `TrackSizings` (named) /// Defines the gaps between columns. Takes precedence over `gutter`. /// See the [grid documentation]($func/grid) for more information on gutters. /// /// - row-gutter: `TrackSizings` (named) /// Defines the gaps between rows. Takes precedence over `gutter`. /// See the [grid documentation]($func/grid) for more information on gutters. /// /// ## Category /// layout #[func] #[capable(Layout)] #[derive(Debug, Hash)] pub struct TableNode { /// Defines sizing for content rows and columns. pub tracks: Axes>, /// Defines sizing of gutter rows and columns between content. pub gutter: Axes>, /// The content to be arranged in the table. pub cells: Vec, } #[node] impl TableNode { /// 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 €], /// ) /// ``` #[property(referenced)] pub const FILL: Celled> = Celled::Value(None); /// 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. #[property(referenced)] pub const ALIGN: Celled>>> = Celled::Value(Smart::Auto); /// How to stroke the cells. /// /// This can be a color, a stroke width, both, or `{none}` to disable /// the stroke. #[property(resolve, fold)] pub const STROKE: Option = Some(PartialStroke::default()); /// How much to pad the cells's content. /// /// The default value is `{5pt}`. pub const INSET: Rel = Abs::pt(5.0).into(); fn construct(_: &Vm, args: &mut Args) -> SourceResult { let TrackSizings(columns) = args.named("columns")?.unwrap_or_default(); let TrackSizings(rows) = args.named("rows")?.unwrap_or_default(); let TrackSizings(base_gutter) = args.named("gutter")?.unwrap_or_default(); let column_gutter = args.named("column-gutter")?.map(|TrackSizings(v)| v); let row_gutter = args.named("row-gutter")?.map(|TrackSizings(v)| v); Ok(Self { tracks: Axes::new(columns, rows), gutter: Axes::new( column_gutter.unwrap_or_else(|| base_gutter.clone()), row_gutter.unwrap_or(base_gutter), ), cells: args.all()?, } .pack()) } fn field(&self, name: &str) -> Option { match name { "columns" => Some(Sizing::encode_slice(&self.tracks.x)), "rows" => Some(Sizing::encode_slice(&self.tracks.y)), "column-gutter" => Some(Sizing::encode_slice(&self.gutter.x)), "row-gutter" => Some(Sizing::encode_slice(&self.gutter.y)), "cells" => Some(Value::Array( self.cells.iter().cloned().map(Value::Content).collect(), )), _ => None, } } } impl Layout for TableNode { fn layout( &self, vt: &mut Vt, styles: StyleChain, regions: Regions, ) -> SourceResult { let inset = styles.get(Self::INSET); let align = styles.get(Self::ALIGN); let cols = self.tracks.x.len().max(1); let cells: Vec<_> = self .cells .iter() .cloned() .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(AlignNode::ALIGNS, alignment) } Ok(child) }) .collect::>()?; let fill = styles.get(Self::FILL); let stroke = styles.get(Self::STROKE).map(PartialStroke::unwrap_or_default); // Prepare grid layout by unifying content and gutter tracks. let layouter = GridLayouter::new( vt, self.tracks.as_deref(), self.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); frame.prepend(Point::new(-half, offset), Element::Shape(hline)); } // 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); frame.prepend(Point::new(offset, -half), Element::Shape(vline)); } } // 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, Element::Shape(rect)); } 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: &Vt, x: usize, y: usize) -> SourceResult { Ok(match self { Self::Value(value) => value.clone(), Self::Func(func) => { let args = Args::new(func.span(), [Value::Int(x as i64), Value::Int(y as i64)]); func.call_detached(vt.world(), args)?.cast().at(func.span())? } }) } } 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") } }