diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-11-29 13:37:25 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-11-29 14:18:13 +0100 |
| commit | 0efe669278a5e1c3f2985eba2f3360e91159c54a (patch) | |
| tree | 502712857c48f0decb5e698257c0a96d358a436e /library/src/basics | |
| parent | 836692e73cff0356e409a9ba5b4887b86809d4ca (diff) | |
Reorganize library and tests
Diffstat (limited to 'library/src/basics')
| -rw-r--r-- | library/src/basics/heading.rs | 62 | ||||
| -rw-r--r-- | library/src/basics/list.rs | 272 | ||||
| -rw-r--r-- | library/src/basics/mod.rs | 9 | ||||
| -rw-r--r-- | library/src/basics/table.rs | 136 |
4 files changed, 479 insertions, 0 deletions
diff --git a/library/src/basics/heading.rs b/library/src/basics/heading.rs new file mode 100644 index 00000000..b251f27b --- /dev/null +++ b/library/src/basics/heading.rs @@ -0,0 +1,62 @@ +use typst::font::FontWeight; + +use crate::layout::{BlockNode, VNode}; +use crate::prelude::*; +use crate::text::{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, Finalize)] +impl HeadingNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { + Ok(Self { + body: args.expect("body")?, + level: args.named("level")?.unwrap_or(NonZeroUsize::new(1).unwrap()), + } + .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, + } + } +} + +impl Show for HeadingNode { + fn show(&self, _: Tracked<dyn World>, _: StyleChain) -> Content { + BlockNode(self.body.clone()).pack() + } +} + +impl Finalize for HeadingNode { + fn finalize(&self, realized: Content) -> Content { + let scale = match self.level.get() { + 1 => 1.4, + 2 => 1.2, + _ => 1.0, + }; + + let size = Em::new(scale); + let above = Em::new(if self.level.get() == 1 { 1.8 } else { 1.44 }) / scale; + let below = Em::new(0.66) / scale; + + let mut map = StyleMap::new(); + map.set(TextNode::SIZE, TextSize(size.into())); + map.set(TextNode::WEIGHT, FontWeight::BOLD); + map.set(BlockNode::ABOVE, VNode::block_around(above.into())); + map.set(BlockNode::BELOW, VNode::block_around(below.into())); + map.set(BlockNode::STICKY, true); + realized.styled_with_map(map) + } +} diff --git a/library/src/basics/list.rs b/library/src/basics/list.rs new file mode 100644 index 00000000..c73ffea6 --- /dev/null +++ b/library/src/basics/list.rs @@ -0,0 +1,272 @@ +use crate::compute::NumberingPattern; +use crate::layout::{BlockNode, GridNode, HNode, ParNode, Spacing, TrackSizing}; +use crate::prelude::*; +use crate::text::{SpaceNode, TextNode}; + +/// 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, + /// 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(Layout)] +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 between the items of a wide (non-tight) list. + pub const SPACING: Smart<Spacing> = Smart::Auto; + + fn construct(_: &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), items }.pack()) + } + + fn field(&self, name: &str) -> Option<Value> { + match name { + "tight" => Some(Value::Bool(self.tight)), + "items" => { + Some(Value::Array(self.items.items().map(|item| item.encode()).collect())) + } + _ => None, + } + } +} + +impl<const L: ListKind> Layout for ListNode<L> { + fn layout( + &self, + world: Tracked<dyn World>, + styles: StyleChain, + regions: &Regions, + ) -> SourceResult<Fragment> { + 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).into() + } else { + styles + .get(Self::SPACING) + .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount) + }; + + 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::packed(':')).strong(), + SpaceNode.pack(), + item.body.clone(), + ]), + }; + + cells.push(body.styled_with_map(map.clone())); + number += 1; + } + + GridNode { + tracks: Axes::with_x(vec![ + TrackSizing::Relative(indent.into()), + TrackSizing::Auto, + TrackSizing::Relative(body_indent.into()), + TrackSizing::Auto, + ]), + gutter: Axes::with_y(vec![gutter.into()]), + cells, + } + .layout(world, styles, regions) + } +} + +/// An item in a list. +#[derive(Debug, Clone, 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, + } + } + + /// 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()), + }), + } + } +} + +#[node] +impl ListItem {} + +/// A description list item. +#[derive(Debug, Clone, 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 } + }, +} + +/// 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(NumberingPattern), + /// Bare content. + Content(Content), + /// A closure mapping from an item number to a value. + Func(Func, Span), +} + +impl Label { + /// Resolve the label 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::packed('•'), + ENUM => TextNode::packed(format_eco!("{}.", number)), + DESC | _ => panic!("description lists don't have a label"), + }, + Self::Pattern(pattern) => TextNode::packed(pattern.apply(number)), + 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() + } + }) + } +} + +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(v) => Ok(Self::Pattern(v.parse()?)), + Value::Content(v) => Ok(Self::Content(v)), + Value::Func(v) => Ok(Self::Func(v, value.span)), + v => Err(format_eco!( + "expected string, content or function, found {}", + v.type_name(), + )), + } + } +} diff --git a/library/src/basics/mod.rs b/library/src/basics/mod.rs new file mode 100644 index 00000000..5916df6b --- /dev/null +++ b/library/src/basics/mod.rs @@ -0,0 +1,9 @@ +//! Common document elements. + +mod heading; +mod list; +mod table; + +pub use self::heading::*; +pub use self::list::*; +pub use self::table::*; diff --git a/library/src/basics/table.rs b/library/src/basics/table.rs new file mode 100644 index 00000000..bb900f3d --- /dev/null +++ b/library/src/basics/table.rs @@ -0,0 +1,136 @@ +use crate::layout::{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(Layout)] +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(); + + fn construct(_: &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()) + } + + fn field(&self, name: &str) -> Option<Value> { + match name { + "cells" => Some(Value::Array( + self.cells.iter().cloned().map(Value::Content).collect(), + )), + _ => None, + } + } +} + +impl Layout for TableNode { + fn layout( + &self, + world: Tracked<dyn World>, + styles: StyleChain, + regions: &Regions, + ) -> SourceResult<Fragment> { + 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<_>>()?; + + GridNode { + tracks: self.tracks.clone(), + gutter: self.gutter.clone(), + cells, + } + .layout(world, styles, regions) + } +} + +/// 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")), + } + } +} |
