summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/layout/table.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-07-02 19:59:52 +0200
committerLaurenz <laurmaedje@gmail.com>2023-07-02 20:07:43 +0200
commitebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch)
tree2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-library/src/layout/table.rs
parent3ab19185093d7709f824b95b979060ce125389d8 (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.rs334
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 {}