diff options
Diffstat (limited to 'library/src/structure/table.rs')
| -rw-r--r-- | library/src/structure/table.rs | 160 |
1 files changed, 160 insertions, 0 deletions
diff --git a/library/src/structure/table.rs b/library/src/structure/table.rs new file mode 100644 index 00000000..722f11e6 --- /dev/null +++ b/library/src/structure/table.rs @@ -0,0 +1,160 @@ +use crate::layout::{BlockSpacing, GridNode, TrackSizing, TrackSizings}; +use crate::prelude::*; + +/// A table of items. +#[derive(Debug, Hash)] +pub struct TableNode { + /// Defines sizing for content rows and columns. + pub tracks: Axes<Vec<TrackSizing>>, + /// Defines sizing of gutter rows and columns between content. + pub gutter: Axes<Vec<TrackSizing>>, + /// The content to be arranged in the table. + pub cells: Vec<Content>, +} + +#[node(Show)] +impl TableNode { + /// How to fill the cells. + #[property(referenced)] + pub const FILL: Celled<Option<Paint>> = Celled::Value(None); + /// How to stroke the cells. + #[property(resolve, fold)] + pub const STROKE: Option<PartialStroke> = Some(PartialStroke::default()); + /// How much to pad the cells's content. + pub const PADDING: Rel<Length> = Abs::pt(5.0).into(); + + /// The spacing above the table. + #[property(resolve, shorthand(around))] + pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into()); + /// The spacing below the table. + #[property(resolve, shorthand(around))] + pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into()); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { + 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()) + } +} + +impl Show for TableNode { + fn unguard_parts(&self, sel: Selector) -> Content { + Self { + tracks: self.tracks.clone(), + gutter: self.gutter.clone(), + cells: self.cells.iter().map(|cell| cell.unguard(sel)).collect(), + } + .pack() + } + + fn field(&self, name: &str) -> Option<Value> { + match name { + "cells" => Some(Value::Array( + self.cells.iter().cloned().map(Value::Content).collect(), + )), + _ => None, + } + } + + fn realize( + &self, + world: Tracked<dyn World>, + styles: StyleChain, + ) -> SourceResult<Content> { + let fill = styles.get(Self::FILL); + let stroke = styles.get(Self::STROKE).map(PartialStroke::unwrap_or_default); + let padding = styles.get(Self::PADDING); + + let cols = self.tracks.x.len().max(1); + let cells = self + .cells + .iter() + .cloned() + .enumerate() + .map(|(i, child)| { + let mut child = child.padded(Sides::splat(padding)); + + if let Some(stroke) = stroke { + child = child.stroked(stroke); + } + + let x = i % cols; + let y = i / cols; + if let Some(fill) = fill.resolve(world, x, y)? { + child = child.filled(fill); + } + + Ok(child) + }) + .collect::<SourceResult<_>>()?; + + Ok(GridNode { + tracks: self.tracks.clone(), + gutter: self.gutter.clone(), + cells, + } + .pack()) + } + + fn finalize( + &self, + _: Tracked<dyn World>, + styles: StyleChain, + realized: Content, + ) -> SourceResult<Content> { + Ok(realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW))) + } +} + +/// 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, Span), +} + +impl<T: Cast + Clone> Celled<T> { + /// Resolve the value based on the cell position. + pub fn resolve( + &self, + world: Tracked<dyn World>, + x: usize, + y: usize, + ) -> SourceResult<T> { + Ok(match self { + Self::Value(value) => value.clone(), + Self::Func(func, span) => { + let args = Args::new(*span, [Value::Int(x as i64), Value::Int(y as i64)]); + func.call_detached(world, args)?.cast().at(*span)? + } + }) + } +} + +impl<T: Cast> Cast<Spanned<Value>> for Celled<T> { + fn is(value: &Spanned<Value>) -> bool { + matches!(&value.v, Value::Func(_)) || T::is(&value.v) + } + + fn cast(value: Spanned<Value>) -> StrResult<Self> { + match value.v { + Value::Func(v) => Ok(Self::Func(v, value.span)), + v => T::cast(v) + .map(Self::Value) + .map_err(|msg| with_alternative(msg, "function")), + } + } +} |
