diff options
Diffstat (limited to 'src/library/elements')
| -rw-r--r-- | src/library/elements/heading.rs | 150 | ||||
| -rw-r--r-- | src/library/elements/image.rs | 116 | ||||
| -rw-r--r-- | src/library/elements/list.rs | 212 | ||||
| -rw-r--r-- | src/library/elements/math.rs | 37 | ||||
| -rw-r--r-- | src/library/elements/mod.rs | 15 | ||||
| -rw-r--r-- | src/library/elements/shape.rs | 166 | ||||
| -rw-r--r-- | src/library/elements/table.rs | 100 |
7 files changed, 796 insertions, 0 deletions
diff --git a/src/library/elements/heading.rs b/src/library/elements/heading.rs new file mode 100644 index 00000000..a67f4f24 --- /dev/null +++ b/src/library/elements/heading.rs @@ -0,0 +1,150 @@ +use crate::library::prelude::*; +use crate::library::text::{FontFamily, TextNode}; + +/// 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: usize, + /// The heading's contents. + pub body: Template, +} + +#[class] +impl HeadingNode { + /// The heading's font family. Just the normal text family if `auto`. + pub const FAMILY: Leveled<Smart<FontFamily>> = Leveled::Value(Smart::Auto); + /// The color of text in the heading. Just the normal text color if `auto`. + pub const FILL: Leveled<Smart<Paint>> = Leveled::Value(Smart::Auto); + /// The size of text in the heading. + pub const SIZE: Leveled<Linear> = Leveled::Mapping(|level| { + let upscale = (1.6 - 0.1 * level as f64).max(0.75); + Relative::new(upscale).into() + }); + /// Whether text in the heading is strengthend. + pub const STRONG: Leveled<bool> = Leveled::Value(true); + /// Whether text in the heading is emphasized. + pub const EMPH: Leveled<bool> = Leveled::Value(false); + /// Whether the heading is underlined. + pub const UNDERLINE: Leveled<bool> = Leveled::Value(false); + /// The extra padding above the heading. + pub const ABOVE: Leveled<Length> = Leveled::Value(Length::zero()); + /// The extra padding below the heading. + pub const BELOW: Leveled<Length> = Leveled::Value(Length::zero()); + /// Whether the heading is block-level. + pub const BLOCK: Leveled<bool> = Leveled::Value(true); + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::show(Self { + body: args.expect("body")?, + level: args.named("level")?.unwrap_or(1), + })) + } +} + +impl Show for HeadingNode { + fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Template> { + macro_rules! resolve { + ($key:expr) => { + styles.get_cloned($key).resolve(ctx, self.level)? + }; + } + + // Resolve the user recipe. + let mut body = styles + .show(self, ctx, [ + Value::Int(self.level as i64), + Value::Template(self.body.clone()), + ])? + .unwrap_or_else(|| self.body.clone()); + + let mut map = StyleMap::new(); + map.set(TextNode::SIZE, resolve!(Self::SIZE)); + + if let Smart::Custom(family) = resolve!(Self::FAMILY) { + map.set( + TextNode::FAMILY, + std::iter::once(family) + .chain(styles.get_ref(TextNode::FAMILY).iter().cloned()) + .collect(), + ); + } + + if let Smart::Custom(fill) = resolve!(Self::FILL) { + map.set(TextNode::FILL, fill); + } + + if resolve!(Self::STRONG) { + map.set(TextNode::STRONG, true); + } + + if resolve!(Self::EMPH) { + map.set(TextNode::EMPH, true); + } + + let mut seq = vec![]; + if resolve!(Self::UNDERLINE) { + body = body.underlined(); + } + + let above = resolve!(Self::ABOVE); + if !above.is_zero() { + seq.push(Template::Vertical(above.into())); + } + + seq.push(body); + + let below = resolve!(Self::BELOW); + if !below.is_zero() { + seq.push(Template::Vertical(below.into())); + } + + let mut template = Template::sequence(seq).styled_with_map(map); + if resolve!(Self::BLOCK) { + template = Template::block(template); + } + + Ok(template) + } +} + +/// 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(usize) -> T), + /// A closure mapping from a heading level to a value. + Func(Func, Span), +} + +impl<T: Cast> Leveled<T> { + /// Resolve the value based on the level. + pub fn resolve(self, ctx: &mut Context, level: usize) -> TypResult<T> { + Ok(match self { + Self::Value(value) => value, + Self::Mapping(mapping) => mapping(level), + Self::Func(func, span) => { + let args = Args::from_values(span, [Value::Int(level as i64)]); + func.call(ctx, 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/src/library/elements/image.rs b/src/library/elements/image.rs new file mode 100644 index 00000000..66fb8f4b --- /dev/null +++ b/src/library/elements/image.rs @@ -0,0 +1,116 @@ +use crate::diag::Error; +use crate::image::ImageId; +use crate::library::prelude::*; +use crate::library::text::TextNode; + +/// Show a raster or vector graphic. +#[derive(Debug, Hash)] +pub struct ImageNode(pub ImageId); + +#[class] +impl ImageNode { + /// How the image should adjust itself to a given area. + pub const FIT: ImageFit = ImageFit::Cover; + + fn construct(ctx: &mut Context, args: &mut Args) -> TypResult<Template> { + let path = args.expect::<Spanned<EcoString>>("path to image file")?; + let full = ctx.resolve(&path.v); + let id = ctx.images.load(&full).map_err(|err| { + Error::boxed(path.span, match err.kind() { + std::io::ErrorKind::NotFound => "file not found".into(), + _ => format!("failed to load image ({})", err), + }) + })?; + + let width = args.named("width")?; + let height = args.named("height")?; + + Ok(Template::inline( + ImageNode(id).pack().sized(Spec::new(width, height)), + )) + } +} + +impl Layout for ImageNode { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + let img = ctx.images.get(self.0); + let pxw = img.width() as f64; + let pxh = img.height() as f64; + let px_ratio = pxw / pxh; + + // Find out whether the image is wider or taller than the target size. + let &Regions { first, expand, .. } = regions; + let region_ratio = first.x / first.y; + let wide = px_ratio > region_ratio; + + // The space into which the image will be placed according to its fit. + let target = if expand.x && expand.y { + first + } else if expand.x || (!expand.y && wide && first.x.is_finite()) { + Size::new(first.x, first.y.min(first.x.safe_div(px_ratio))) + } else if first.y.is_finite() { + Size::new(first.x.min(first.y * px_ratio), first.y) + } else { + Size::new(Length::pt(pxw), Length::pt(pxh)) + }; + + // Compute the actual size of the fitted image. + let fit = styles.get(Self::FIT); + let fitted = match fit { + ImageFit::Cover | ImageFit::Contain => { + if wide == (fit == ImageFit::Contain) { + Size::new(target.x, target.x / px_ratio) + } else { + Size::new(target.y * px_ratio, target.y) + } + } + ImageFit::Stretch => target, + }; + + // First, place the image in a frame of exactly its size and then resize + // the frame to the target size, center aligning the image in the + // process. + let mut frame = Frame::new(fitted); + frame.push(Point::zero(), Element::Image(self.0, fitted)); + frame.resize(target, Align::CENTER_HORIZON); + + // Create a clipping group if only part of the image should be visible. + if fit == ImageFit::Cover && !target.fits(fitted) { + frame.clip(); + } + + // Apply link if it exists. + if let Some(url) = styles.get_ref(TextNode::LINK) { + frame.link(url); + } + + Ok(vec![Arc::new(frame)]) + } +} + +/// How an image should adjust itself to a given area. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ImageFit { + /// The image should completely cover the area. + Cover, + /// The image should be fully contained in the area. + Contain, + /// The image should be stretched so that it exactly fills the area. + Stretch, +} + +castable! { + ImageFit, + Expected: "string", + Value::Str(string) => match string.as_str() { + "cover" => Self::Cover, + "contain" => Self::Contain, + "stretch" => Self::Stretch, + _ => Err(r#"expected "cover", "contain" or "stretch""#)?, + }, +} diff --git a/src/library/elements/list.rs b/src/library/elements/list.rs new file mode 100644 index 00000000..726a2834 --- /dev/null +++ b/src/library/elements/list.rs @@ -0,0 +1,212 @@ +use crate::library::layout::{GridNode, TrackSizing}; +use crate::library::prelude::*; +use crate::library::text::{ParNode, TextNode}; +use crate::library::utility::Numbering; +use crate::parse::Scanner; + +/// An unordered (bulleted) or ordered (numbered) list. +#[derive(Debug, Hash)] +pub struct ListNode<const L: ListKind = UNORDERED> { + /// Where the list starts. + pub start: usize, + /// If true, there is paragraph spacing between the items, if false + /// there is list spacing between the items. + pub wide: bool, + /// The individual bulleted or numbered items. + pub items: Vec<ListItem>, +} + +/// An item in a list. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct ListItem { + /// The number of the item. + pub number: Option<usize>, + /// The node that produces the item's body. + pub body: Box<Template>, +} + +/// An ordered list. +pub type EnumNode = ListNode<ORDERED>; + +#[class] +impl<const L: ListKind> ListNode<L> { + /// How the list is labelled. + pub const LABEL: Label = Label::Default; + /// The spacing between the list items of a non-wide list. + pub const SPACING: Linear = Linear::zero(); + /// The indentation of each item's label. + pub const INDENT: Linear = Relative::new(0.0).into(); + /// The space between the label and the body of each item. + pub const BODY_INDENT: Linear = Relative::new(0.5).into(); + /// The extra padding above the list. + pub const ABOVE: Length = Length::zero(); + /// The extra padding below the list. + pub const BELOW: Length = Length::zero(); + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::show(Self { + start: args.named("start")?.unwrap_or(0), + wide: args.named("wide")?.unwrap_or(false), + items: args + .all()? + .into_iter() + .map(|body| ListItem { number: None, body: Box::new(body) }) + .collect(), + })) + } +} + +impl<const L: ListKind> Show for ListNode<L> { + fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Template> { + let template = if let Some(template) = styles.show( + self, + ctx, + self.items.iter().map(|item| Value::Template((*item.body).clone())), + )? { + template + } else { + let mut children = vec![]; + let mut number = self.start; + + let label = styles.get_ref(Self::LABEL); + + for item in &self.items { + number = item.number.unwrap_or(number); + if L == UNORDERED { + number = 1; + } + + children.push(LayoutNode::default()); + children.push(label.resolve(ctx, L, number)?.pack()); + children.push(LayoutNode::default()); + children.push((*item.body).clone().pack()); + number += 1; + } + + let em = styles.get(TextNode::SIZE).abs; + let leading = styles.get(ParNode::LEADING); + let spacing = if self.wide { + styles.get(ParNode::SPACING) + } else { + styles.get(Self::SPACING) + }; + + let gutter = (leading + spacing).resolve(em); + let indent = styles.get(Self::INDENT).resolve(em); + let body_indent = styles.get(Self::BODY_INDENT).resolve(em); + + Template::block(GridNode { + tracks: Spec::with_x(vec![ + TrackSizing::Linear(indent.into()), + TrackSizing::Auto, + TrackSizing::Linear(body_indent.into()), + TrackSizing::Auto, + ]), + gutter: Spec::with_y(vec![TrackSizing::Linear(gutter.into())]), + children, + }) + }; + + let mut seq = vec![]; + let above = styles.get(Self::ABOVE); + if !above.is_zero() { + seq.push(Template::Vertical(above.into())); + } + + seq.push(template); + + let below = styles.get(Self::BELOW); + if !below.is_zero() { + seq.push(Template::Vertical(below.into())); + } + + Ok(Template::sequence(seq)) + } +} + +impl<const L: ListKind> From<ListItem> for ListNode<L> { + fn from(item: ListItem) -> Self { + Self { items: vec![item], wide: false, start: 1 } + } +} + +/// How to label a list. +pub type ListKind = usize; + +/// Unordered list labelling style. +pub const UNORDERED: ListKind = 0; + +/// Ordered list labelling style. +pub const ORDERED: ListKind = 1; + +/// Either a template or a closure mapping to a template. +#[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), + /// A bare template. + Template(Template), + /// 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, + ctx: &mut Context, + kind: ListKind, + number: usize, + ) -> TypResult<Template> { + Ok(match self { + Self::Default => match kind { + UNORDERED => Template::Text('•'.into()), + ORDERED | _ => Template::Text(format_eco!("{}.", number)), + }, + Self::Pattern(prefix, numbering, upper, suffix) => { + let fmt = numbering.apply(number); + let mid = if *upper { fmt.to_uppercase() } else { fmt.to_lowercase() }; + Template::Text(format_eco!("{}{}{}", prefix, mid, suffix)) + } + Self::Template(template) => template.clone(), + Self::Func(func, span) => { + let args = Args::from_values(*span, [Value::Int(number as i64)]); + func.call(ctx, args)?.cast().at(*span)? + } + }) + } +} + +impl Cast<Spanned<Value>> for Label { + fn is(value: &Spanned<Value>) -> bool { + matches!(&value.v, Value::Template(_) | Value::Func(_)) + } + + fn cast(value: Spanned<Value>) -> StrResult<Self> { + match value.v { + Value::Str(pattern) => { + let mut s = Scanner::new(&pattern); + let mut prefix; + let numbering = loop { + prefix = s.eaten(); + 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.prev(0).map_or(false, char::is_uppercase); + let suffix = s.rest().into(); + Ok(Self::Pattern(prefix.into(), numbering, upper, suffix)) + } + Value::Template(v) => Ok(Self::Template(v)), + Value::Func(v) => Ok(Self::Func(v, value.span)), + _ => Err("expected pattern, template or function")?, + } + } +} diff --git a/src/library/elements/math.rs b/src/library/elements/math.rs new file mode 100644 index 00000000..761b4480 --- /dev/null +++ b/src/library/elements/math.rs @@ -0,0 +1,37 @@ +use crate::library::prelude::*; + +/// A mathematical formula. +#[derive(Debug, Hash)] +pub struct MathNode { + /// The formula. + pub formula: EcoString, + /// Whether the formula is display-level. + pub display: bool, +} + +#[class] +impl MathNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::show(Self { + formula: args.expect("formula")?, + display: args.named("display")?.unwrap_or(false), + })) + } +} + +impl Show for MathNode { + fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Template> { + Ok(styles + .show(self, ctx, [ + Value::Str(self.formula.clone()), + Value::Bool(self.display), + ])? + .unwrap_or_else(|| { + let mut template = Template::Text(self.formula.trim().into()); + if self.display { + template = Template::Block(template.pack()); + } + template.monospaced() + })) + } +} diff --git a/src/library/elements/mod.rs b/src/library/elements/mod.rs new file mode 100644 index 00000000..572010bb --- /dev/null +++ b/src/library/elements/mod.rs @@ -0,0 +1,15 @@ +//! Primitive and semantic elements. + +mod heading; +mod image; +mod list; +mod math; +mod shape; +mod table; + +pub use self::image::*; +pub use heading::*; +pub use list::*; +pub use math::*; +pub use shape::*; +pub use table::*; diff --git a/src/library/elements/shape.rs b/src/library/elements/shape.rs new file mode 100644 index 00000000..8b967412 --- /dev/null +++ b/src/library/elements/shape.rs @@ -0,0 +1,166 @@ +use std::f64::consts::SQRT_2; + +use crate::library::prelude::*; +use crate::library::text::TextNode; + +/// Place a node into a sizable and fillable shape. +#[derive(Debug, Hash)] +pub struct ShapeNode<const S: ShapeKind>(pub Option<LayoutNode>); + +/// Place a node into a square. +pub type SquareNode = ShapeNode<SQUARE>; + +/// Place a node into a rectangle. +pub type RectNode = ShapeNode<RECT>; + +/// Place a node into a circle. +pub type CircleNode = ShapeNode<CIRCLE>; + +/// Place a node into an ellipse. +pub type EllipseNode = ShapeNode<ELLIPSE>; + +#[class] +impl<const S: ShapeKind> ShapeNode<S> { + /// How to fill the shape. + pub const FILL: Option<Paint> = None; + /// How the stroke the shape. + pub const STROKE: Smart<Option<Paint>> = Smart::Auto; + /// The stroke's thickness. + pub const THICKNESS: Length = Length::pt(1.0); + /// How much to pad the shape's content. + pub const PADDING: Linear = Linear::zero(); + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + let size = match S { + SQUARE => args.named::<Length>("size")?.map(Linear::from), + CIRCLE => args.named::<Length>("radius")?.map(|r| 2.0 * Linear::from(r)), + _ => None, + }; + + let width = match size { + None => args.named("width")?, + size => size, + }; + + let height = match size { + None => args.named("height")?, + size => size, + }; + + Ok(Template::inline( + Self(args.find()?).pack().sized(Spec::new(width, height)), + )) + } +} + +impl<const S: ShapeKind> Layout for ShapeNode<S> { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + let mut frames; + if let Some(child) = &self.0 { + let mut padding = styles.get(Self::PADDING); + if is_round(S) { + padding.rel += Relative::new(0.5 - SQRT_2 / 4.0); + } + + // Pad the child. + let child = child.clone().padded(Sides::splat(padding)); + + let mut pod = Regions::one(regions.first, regions.base, regions.expand); + frames = child.layout(ctx, &pod, styles)?; + + // Relayout with full expansion into square region to make sure + // the result is really a square or circle. + if is_quadratic(S) { + let length = if regions.expand.x || regions.expand.y { + let target = regions.expand.select(regions.first, Size::zero()); + target.x.max(target.y) + } else { + let size = frames[0].size; + let desired = size.x.max(size.y); + desired.min(regions.first.x).min(regions.first.y) + }; + + pod.first = Size::splat(length); + pod.expand = Spec::splat(true); + frames = child.layout(ctx, &pod, styles)?; + } + } else { + // The default size that a shape takes on if it has no child and + // enough space. + let mut size = + Size::new(Length::pt(45.0), Length::pt(30.0)).min(regions.first); + + if is_quadratic(S) { + let length = if regions.expand.x || regions.expand.y { + let target = regions.expand.select(regions.first, Size::zero()); + target.x.max(target.y) + } else { + size.x.min(size.y) + }; + size = Size::splat(length); + } else { + size = regions.expand.select(regions.first, size); + } + + frames = vec![Arc::new(Frame::new(size))]; + } + + let frame = Arc::make_mut(&mut frames[0]); + + // Add fill and/or stroke. + let fill = styles.get(Self::FILL); + let thickness = styles.get(Self::THICKNESS); + let stroke = styles + .get(Self::STROKE) + .unwrap_or(fill.is_none().then(|| Color::BLACK.into())) + .map(|paint| Stroke { paint, thickness }); + + if fill.is_some() || stroke.is_some() { + let geometry = if is_round(S) { + Geometry::Ellipse(frame.size) + } else { + Geometry::Rect(frame.size) + }; + + let shape = Shape { geometry, fill, stroke }; + frame.prepend(Point::zero(), Element::Shape(shape)); + } + + // Apply link if it exists. + if let Some(url) = styles.get_ref(TextNode::LINK) { + frame.link(url); + } + + Ok(frames) + } +} + +/// A category of shape. +pub type ShapeKind = usize; + +/// A rectangle with equal side lengths. +const SQUARE: ShapeKind = 0; + +/// A quadrilateral with four right angles. +const RECT: ShapeKind = 1; + +/// An ellipse with coinciding foci. +const CIRCLE: ShapeKind = 2; + +/// A curve around two focal points. +const ELLIPSE: ShapeKind = 3; + +/// Whether a shape kind is curvy. +fn is_round(kind: ShapeKind) -> bool { + matches!(kind, CIRCLE | ELLIPSE) +} + +/// Whether a shape kind has equal side length. +fn is_quadratic(kind: ShapeKind) -> bool { + matches!(kind, SQUARE | CIRCLE) +} diff --git a/src/library/elements/table.rs b/src/library/elements/table.rs new file mode 100644 index 00000000..555dcc44 --- /dev/null +++ b/src/library/elements/table.rs @@ -0,0 +1,100 @@ +use crate::library::layout::{GridNode, TrackSizing}; +use crate::library::prelude::*; + +/// A table of items. +#[derive(Debug, Hash)] +pub struct TableNode { + /// Defines sizing for content rows and columns. + pub tracks: Spec<Vec<TrackSizing>>, + /// Defines sizing of gutter rows and columns between content. + pub gutter: Spec<Vec<TrackSizing>>, + /// The nodes to be arranged in the table. + pub children: Vec<Template>, +} + +#[class] +impl TableNode { + /// The primary cell fill color. + pub const PRIMARY: Option<Paint> = None; + /// The secondary cell fill color. + pub const SECONDARY: Option<Paint> = None; + /// How the stroke the cells. + pub const STROKE: Option<Paint> = Some(Color::BLACK.into()); + /// The stroke's thickness. + pub const THICKNESS: Length = Length::pt(1.0); + /// How much to pad the cells's content. + pub const PADDING: Linear = Length::pt(5.0).into(); + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + let columns = args.named("columns")?.unwrap_or_default(); + let rows = args.named("rows")?.unwrap_or_default(); + let base_gutter: Vec<TrackSizing> = args.named("gutter")?.unwrap_or_default(); + let column_gutter = args.named("column-gutter")?; + let row_gutter = args.named("row-gutter")?; + Ok(Template::show(Self { + tracks: Spec::new(columns, rows), + gutter: Spec::new( + column_gutter.unwrap_or_else(|| base_gutter.clone()), + row_gutter.unwrap_or(base_gutter), + ), + children: args.all()?, + })) + } + + fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { + let fill = args.named("fill")?; + styles.set_opt(Self::PRIMARY, args.named("primary")?.or(fill)); + styles.set_opt(Self::SECONDARY, args.named("secondary")?.or(fill)); + styles.set_opt(Self::STROKE, args.named("stroke")?); + styles.set_opt(Self::THICKNESS, args.named("thickness")?); + styles.set_opt(Self::PADDING, args.named("padding")?); + Ok(()) + } +} + +impl Show for TableNode { + fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Template> { + if let Some(template) = styles.show( + self, + ctx, + self.children.iter().map(|child| Value::Template(child.clone())), + )? { + return Ok(template); + } + + let primary = styles.get(Self::PRIMARY); + let secondary = styles.get(Self::SECONDARY); + let thickness = styles.get(Self::THICKNESS); + let stroke = styles.get(Self::STROKE).map(|paint| Stroke { paint, thickness }); + let padding = styles.get(Self::PADDING); + + let cols = self.tracks.x.len().max(1); + let children = self + .children + .iter() + .cloned() + .enumerate() + .map(|(i, child)| { + let mut child = child.pack().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) = [primary, secondary][(x + y) % 2] { + child = child.filled(fill); + } + + child + }) + .collect(); + + Ok(Template::block(GridNode { + tracks: self.tracks.clone(), + gutter: self.gutter.clone(), + children, + })) + } +} |
