summaryrefslogtreecommitdiff
path: root/src/library/elements
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-02-28 15:50:48 +0100
committerLaurenz <laurmaedje@gmail.com>2022-02-28 23:54:34 +0100
commit3ca5b238238e1128aa7bbfbd5db9e632045d8600 (patch)
tree2471f4b340a15695b7f4d518c0b39fabaea676c4 /src/library/elements
parentb63c21c91d99a1554a019dc275f955d3e6a34271 (diff)
Reorganize library
Diffstat (limited to 'src/library/elements')
-rw-r--r--src/library/elements/heading.rs150
-rw-r--r--src/library/elements/image.rs116
-rw-r--r--src/library/elements/list.rs212
-rw-r--r--src/library/elements/math.rs37
-rw-r--r--src/library/elements/mod.rs15
-rw-r--r--src/library/elements/shape.rs166
-rw-r--r--src/library/elements/table.rs100
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,
+ }))
+ }
+}