summaryrefslogtreecommitdiff
path: root/library/src/structure
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-11-03 11:44:53 +0100
committerLaurenz <laurmaedje@gmail.com>2022-11-03 13:35:39 +0100
commit37a7afddfaffd44cb9bc013c9506599267e08983 (patch)
tree20e7d62d3c5418baff01a21d0406b91bf3096214 /library/src/structure
parent56342bd972a13ffe21beaf2b87ab7eb1597704b4 (diff)
Split crates
Diffstat (limited to 'library/src/structure')
-rw-r--r--library/src/structure/doc.rs29
-rw-r--r--library/src/structure/heading.rs176
-rw-r--r--library/src/structure/list.rs371
-rw-r--r--library/src/structure/mod.rs13
-rw-r--r--library/src/structure/reference.rs29
-rw-r--r--library/src/structure/table.rs160
6 files changed, 778 insertions, 0 deletions
diff --git a/library/src/structure/doc.rs b/library/src/structure/doc.rs
new file mode 100644
index 00000000..ac12c3ab
--- /dev/null
+++ b/library/src/structure/doc.rs
@@ -0,0 +1,29 @@
+use crate::layout::PageNode;
+use crate::prelude::*;
+
+/// A sequence of page runs.
+#[derive(Hash)]
+pub struct DocNode(pub StyleVec<PageNode>);
+
+impl DocNode {
+ /// Layout the document into a sequence of frames, one per page.
+ pub fn layout(
+ &self,
+ world: Tracked<dyn World>,
+ styles: StyleChain,
+ ) -> SourceResult<Vec<Frame>> {
+ let mut frames = vec![];
+ for (page, map) in self.0.iter() {
+ let number = 1 + frames.len();
+ frames.extend(page.layout(world, number, map.chain(&styles))?);
+ }
+ Ok(frames)
+ }
+}
+
+impl Debug for DocNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_str("Doc ")?;
+ self.0.fmt(f)
+ }
+}
diff --git a/library/src/structure/heading.rs b/library/src/structure/heading.rs
new file mode 100644
index 00000000..62a67000
--- /dev/null
+++ b/library/src/structure/heading.rs
@@ -0,0 +1,176 @@
+use crate::layout::{BlockNode, BlockSpacing};
+use crate::prelude::*;
+use crate::text::{FontFamily, TextNode, TextSize};
+
+/// A section heading.
+#[derive(Debug, Hash)]
+pub struct HeadingNode {
+ /// The logical nesting depth of the section, starting from one. In the
+ /// default style, this controls the text size of the heading.
+ pub level: NonZeroUsize,
+ /// The heading's contents.
+ pub body: Content,
+}
+
+#[node(Show)]
+impl HeadingNode {
+ /// The heading's font family. Just the normal text family if `auto`.
+ #[property(referenced)]
+ pub const FAMILY: Leveled<Smart<FontFamily>> = Leveled::Value(Smart::Auto);
+ /// The color of text in the heading. Just the normal text color if `auto`.
+ #[property(referenced)]
+ pub const FILL: Leveled<Smart<Paint>> = Leveled::Value(Smart::Auto);
+ /// The size of text in the heading.
+ #[property(referenced)]
+ pub const SIZE: Leveled<TextSize> = Leveled::Mapping(|level| {
+ let size = match level.get() {
+ 1 => 1.4,
+ 2 => 1.2,
+ _ => 1.0,
+ };
+ TextSize(Em::new(size).into())
+ });
+
+ /// Whether text in the heading is strengthend.
+ #[property(referenced)]
+ pub const STRONG: Leveled<bool> = Leveled::Value(true);
+ /// Whether text in the heading is emphasized.
+ #[property(referenced)]
+ pub const EMPH: Leveled<bool> = Leveled::Value(false);
+ /// Whether the heading is underlined.
+ #[property(referenced)]
+ pub const UNDERLINE: Leveled<bool> = Leveled::Value(false);
+
+ /// The spacing above the heading.
+ #[property(referenced, shorthand(around))]
+ pub const ABOVE: Leveled<Option<BlockSpacing>> = Leveled::Mapping(|level| {
+ let ratio = match level.get() {
+ 1 => 1.5,
+ _ => 1.2,
+ };
+ Some(Ratio::new(ratio).into())
+ });
+ /// The spacing below the heading.
+ #[property(referenced, shorthand(around))]
+ pub const BELOW: Leveled<Option<BlockSpacing>> =
+ Leveled::Value(Some(Ratio::new(0.55).into()));
+
+ /// Whether the heading appears in the outline.
+ pub const OUTLINED: bool = true;
+ /// Whether the heading is numbered.
+ pub const NUMBERED: bool = true;
+
+ fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
+ Ok(Self {
+ body: args.expect("body")?,
+ level: args.named("level")?.unwrap_or(NonZeroUsize::new(1).unwrap()),
+ }
+ .pack())
+ }
+}
+
+impl Show for HeadingNode {
+ fn unguard_parts(&self, sel: Selector) -> Content {
+ Self { body: self.body.unguard(sel), ..*self }.pack()
+ }
+
+ fn field(&self, name: &str) -> Option<Value> {
+ match name {
+ "level" => Some(Value::Int(self.level.get() as i64)),
+ "body" => Some(Value::Content(self.body.clone())),
+ _ => None,
+ }
+ }
+
+ fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
+ Ok(BlockNode(self.body.clone()).pack())
+ }
+
+ fn finalize(
+ &self,
+ world: Tracked<dyn World>,
+ styles: StyleChain,
+ mut realized: Content,
+ ) -> SourceResult<Content> {
+ macro_rules! resolve {
+ ($key:expr) => {
+ styles.get($key).resolve(world, self.level)?
+ };
+ }
+
+ let mut map = StyleMap::new();
+ map.set(TextNode::SIZE, resolve!(Self::SIZE));
+
+ if let Smart::Custom(family) = resolve!(Self::FAMILY) {
+ map.set_family(family, styles);
+ }
+
+ if let Smart::Custom(fill) = resolve!(Self::FILL) {
+ map.set(TextNode::FILL, fill);
+ }
+
+ if resolve!(Self::STRONG) {
+ realized = realized.strong();
+ }
+
+ if resolve!(Self::EMPH) {
+ realized = realized.emph();
+ }
+
+ if resolve!(Self::UNDERLINE) {
+ realized = realized.underlined();
+ }
+
+ realized = realized.styled_with_map(map);
+ realized = realized.spaced(
+ resolve!(Self::ABOVE).resolve(styles),
+ resolve!(Self::BELOW).resolve(styles),
+ );
+
+ Ok(realized)
+ }
+}
+
+/// Either the value or a closure mapping to the value.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub enum Leveled<T> {
+ /// A bare value.
+ Value(T),
+ /// A simple mapping from a heading level to a value.
+ Mapping(fn(NonZeroUsize) -> T),
+ /// A closure mapping from a heading level to a value.
+ Func(Func, Span),
+}
+
+impl<T: Cast + Clone> Leveled<T> {
+ /// Resolve the value based on the level.
+ pub fn resolve(
+ &self,
+ world: Tracked<dyn World>,
+ level: NonZeroUsize,
+ ) -> SourceResult<T> {
+ Ok(match self {
+ Self::Value(value) => value.clone(),
+ Self::Mapping(mapping) => mapping(level),
+ Self::Func(func, span) => {
+ let args = Args::new(*span, [Value::Int(level.get() as i64)]);
+ func.call_detached(world, args)?.cast().at(*span)?
+ }
+ })
+ }
+}
+
+impl<T: Cast> Cast<Spanned<Value>> for Leveled<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")),
+ }
+ }
+}
diff --git a/library/src/structure/list.rs b/library/src/structure/list.rs
new file mode 100644
index 00000000..2015f19b
--- /dev/null
+++ b/library/src/structure/list.rs
@@ -0,0 +1,371 @@
+use unscanny::Scanner;
+
+use crate::layout::{BlockSpacing, GridNode, HNode, TrackSizing};
+use crate::prelude::*;
+use crate::text::{ParNode, SpaceNode};
+use crate::utility::Numbering;
+
+/// An unordered (bulleted) or ordered (numbered) list.
+#[derive(Debug, Hash)]
+pub struct ListNode<const L: ListKind = LIST> {
+ /// If true, the items are separated by leading instead of list spacing.
+ pub tight: bool,
+ /// If true, the spacing above the list is leading instead of above spacing.
+ pub attached: bool,
+ /// The individual bulleted or numbered items.
+ pub items: StyleVec<ListItem>,
+}
+
+/// An ordered list.
+pub type EnumNode = ListNode<ENUM>;
+
+/// A description list.
+pub type DescNode = ListNode<DESC>;
+
+#[node(Show)]
+impl<const L: ListKind> ListNode<L> {
+ /// How the list is labelled.
+ #[property(referenced)]
+ pub const LABEL: Label = Label::Default;
+ /// The indentation of each item's label.
+ #[property(resolve)]
+ pub const INDENT: Length = Length::zero();
+ /// The space between the label and the body of each item.
+ #[property(resolve)]
+ pub const BODY_INDENT: Length = Em::new(match L {
+ LIST | ENUM => 0.5,
+ DESC | _ => 1.0,
+ })
+ .into();
+
+ /// The spacing above the list.
+ #[property(resolve, shorthand(around))]
+ pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into());
+ /// The spacing below the list.
+ #[property(resolve, shorthand(around))]
+ pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into());
+ /// The spacing between the items of a wide (non-tight) list.
+ #[property(resolve)]
+ pub const SPACING: BlockSpacing = Ratio::one().into();
+
+ fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
+ let items = match L {
+ LIST => args
+ .all()?
+ .into_iter()
+ .map(|body| ListItem::List(Box::new(body)))
+ .collect(),
+ ENUM => {
+ let mut number: usize = args.named("start")?.unwrap_or(1);
+ args.all()?
+ .into_iter()
+ .map(|body| {
+ let item = ListItem::Enum(Some(number), Box::new(body));
+ number += 1;
+ item
+ })
+ .collect()
+ }
+ DESC | _ => args
+ .all()?
+ .into_iter()
+ .map(|item| ListItem::Desc(Box::new(item)))
+ .collect(),
+ };
+
+ Ok(Self {
+ tight: args.named("tight")?.unwrap_or(true),
+ attached: args.named("attached")?.unwrap_or(false),
+ items,
+ }
+ .pack())
+ }
+}
+
+impl<const L: ListKind> Show for ListNode<L> {
+ fn unguard_parts(&self, sel: Selector) -> Content {
+ Self {
+ items: self.items.map(|item| item.unguard(sel)),
+ ..*self
+ }
+ .pack()
+ }
+
+ fn field(&self, name: &str) -> Option<Value> {
+ match name {
+ "tight" => Some(Value::Bool(self.tight)),
+ "attached" => Some(Value::Bool(self.attached)),
+ "items" => Some(Value::Array(
+ self.items.items().map(|item| item.encode()).collect(),
+ )),
+ _ => None,
+ }
+ }
+
+ fn realize(
+ &self,
+ world: Tracked<dyn World>,
+ styles: StyleChain,
+ ) -> SourceResult<Content> {
+ let mut cells = vec![];
+ let mut number = 1;
+
+ let label = styles.get(Self::LABEL);
+ let indent = styles.get(Self::INDENT);
+ let body_indent = styles.get(Self::BODY_INDENT);
+ let gutter = if self.tight {
+ styles.get(ParNode::LEADING)
+ } else {
+ styles.get(Self::SPACING)
+ };
+
+ for (item, map) in self.items.iter() {
+ if let &ListItem::Enum(Some(n), _) = item {
+ number = n;
+ }
+
+ cells.push(Content::empty());
+
+ let label = if L == LIST || L == ENUM {
+ label.resolve(world, L, number)?.styled_with_map(map.clone())
+ } else {
+ Content::empty()
+ };
+
+ cells.push(label);
+ cells.push(Content::empty());
+
+ let body = match &item {
+ ListItem::List(body) => body.as_ref().clone(),
+ ListItem::Enum(_, body) => body.as_ref().clone(),
+ ListItem::Desc(item) => Content::sequence(vec![
+ HNode {
+ amount: (-body_indent).into(),
+ weak: false,
+ }
+ .pack(),
+ (item.term.clone() + TextNode(':'.into()).pack()).strong(),
+ SpaceNode.pack(),
+ item.body.clone(),
+ ]),
+ };
+
+ cells.push(body.styled_with_map(map.clone()));
+ number += 1;
+ }
+
+ Ok(GridNode {
+ tracks: Axes::with_x(vec![
+ TrackSizing::Relative(indent.into()),
+ TrackSizing::Auto,
+ TrackSizing::Relative(body_indent.into()),
+ TrackSizing::Auto,
+ ]),
+ gutter: Axes::with_y(vec![TrackSizing::Relative(gutter.into())]),
+ cells,
+ }
+ .pack())
+ }
+
+ fn finalize(
+ &self,
+ _: Tracked<dyn World>,
+ styles: StyleChain,
+ realized: Content,
+ ) -> SourceResult<Content> {
+ let mut above = styles.get(Self::ABOVE);
+ let mut below = styles.get(Self::BELOW);
+
+ if self.attached {
+ if above.is_some() {
+ above = Some(styles.get(ParNode::LEADING));
+ }
+ if below.is_some() {
+ below = Some(styles.get(ParNode::SPACING));
+ }
+ }
+
+ Ok(realized.spaced(above, below))
+ }
+}
+
+/// An item in a list.
+#[derive(Clone, PartialEq, Hash)]
+pub enum ListItem {
+ /// An item of an unordered list.
+ List(Box<Content>),
+ /// An item of an ordered list.
+ Enum(Option<usize>, Box<Content>),
+ /// An item of a description list.
+ Desc(Box<DescItem>),
+}
+
+impl ListItem {
+ /// What kind of item this is.
+ pub fn kind(&self) -> ListKind {
+ match self {
+ Self::List(_) => LIST,
+ Self::Enum { .. } => ENUM,
+ Self::Desc { .. } => DESC,
+ }
+ }
+
+ fn unguard(&self, sel: Selector) -> Self {
+ match self {
+ Self::List(body) => Self::List(Box::new(body.unguard(sel))),
+ Self::Enum(number, body) => Self::Enum(*number, Box::new(body.unguard(sel))),
+ Self::Desc(item) => Self::Desc(Box::new(DescItem {
+ term: item.term.unguard(sel),
+ body: item.body.unguard(sel),
+ })),
+ }
+ }
+
+ /// Encode the item into a value.
+ fn encode(&self) -> Value {
+ match self {
+ Self::List(body) => Value::Content(body.as_ref().clone()),
+ Self::Enum(number, body) => Value::Dict(dict! {
+ "number" => match *number {
+ Some(n) => Value::Int(n as i64),
+ None => Value::None,
+ },
+ "body" => Value::Content(body.as_ref().clone()),
+ }),
+ Self::Desc(item) => Value::Dict(dict! {
+ "term" => Value::Content(item.term.clone()),
+ "body" => Value::Content(item.body.clone()),
+ }),
+ }
+ }
+}
+
+impl Debug for ListItem {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::List(body) => write!(f, "- {body:?}"),
+ Self::Enum(number, body) => match number {
+ Some(n) => write!(f, "{n}. {body:?}"),
+ None => write!(f, "+ {body:?}"),
+ },
+ Self::Desc(item) => item.fmt(f),
+ }
+ }
+}
+
+#[node]
+impl ListItem {}
+
+/// A description list item.
+#[derive(Clone, PartialEq, Hash)]
+pub struct DescItem {
+ /// The term described by the list item.
+ pub term: Content,
+ /// The description of the term.
+ pub body: Content,
+}
+
+castable! {
+ DescItem,
+ Expected: "dictionary with `term` and `body` keys",
+ Value::Dict(dict) => {
+ let term: Content = dict.get("term")?.clone().cast()?;
+ let body: Content = dict.get("body")?.clone().cast()?;
+ Self { term, body }
+ },
+}
+
+impl Debug for DescItem {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "/ {:?}: {:?}", self.term, self.body)
+ }
+}
+
+/// How to label a list.
+pub type ListKind = usize;
+
+/// An unordered list.
+pub const LIST: ListKind = 0;
+
+/// An ordered list.
+pub const ENUM: ListKind = 1;
+
+/// A description list.
+pub const DESC: ListKind = 2;
+
+/// How to label a list or enumeration.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub enum Label {
+ /// The default labelling.
+ Default,
+ /// A pattern with prefix, numbering, lower / upper case and suffix.
+ Pattern(EcoString, Numbering, bool, EcoString),
+ /// Bare content.
+ Content(Content),
+ /// A closure mapping from an item number to a value.
+ Func(Func, Span),
+}
+
+impl Label {
+ /// Resolve the value based on the level.
+ pub fn resolve(
+ &self,
+ world: Tracked<dyn World>,
+ kind: ListKind,
+ number: usize,
+ ) -> SourceResult<Content> {
+ Ok(match self {
+ Self::Default => match kind {
+ LIST => TextNode('•'.into()).pack(),
+ ENUM => TextNode(format_eco!("{}.", number)).pack(),
+ DESC | _ => panic!("description lists don't have a label"),
+ },
+ Self::Pattern(prefix, numbering, upper, suffix) => {
+ let fmt = numbering.apply(number);
+ let mid = if *upper { fmt.to_uppercase() } else { fmt.to_lowercase() };
+ TextNode(format_eco!("{}{}{}", prefix, mid, suffix)).pack()
+ }
+ Self::Content(content) => content.clone(),
+ Self::Func(func, span) => {
+ let args = Args::new(*span, [Value::Int(number as i64)]);
+ func.call_detached(world, args)?.display(world)
+ }
+ })
+ }
+}
+
+impl Cast<Spanned<Value>> for Label {
+ fn is(value: &Spanned<Value>) -> bool {
+ matches!(&value.v, Value::Content(_) | Value::Func(_))
+ }
+
+ fn cast(value: Spanned<Value>) -> StrResult<Self> {
+ match value.v {
+ Value::None => Ok(Self::Content(Content::empty())),
+ Value::Str(pattern) => {
+ let mut s = Scanner::new(&pattern);
+ let mut prefix;
+ let numbering = loop {
+ prefix = s.before();
+ match s.eat().map(|c| c.to_ascii_lowercase()) {
+ Some('1') => break Numbering::Arabic,
+ Some('a') => break Numbering::Letter,
+ Some('i') => break Numbering::Roman,
+ Some('*') => break Numbering::Symbol,
+ Some(_) => {}
+ None => Err("invalid pattern")?,
+ }
+ };
+ let upper = s.scout(-1).map_or(false, char::is_uppercase);
+ let suffix = s.after().into();
+ Ok(Self::Pattern(prefix.into(), numbering, upper, suffix))
+ }
+ Value::Content(v) => Ok(Self::Content(v)),
+ Value::Func(v) => Ok(Self::Func(v, value.span)),
+ v => Err(format!(
+ "expected string, content or function, found {}",
+ v.type_name(),
+ )),
+ }
+ }
+}
diff --git a/library/src/structure/mod.rs b/library/src/structure/mod.rs
new file mode 100644
index 00000000..088d1e6c
--- /dev/null
+++ b/library/src/structure/mod.rs
@@ -0,0 +1,13 @@
+//! Document structuring.
+
+mod doc;
+mod heading;
+mod list;
+mod reference;
+mod table;
+
+pub use doc::*;
+pub use heading::*;
+pub use list::*;
+pub use reference::*;
+pub use table::*;
diff --git a/library/src/structure/reference.rs b/library/src/structure/reference.rs
new file mode 100644
index 00000000..632ecba5
--- /dev/null
+++ b/library/src/structure/reference.rs
@@ -0,0 +1,29 @@
+use crate::prelude::*;
+
+/// A reference to a label.
+#[derive(Debug, Hash)]
+pub struct RefNode(pub EcoString);
+
+#[node(Show)]
+impl RefNode {
+ fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
+ Ok(Self(args.expect("label")?).pack())
+ }
+}
+
+impl Show for RefNode {
+ fn unguard_parts(&self, _: Selector) -> Content {
+ Self(self.0.clone()).pack()
+ }
+
+ fn field(&self, name: &str) -> Option<Value> {
+ match name {
+ "label" => Some(Value::Str(self.0.clone().into())),
+ _ => None,
+ }
+ }
+
+ fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
+ Ok(TextNode(format_eco!("@{}", self.0)).pack())
+ }
+}
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")),
+ }
+ }
+}