diff options
Diffstat (limited to 'library/src/layout')
| -rw-r--r-- | library/src/layout/enum.rs | 206 | ||||
| -rw-r--r-- | library/src/layout/list.rs | 160 | ||||
| -rw-r--r-- | library/src/layout/mod.rs | 10 | ||||
| -rw-r--r-- | library/src/layout/table.rs | 236 | ||||
| -rw-r--r-- | library/src/layout/terms.rs | 178 |
5 files changed, 789 insertions, 1 deletions
diff --git a/library/src/layout/enum.rs b/library/src/layout/enum.rs new file mode 100644 index 00000000..b1b18680 --- /dev/null +++ b/library/src/layout/enum.rs @@ -0,0 +1,206 @@ +use std::str::FromStr; + +use crate::compute::{Numbering, NumberingPattern}; +use crate::layout::{BlockNode, GridNode, ParNode, Spacing, TrackSizing}; +use crate::prelude::*; + +/// # Numbered List +/// A numbered list. +/// +/// Displays a sequence of items vertically and numbers them consecutively. +/// +/// ## Example +/// ```example +/// Automatically numbered: +/// + Preparations +/// + Analysis +/// + Conclusions +/// +/// Manually numbered: +/// 2. What is the first step? +/// 5. I am confused. +/// + Moving on ... +/// +/// Function call. +/// #enum[First][Second] +/// ``` +/// +/// ## Syntax +/// This functions also has dedicated syntax: +/// +/// - Starting a line with a plus sign creates an automatically numbered +/// enumeration item. +/// - Start a line with a number followed by a dot creates an explicitly +/// numbered enumeration item. +/// +/// Enumeration items can contain multiple paragraphs and other block-level +/// content. All content that is indented more than an item's plus sign or dot +/// becomes part of that item. +/// +/// ## Parameters +/// - items: `Content` (positional, variadic) +/// The enumeration's children. +/// +/// When using the enum syntax, adjacent items are automatically collected +/// into enumerations, even through constructs like for loops. +/// +/// ```example +/// #for phase in ( +/// "Launch", +/// "Orbit", +/// "Descent", +/// ) [+ #phase] +/// ``` +/// +/// - start: `NonZeroUsize` (named) +/// Which number to start the enumeration with. +/// +/// ```example +/// #enum( +/// start: 3, +/// [Skipping], +/// [Ahead], +/// ) +/// ``` +/// +/// - tight: `bool` (named) +/// If this is `{false}`, the items are spaced apart with +/// [enum spacing]($func/enum.spacing). If it is `{true}`, they use normal +/// [leading]($func/par.leading) instead. This makes the enumeration more +/// compact, which can look better if the items are short. +/// +/// ```example +/// + If an enum has a lot of text, and +/// maybe other inline content, it +/// should not be tight anymore. +/// +/// + To make an enum wide, simply +/// insert a blank line between the +/// items. +/// ``` +/// +/// ## Category +/// layout +#[func] +#[capable(Layout)] +#[derive(Debug, Hash)] +pub struct EnumNode { + /// If true, the items are separated by leading instead of list spacing. + pub tight: bool, + /// The individual numbered items. + pub items: StyleVec<(Option<NonZeroUsize>, Content)>, +} + +#[node] +impl EnumNode { + /// How to number the enumeration. Accepts a + /// [numbering pattern or function]($func/numbering). + /// + /// ```example + /// #set enum(numbering: "(a)") + /// + /// + Different + /// + Numbering + /// + Style + /// ``` + #[property(referenced)] + pub const NUMBERING: Numbering = + Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()); + + /// The indentation of each item's label. + #[property(resolve)] + pub const INDENT: Length = Length::zero(); + + /// The space between the numbering and the body of each item. + #[property(resolve)] + pub const BODY_INDENT: Length = Em::new(0.5).into(); + + /// The spacing between the items of a wide (non-tight) enumeration. + /// + /// If set to `{auto}` uses the spacing [below blocks]($func/block.below). + pub const SPACING: Smart<Spacing> = Smart::Auto; + + fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { + let mut number: NonZeroUsize = + args.named("start")?.unwrap_or(NonZeroUsize::new(1).unwrap()); + + Ok(Self { + tight: args.named("tight")?.unwrap_or(true), + items: args + .all()? + .into_iter() + .map(|body| { + let item = (Some(number), body); + number = number.saturating_add(1); + item + }) + .collect(), + } + .pack()) + } + + fn field(&self, name: &str) -> Option<Value> { + match name { + "tight" => Some(Value::Bool(self.tight)), + "items" => Some(Value::Array( + self.items + .items() + .map(|(number, body)| { + Value::Dict(dict! { + "number" => match *number { + Some(n) => Value::Int(n.get() as i64), + None => Value::None, + }, + "body" => Value::Content(body.clone()), + }) + }) + .collect(), + )), + _ => None, + } + } +} + +impl Layout for EnumNode { + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let numbering = styles.get(Self::NUMBERING); + 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) + }; + + let mut cells = vec![]; + let mut number = NonZeroUsize::new(1).unwrap(); + for ((n, item), map) in self.items.iter() { + number = n.unwrap_or(number); + let resolved = numbering.apply(vt.world(), &[number])?.display(); + cells.push(Content::empty()); + cells.push(resolved.styled_with_map(map.clone())); + cells.push(Content::empty()); + cells.push(item.clone().styled_with_map(map.clone())); + number = number.saturating_add(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(vt, styles, regions) + } +} diff --git a/library/src/layout/list.rs b/library/src/layout/list.rs new file mode 100644 index 00000000..8bdbe737 --- /dev/null +++ b/library/src/layout/list.rs @@ -0,0 +1,160 @@ +use crate::layout::{BlockNode, GridNode, ParNode, Spacing, TrackSizing}; +use crate::prelude::*; +use crate::text::TextNode; + +/// # Bullet List +/// A bullet list. +/// +/// Displays a sequence of items vertically, with each item introduced by a +/// marker. +/// +/// ## Example +/// ```example +/// - *Content* +/// - Basics +/// - Text +/// - Math +/// - Layout +/// - Visualize +/// - Meta +/// +/// - *Compute* +/// #list( +/// [Foundations], +/// [Calculate], +/// [Create], +/// [Data Loading], +/// [Utility], +/// ) +/// ``` +/// +/// ## Syntax +/// This functions also has dedicated syntax: Start a line with a hyphen, +/// followed by a space to create a list item. A list item can contain multiple +/// paragraphs and other block-level content. All content that is indented +/// more than an item's hyphen becomes part of that item. +/// +/// ## Parameters +/// - items: `Content` (positional, variadic) +/// The list's children. +/// +/// When using the list syntax, adjacent items are automatically collected +/// into lists, even through constructs like for loops. +/// +/// ```example +/// #for letter in "ABC" [ +/// - Letter #letter +/// ] +/// ``` +/// +/// - tight: `bool` (named) +/// If this is `{false}`, the items are spaced apart with [list +/// spacing]($func/list.spacing). If it is `{true}`, they use normal +/// [leading]($func/par.leading) instead. This makes the list more compact, +/// which can look better if the items are short. +/// +/// ```example +/// - If a list has a lot of text, and +/// maybe other inline content, it +/// should not be tight anymore. +/// +/// - To make a list wide, simply insert +/// a blank line between the items. +/// ``` +/// +/// ## Category +/// layout +#[func] +#[capable(Layout)] +#[derive(Debug, Hash)] +pub struct ListNode { + /// If true, the items are separated by leading instead of list spacing. + pub tight: bool, + /// The individual bulleted or numbered items. + pub items: StyleVec<Content>, +} + +#[node] +impl ListNode { + /// The marker which introduces each element. + /// + /// ```example + /// #set list(marker: [--]) + /// + /// - A more classic list + /// - With en-dashes + /// ``` + #[property(referenced)] + pub const MARKER: Content = TextNode::packed('•'); + + /// The indent of each item's marker. + #[property(resolve)] + pub const INDENT: Length = Length::zero(); + + /// The spacing between the marker and the body of each item. + #[property(resolve)] + pub const BODY_INDENT: Length = Em::new(0.5).into(); + + /// The spacing between the items of a wide (non-tight) list. + /// + /// If set to `{auto}` uses the spacing [below blocks]($func/block.below). + pub const SPACING: Smart<Spacing> = Smart::Auto; + + fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { + Ok(Self { + tight: args.named("tight")?.unwrap_or(true), + items: args.all()?.into_iter().collect(), + } + .pack()) + } + + fn field(&self, name: &str) -> Option<Value> { + match name { + "tight" => Some(Value::Bool(self.tight)), + "items" => Some(Value::Array( + self.items.items().cloned().map(Value::Content).collect(), + )), + _ => None, + } + } +} + +impl Layout for ListNode { + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let marker = styles.get(Self::MARKER); + 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) + }; + + let mut cells = vec![]; + for (item, map) in self.items.iter() { + cells.push(Content::empty()); + cells.push(marker.clone()); + cells.push(Content::empty()); + cells.push(item.clone().styled_with_map(map.clone())); + } + + 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(vt, styles, regions) + } +} diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs index d3a9370b..ab5ebaf8 100644 --- a/library/src/layout/mod.rs +++ b/library/src/layout/mod.rs @@ -3,9 +3,12 @@ mod align; mod columns; mod container; +#[path = "enum.rs"] +mod enum_; mod flow; mod grid; mod hide; +mod list; mod pad; mod page; mod par; @@ -13,14 +16,18 @@ mod place; mod repeat; mod spacing; mod stack; +mod table; +mod terms; mod transform; pub use self::align::*; pub use self::columns::*; pub use self::container::*; +pub use self::enum_::*; pub use self::flow::*; pub use self::grid::*; pub use self::hide::*; +pub use self::list::*; pub use self::pad::*; pub use self::page::*; pub use self::par::*; @@ -28,6 +35,8 @@ pub use self::place::*; pub use self::repeat::*; pub use self::spacing::*; pub use self::stack::*; +pub use self::table::*; +pub use self::terms::*; pub use self::transform::*; use std::mem; @@ -40,7 +49,6 @@ use typst::model::{ StyleVecBuilder, StyledNode, }; -use crate::basics::{EnumNode, ListNode, TermItem, TermsNode}; use crate::math::{FormulaNode, LayoutMath}; use crate::meta::DocumentNode; use crate::prelude::*; diff --git a/library/src/layout/table.rs b/library/src/layout/table.rs new file mode 100644 index 00000000..12d2455f --- /dev/null +++ b/library/src/layout/table.rs @@ -0,0 +1,236 @@ +use crate::layout::{AlignNode, GridNode, TrackSizing, TrackSizings}; +use crate::prelude::*; + +/// # Table +/// A table of items. +/// +/// Tables are used to arrange content in cells. Cells can contain arbitrary +/// content, including multiple paragraphs and are specified in row-major order. +/// Because tables are just grids with configurable cell properties, refer to +/// the [grid documentation]($func/grid) for more information on how to size the +/// table tracks. +/// +/// ## Example +/// ```example +/// #table( +/// columns: (1fr, auto, auto), +/// inset: 10pt, +/// align: horizon, +/// [], [*Area*], [*Parameters*], +/// image("cylinder.svg"), +/// $ pi h (D^2 - d^2) / 4 $, +/// [ +/// $h$: height \ +/// $D$: outer radius \ +/// $d$: inner radius +/// ], +/// image("tetrahedron.svg"), +/// $ sqrt(2) / 12 a^3 $, +/// [$a$: edge length] +/// ) +/// ``` +/// +/// ## Parameters +/// - cells: `Content` (positional, variadic) +/// The contents of the table cells. +/// +/// - rows: `TrackSizings` (named) +/// Defines the row sizes. +/// See the [grid documentation]($func/grid) for more information on track +/// sizing. +/// +/// - columns: `TrackSizings` (named) +/// Defines the column sizes. +/// See the [grid documentation]($func/grid) for more information on track +/// sizing. +/// +/// - gutter: `TrackSizings` (named) +/// Defines the gaps between rows & columns. +/// See the [grid documentation]($func/grid) for more information on gutters. +/// +/// - column-gutter: `TrackSizings` (named) +/// Defines the gaps between columns. Takes precedence over `gutter`. +/// See the [grid documentation]($func/grid) for more information on gutters. +/// +/// - row-gutter: `TrackSizings` (named) +/// Defines the gaps between rows. Takes precedence over `gutter`. +/// See the [grid documentation]($func/grid) for more information on gutters. +/// +/// ## Category +/// layout +#[func] +#[capable(Layout)] +#[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] +impl TableNode { + /// How to fill the cells. + /// + /// This can be a color or a function that returns a color. The function is + /// passed the cell's column and row index, starting at zero. This can be + /// used to implement striped tables. + /// + /// ```example + /// #table( + /// fill: (col, _) => if calc.odd(col) { luma(240) } else { white }, + /// align: (col, row) => + /// if row == 0 { center } + /// else if col == 0 { left } + /// else { right }, + /// columns: 4, + /// [], [*Q1*], [*Q2*], [*Q3*], + /// [Revenue:], [1000 €], [2000 €], [3000 €], + /// [Expenses:], [500 €], [1000 €], [1500 €], + /// [Profit:], [500 €], [1000 €], [1500 €], + /// ) + /// ``` + #[property(referenced)] + pub const FILL: Celled<Option<Paint>> = Celled::Value(None); + + /// How to align the cell's content. + /// + /// This can either be a single alignment or a function that returns an + /// alignment. The function is passed the cell's column and row index, + /// starting at zero. If set to `{auto}`, the outer alignment is used. + #[property(referenced)] + pub const ALIGN: Celled<Smart<Axes<Option<GenAlign>>>> = Celled::Value(Smart::Auto); + + /// How to stroke the cells. + /// + /// This can be a color, a stroke width, both, or `{none}` to disable + /// the stroke. + #[property(resolve, fold)] + pub const STROKE: Option<PartialStroke> = Some(PartialStroke::default()); + + /// How much to pad the cells's content. + /// + /// The default value is `{5pt}`. + pub const INSET: 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 { + "columns" => Some(TrackSizing::encode_slice(&self.tracks.x)), + "rows" => Some(TrackSizing::encode_slice(&self.tracks.y)), + "column-gutter" => Some(TrackSizing::encode_slice(&self.gutter.x)), + "row-gutter" => Some(TrackSizing::encode_slice(&self.gutter.y)), + "cells" => Some(Value::Array( + self.cells.iter().cloned().map(Value::Content).collect(), + )), + _ => None, + } + } +} + +impl Layout for TableNode { + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let fill = styles.get(Self::FILL); + let stroke = styles.get(Self::STROKE).map(PartialStroke::unwrap_or_default); + let inset = styles.get(Self::INSET); + let align = styles.get(Self::ALIGN); + + 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(inset)); + + let x = i % cols; + let y = i / cols; + if let Smart::Custom(alignment) = align.resolve(vt, x, y)? { + child = child.styled(AlignNode::ALIGNS, alignment) + } + + if let Some(stroke) = stroke { + child = child.stroked(stroke); + } + + if let Some(fill) = fill.resolve(vt, x, y)? { + child = child.filled(fill); + } + + Ok(child) + }) + .collect::<SourceResult<_>>()?; + + GridNode { + tracks: self.tracks.clone(), + gutter: self.gutter.clone(), + cells, + } + .layout(vt, 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), +} + +impl<T: Cast + Clone> Celled<T> { + /// Resolve the value based on the cell position. + pub fn resolve(&self, vt: &Vt, x: usize, y: usize) -> SourceResult<T> { + Ok(match self { + Self::Value(value) => value.clone(), + Self::Func(func) => { + let args = + Args::new(func.span(), [Value::Int(x as i64), Value::Int(y as i64)]); + func.call_detached(vt.world(), args)?.cast().at(func.span())? + } + }) + } +} + +impl<T: Cast> Cast for Celled<T> { + fn is(value: &Value) -> bool { + matches!(value, Value::Func(_)) || T::is(value) + } + + fn cast(value: Value) -> StrResult<Self> { + match value { + Value::Func(v) => Ok(Self::Func(v)), + v if T::is(&v) => Ok(Self::Value(T::cast(v)?)), + v => <Self as Cast>::error(v), + } + } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("function") + } +} diff --git a/library/src/layout/terms.rs b/library/src/layout/terms.rs new file mode 100644 index 00000000..cf214084 --- /dev/null +++ b/library/src/layout/terms.rs @@ -0,0 +1,178 @@ +use crate::layout::{BlockNode, GridNode, HNode, ParNode, Spacing, TrackSizing}; +use crate::prelude::*; +use crate::text::{SpaceNode, TextNode}; + +/// # Term List +/// A list of terms and their descriptions. +/// +/// Displays a sequence of terms and their descriptions vertically. When the +/// descriptions span over multiple lines, they use hanging indent to +/// communicate the visual hierarchy. +/// +/// ## Syntax +/// This function also has dedicated syntax: Starting a line with a slash, +/// followed by a term, a colon and a description creates a term list item. +/// +/// ## Example +/// ```example +/// / Ligature: A merged glyph. +/// / Kerning: A spacing adjustment +/// between two adjacent letters. +/// ``` +/// +/// ## Parameters +/// - items: `Content` (positional, variadic) +/// The term list's children. +/// +/// When using the term list syntax, adjacent items are automatically +/// collected into term lists, even through constructs like for loops. +/// +/// ```example +/// #for year, product in ( +/// "1978": "TeX", +/// "1984": "LaTeX", +/// "2019": "Typst", +/// ) [/ #product: Born in #year.] +/// ``` +/// +/// - tight: `bool` (named) +/// If this is `{false}`, the items are spaced apart with [term list +/// spacing]($func/terms.spacing). If it is `{true}`, they use normal +/// [leading]($func/par.leading) instead. This makes the term list more +/// compact, which can look better if the items are short. +/// +/// ```example +/// / Fact: If a term list has a lot +/// of text, and maybe other inline +/// content, it should not be tight +/// anymore. +/// +/// / Tip: To make it wide, simply +/// insert a blank line between the +/// items. +/// ``` +/// +/// ## Category +/// layout +#[func] +#[capable(Layout)] +#[derive(Debug, Hash)] +pub struct TermsNode { + /// If true, the items are separated by leading instead of list spacing. + pub tight: bool, + /// The individual bulleted or numbered items. + pub items: StyleVec<TermItem>, +} + +#[node] +impl TermsNode { + /// The indentation of each item's term. + #[property(resolve)] + pub const INDENT: Length = Length::zero(); + + /// The hanging indent of the description. + /// + /// ```example + /// #set terms(hanging-indent: 0pt) + /// / Term: This term list does not + /// make use of hanging indents. + /// ``` + #[property(resolve)] + pub const HANGING_INDENT: Length = Em::new(1.0).into(); + + /// The spacing between the items of a wide (non-tight) term list. + /// + /// If set to `{auto}` uses the spacing [below blocks]($func/block.below). + pub const SPACING: Smart<Spacing> = Smart::Auto; + + fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { + Ok(Self { + tight: args.named("tight")?.unwrap_or(true), + items: args.all()?.into_iter().collect(), + } + .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 Layout for TermsNode { + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let indent = styles.get(Self::INDENT); + let body_indent = styles.get(Self::HANGING_INDENT); + let gutter = if self.tight { + styles.get(ParNode::LEADING).into() + } else { + styles + .get(Self::SPACING) + .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount) + }; + + let mut cells = vec![]; + for (item, map) in self.items.iter() { + let body = Content::sequence(vec![ + HNode { amount: (-body_indent).into(), weak: false }.pack(), + (item.term.clone() + TextNode::packed(':')).strong(), + SpaceNode.pack(), + item.description.clone(), + ]); + + cells.push(Content::empty()); + cells.push(body.styled_with_map(map.clone())); + } + + GridNode { + tracks: Axes::with_x(vec![ + TrackSizing::Relative((indent + body_indent).into()), + TrackSizing::Auto, + ]), + gutter: Axes::with_y(vec![gutter.into()]), + cells, + } + .layout(vt, styles, regions) + } +} + +/// A term list item. +#[derive(Debug, Clone, Hash)] +pub struct TermItem { + /// The term described by the list item. + pub term: Content, + /// The description of the term. + pub description: Content, +} + +impl TermItem { + /// Encode the item into a value. + fn encode(&self) -> Value { + Value::Array(array![ + Value::Content(self.term.clone()), + Value::Content(self.description.clone()), + ]) + } +} + +castable! { + TermItem, + array: Array => { + let mut iter = array.into_iter(); + let (term, description) = match (iter.next(), iter.next(), iter.next()) { + (Some(a), Some(b), None) => (a.cast()?, b.cast()?), + _ => Err("array must contain exactly two entries")?, + }; + Self { term, description } + }, +} |
