From 111a69f6aaf0dd470dd2319f8cff29194aa0da08 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 19 Dec 2023 06:28:41 -0300 Subject: [PATCH] Grid and Table API Unification [More Flexible Tables Pt.1] (#3009) --- crates/typst/src/layout/grid.rs | 274 +++++++++++++++++++++++++++++- crates/typst/src/model/enum.rs | 8 +- crates/typst/src/model/list.rs | 8 +- crates/typst/src/model/table.rs | 184 +++----------------- tests/ref/layout/grid-styling.png | Bin 0 -> 18429 bytes tests/typ/layout/grid-styling.typ | 89 ++++++++++ 6 files changed, 389 insertions(+), 174 deletions(-) create mode 100644 tests/ref/layout/grid-styling.png create mode 100644 tests/typ/layout/grid-styling.typ diff --git a/crates/typst/src/layout/grid.rs b/crates/typst/src/layout/grid.rs index 606aa4a75..bbe2ea21d 100644 --- a/crates/typst/src/layout/grid.rs +++ b/crates/typst/src/layout/grid.rs @@ -2,18 +2,20 @@ use std::num::NonZeroUsize; use smallvec::{smallvec, SmallVec}; -use crate::diag::{bail, SourceResult, StrResult}; +use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Array, Content, NativeElement, Resolve, StyleChain, Value, + cast, elem, Array, CastInfo, Content, FromValue, Func, IntoValue, NativeElement, + Reflect, Resolve, Smart, StyleChain, Value, }; use crate::layout::{ - Abs, Axes, Dir, Fr, Fragment, Frame, Layout, Length, Point, Regions, Rel, Size, - Sizing, + Abs, Align, AlignElem, Axes, Dir, Fr, Fragment, Frame, FrameItem, Layout, Length, + Point, Regions, Rel, Sides, Size, Sizing, }; use crate::syntax::Span; use crate::text::TextElem; use crate::util::Numeric; +use crate::visualize::{FixedStroke, Geometry, Paint, Stroke}; /// Arranges content in a grid. /// @@ -118,6 +120,81 @@ pub struct GridElem { #[borrowed] 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 grids. + /// + /// ```example + /// #grid( + /// fill: (col, row) => if calc.even(col + row) { luma(240) } else { white }, + /// align: center + horizon, + /// columns: 4, + /// [X], [O], [X], [O], + /// [O], [X], [O], [X], + /// [X], [O], [X], [O], + /// [O], [X], [O], [X] + /// ) + /// ``` + #[borrowed] + 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 + /// #grid( + /// columns: 3, + /// align: (x, y) => (left, center, right).at(x), + /// [Hello], [Hello], [Hello], + /// [A], [B], [C], + /// ) + /// ``` + #[borrowed] + pub align: Celled>, + + /// How to [stroke]($stroke) the cells. + /// + /// Grids have no strokes by default, which can be changed by setting this + /// option to the desired stroke. + /// + /// _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] + pub stroke: Option, + + /// How much to pad the cells' content. + /// + /// ```example + /// #grid( + /// inset: 10pt, + /// fill: (_, row) => (red, blue).at(row), + /// [Hello], + /// [World], + /// ) + /// + /// #grid( + /// columns: 2, + /// inset: ( + /// x: 20pt, + /// y: 10pt, + /// ), + /// fill: (col, _) => (red, blue).at(col), + /// [Hello], + /// [World], + /// ) + /// ``` + #[fold] + #[default(Sides::splat(Abs::pt(0.0).into()))] + pub inset: Sides>>, + /// The contents of the grid cells. /// /// The cells are populated in row-major order. @@ -133,16 +210,27 @@ impl Layout for GridElem { styles: StyleChain, regions: Regions, ) -> SourceResult { + let inset = self.inset(styles); + let align = self.align(styles); let columns = self.columns(styles); let rows = self.rows(styles); let column_gutter = self.column_gutter(styles); let row_gutter = self.row_gutter(styles); + let fill = self.fill(styles); + let stroke = self.stroke(styles).map(Stroke::unwrap_or_default); + + let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); + let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); + let cells = + apply_align_inset_to_cells(engine, &tracks, &self.children, align, inset)?; // Prepare grid layout by unifying content and gutter tracks. let layouter = GridLayouter::new( - Axes::new(&columns.0, &rows.0), - Axes::new(&column_gutter.0, &row_gutter.0), - &self.children, + tracks, + gutter, + &cells, + fill, + &stroke, regions, styles, self.span(), @@ -153,6 +241,31 @@ impl Layout for GridElem { } } +pub fn apply_align_inset_to_cells( + engine: &mut Engine, + tracks: &Axes<&[Sizing]>, + cells: &[Content], + align: &Celled>, + inset: Sides>, +) -> SourceResult> { + let cols = tracks.x.len().max(1); + cells + .iter() + .enumerate() + .map(|(i, child)| { + let mut child = child.clone().padded(inset); + + let x = i % cols; + let y = i / cols; + if let Smart::Custom(alignment) = align.resolve(engine, x, y)? { + child = child.styled(AlignElem::set_alignment(alignment)); + } + + Ok(child) + }) + .collect() +} + /// Track sizing definitions. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct TrackSizings(pub SmallVec<[Sizing; 4]>); @@ -165,6 +278,75 @@ cast! { values: Array => Self(values.into_iter().map(Value::cast).collect::>()?), } +/// 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, engine: &mut Engine, x: usize, y: usize) -> SourceResult { + Ok(match self { + Self::Value(value) => value.clone(), + Self::Func(func) => func.call(engine, [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)), + } + } +} + /// Performs grid layout. pub struct GridLayouter<'a> { /// The grid cells. @@ -177,6 +359,12 @@ pub struct GridLayouter<'a> { cols: Vec, /// The row tracks including gutter tracks. rows: Vec, + // How to fill the cells. + #[allow(dead_code)] + fill: &'a Celled>, + // How to stroke the cells. + #[allow(dead_code)] + stroke: &'a Option, /// The regions to layout children into. regions: Regions<'a>, /// The inherited styles. @@ -230,10 +418,13 @@ impl<'a> GridLayouter<'a> { /// Create a new grid layouter. /// /// This prepares grid layout by unifying content and gutter tracks. + #[allow(clippy::too_many_arguments)] pub fn new( tracks: Axes<&[Sizing]>, gutter: Axes<&[Sizing]>, cells: &'a [Content], + fill: &'a Celled>, + stroke: &'a Option, regions: Regions<'a>, styles: StyleChain<'a>, span: Span, @@ -298,6 +489,8 @@ impl<'a> GridLayouter<'a> { is_rtl, has_gutter, rows, + fill, + stroke, regions, styles, rcols: vec![Abs::zero(); cols.len()], @@ -331,6 +524,10 @@ impl<'a> GridLayouter<'a> { self.finish_region(engine)?; + if self.stroke.is_some() || !matches!(self.fill, Celled::Value(None)) { + self.render_fills_strokes(engine)?; + } + Ok(GridLayout { fragment: Fragment::frames(self.finished), cols: self.rcols, @@ -338,6 +535,59 @@ impl<'a> GridLayouter<'a> { }) } + /// Add lines and backgrounds. + fn render_fills_strokes(&mut self, engine: &mut Engine) -> SourceResult<()> { + for (frame, rows) in self.finished.iter_mut().zip(&self.rrows) { + if self.rcols.is_empty() || rows.is_empty() { + continue; + } + + // Render table lines. + if let Some(stroke) = self.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(self.rcols.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 self.rcols.iter().enumerate() { + let mut dy = Abs::zero(); + for row in rows { + if let Some(fill) = self.fill.resolve(engine, 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(()) + } + /// Determine all column sizes. #[tracing::instrument(name = "GridLayouter::measure_columns", skip_all)] fn measure_columns(&mut self, engine: &mut Engine) -> SourceResult<()> { @@ -743,3 +993,13 @@ impl<'a> GridLayouter<'a> { } } } + +/// 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 + }) +} diff --git a/crates/typst/src/model/enum.rs b/crates/typst/src/model/enum.rs index 0d81b16a3..1d37f89cf 100644 --- a/crates/typst/src/model/enum.rs +++ b/crates/typst/src/model/enum.rs @@ -6,8 +6,8 @@ use crate::foundations::{ cast, elem, scope, Array, Content, Fold, NativeElement, Smart, StyleChain, }; use crate::layout::{ - Align, Axes, BlockElem, Em, Fragment, GridLayouter, HAlign, Layout, Length, Regions, - Sizing, Spacing, VAlign, + Align, Axes, BlockElem, Celled, Em, Fragment, GridLayouter, HAlign, Layout, Length, + Regions, Sizing, Spacing, VAlign, }; use crate::model::{Numbering, NumberingPattern, ParElem}; use crate::text::TextElem; @@ -266,6 +266,8 @@ impl Layout for EnumElem { number = number.saturating_add(1); } + let fill = Celled::Value(None); + let stroke = None; let layouter = GridLayouter::new( Axes::with_x(&[ Sizing::Rel(indent.into()), @@ -275,6 +277,8 @@ impl Layout for EnumElem { ]), Axes::with_y(&[gutter.into()]), &cells, + &fill, + &stroke, regions, styles, self.span(), diff --git a/crates/typst/src/model/list.rs b/crates/typst/src/model/list.rs index afbf9472f..a0a2609c4 100644 --- a/crates/typst/src/model/list.rs +++ b/crates/typst/src/model/list.rs @@ -5,8 +5,8 @@ use crate::foundations::{ Value, }; use crate::layout::{ - Axes, BlockElem, Em, Fragment, GridLayouter, HAlign, Layout, Length, Regions, Sizing, - Spacing, VAlign, + Axes, BlockElem, Celled, Em, Fragment, GridLayouter, HAlign, Layout, Length, Regions, + Sizing, Spacing, VAlign, }; use crate::model::ParElem; use crate::text::TextElem; @@ -166,6 +166,8 @@ impl Layout for ListElem { cells.push(item.body().clone().styled(Self::set_depth(Depth))); } + let fill = Celled::Value(None); + let stroke = None; let layouter = GridLayouter::new( Axes::with_x(&[ Sizing::Rel(indent.into()), @@ -175,6 +177,8 @@ impl Layout for ListElem { ]), Axes::with_y(&[gutter.into()]), &cells, + &fill, + &stroke, regions, styles, self.span(), diff --git a/crates/typst/src/model/table.rs b/crates/typst/src/model/table.rs index b1e938ad1..5b7715ab5 100644 --- a/crates/typst/src/model/table.rs +++ b/crates/typst/src/model/table.rs @@ -1,16 +1,13 @@ -use crate::diag::{At, SourceResult, StrResult}; +use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{ - elem, Array, CastInfo, Content, FromValue, Func, IntoValue, NativeElement, Reflect, - Smart, StyleChain, Value, -}; +use crate::foundations::{elem, Content, NativeElement, Smart, StyleChain}; use crate::layout::{ - Abs, Align, AlignElem, Axes, Fragment, FrameItem, GridLayouter, Layout, Length, - Point, Regions, Rel, Sides, Size, TrackSizings, + apply_align_inset_to_cells, Abs, Align, Axes, Celled, Fragment, GridLayouter, Layout, + Length, Regions, Rel, Sides, TrackSizings, }; use crate::model::Figurable; use crate::text::{Lang, LocalName, Region}; -use crate::visualize::{Geometry, Paint, Stroke}; +use crate::visualize::{Paint, Stroke}; /// A table of items. /// @@ -169,166 +166,27 @@ impl Layout for TableElem { let rows = self.rows(styles); let column_gutter = self.column_gutter(styles); let row_gutter = self.row_gutter(styles); - - let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); - let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); - let cols = tracks.x.len().max(1); - let cells: Vec<_> = self - .children() - .iter() - .enumerate() - .map(|(i, child)| { - let mut child = child.clone().padded(inset); - - let x = i % cols; - let y = i / cols; - if let Smart::Custom(alignment) = align.resolve(engine, 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); + let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); + let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); + let cells = + apply_align_inset_to_cells(engine, &tracks, self.children(), align, inset)?; + // Prepare grid layout by unifying content and gutter tracks. - let layouter = - GridLayouter::new(tracks, gutter, &cells, regions, styles, self.span()); + let layouter = GridLayouter::new( + tracks, + gutter, + &cells, + fill, + &stroke, + regions, + styles, + self.span(), + ); - // Measure the columns and layout the grid row-by-row. - let mut layout = layouter.layout(engine)?; - - // 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(engine, 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, engine: &mut Engine, x: usize, y: usize) -> SourceResult { - Ok(match self { - Self::Value(value) => value.clone(), - Self::Func(func) => func.call(engine, [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)), - } + Ok(layouter.layout(engine)?.fragment) } } diff --git a/tests/ref/layout/grid-styling.png b/tests/ref/layout/grid-styling.png new file mode 100644 index 0000000000000000000000000000000000000000..ae5c05193bebad1aac179cfae7d2c32105611db8 GIT binary patch literal 18429 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 if calc.even(x + y) { rgb("aaa") }) + +#grid( + columns: (1fr,) * 3, + stroke: 2pt + rgb("333"), + [A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H], +) + +--- +#grid(columns: 3, stroke: none, fill: green, [A], [B], [C]) + +--- +// Test general alignment. +#grid( + columns: 3, + align: left, + [Hello], [Hello], [Hello], + [A], [B], [C], +) + +// Test alignment with a function. +#grid( + columns: 3, + align: (x, y) => (left, center, right).at(x), + [Hello], [Hello], [Hello], + [A], [B], [C], +) + +// Test alignment with array. +#grid( + columns: (1fr, 1fr, 1fr), + align: (left, center, right), + [A], [B], [C] +) + +// Test empty array. +#set align(center) +#grid( + columns: (1fr, 1fr, 1fr), + align: (), + [A], [B], [C] +) + +a + +--- +// Test inset. +#grid( + columns: (1fr,) * 3, + stroke: 2pt + rgb("333"), + inset: 5pt, + [A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H], +) + +#grid( + columns: 3, + inset: 10pt, + fill: blue, + [A], [B], [C] +) + +#grid( + columns: 3, + inset: (y: 10pt), + [A], [B], [C] +) + +#grid( + columns: 3, + inset: (left: 20pt, rest: 10pt), + stroke: 3pt + red, + [A], [B], [C] +) + +#grid( + columns: 2, + inset: ( + left: 20pt, + right: 5pt, + top: 10pt, + bottom: 3pt, + ), + [A], + [B], +)