diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-07-02 19:59:52 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-07-02 20:07:43 +0200 |
| commit | ebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch) | |
| tree | 2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-library/src/layout/table.rs | |
| parent | 3ab19185093d7709f824b95b979060ce125389d8 (diff) | |
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-library/src/layout/table.rs')
| -rw-r--r-- | crates/typst-library/src/layout/table.rs | 334 |
1 files changed, 334 insertions, 0 deletions
diff --git a/crates/typst-library/src/layout/table.rs b/crates/typst-library/src/layout/table.rs new file mode 100644 index 00000000..c2faedba --- /dev/null +++ b/crates/typst-library/src/layout/table.rs @@ -0,0 +1,334 @@ +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]($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 } +/// ```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 { + /// The column sizes. See the [grid documentation]($func/grid) for more + /// information on track sizing. + pub columns: TrackSizings, + + /// The row sizes. See the [grid documentation]($func/grid) for more + /// information on track sizing. + pub rows: TrackSizings, + + /// The gaps between rows & columns. See the [grid + /// documentation]($func/grid) for more information on gutters. + #[external] + pub gutter: TrackSizings, + + /// 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, + + /// 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 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<Option<Paint>>, + + /// 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<Smart<Axes<Option<GenAlign>>>>, + + /// How to stroke the cells. + /// + /// See the [line's documentation]($func/line.stroke) for more details. + /// 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(PartialStroke::default()))] + pub stroke: Option<PartialStroke>, + + /// How much to pad the cells' content. + #[default(Abs::pt(5.0).into())] + pub inset: Rel<Length>, + + /// The contents of the table cells. + #[variadic] + pub children: Vec<Content>, +} + +impl Layout for TableElem { + #[tracing::instrument(name = "TableElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + 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::<SourceResult<_>>()?; + + 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( + 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<Item = Abs>) -> impl Iterator<Item = Abs> { + 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<T> { + /// 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<T>), +} + +impl<T: Default + Clone + FromValue> Celled<T> { + /// Resolve the value based on the cell position. + pub fn resolve(&self, vt: &mut Vt, x: usize, y: usize) -> SourceResult<T> { + 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<T: Default> Default for Celled<T> { + fn default() -> Self { + Self::Value(T::default()) + } +} + +impl<T: Reflect> Reflect for Celled<T> { + fn describe() -> CastInfo { + T::describe() + Array::describe() + Func::describe() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) || Func::castable(value) || T::castable(value) + } +} + +impl<T: IntoValue> IntoValue for Celled<T> { + 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<T: FromValue> FromValue for Celled<T> { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::Func(v) => Ok(Self::Func(v)), + Value::Array(array) => Ok(Self::Array( + array.into_iter().map(T::from_value).collect::<StrResult<_>>()?, + )), + 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<Region>) -> &'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::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::SWEDISH => "Tabell", + Lang::TURKISH => "Tablo", + Lang::UKRAINIAN => "Таблиця", + Lang::VIETNAMESE => "Bảng", + Lang::ENGLISH | _ => "Table", + } + } +} + +impl Figurable for TableElem {} |
