diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-03-07 15:17:13 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-03-07 15:17:13 +0100 |
| commit | 25b5bd117529cd04bb789e1988eb3a3db8025a0e (patch) | |
| tree | 2fbb4650903123da047a1f1f11a0abda95286e12 /library/src/layout | |
| parent | 6ab7760822ccd24b4ef126d4737d41f1be15fe19 (diff) | |
Fully untyped model
Diffstat (limited to 'library/src/layout')
| -rw-r--r-- | library/src/layout/align.rs | 109 | ||||
| -rw-r--r-- | library/src/layout/columns.rs | 79 | ||||
| -rw-r--r-- | library/src/layout/container.rs | 347 | ||||
| -rw-r--r-- | library/src/layout/enum.rs | 175 | ||||
| -rw-r--r-- | library/src/layout/flow.rs | 55 | ||||
| -rw-r--r-- | library/src/layout/grid.rs | 107 | ||||
| -rw-r--r-- | library/src/layout/hide.rs | 27 | ||||
| -rw-r--r-- | library/src/layout/list.rs | 191 | ||||
| -rw-r--r-- | library/src/layout/mod.rs | 165 | ||||
| -rw-r--r-- | library/src/layout/pad.rs | 66 | ||||
| -rw-r--r-- | library/src/layout/page.rs | 171 | ||||
| -rw-r--r-- | library/src/layout/par.rs | 155 | ||||
| -rw-r--r-- | library/src/layout/place.rs | 85 | ||||
| -rw-r--r-- | library/src/layout/repeat.rs | 29 | ||||
| -rw-r--r-- | library/src/layout/spacing.rs | 161 | ||||
| -rw-r--r-- | library/src/layout/stack.rs | 84 | ||||
| -rw-r--r-- | library/src/layout/table.rs | 151 | ||||
| -rw-r--r-- | library/src/layout/terms.rs | 174 | ||||
| -rw-r--r-- | library/src/layout/transform.rs | 170 |
19 files changed, 1198 insertions, 1303 deletions
diff --git a/library/src/layout/align.rs b/library/src/layout/align.rs index b84ccfdc..96c0ae3b 100644 --- a/library/src/layout/align.rs +++ b/library/src/layout/align.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -/// # Align /// Align content horizontally and vertically. /// /// ## Example @@ -13,63 +12,59 @@ use crate::prelude::*; /// A work of art, a visual throne /// ``` /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The content to align. -/// -/// - alignment: `Axes<Option<GenAlign>>` (positional, settable) -/// The alignment along both axes. -/// -/// Possible values for horizontal alignments are: -/// - `start` -/// - `end` -/// - `left` -/// - `center` -/// - `right` -/// -/// The `start` and `end` alignments are relative to the current [text -/// direction]($func/text.dir). -/// -/// Possible values for vertical alignments are: -/// - `top` -/// - `horizon` -/// - `bottom` -/// -/// To align along both axes at the same time, add the two alignments using -/// the `+` operator to get a `2d alignment`. For example, `top + right` -/// aligns the content to the top right corner. -/// -/// ```example -/// #set page(height: 6cm) -/// #set text(lang: "ar") -/// -/// مثال -/// #align( -/// end + horizon, -/// rect(inset: 12pt)[ركن] -/// ) -/// ``` -/// -/// ## Category -/// layout -#[func] -#[capable] -#[derive(Debug, Hash)] -pub enum AlignNode {} - -#[node] -impl AlignNode { - /// The alignment. - #[property(fold, skip)] - pub const ALIGNS: Axes<Option<GenAlign>> = - Axes::new(GenAlign::Start, GenAlign::Specific(Align::Top)); +/// Display: Align +/// Category: layout +#[node(Show)] +#[set({ + let aligns: Axes<Option<GenAlign>> = args.find()?.unwrap_or_default(); + styles.set(Self::ALIGNMENT, aligns); +})] +pub struct AlignNode { + /// The content to align. + #[positional] + #[required] + pub body: Content, - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - args.expect("body") - } + /// The alignment along both axes. + /// + /// Possible values for horizontal alignments are: + /// - `start` + /// - `end` + /// - `left` + /// - `center` + /// - `right` + /// + /// The `start` and `end` alignments are relative to the current [text + /// direction]($func/text.dir). + /// + /// Possible values for vertical alignments are: + /// - `top` + /// - `horizon` + /// - `bottom` + /// + /// To align along both axes at the same time, add the two alignments using + /// the `+` operator to get a `2d alignment`. For example, `top + right` + /// aligns the content to the top right corner. + /// + /// ```example + /// #set page(height: 6cm) + /// #set text(lang: "ar") + /// + /// مثال + /// #align( + /// end + horizon, + /// rect(inset: 12pt)[ركن] + /// ) + /// ``` + #[settable] + #[fold] + #[skip] + #[default(Axes::new(GenAlign::Start, GenAlign::Specific(Align::Top)))] + pub alignment: Axes<Option<GenAlign>>, +} - fn set(...) { - let aligns: Axes<Option<GenAlign>> = args.find()?.unwrap_or_default(); - styles.set(Self::ALIGNS, aligns); +impl Show for AlignNode { + fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> { + Ok(self.body()) } } diff --git a/library/src/layout/columns.rs b/library/src/layout/columns.rs index 0353e077..94c04509 100644 --- a/library/src/layout/columns.rs +++ b/library/src/layout/columns.rs @@ -1,7 +1,6 @@ use crate::prelude::*; use crate::text::TextNode; -/// # Columns /// Separate a region into multiple equally sized columns. /// /// The `column` function allows to separate the interior of any container into @@ -31,39 +30,25 @@ use crate::text::TextNode; /// variety of problems. /// ``` /// -/// ## Parameters -/// - count: `usize` (positional, required) -/// The number of columns. -/// -/// - body: `Content` (positional, required) -/// The content that should be layouted into the columns. -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Columns +/// Category: layout +#[node(Layout)] pub struct ColumnsNode { - /// How many columns there should be. + /// The number of columns. + #[positional] + #[required] pub count: NonZeroUsize, - /// The child to be layouted into the columns. Most likely, this should be a - /// flow or stack node. + + /// The content that should be layouted into the columns. + #[positional] + #[required] pub body: Content, -} -#[node] -impl ColumnsNode { /// The size of the gutter space between each column. - #[property(resolve)] - pub const GUTTER: Rel<Length> = Ratio::new(0.04).into(); - - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - Ok(Self { - count: args.expect("column count")?, - body: args.expect("body")?, - } - .pack()) - } + #[settable] + #[resolve] + #[default(Ratio::new(0.04).into())] + pub gutter: Rel<Length>, } impl Layout for ColumnsNode { @@ -73,14 +58,16 @@ impl Layout for ColumnsNode { styles: StyleChain, regions: Regions, ) -> SourceResult<Fragment> { + let body = self.body(); + // Separating the infinite space into infinite columns does not make // much sense. if !regions.size.x.is_finite() { - return self.body.layout(vt, styles, regions); + return body.layout(vt, styles, regions); } // Determine the width of the gutter and each column. - let columns = self.count.get(); + let columns = self.count().get(); let gutter = styles.get(Self::GUTTER).relative_to(regions.base().x); let width = (regions.size.x - gutter * (columns - 1) as f64) / columns as f64; @@ -100,7 +87,7 @@ impl Layout for ColumnsNode { }; // Layout the children. - let mut frames = self.body.layout(vt, styles, pod)?.into_iter(); + let mut frames = body.layout(vt, styles, pod)?.into_iter(); let mut finished = vec![]; let dir = styles.get(TextNode::DIR); @@ -140,7 +127,6 @@ impl Layout for ColumnsNode { } } -/// # Column Break /// A forced column break. /// /// The function will behave like a [page break]($func/pagebreak) when used in a @@ -165,31 +151,20 @@ impl Layout for ColumnsNode { /// laws of nature. /// ``` /// -/// ## Parameters -/// - weak: `bool` (named) -/// If `{true}`, the column break is skipped if the current column is already -/// empty. -/// -/// ## Category -/// layout -#[func] -#[capable(Behave)] -#[derive(Debug, Hash)] +/// Display: Column Break +/// Category: layout +#[node(Behave)] pub struct ColbreakNode { + /// If `{true}`, the column break is skipped if the current column is + /// already empty. + #[named] + #[default(false)] pub weak: bool, } -#[node] -impl ColbreakNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - let weak = args.named("weak")?.unwrap_or(false); - Ok(Self { weak }.pack()) - } -} - impl Behave for ColbreakNode { fn behaviour(&self) -> Behaviour { - if self.weak { + if self.weak() { Behaviour::Weak(1) } else { Behaviour::Destructive diff --git a/library/src/layout/container.rs b/library/src/layout/container.rs index 8b10f7a6..67504ca3 100644 --- a/library/src/layout/container.rs +++ b/library/src/layout/container.rs @@ -2,7 +2,6 @@ use super::VNode; use crate::layout::Spacing; use crate::prelude::*; -/// # Box /// An inline-level container that sizes content. /// /// All elements except inline math, text, and boxes are block-level and cannot @@ -20,69 +19,75 @@ use crate::prelude::*; /// for more information. /// ``` /// -/// ## Parameters -/// - body: `Content` (positional) -/// The contents of the box. -/// -/// - width: `Sizing` (named) -/// The width of the box. -/// -/// Boxes can have [fractional]($type/fraction) widths, as the example -/// below demonstrates. -/// -/// _Note:_ Currently, only boxes and only their widths might be fractionally -/// sized within paragraphs. Support for fractionally sized images, shapes, -/// and more might be added in the future. -/// -/// ```example -/// Line in #box(width: 1fr, line(length: 100%)) between. -/// ``` -/// -/// - height: `Rel<Length>` (named) -/// The height of the box. -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Box +/// Category: layout +#[node(Layout)] pub struct BoxNode { - /// The box's content. + /// The contents of the box. + #[positional] + #[default] pub body: Content, - /// The box's width. + + /// The width of the box. + /// + /// Boxes can have [fractional]($type/fraction) widths, as the example + /// below demonstrates. + /// + /// _Note:_ Currently, only boxes and only their widths might be fractionally + /// sized within paragraphs. Support for fractionally sized images, shapes, + /// and more might be added in the future. + /// + /// ```example + /// Line in #box(width: 1fr, line(length: 100%)) between. + /// ``` + #[named] + #[default] pub width: Sizing, - /// The box's height. + + /// The height of the box. + #[named] + #[default] pub height: Smart<Rel<Length>>, -} -#[node] -impl BoxNode { /// An amount to shift the box's baseline by. /// /// ```example /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)). /// ``` - #[property(resolve)] - pub const BASELINE: Rel<Length> = Rel::zero(); + #[settable] + #[resolve] + #[default] + pub baseline: Rel<Length>, /// The box's background color. See the /// [rectangle's documentation]($func/rect.fill) for more details. - pub const FILL: Option<Paint> = None; + #[settable] + #[default] + pub fill: Option<Paint>, /// The box's border color. See the /// [rectangle's documentation]($func/rect.stroke) for more details. - #[property(resolve, fold)] - pub const STROKE: Sides<Option<Option<PartialStroke>>> = Sides::splat(None); + #[settable] + #[resolve] + #[fold] + #[default] + pub stroke: Sides<Option<Option<PartialStroke>>>, /// How much to round the box's corners. See the [rectangle's /// documentation]($func/rect.radius) for more details. - #[property(resolve, fold)] - pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub radius: Corners<Option<Rel<Length>>>, /// How much to pad the box's content. See the [rectangle's /// documentation]($func/rect.inset) for more details. - #[property(resolve, fold)] - pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub inset: Sides<Option<Rel<Length>>>, /// How much to expand the box's size without affecting the layout. /// @@ -98,15 +103,11 @@ impl BoxNode { /// outset: (y: 3pt), /// radius: 2pt, /// )[rectangle]. - #[property(resolve, fold)] - pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); - - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - let body = args.eat()?.unwrap_or_default(); - let width = args.named("width")?.unwrap_or_default(); - let height = args.named("height")?.unwrap_or_default(); - Ok(Self { body, width, height }.pack()) - } + #[settable] + #[resolve] + #[fold] + #[default] + pub outset: Sides<Option<Rel<Length>>>, } impl Layout for BoxNode { @@ -116,14 +117,14 @@ impl Layout for BoxNode { styles: StyleChain, regions: Regions, ) -> SourceResult<Fragment> { - let width = match self.width { + let width = match self.width() { Sizing::Auto => Smart::Auto, Sizing::Rel(rel) => Smart::Custom(rel), Sizing::Fr(_) => Smart::Custom(Ratio::one().into()), }; // Resolve the sizing to a concrete size. - let sizing = Axes::new(width, self.height); + let sizing = Axes::new(width, self.height()); let expand = sizing.as_ref().map(Smart::is_custom); let size = sizing .resolve(styles) @@ -132,10 +133,10 @@ impl Layout for BoxNode { .unwrap_or(regions.base()); // Apply inset. - let mut child = self.body.clone(); + let mut child = self.body(); let inset = styles.get(Self::INSET); if inset.iter().any(|v| !v.is_zero()) { - child = child.clone().padded(inset.map(|side| side.map(Length::from))); + child = child.padded(inset.map(|side| side.map(Length::from))); } // Select the appropriate base and expansion for the child depending @@ -169,7 +170,6 @@ impl Layout for BoxNode { } } -/// # Block /// A block-level container. /// /// Such a container can be used to separate content, size it and give it a @@ -201,37 +201,6 @@ impl Layout for BoxNode { /// ``` /// /// ## Parameters -/// - body: `Content` (positional) -/// The contents of the block. -/// -/// - width: `Smart<Rel<Length>>` (named) -/// The block's width. -/// -/// ```example -/// #set align(center) -/// #block( -/// width: 60%, -/// inset: 8pt, -/// fill: silver, -/// lorem(10), -/// ) -/// ``` -/// -/// - height: `Smart<Rel<Length>>` (named) -/// The block's height. When the height is larger than the remaining space on -/// a page and [`breakable`]($func/block.breakable) is `{true}`, the block -/// will continue on the next page with the remaining height. -/// -/// ```example -/// #set page(height: 80pt) -/// #set align(center) -/// #block( -/// width: 80%, -/// height: 150%, -/// fill: aqua, -/// ) -/// ``` -/// /// - spacing: `Spacing` (named, settable) /// The spacing around this block. This is shorthand to set `above` and /// `below` to the same value. @@ -245,35 +214,62 @@ impl Layout for BoxNode { /// A second paragraph. /// ``` /// -/// - above: `Spacing` (named, settable) -/// The spacing between this block and its predecessor. Takes precedence over -/// `spacing`. Can be used in combination with a show rule to adjust the -/// spacing around arbitrary block-level elements. -/// -/// The default value is `{1.2em}`. -/// -/// - below: `Spacing` (named, settable) -/// The spacing between this block and its successor. Takes precedence -/// over `spacing`. -/// -/// The default value is `{1.2em}`. -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Block +/// Category: layout +#[node(Layout)] +#[set({ + let spacing = args.named("spacing")?; + styles.set_opt( + Self::ABOVE, + args.named("above")? + .map(VNode::block_around) + .or_else(|| spacing.map(VNode::block_spacing)), + ); + styles.set_opt( + Self::BELOW, + args.named("below")? + .map(VNode::block_around) + .or_else(|| spacing.map(VNode::block_spacing)), + ); +})] pub struct BlockNode { - /// The block's content. + /// The contents of the block. + #[positional] + #[default] pub body: Content, - /// The box's width. + + /// The block's width. + /// + /// ```example + /// #set align(center) + /// #block( + /// width: 60%, + /// inset: 8pt, + /// fill: silver, + /// lorem(10), + /// ) + /// ``` + #[named] + #[default] pub width: Smart<Rel<Length>>, - /// The box's height. + + /// The block's height. When the height is larger than the remaining space on + /// a page and [`breakable`]($func/block.breakable) is `{true}`, the block + /// will continue on the next page with the remaining height. + /// + /// ```example + /// #set page(height: 80pt) + /// #set align(center) + /// #block( + /// width: 80%, + /// height: 150%, + /// fill: aqua, + /// ) + /// ``` + #[named] + #[default] pub height: Smart<Rel<Length>>, -} -#[node] -impl BlockNode { /// Whether the block can be broken and continue on the next page. /// /// Defaults to `{true}`. @@ -286,64 +282,74 @@ impl BlockNode { /// lorem(15), /// ) /// ``` - pub const BREAKABLE: bool = true; + #[settable] + #[default(true)] + pub breakable: bool, /// The block's background color. See the /// [rectangle's documentation]($func/rect.fill) for more details. - pub const FILL: Option<Paint> = None; + #[settable] + #[default] + pub fill: Option<Paint>, /// The block's border color. See the /// [rectangle's documentation]($func/rect.stroke) for more details. - #[property(resolve, fold)] - pub const STROKE: Sides<Option<Option<PartialStroke>>> = Sides::splat(None); + #[settable] + #[resolve] + #[fold] + #[default] + pub stroke: Sides<Option<Option<PartialStroke>>>, /// How much to round the block's corners. See the [rectangle's /// documentation]($func/rect.radius) for more details. - #[property(resolve, fold)] - pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub radius: Corners<Option<Rel<Length>>>, /// How much to pad the block's content. See the [rectangle's /// documentation]($func/rect.inset) for more details. - #[property(resolve, fold)] - pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub inset: Sides<Option<Rel<Length>>>, /// How much to expand the block's size without affecting the layout. See /// the [rectangle's documentation]($func/rect.outset) for more details. - #[property(resolve, fold)] - pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); - - /// The spacing between the previous and this block. - #[property(skip)] - pub const ABOVE: VNode = VNode::block_spacing(Em::new(1.2).into()); - - /// The spacing between this and the following block. - #[property(skip)] - pub const BELOW: VNode = VNode::block_spacing(Em::new(1.2).into()); + #[settable] + #[resolve] + #[fold] + #[default] + pub outset: Sides<Option<Rel<Length>>>, + + /// The spacing between this block and its predecessor. Takes precedence over + /// `spacing`. Can be used in combination with a show rule to adjust the + /// spacing around arbitrary block-level elements. + /// + /// The default value is `{1.2em}`. + #[settable] + #[skip] + #[default(VNode::block_spacing(Em::new(1.2).into()))] + pub above: VNode, + + /// The spacing between this block and its successor. Takes precedence + /// over `spacing`. + /// + /// The default value is `{1.2em}`. + #[settable] + #[skip] + #[default(VNode::block_spacing(Em::new(1.2).into()))] + pub below: VNode, /// Whether this block must stick to the following one. /// /// Use this to prevent page breaks between e.g. a heading and its body. - #[property(skip)] - pub const STICKY: bool = false; - - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - let body = args.eat()?.unwrap_or_default(); - let width = args.named("width")?.unwrap_or_default(); - let height = args.named("height")?.unwrap_or_default(); - Ok(Self { body, width, height }.pack()) - } - - fn set(...) { - let spacing = args.named("spacing")?.map(VNode::block_spacing); - styles.set_opt( - Self::ABOVE, - args.named("above")?.map(VNode::block_around).or(spacing), - ); - styles.set_opt( - Self::BELOW, - args.named("below")?.map(VNode::block_around).or(spacing), - ); - } + #[settable] + #[skip] + #[default(false)] + pub sticky: bool, } impl Layout for BlockNode { @@ -354,14 +360,14 @@ impl Layout for BlockNode { regions: Regions, ) -> SourceResult<Fragment> { // Apply inset. - let mut child = self.body.clone(); + let mut child = self.body(); let inset = styles.get(Self::INSET); if inset.iter().any(|v| !v.is_zero()) { child = child.clone().padded(inset.map(|side| side.map(Length::from))); } // Resolve the sizing to a concrete size. - let sizing = Axes::new(self.width, self.height); + let sizing = Axes::new(self.width(), self.height()); let mut expand = sizing.as_ref().map(Smart::is_custom); let mut size = sizing .resolve(styles) @@ -372,7 +378,7 @@ impl Layout for BlockNode { // Layout the child. let mut frames = if styles.get(Self::BREAKABLE) { // Measure to ensure frames for all regions have the same width. - if self.width == Smart::Auto { + if sizing.x == Smart::Auto { let pod = Regions::one(size, Axes::splat(false)); let frame = child.layout(vt, styles, pod)?.into_frame(); size.x = frame.width(); @@ -385,7 +391,7 @@ impl Layout for BlockNode { // Generate backlog for fixed height. let mut heights = vec![]; - if self.height.is_custom() { + if sizing.y.is_custom() { let mut remaining = size.y; for region in regions.iter() { let limited = region.y.min(remaining); @@ -454,18 +460,6 @@ impl Sizing { pub fn is_fractional(self) -> bool { matches!(self, Self::Fr(_)) } - - pub fn encode(self) -> Value { - match self { - Self::Auto => Value::Auto, - Self::Rel(rel) => Spacing::Rel(rel).encode(), - Self::Fr(fr) => Spacing::Fr(fr).encode(), - } - } - - pub fn encode_slice(vec: &[Sizing]) -> Value { - Value::Array(vec.iter().copied().map(Self::encode).collect()) - } } impl Default for Sizing { @@ -474,11 +468,26 @@ impl Default for Sizing { } } -impl From<Spacing> for Sizing { - fn from(spacing: Spacing) -> Self { - match spacing { +impl<T: Into<Spacing>> From<T> for Sizing { + fn from(spacing: T) -> Self { + match spacing.into() { Spacing::Rel(rel) => Self::Rel(rel), Spacing::Fr(fr) => Self::Fr(fr), } } } + +cast_from_value! { + Sizing, + _: Smart<Never> => Self::Auto, + v: Rel<Length> => Self::Rel(v), + v: Fr => Self::Fr(v), +} + +cast_to_value! { + v: Sizing => match v { + Sizing::Auto => Value::Auto, + Sizing::Rel(rel) => Value::Relative(rel), + Sizing::Fr(fr) => Value::Fraction(fr), + } +} diff --git a/library/src/layout/enum.rs b/library/src/layout/enum.rs index 53fc3327..990c0fb9 100644 --- a/library/src/layout/enum.rs +++ b/library/src/layout/enum.rs @@ -1,11 +1,12 @@ use std::str::FromStr; -use crate::layout::{BlockNode, GridNode, ParNode, Sizing, Spacing}; +use crate::layout::{BlockNode, ParNode, Sizing, Spacing}; use crate::meta::{Numbering, NumberingPattern}; use crate::prelude::*; use crate::text::TextNode; -/// # Numbered List +use super::GridLayouter; + /// A numbered list. /// /// Displays a sequence of items vertically and numbers them consecutively. @@ -89,20 +90,19 @@ use crate::text::TextNode; /// items. /// ``` /// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Numbered List +/// Category: layout +#[node(Construct, Layout)] pub struct EnumNode { + /// The numbered list's items. + #[variadic] + pub items: Vec<EnumItem>, + /// If true, the items are separated by leading instead of list spacing. + #[named] + #[default(true)] 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). /// @@ -122,9 +122,9 @@ impl EnumNode { /// + Superscript /// + Numbering! /// ``` - #[property(referenced)] - pub const NUMBERING: Numbering = - Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()); + #[settable] + #[default(Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()))] + pub numbering: Numbering, /// Whether to display the full numbering, including the numbers of /// all parent enumerations. @@ -138,63 +138,51 @@ impl EnumNode { /// + Add integredients /// + Eat /// ``` - pub const FULL: bool = false; + #[settable] + #[default(false)] + pub full: bool, /// The indentation of each item's label. - #[property(resolve)] - pub const INDENT: Length = Length::zero(); + #[settable] + #[resolve] + #[default] + pub indent: Length, /// The space between the numbering and the body of each item. - #[property(resolve)] - pub const BODY_INDENT: Length = Em::new(0.5).into(); + #[settable] + #[resolve] + #[default(Em::new(0.5).into())] + pub body_indent: Length, /// 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; + #[settable] + #[default] + pub spacing: Smart<Spacing>, /// The numbers of parent items. - #[property(skip, fold)] - const PARENTS: Parent = vec![]; + #[settable] + #[fold] + #[skip] + #[default] + parents: Parent, +} +impl Construct for EnumNode { 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(), + let mut items = args.all::<EnumItem>()?; + if let Some(number) = args.named::<NonZeroUsize>("start")? { + if let Some(first) = items.first_mut() { + if first.number().is_none() { + *first = EnumItem::new(first.body()).with_number(Some(number)); + } + } } - .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, - } + Ok(Self::new(items) + .with_tight(args.named("tight")?.unwrap_or(true)) + .pack()) } } @@ -208,12 +196,12 @@ impl Layout for EnumNode { 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 { + let gutter = if self.tight() { styles.get(ParNode::LEADING).into() } else { styles .get(Self::SPACING) - .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount) + .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount()) }; let mut cells = vec![]; @@ -221,8 +209,8 @@ impl Layout for EnumNode { let mut parents = styles.get(Self::PARENTS); let full = styles.get(Self::FULL); - for ((n, item), map) in self.items.iter() { - number = n.unwrap_or(number); + for item in self.items() { + number = item.number().unwrap_or(number); let resolved = if full { parents.push(number); @@ -230,7 +218,7 @@ impl Layout for EnumNode { parents.pop(); content } else { - match numbering { + match &numbering { Numbering::Pattern(pattern) => { TextNode::packed(pattern.apply_kth(parents.len(), number)) } @@ -239,33 +227,68 @@ impl Layout for EnumNode { }; cells.push(Content::empty()); - cells.push(resolved.styled_with_map(map.clone())); + cells.push(resolved); cells.push(Content::empty()); - cells.push( - item.clone() - .styled_with_map(map.clone()) - .styled(Self::PARENTS, Parent(number)), - ); + cells.push(item.body().styled(Self::PARENTS, Parent(number))); number = number.saturating_add(1); } - GridNode { - tracks: Axes::with_x(vec![ + let layouter = GridLayouter::new( + vt, + Axes::with_x(&[ Sizing::Rel(indent.into()), Sizing::Auto, Sizing::Rel(body_indent.into()), Sizing::Auto, ]), - gutter: Axes::with_y(vec![gutter.into()]), - cells, - } - .layout(vt, styles, regions) + Axes::with_y(&[gutter.into()]), + &cells, + regions, + styles, + ); + + Ok(layouter.layout()?.fragment) } } -#[derive(Debug, Clone, Hash)] +/// An enumeration item. +#[node] +pub struct EnumItem { + /// The item's number. + #[positional] + #[default] + pub number: Option<NonZeroUsize>, + + /// The item's body. + #[positional] + #[required] + pub body: Content, +} + +cast_from_value! { + EnumItem, + array: Array => { + let mut iter = array.into_iter(); + let (number, body) = match (iter.next(), iter.next(), iter.next()) { + (Some(a), Some(b), None) => (a.cast()?, b.cast()?), + _ => Err("array must contain exactly two entries")?, + }; + Self::new(body).with_number(number) + }, + v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())), +} + struct Parent(NonZeroUsize); +cast_from_value! { + Parent, + v: NonZeroUsize => Self(v), +} + +cast_to_value! { + v: Parent => v.0.into() +} + impl Fold for Parent { type Output = Vec<NonZeroUsize>; diff --git a/library/src/layout/flow.rs b/library/src/layout/flow.rs index ee845f06..ea31752b 100644 --- a/library/src/layout/flow.rs +++ b/library/src/layout/flow.rs @@ -1,4 +1,4 @@ -use typst::model::Style; +use typst::model::{Style, StyledNode}; use super::{AlignNode, BlockNode, ColbreakNode, ParNode, PlaceNode, Spacing, VNode}; use crate::prelude::*; @@ -8,12 +8,12 @@ use crate::visualize::{CircleNode, EllipseNode, ImageNode, RectNode, SquareNode} /// /// This node is responsible for layouting both the top-level content flow and /// the contents of boxes. -#[capable(Layout)] -#[derive(Hash)] -pub struct FlowNode(pub StyleVec<Content>); - -#[node] -impl FlowNode {} +#[node(Layout)] +pub struct FlowNode { + /// The children that will be arranges into a flow. + #[variadic] + pub children: Vec<Content>, +} impl Layout for FlowNode { fn layout( @@ -24,9 +24,17 @@ impl Layout for FlowNode { ) -> SourceResult<Fragment> { let mut layouter = FlowLayouter::new(regions); - for (child, map) in self.0.iter() { - let styles = styles.chain(&map); - if let Some(&node) = child.to::<VNode>() { + for mut child in self.children() { + let map; + let outer = styles; + let mut styles = outer; + if let Some(node) = child.to::<StyledNode>() { + map = node.map(); + styles = outer.chain(&map); + child = node.sub(); + } + + if let Some(node) = child.to::<VNode>() { layouter.layout_spacing(node, styles); } else if let Some(node) = child.to::<ParNode>() { let barrier = Style::Barrier(child.id()); @@ -40,16 +48,16 @@ impl Layout for FlowNode { { let barrier = Style::Barrier(child.id()); let styles = styles.chain_one(&barrier); - layouter.layout_single(vt, child, styles)?; + layouter.layout_single(vt, &child, styles)?; } else if child.has::<dyn Layout>() { - layouter.layout_multiple(vt, child, styles)?; + layouter.layout_multiple(vt, &child, styles)?; } else if child.is::<ColbreakNode>() { if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some() { layouter.finish_region(); } - } else { - panic!("unexpected flow child: {child:?}"); + } else if let Some(span) = child.span() { + bail!(span, "unexpected flow child"); } } @@ -57,13 +65,6 @@ impl Layout for FlowNode { } } -impl Debug for FlowNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Flow ")?; - self.0.fmt(f) - } -} - /// Performs flow layout. struct FlowLayouter<'a> { /// The regions to layout children into. @@ -113,11 +114,11 @@ impl<'a> FlowLayouter<'a> { } /// Layout vertical spacing. - fn layout_spacing(&mut self, node: VNode, styles: StyleChain) { - self.layout_item(match node.amount { + fn layout_spacing(&mut self, node: &VNode, styles: StyleChain) { + self.layout_item(match node.amount() { Spacing::Rel(v) => FlowItem::Absolute( v.resolve(styles).relative_to(self.initial.y), - node.weakness > 0, + node.weakness() > 0, ), Spacing::Fr(v) => FlowItem::Fractional(v), }); @@ -130,7 +131,7 @@ impl<'a> FlowLayouter<'a> { par: &ParNode, styles: StyleChain, ) -> SourceResult<()> { - let aligns = styles.get(AlignNode::ALIGNS).resolve(styles); + let aligns = styles.get(AlignNode::ALIGNMENT).resolve(styles); let leading = styles.get(ParNode::LEADING); let consecutive = self.last_was_par; let frames = par @@ -176,7 +177,7 @@ impl<'a> FlowLayouter<'a> { content: &Content, styles: StyleChain, ) -> SourceResult<()> { - let aligns = styles.get(AlignNode::ALIGNS).resolve(styles); + let aligns = styles.get(AlignNode::ALIGNMENT).resolve(styles); let sticky = styles.get(BlockNode::STICKY); let pod = Regions::one(self.regions.base(), Axes::splat(false)); let layoutable = content.with::<dyn Layout>().unwrap(); @@ -204,7 +205,7 @@ impl<'a> FlowLayouter<'a> { } // How to align the block. - let aligns = styles.get(AlignNode::ALIGNS).resolve(styles); + let aligns = styles.get(AlignNode::ALIGNMENT).resolve(styles); // Layout the block itself. let sticky = styles.get(BlockNode::STICKY); diff --git a/library/src/layout/grid.rs b/library/src/layout/grid.rs index d0df8794..d3758fd6 100644 --- a/library/src/layout/grid.rs +++ b/library/src/layout/grid.rs @@ -3,7 +3,6 @@ use crate::text::TextNode; use super::Sizing; -/// # Grid /// Arrange content in a grid. /// /// The grid element allows you to arrange content in a grid. You can define the @@ -61,64 +60,50 @@ use super::Sizing; /// ``` /// /// ## Parameters -/// - cells: `Content` (positional, variadic) The contents of the table cells. -/// -/// The cells are populated in row-major order. -/// -/// - rows: `TrackSizings` (named) Defines the row sizes. -/// -/// If there are more cells than fit the defined rows, the last row is -/// repeated until there are no more cells. -/// -/// - columns: `TrackSizings` (named) Defines the column sizes. -/// -/// Either specify a track size array or provide an integer to create a grid -/// with that many `{auto}`-sized columns. Note that opposed to rows and -/// gutters, providing a single track size will only ever create a single -/// column. -/// -/// - gutter: `TrackSizings` (named) Defines the gaps between rows & columns. +/// - gutter: `TrackSizings` (named) +/// Defines the gaps between rows & columns. /// /// If there are more gutters than defined sizes, the last gutter is repeated. /// -/// - column-gutter: `TrackSizings` (named) Defines the gaps between columns. -/// Takes precedence over `gutter`. -/// -/// - row-gutter: `TrackSizings` (named) Defines the gaps between rows. Takes -/// precedence over `gutter`. -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Grid +/// Category: layout +#[node(Layout)] pub struct GridNode { - /// Defines sizing for content rows and columns. - pub tracks: Axes<Vec<Sizing>>, - /// Defines sizing of gutter rows and columns between content. - pub gutter: Axes<Vec<Sizing>>, - /// The content to be arranged in a grid. + /// The contents of the table cells. + /// + /// The cells are populated in row-major order. + #[variadic] pub cells: Vec<Content>, -} -#[node] -impl GridNode { - 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()) - } + /// Defines the column sizes. + /// + /// Either specify a track size array or provide an integer to create a grid + /// with that many `{auto}`-sized columns. Note that opposed to rows and + /// gutters, providing a single track size will only ever create a single + /// column. + #[named] + #[default] + pub columns: TrackSizings, + + /// Defines the row sizes. + /// + /// If there are more cells than fit the defined rows, the last row is + /// repeated until there are no more cells. + #[named] + #[default] + pub rows: TrackSizings, + + /// Defines the gaps between columns. Takes precedence over `gutter`. + #[named] + #[shorthand(gutter)] + #[default] + pub column_gutter: TrackSizings, + + /// Defines the gaps between rows. Takes precedence over `gutter`. + #[named] + #[shorthand(gutter)] + #[default] + pub row_gutter: TrackSizings, } impl Layout for GridNode { @@ -129,11 +114,12 @@ impl Layout for GridNode { regions: Regions, ) -> SourceResult<Fragment> { // Prepare grid layout by unifying content and gutter tracks. + let cells = self.cells(); let layouter = GridLayouter::new( vt, - self.tracks.as_deref(), - self.gutter.as_deref(), - &self.cells, + Axes::new(&self.columns().0, &self.rows().0), + Axes::new(&self.column_gutter().0, &self.row_gutter().0), + &cells, regions, styles, ); @@ -147,18 +133,15 @@ impl Layout for GridNode { #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct TrackSizings(pub Vec<Sizing>); -castable! { +cast_from_value! { TrackSizings, sizing: Sizing => Self(vec![sizing]), count: NonZeroUsize => Self(vec![Sizing::Auto; count.get()]), values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?), } -castable! { - Sizing, - _: AutoValue => Self::Auto, - v: Rel<Length> => Self::Rel(v), - v: Fr => Self::Fr(v), +cast_to_value! { + v: TrackSizings => v.0.into() } /// Performs grid layout. diff --git a/library/src/layout/hide.rs b/library/src/layout/hide.rs index 019dd2a6..5ba7dea4 100644 --- a/library/src/layout/hide.rs +++ b/library/src/layout/hide.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -/// # Hide /// Hide content without affecting layout. /// /// The `hide` function allows you to hide content while the layout still 'sees' @@ -14,26 +13,18 @@ use crate::prelude::*; /// #hide[Hello] Joe /// ``` /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The content to hide. -/// -/// ## Category -/// layout -#[func] -#[capable(Show)] -#[derive(Debug, Hash)] -pub struct HideNode(pub Content); - -#[node] -impl HideNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - Ok(Self(args.expect("body")?).pack()) - } +/// Display: Hide +/// Category: layout +#[node(Show)] +pub struct HideNode { + /// The content to hide. + #[positional] + #[required] + pub body: Content, } impl Show for HideNode { fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> { - Ok(self.0.clone().styled(Meta::DATA, vec![Meta::Hidden])) + Ok(self.body().styled(MetaNode::DATA, vec![Meta::Hidden])) } } diff --git a/library/src/layout/list.rs b/library/src/layout/list.rs index e83b91ab..0ca5ccf6 100644 --- a/library/src/layout/list.rs +++ b/library/src/layout/list.rs @@ -1,8 +1,9 @@ -use crate::layout::{BlockNode, GridNode, ParNode, Sizing, Spacing}; +use crate::layout::{BlockNode, ParNode, Sizing, Spacing}; use crate::prelude::*; use crate::text::TextNode; -/// # Bullet List +use super::GridLayouter; + /// A bullet list. /// /// Displays a sequence of items vertically, with each item introduced by a @@ -33,48 +34,40 @@ use crate::text::TextNode; /// 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)] +/// Display: Bullet List +/// Category: layout +#[node(Layout)] pub struct ListNode { - /// If true, the items are separated by leading instead of list spacing. + /// The bullet 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 + /// ] + /// ``` + #[variadic] + pub items: Vec<ListItem>, + + /// 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. + /// ``` + #[named] + #[default(true)] pub tight: bool, - /// The individual bulleted or numbered items. - pub items: StyleVec<Content>, -} -#[node] -impl ListNode { /// The marker which introduces each item. /// /// Instead of plain content, you can also pass an array with multiple @@ -96,43 +89,35 @@ impl ListNode { /// - Items /// - Items /// ``` - #[property(referenced)] - pub const MARKER: Marker = Marker::Content(vec![]); + #[settable] + #[default(ListMarker::Content(vec![]))] + pub marker: ListMarker, /// The indent of each item's marker. - #[property(resolve)] - pub const INDENT: Length = Length::zero(); + #[settable] + #[resolve] + #[default] + pub indent: Length, /// The spacing between the marker and the body of each item. - #[property(resolve)] - pub const BODY_INDENT: Length = Em::new(0.5).into(); + #[settable] + #[resolve] + #[default(Em::new(0.5).into())] + pub body_indent: Length, /// 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; + #[settable] + #[default] + pub spacing: Smart<Spacing>, /// The nesting depth. - #[property(skip, fold)] - const DEPTH: Depth = 0; - - 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, - } - } + #[settable] + #[fold] + #[skip] + #[default] + depth: Depth, } impl Layout for ListNode { @@ -144,49 +129,65 @@ impl Layout for ListNode { ) -> SourceResult<Fragment> { let indent = styles.get(Self::INDENT); let body_indent = styles.get(Self::BODY_INDENT); - let gutter = if self.tight { + let gutter = if self.tight() { styles.get(ParNode::LEADING).into() } else { styles .get(Self::SPACING) - .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount) + .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount()) }; let depth = styles.get(Self::DEPTH); let marker = styles.get(Self::MARKER).resolve(vt.world(), depth)?; let mut cells = vec![]; - for (item, map) in self.items.iter() { + for item in self.items() { cells.push(Content::empty()); cells.push(marker.clone()); cells.push(Content::empty()); - cells.push( - item.clone().styled_with_map(map.clone()).styled(Self::DEPTH, Depth), - ); + cells.push(item.body().styled(Self::DEPTH, Depth)); } - GridNode { - tracks: Axes::with_x(vec![ + let layouter = GridLayouter::new( + vt, + Axes::with_x(&[ Sizing::Rel(indent.into()), Sizing::Auto, Sizing::Rel(body_indent.into()), Sizing::Auto, ]), - gutter: Axes::with_y(vec![gutter.into()]), - cells, - } - .layout(vt, styles, regions) + Axes::with_y(&[gutter.into()]), + &cells, + regions, + styles, + ); + + Ok(layouter.layout()?.fragment) } } +/// A bullet list item. +#[node] +pub struct ListItem { + /// The item's body. + #[positional] + #[required] + pub body: Content, +} + +cast_from_value! { + ListItem, + v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())), +} + /// A list's marker. #[derive(Debug, Clone, Hash)] -pub enum Marker { +pub enum ListMarker { Content(Vec<Content>), Func(Func), } -impl Marker { +impl ListMarker { /// Resolve the marker for the given depth. fn resolve(&self, world: Tracked<dyn World>, depth: usize) -> SourceResult<Content> { Ok(match self { @@ -203,8 +204,8 @@ impl Marker { } } -castable! { - Marker, +cast_from_value! { + ListMarker, v: Content => Self::Content(vec![v]), array: Array => { if array.len() == 0 { @@ -215,14 +216,28 @@ castable! { v: Func => Self::Func(v), } -#[derive(Debug, Clone, Hash)] +cast_to_value! { + v: ListMarker => match v { + ListMarker::Content(vec) => vec.into(), + ListMarker::Func(func) => func.into(), + } +} + struct Depth; +cast_from_value! { + Depth, + _: Value => Self, +} + +cast_to_value! { + _: Depth => Value::None +} + impl Fold for Depth { type Output = usize; - fn fold(self, mut outer: Self::Output) -> Self::Output { - outer += 1; - outer + fn fold(self, outer: Self::Output) -> Self::Output { + outer + 1 } } diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs index 9ee77a61..afdfd795 100644 --- a/library/src/layout/mod.rs +++ b/library/src/layout/mod.rs @@ -48,8 +48,8 @@ use std::mem; use typed_arena::Arena; use typst::diag::SourceResult; use typst::model::{ - applicable, capability, realize, Content, Node, SequenceNode, Style, StyleChain, - StyleVecBuilder, StyledNode, + applicable, realize, Content, Node, SequenceNode, Style, StyleChain, StyleVecBuilder, + StyledNode, }; use crate::math::{FormulaNode, LayoutMath}; @@ -60,7 +60,6 @@ use crate::text::{LinebreakNode, SmartQuoteNode, SpaceNode, TextNode}; use crate::visualize::{CircleNode, EllipseNode, ImageNode, RectNode, SquareNode}; /// Root-level layout. -#[capability] pub trait LayoutRoot { /// Layout into one frame per page. fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document>; @@ -96,7 +95,6 @@ impl LayoutRoot for Content { } /// Layout into regions. -#[capability] pub trait Layout { /// Layout into one frame per region. fn layout( @@ -160,7 +158,7 @@ fn realize_root<'a>( builder.accept(content, styles)?; builder.interrupt_page(Some(styles))?; let (pages, shared) = builder.doc.unwrap().pages.finish(); - Ok((DocumentNode(pages).pack(), shared)) + Ok((DocumentNode::new(pages.to_vec()).pack(), shared)) } /// Realize into a node that is capable of block-level layout. @@ -185,7 +183,7 @@ fn realize_block<'a>( builder.accept(content, styles)?; builder.interrupt_par()?; let (children, shared) = builder.flow.0.finish(); - Ok((FlowNode(children).pack(), shared)) + Ok((FlowNode::new(children.to_vec()).pack(), shared)) } /// Builds a document or a flow node from content. @@ -211,6 +209,7 @@ struct Scratch<'a> { styles: Arena<StyleChain<'a>>, /// An arena where intermediate content resulting from show rules is stored. content: Arena<Content>, + maps: Arena<StyleMap>, } impl<'a, 'v, 't> Builder<'a, 'v, 't> { @@ -231,10 +230,8 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { styles: StyleChain<'a>, ) -> SourceResult<()> { if content.has::<dyn LayoutMath>() && !content.is::<FormulaNode>() { - content = self - .scratch - .content - .alloc(FormulaNode { body: content.clone(), block: false }.pack()); + content = + self.scratch.content.alloc(FormulaNode::new(content.clone()).pack()); } // Prepare only if this is the first application for this node. @@ -252,8 +249,9 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { } if let Some(seq) = content.to::<SequenceNode>() { - for sub in &seq.0 { - self.accept(sub, styles)?; + for sub in seq.children() { + let stored = self.scratch.content.alloc(sub); + self.accept(stored, styles)?; } return Ok(()); } @@ -269,8 +267,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { self.interrupt_list()?; - if content.is::<ListItem>() { - self.list.accept(content, styles); + if self.list.accept(content, styles) { return Ok(()); } @@ -286,7 +283,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { let keep = content .to::<PagebreakNode>() - .map_or(false, |pagebreak| !pagebreak.weak); + .map_or(false, |pagebreak| !pagebreak.weak()); self.interrupt_page(keep.then(|| styles))?; @@ -308,11 +305,13 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { styled: &'a StyledNode, styles: StyleChain<'a>, ) -> SourceResult<()> { + let map = self.scratch.maps.alloc(styled.map()); let stored = self.scratch.styles.alloc(styles); - let styles = stored.chain(&styled.map); - self.interrupt_style(&styled.map, None)?; - self.accept(&styled.sub, styles)?; - self.interrupt_style(&styled.map, Some(styles))?; + let content = self.scratch.content.alloc(styled.sub()); + let styles = stored.chain(map); + self.interrupt_style(&map, None)?; + self.accept(content, styles)?; + self.interrupt_style(map, Some(styles))?; Ok(()) } @@ -381,7 +380,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { let (flow, shared) = mem::take(&mut self.flow).0.finish(); let styles = if shared == StyleChain::default() { styles.unwrap() } else { shared }; - let page = PageNode(FlowNode(flow).pack()).pack(); + let page = PageNode::new(FlowNode::new(flow.to_vec()).pack()).pack(); let stored = self.scratch.content.alloc(page); self.accept(stored, styles)?; } @@ -392,7 +391,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { /// Accepts pagebreaks and pages. struct DocBuilder<'a> { /// The page runs built so far. - pages: StyleVecBuilder<'a, PageNode>, + pages: StyleVecBuilder<'a, Content>, /// Whether to keep a following page even if it is empty. keep_next: bool, } @@ -400,12 +399,12 @@ struct DocBuilder<'a> { impl<'a> DocBuilder<'a> { fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool { if let Some(pagebreak) = content.to::<PagebreakNode>() { - self.keep_next = !pagebreak.weak; + self.keep_next = !pagebreak.weak(); return true; } - if let Some(page) = content.to::<PageNode>() { - self.pages.push(page.clone(), styles); + if content.is::<PageNode>() { + self.pages.push(content.clone(), styles); self.keep_next = false; return true; } @@ -441,11 +440,11 @@ impl<'a> FlowBuilder<'a> { if content.has::<dyn Layout>() || content.is::<ParNode>() { let is_tight_list = if let Some(node) = content.to::<ListNode>() { - node.tight + node.tight() } else if let Some(node) = content.to::<EnumNode>() { - node.tight + node.tight() } else if let Some(node) = content.to::<TermsNode>() { - node.tight + node.tight() } else { false }; @@ -458,9 +457,9 @@ impl<'a> FlowBuilder<'a> { let above = styles.get(BlockNode::ABOVE); let below = styles.get(BlockNode::BELOW); - self.0.push(above.pack(), styles); + self.0.push(above.clone().pack(), styles); self.0.push(content.clone(), styles); - self.0.push(below.pack(), styles); + self.0.push(below.clone().pack(), styles); return true; } @@ -479,7 +478,7 @@ impl<'a> ParBuilder<'a> { || content.is::<HNode>() || content.is::<LinebreakNode>() || content.is::<SmartQuoteNode>() - || content.to::<FormulaNode>().map_or(false, |node| !node.block) + || content.to::<FormulaNode>().map_or(false, |node| !node.block()) || content.is::<BoxNode>() { self.0.push(content.clone(), styles); @@ -491,14 +490,14 @@ impl<'a> ParBuilder<'a> { fn finish(self) -> (Content, StyleChain<'a>) { let (children, shared) = self.0.finish(); - (ParNode(children).pack(), shared) + (ParNode::new(children.to_vec()).pack(), shared) } } /// Accepts list / enum items, spaces, paragraph breaks. struct ListBuilder<'a> { /// The list items collected so far. - items: StyleVecBuilder<'a, ListItem>, + items: StyleVecBuilder<'a, Content>, /// Whether the list contains no paragraph breaks. tight: bool, /// Trailing content for which it is unclear whether it is part of the list. @@ -514,14 +513,18 @@ impl<'a> ListBuilder<'a> { return true; } - if let Some(item) = content.to::<ListItem>() { - if self.items.items().next().map_or(true, |first| { - std::mem::discriminant(item) == std::mem::discriminant(first) - }) { - self.items.push(item.clone(), styles); - self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakNode>()); - return true; - } + if (content.is::<ListItem>() + || content.is::<EnumItem>() + || content.is::<TermItem>()) + && self + .items + .items() + .next() + .map_or(true, |first| first.id() == content.id()) + { + self.items.push(content.clone(), styles); + self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakNode>()); + return true; } false @@ -530,31 +533,48 @@ impl<'a> ListBuilder<'a> { fn finish(self) -> (Content, StyleChain<'a>) { let (items, shared) = self.items.finish(); let item = items.items().next().unwrap(); - let output = match item { - ListItem::List(_) => ListNode { - tight: self.tight, - items: items.map(|item| match item { - ListItem::List(item) => item.clone(), - _ => panic!("wrong list item"), - }), - } - .pack(), - ListItem::Enum(..) => EnumNode { - tight: self.tight, - items: items.map(|item| match item { - ListItem::Enum(number, body) => (*number, body.clone()), - _ => panic!("wrong list item"), - }), - } - .pack(), - ListItem::Term(_) => TermsNode { - tight: self.tight, - items: items.map(|item| match item { - ListItem::Term(item) => item.clone(), - _ => panic!("wrong list item"), - }), - } - .pack(), + let output = if item.is::<ListItem>() { + ListNode::new( + items + .iter() + .map(|(item, map)| { + let item = item.to::<ListItem>().unwrap(); + ListItem::new(item.body().styled_with_map(map.clone())) + }) + .collect::<Vec<_>>(), + ) + .with_tight(self.tight) + .pack() + } else if item.is::<EnumItem>() { + EnumNode::new( + items + .iter() + .map(|(item, map)| { + let item = item.to::<EnumItem>().unwrap(); + EnumItem::new(item.body().styled_with_map(map.clone())) + .with_number(item.number()) + }) + .collect::<Vec<_>>(), + ) + .with_tight(self.tight) + .pack() + } else if item.is::<TermItem>() { + TermsNode::new( + items + .iter() + .map(|(item, map)| { + let item = item.to::<TermItem>().unwrap(); + TermItem::new( + item.term().styled_with_map(map.clone()), + item.description().styled_with_map(map.clone()), + ) + }) + .collect::<Vec<_>>(), + ) + .with_tight(self.tight) + .pack() + } else { + unreachable!() }; (output, shared) } @@ -569,18 +589,3 @@ impl Default for ListBuilder<'_> { } } } - -/// An item in a list. -#[capable] -#[derive(Debug, Clone, Hash)] -pub enum ListItem { - /// An item of a bullet list. - List(Content), - /// An item of a numbered list. - Enum(Option<NonZeroUsize>, Content), - /// An item of a term list. - Term(TermItem), -} - -#[node] -impl ListItem {} diff --git a/library/src/layout/pad.rs b/library/src/layout/pad.rs index 4fc2ff29..05aafc76 100644 --- a/library/src/layout/pad.rs +++ b/library/src/layout/pad.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -/// # Padding /// Add spacing around content. /// /// The `pad` function adds spacing around content. The spacing can be specified @@ -17,21 +16,6 @@ use crate::prelude::*; /// ``` /// /// ## Parameters -/// - body: `Content` (positional, required) -/// The content to pad at the sides. -/// -/// - left: `Rel<Length>` (named) -/// The padding at the left side. -/// -/// - right: `Rel<Length>` (named) -/// The padding at the right side. -/// -/// - top: `Rel<Length>` (named) -/// The padding at the top side. -/// -/// - bottom: `Rel<Length>` (named) -/// The padding at the bottom side. -/// /// - x: `Rel<Length>` (named) /// The horizontal padding. Both `left` and `right` take precedence over this. /// @@ -41,20 +25,37 @@ use crate::prelude::*; /// - rest: `Rel<Length>` (named) /// The padding for all sides. All other parameters take precedence over this. /// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Padding +/// Category: layout +#[node(Construct, Layout)] pub struct PadNode { - /// The amount of padding. - pub padding: Sides<Rel<Length>>, - /// The content whose sides to pad. + /// The content to pad at the sides. + #[positional] + #[required] pub body: Content, + + /// The padding at the left side. + #[named] + #[default] + pub left: Rel<Length>, + + /// The padding at the right side. + #[named] + #[default] + pub right: Rel<Length>, + + /// The padding at the top side. + #[named] + #[default] + pub top: Rel<Length>, + + /// The padding at the bottom side. + #[named] + #[default] + pub bottom: Rel<Length>, } -#[node] -impl PadNode { +impl Construct for PadNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { let all = args.named("rest")?.or(args.find()?); let x = args.named("x")?; @@ -64,8 +65,12 @@ impl PadNode { let right = args.named("right")?.or(x).or(all).unwrap_or_default(); let bottom = args.named("bottom")?.or(y).or(all).unwrap_or_default(); let body = args.expect::<Content>("body")?; - let padding = Sides::new(left, top, right, bottom); - Ok(Self { padding, body }.pack()) + Ok(Self::new(body) + .with_left(left) + .with_top(top) + .with_bottom(bottom) + .with_right(right) + .pack()) } } @@ -79,9 +84,10 @@ impl Layout for PadNode { let mut backlog = vec![]; // Layout child into padded regions. - let padding = self.padding.resolve(styles); + let sides = Sides::new(self.left(), self.top(), self.right(), self.bottom()); + let padding = sides.resolve(styles); let pod = regions.map(&mut backlog, |size| shrink(size, padding)); - let mut fragment = self.body.layout(vt, styles, pod)?; + let mut fragment = self.body().layout(vt, styles, pod)?; for frame in &mut fragment { // Apply the padding inversely such that the grown size padded diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs index 022619d7..5d1d530d 100644 --- a/library/src/layout/page.rs +++ b/library/src/layout/page.rs @@ -3,7 +3,6 @@ use std::str::FromStr; use super::ColumnsNode; use crate::prelude::*; -/// # Page /// Layouts its child onto one or multiple pages. /// /// Although this function is primarily used in set rules to affect page @@ -14,13 +13,6 @@ use crate::prelude::*; /// the pages will grow to fit their content on the respective axis. /// /// ## Parameters -/// - body: `Content` (positional, required) -/// The contents of the page(s). -/// -/// Multiple pages will be created if the content does not fit on a single -/// page. A new page with the page properties prior to the function invocation -/// will be created after the body has been typeset. -/// /// - paper: `Paper` (positional, settable) /// A standard paper size to set width and height. When this is not specified, /// Typst defaults to `{"a4"}` paper. @@ -33,15 +25,25 @@ use crate::prelude::*; /// There you go, US friends! /// ``` /// -/// ## Category -/// layout -#[func] -#[capable] -#[derive(Clone, Hash)] -pub struct PageNode(pub Content); - +/// Display: Page +/// Category: layout #[node] -impl PageNode { +#[set({ + if let Some(paper) = args.named_or_find::<Paper>("paper")? { + styles.set(Self::WIDTH, Smart::Custom(paper.width().into())); + styles.set(Self::HEIGHT, Smart::Custom(paper.height().into())); + } +})] +pub struct PageNode { + /// The contents of the page(s). + /// + /// Multiple pages will be created if the content does not fit on a single + /// page. A new page with the page properties prior to the function invocation + /// will be created after the body has been typeset. + #[positional] + #[required] + pub body: Content, + /// The width of the page. /// /// ```example @@ -54,8 +56,10 @@ impl PageNode { /// box(square(width: 1cm)) /// } /// ``` - #[property(resolve)] - pub const WIDTH: Smart<Length> = Smart::Custom(Paper::A4.width().into()); + #[settable] + #[resolve] + #[default(Smart::Custom(Paper::A4.width().into()))] + pub width: Smart<Length>, /// The height of the page. /// @@ -63,8 +67,10 @@ impl PageNode { /// by inserting a [page break]($func/pagebreak). Most examples throughout /// this documentation use `{auto}` for the height of the page to /// dynamically grow and shrink to fit their content. - #[property(resolve)] - pub const HEIGHT: Smart<Length> = Smart::Custom(Paper::A4.height().into()); + #[settable] + #[resolve] + #[default(Smart::Custom(Paper::A4.height().into()))] + pub height: Smart<Length>, /// Whether the page is flipped into landscape orientation. /// @@ -84,7 +90,9 @@ impl PageNode { /// New York, NY 10001 \ /// +1 555 555 5555 /// ``` - pub const FLIPPED: bool = false; + #[settable] + #[default(false)] + pub flipped: bool, /// The page's margins. /// @@ -114,8 +122,10 @@ impl PageNode { /// fill: aqua, /// ) /// ``` - #[property(fold)] - pub const MARGIN: Sides<Option<Smart<Rel<Length>>>> = Sides::splat(Smart::Auto); + #[settable] + #[fold] + #[default] + pub margin: Sides<Option<Smart<Rel<Length>>>>, /// How many columns the page has. /// @@ -131,7 +141,9 @@ impl PageNode { /// emissions and mitigate the impacts /// of a rapidly changing climate. /// ``` - pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); + #[settable] + #[default(NonZeroUsize::new(1).unwrap())] + pub columns: NonZeroUsize, /// The page's background color. /// @@ -145,7 +157,9 @@ impl PageNode { /// #set text(fill: rgb("fdfdfd")) /// *Dark mode enabled.* /// ``` - pub const FILL: Option<Paint> = None; + #[settable] + #[default] + pub fill: Option<Paint>, /// The page's header. /// @@ -166,8 +180,9 @@ impl PageNode { /// /// #lorem(18) /// ``` - #[property(referenced)] - pub const HEADER: Option<Marginal> = None; + #[settable] + #[default] + pub header: Option<Marginal>, /// The page's footer. /// @@ -190,8 +205,9 @@ impl PageNode { /// /// #lorem(18) /// ``` - #[property(referenced)] - pub const FOOTER: Option<Marginal> = None; + #[settable] + #[default] + pub footer: Option<Marginal>, /// Content in the page's background. /// @@ -211,8 +227,9 @@ impl PageNode { /// In the year 2023, we plan to take over the world /// (of typesetting). /// ``` - #[property(referenced)] - pub const BACKGROUND: Option<Marginal> = None; + #[settable] + #[default] + pub background: Option<Marginal>, /// Content in the page's foreground. /// @@ -228,26 +245,9 @@ impl PageNode { /// "Weak Reject" because they did /// not understand our approach... /// ``` - #[property(referenced)] - pub const FOREGROUND: Option<Marginal> = None; - - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - Ok(Self(args.expect("body")?).pack()) - } - - fn set(...) { - if let Some(paper) = args.named_or_find::<Paper>("paper")? { - styles.set(Self::WIDTH, Smart::Custom(paper.width().into())); - styles.set(Self::HEIGHT, Smart::Custom(paper.height().into())); - } - } - - fn field(&self, name: &str) -> Option<Value> { - match name { - "body" => Some(Value::Content(self.0.clone())), - _ => None, - } - } + #[settable] + #[default] + pub foreground: Option<Marginal>, } impl PageNode { @@ -276,26 +276,22 @@ impl PageNode { let default = Rel::from(0.1190 * min); let padding = styles.get(Self::MARGIN).map(|side| side.unwrap_or(default)); - let mut child = self.0.clone(); + let mut child = self.body(); // Realize columns. let columns = styles.get(Self::COLUMNS); if columns.get() > 1 { - child = ColumnsNode { count: columns, body: self.0.clone() }.pack(); + child = ColumnsNode::new(columns, child).pack(); } // Realize margins. child = child.padded(padding); - // Realize background fill. - if let Some(fill) = styles.get(Self::FILL) { - child = child.filled(fill); - } - // Layout the child. let regions = Regions::repeat(size, size.map(Abs::is_finite)); let mut fragment = child.layout(vt, styles, regions)?; + let fill = styles.get(Self::FILL); let header = styles.get(Self::HEADER); let footer = styles.get(Self::FOOTER); let foreground = styles.get(Self::FOREGROUND); @@ -303,17 +299,21 @@ impl PageNode { // Realize overlays. for frame in &mut fragment { + if let Some(fill) = fill { + frame.fill(fill); + } + let size = frame.size(); let pad = padding.resolve(styles).relative_to(size); let pw = size.x - pad.left - pad.right; let py = size.y - pad.bottom; for (marginal, pos, area) in [ - (header, Point::with_x(pad.left), Size::new(pw, pad.top)), - (footer, Point::new(pad.left, py), Size::new(pw, pad.bottom)), - (foreground, Point::zero(), size), - (background, Point::zero(), size), + (&header, Point::with_x(pad.left), Size::new(pw, pad.top)), + (&footer, Point::new(pad.left, py), Size::new(pw, pad.bottom)), + (&foreground, Point::zero(), size), + (&background, Point::zero(), size), ] { - let in_background = std::ptr::eq(marginal, background); + let in_background = std::ptr::eq(marginal, &background); let Some(marginal) = marginal else { continue }; let content = marginal.resolve(vt, page)?; let pod = Regions::one(area, Axes::splat(true)); @@ -332,15 +332,6 @@ impl PageNode { } } -impl Debug for PageNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Page(")?; - self.0.fmt(f)?; - f.write_str(")") - } -} - -/// # Page Break /// A manual page break. /// /// Must not be used inside any containers. @@ -355,28 +346,17 @@ impl Debug for PageNode { /// In 1984, the first ... /// ``` /// -/// ## Parameters -/// - weak: `bool` (named) -/// If `{true}`, the page break is skipped if the current page is already -/// empty. -/// -/// ## Category -/// layout -#[func] -#[capable] -#[derive(Debug, Copy, Clone, Hash)] +/// Display: Page Break +/// Category: layout +#[node] pub struct PagebreakNode { + /// If `{true}`, the page break is skipped if the current page is already + /// empty. + #[named] + #[default(false)] pub weak: bool, } -#[node] -impl PagebreakNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - let weak = args.named("weak")?.unwrap_or(false); - Ok(Self { weak }.pack()) - } -} - /// A header, footer, foreground or background definition. #[derive(Debug, Clone, Hash)] pub enum Marginal { @@ -399,12 +379,19 @@ impl Marginal { } } -castable! { +cast_from_value! { Marginal, v: Content => Self::Content(v), v: Func => Self::Func(v), } +cast_to_value! { + v: Marginal => match v { + Marginal::Content(v) => v.into(), + Marginal::Func(v) => v.into(), + } +} + /// Specification of a paper. #[derive(Debug, Copy, Clone, Hash)] pub struct Paper { @@ -450,7 +437,7 @@ macro_rules! papers { } } - castable! { + cast_from_value! { Paper, $( /// Produces a paper of the respective size. diff --git a/library/src/layout/par.rs b/library/src/layout/par.rs index 64a6c513..1b554d62 100644 --- a/library/src/layout/par.rs +++ b/library/src/layout/par.rs @@ -2,7 +2,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_script::{Script, UnicodeScript}; use xi_unicode::LineBreakIterator; -use typst::model::Key; +use typst::model::{Key, StyledNode}; use super::{BoxNode, HNode, Sizing, Spacing}; use crate::layout::AlignNode; @@ -12,7 +12,6 @@ use crate::text::{ shape, LinebreakNode, Quoter, Quotes, ShapedText, SmartQuoteNode, SpaceNode, TextNode, }; -/// # Paragraph /// Arrange text, spacing and inline-level nodes into a paragraph. /// /// Although this function is primarily used in set rules to affect paragraph @@ -40,15 +39,14 @@ use crate::text::{ /// - body: `Content` (positional, required) /// The contents of the paragraph. /// -/// ## Category -/// layout -#[func] -#[capable] -#[derive(Hash)] -pub struct ParNode(pub StyleVec<Content>); - -#[node] -impl ParNode { +/// Display: Paragraph +/// Category: layout +#[node(Construct)] +pub struct ParNode { + /// The paragraph's children. + #[variadic] + pub children: Vec<Content>, + /// The indent the first line of a consecutive paragraph should have. /// /// The first paragraph on a page will never be indented. @@ -57,14 +55,18 @@ impl ParNode { /// space between paragraphs or indented first lines. Consider turning the /// [paragraph spacing]($func/block.spacing) off when using this property /// (e.g. using `[#show par: set block(spacing: 0pt)]`). - #[property(resolve)] - pub const INDENT: Length = Length::zero(); + #[settable] + #[resolve] + #[default] + pub indent: Length, /// The spacing between lines. /// /// The default value is `{0.65em}`. - #[property(resolve)] - pub const LEADING: Length = Em::new(0.65).into(); + #[settable] + #[resolve] + #[default(Em::new(0.65).into())] + pub leading: Length, /// Whether to justify text in its line. /// @@ -75,7 +77,9 @@ impl ParNode { /// Note that the current [alignment]($func/align) still has an effect on /// the placement of the last line except if it ends with a [justified line /// break]($func/linebreak.justify). - pub const JUSTIFY: bool = false; + #[settable] + #[default(false)] + pub justify: bool, /// How to determine line breaks. /// @@ -100,16 +104,20 @@ impl ParNode { /// very aesthetic example is one /// of them. /// ``` - pub const LINEBREAKS: Smart<Linebreaks> = Smart::Auto; + #[settable] + #[default] + pub linebreaks: Smart<Linebreaks>, +} +impl Construct for ParNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { // The paragraph constructor is special: It doesn't create a paragraph // node. Instead, it just ensures that the passed content lives in a // separate paragraph and styles it. Ok(Content::sequence(vec![ - ParbreakNode.pack(), + ParbreakNode::new().pack(), args.expect("body")?, - ParbreakNode.pack(), + ParbreakNode::new().pack(), ])) } } @@ -136,14 +144,15 @@ impl ParNode { expand: bool, ) -> SourceResult<Fragment> { let mut vt = Vt { world, provider, introspector }; + let children = par.children(); // Collect all text into one string for BiDi analysis. - let (text, segments) = collect(par, &styles, consecutive); + let (text, segments) = collect(&children, &styles, consecutive)?; // Perform BiDi analysis and then prepare paragraph layout by building a // representation on which we can do line breaking without layouting // each and every line from scratch. - let p = prepare(&mut vt, par, &text, segments, styles, region)?; + let p = prepare(&mut vt, &children, &text, segments, styles, region)?; // Break the paragraph into lines. let lines = linebreak(&vt, &p, region.x); @@ -165,18 +174,11 @@ impl ParNode { } } -impl Debug for ParNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Par ")?; - self.0.fmt(f) - } -} - /// A horizontal alignment. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct HorizontalAlign(pub GenAlign); -castable! { +cast_from_value! { HorizontalAlign, align: GenAlign => match align.axis() { Axis::X => Self(align), @@ -201,7 +203,7 @@ pub enum Linebreaks { Optimized, } -castable! { +cast_from_value! { Linebreaks, /// Determine the line breaks in a simple first-fit style. "simple" => Self::Simple, @@ -212,7 +214,13 @@ castable! { "optimized" => Self::Optimized, } -/// # Paragraph Break +cast_to_value! { + v: Linebreaks => Value::from(match v { + Linebreaks::Simple => "simple", + Linebreaks::Optimized => "optimized", + }) +} + /// A paragraph break. /// /// This starts a new paragraph. Especially useful when used within code like @@ -232,19 +240,10 @@ castable! { /// Instead of calling this function, you can insert a blank line into your /// markup to create a paragraph break. /// -/// ## Category -/// layout -#[func] -#[capable(Unlabellable)] -#[derive(Debug, Hash)] -pub struct ParbreakNode; - -#[node] -impl ParbreakNode { - fn construct(_: &Vm, _: &mut Args) -> SourceResult<Content> { - Ok(Self.pack()) - } -} +/// Display: Paragraph Break +/// Category: layout +#[node(Unlabellable)] +pub struct ParbreakNode {} impl Unlabellable for ParbreakNode {} @@ -343,7 +342,7 @@ impl Segment<'_> { match *self { Self::Text(len) => len, Self::Spacing(_) => SPACING_REPLACE.len_utf8(), - Self::Box(node) if node.width.is_fractional() => SPACING_REPLACE.len_utf8(), + Self::Box(node) if node.width().is_fractional() => SPACING_REPLACE.len_utf8(), Self::Formula(_) | Self::Box(_) => NODE_REPLACE.len_utf8(), } } @@ -485,21 +484,20 @@ impl<'a> Line<'a> { /// Collect all text of the paragraph into one string. This also performs /// string-level preprocessing like case transformations. fn collect<'a>( - par: &'a ParNode, + children: &'a [Content], styles: &'a StyleChain<'a>, consecutive: bool, -) -> (String, Vec<(Segment<'a>, StyleChain<'a>)>) { +) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>)> { let mut full = String::new(); let mut quoter = Quoter::new(); let mut segments = vec![]; - let mut iter = par.0.iter().peekable(); + let mut iter = children.iter().peekable(); if consecutive { let indent = styles.get(ParNode::INDENT); if !indent.is_zero() - && par - .0 - .items() + && children + .iter() .find_map(|child| { if child.with::<dyn Behave>().map_or(false, |behaved| { behaved.behaviour() == Behaviour::Ignorant @@ -518,24 +516,30 @@ fn collect<'a>( } } - while let Some((child, map)) = iter.next() { - let styles = styles.chain(map); + while let Some(mut child) = iter.next() { + let outer = styles; + let mut styles = *styles; + if let Some(node) = child.to::<StyledNode>() { + child = Box::leak(Box::new(node.sub())); + styles = outer.chain(Box::leak(Box::new(node.map()))); + } + let segment = if child.is::<SpaceNode>() { full.push(' '); Segment::Text(1) } else if let Some(node) = child.to::<TextNode>() { let prev = full.len(); if let Some(case) = styles.get(TextNode::CASE) { - full.push_str(&case.apply(&node.0)); + full.push_str(&case.apply(&node.text())); } else { - full.push_str(&node.0); + full.push_str(&node.text()); } Segment::Text(full.len() - prev) - } else if let Some(&node) = child.to::<HNode>() { + } else if let Some(node) = child.to::<HNode>() { full.push(SPACING_REPLACE); - Segment::Spacing(node.amount) + Segment::Spacing(node.amount()) } else if let Some(node) = child.to::<LinebreakNode>() { - let c = if node.justify { '\u{2028}' } else { '\n' }; + let c = if node.justify() { '\u{2028}' } else { '\n' }; full.push(c); Segment::Text(c.len_utf8()) } else if let Some(node) = child.to::<SmartQuoteNode>() { @@ -544,9 +548,9 @@ fn collect<'a>( let lang = styles.get(TextNode::LANG); let region = styles.get(TextNode::REGION); let quotes = Quotes::from_lang(lang, region); - let peeked = iter.peek().and_then(|(child, _)| { + let peeked = iter.peek().and_then(|child| { if let Some(node) = child.to::<TextNode>() { - node.0.chars().next() + node.text().chars().next() } else if child.is::<SmartQuoteNode>() { Some('"') } else if child.is::<SpaceNode>() || child.is::<HNode>() { @@ -556,23 +560,25 @@ fn collect<'a>( } }); - full.push_str(quoter.quote("es, node.double, peeked)); + full.push_str(quoter.quote("es, node.double(), peeked)); } else { - full.push(if node.double { '"' } else { '\'' }); + full.push(if node.double() { '"' } else { '\'' }); } Segment::Text(full.len() - prev) } else if let Some(node) = child.to::<FormulaNode>() { full.push(NODE_REPLACE); Segment::Formula(node) } else if let Some(node) = child.to::<BoxNode>() { - full.push(if node.width.is_fractional() { + full.push(if node.width().is_fractional() { SPACING_REPLACE } else { NODE_REPLACE }); Segment::Box(node) + } else if let Some(span) = child.span() { + bail!(span, "unexpected document child"); } else { - panic!("unexpected par child: {child:?}"); + continue; }; if let Some(last) = full.chars().last() { @@ -591,14 +597,14 @@ fn collect<'a>( segments.push((segment, styles)); } - (full, segments) + Ok((full, segments)) } /// Prepare paragraph layout by shaping the whole paragraph and layouting all /// contained inline-level content. fn prepare<'a>( vt: &mut Vt, - par: &'a ParNode, + children: &'a [Content], text: &'a str, segments: Vec<(Segment<'a>, StyleChain<'a>)>, styles: StyleChain<'a>, @@ -639,7 +645,7 @@ fn prepare<'a>( items.push(Item::Frame(frame)); } Segment::Box(node) => { - if let Sizing::Fr(v) = node.width { + if let Sizing::Fr(v) = node.width() { items.push(Item::Fractional(v, Some((node, styles)))); } else { let pod = Regions::one(region, Axes::splat(false)); @@ -657,9 +663,9 @@ fn prepare<'a>( bidi, items, styles, - hyphenate: shared_get(styles, &par.0, TextNode::HYPHENATE), - lang: shared_get(styles, &par.0, TextNode::LANG), - align: styles.get(AlignNode::ALIGNS).x.resolve(styles), + hyphenate: shared_get(styles, children, TextNode::HYPHENATE), + lang: shared_get(styles, children, TextNode::LANG), + align: styles.get(AlignNode::ALIGNMENT).x.resolve(styles), justify: styles.get(ParNode::JUSTIFY), }) } @@ -722,12 +728,13 @@ fn is_compatible(a: Script, b: Script) -> bool { /// paragraph. fn shared_get<'a, K: Key>( styles: StyleChain<'a>, - children: &StyleVec<Content>, + children: &[Content], key: K, -) -> Option<K::Output<'a>> { +) -> Option<K::Output> { children - .styles() - .all(|map| !map.contains(key)) + .iter() + .filter_map(|child| child.to::<StyledNode>()) + .all(|node| !node.map().contains(key)) .then(|| styles.get(key)) } diff --git a/library/src/layout/place.rs b/library/src/layout/place.rs index 05de369b..b4aaf73d 100644 --- a/library/src/layout/place.rs +++ b/library/src/layout/place.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -/// # Place /// Place content at an absolute position. /// /// Placed content will not affect the position of other content. Place is @@ -22,48 +21,41 @@ use crate::prelude::*; /// ) /// ``` /// -/// ## Parameters -/// - alignment: `Axes<Option<GenAlign>>` (positional) -/// Relative to which position in the parent container to place the content. -/// -/// When an axis of the page is `{auto}` sized, all alignments relative to that -/// axis will be ignored, instead, the item will be placed in the origin of the -/// axis. -/// -/// - body: `Content` (positional, required) -/// The content to place. -/// -/// - dx: `Rel<Length>` (named) -/// The horizontal displacement of the placed content. -/// -/// ```example -/// #set page(height: 100pt) -/// #for i in range(16) { -/// let amount = i * 4pt -/// place(center, dx: amount - 32pt, dy: amount)[A] -/// } -/// ``` -/// -/// - dy: `Rel<Length>` (named) -/// The vertical displacement of the placed content. -/// -/// ## Category -/// layout -#[func] -#[capable(Layout, Behave)] -#[derive(Debug, Hash)] -pub struct PlaceNode(pub Content, bool); +/// Display: Place +/// Category: layout +#[node(Layout, Behave)] +pub struct PlaceNode { + /// Relative to which position in the parent container to place the content. + /// + /// When an axis of the page is `{auto}` sized, all alignments relative to that + /// axis will be ignored, instead, the item will be placed in the origin of the + /// axis. + #[positional] + #[default(Axes::with_x(Some(GenAlign::Start)))] + pub alignment: Axes<Option<GenAlign>>, -#[node] -impl PlaceNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - let aligns = args.find()?.unwrap_or(Axes::with_x(Some(GenAlign::Start))); - let dx = args.named("dx")?.unwrap_or_default(); - let dy = args.named("dy")?.unwrap_or_default(); - let body = args.expect::<Content>("body")?; - let out_of_flow = aligns.y.is_some(); - Ok(Self(body.moved(Axes::new(dx, dy)).aligned(aligns), out_of_flow).pack()) - } + /// The content to place. + #[positional] + #[required] + pub body: Content, + + /// The horizontal displacement of the placed content. + /// + /// ```example + /// #set page(height: 100pt) + /// #for i in range(16) { + /// let amount = i * 4pt + /// place(center, dx: amount - 32pt, dy: amount)[A] + /// } + /// ``` + #[named] + #[default] + pub dx: Rel<Length>, + + /// The vertical displacement of the placed content. + #[named] + #[default] + pub dy: Rel<Length>, } impl Layout for PlaceNode { @@ -83,7 +75,12 @@ impl Layout for PlaceNode { Regions::one(regions.base(), expand) }; - let mut frame = self.0.layout(vt, styles, pod)?.into_frame(); + let child = self + .body() + .moved(Axes::new(self.dx(), self.dy())) + .aligned(self.alignment()); + + let mut frame = child.layout(vt, styles, pod)?.into_frame(); // If expansion is off, zero all sizes so that we don't take up any // space in our parent. Otherwise, respect the expand settings. @@ -99,7 +96,7 @@ impl PlaceNode { /// origin. Instead of relative to the parent's current flow/cursor /// position. pub fn out_of_flow(&self) -> bool { - self.1 + self.alignment().y.is_some() } } diff --git a/library/src/layout/repeat.rs b/library/src/layout/repeat.rs index ec582c28..67dca285 100644 --- a/library/src/layout/repeat.rs +++ b/library/src/layout/repeat.rs @@ -2,7 +2,6 @@ use crate::prelude::*; use super::AlignNode; -/// # Repeat /// Repeats content to the available space. /// /// This can be useful when implementing a custom index, reference, or outline. @@ -22,22 +21,14 @@ use super::AlignNode; /// ] /// ``` /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The content to repeat. -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] -pub struct RepeatNode(pub Content); - -#[node] -impl RepeatNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - Ok(Self(args.expect("body")?).pack()) - } +/// Display: Repeat +/// Category: layout +#[node(Layout)] +pub struct RepeatNode { + /// The content to repeat. + #[positional] + #[required] + pub body: Content, } impl Layout for RepeatNode { @@ -48,8 +39,8 @@ impl Layout for RepeatNode { regions: Regions, ) -> SourceResult<Fragment> { let pod = Regions::one(regions.size, Axes::new(false, false)); - let piece = self.0.layout(vt, styles, pod)?.into_frame(); - let align = styles.get(AlignNode::ALIGNS).x.resolve(styles); + let piece = self.body().layout(vt, styles, pod)?.into_frame(); + let align = styles.get(AlignNode::ALIGNMENT).x.resolve(styles); let fill = regions.size.x; let width = piece.width(); diff --git a/library/src/layout/spacing.rs b/library/src/layout/spacing.rs index a94e48da..94517ad5 100644 --- a/library/src/layout/spacing.rs +++ b/library/src/layout/spacing.rs @@ -2,7 +2,6 @@ use std::cmp::Ordering; use crate::prelude::*; -/// # Spacing (H) /// Insert horizontal spacing into a paragraph. /// /// The spacing can be absolute, relative, or fractional. In the last case, the @@ -20,64 +19,39 @@ use crate::prelude::*; /// In [mathematical formulas]($category/math), you can additionally use these /// constants to add spacing between elements: `thin`, `med`, `thick`, `quad`. /// -/// ## Parameters -/// - amount: `Spacing` (positional, required) -/// How much spacing to insert. -/// -/// - weak: `bool` (named) -/// If true, the spacing collapses at the start or end of a paragraph. -/// Moreover, from multiple adjacent weak spacings all but the largest one -/// collapse. -/// -/// ```example -/// #h(1cm, weak: true) -/// We identified a group of -/// _weak_ specimens that fail to -/// manifest in most cases. However, -/// when #h(8pt, weak: true) -/// supported -/// #h(8pt, weak: true) on both -/// sides, they do show up. -/// ``` -/// -/// ## Category -/// layout -#[func] -#[capable(Behave)] -#[derive(Debug, Copy, Clone, Hash)] +/// Display: Spacing (H) +/// Category: layout +#[node(Behave)] pub struct HNode { - /// The amount of horizontal spacing. + /// How much spacing to insert. + #[positional] + #[required] pub amount: Spacing, - /// Whether the node is weak, see also [`Behaviour`]. - pub weak: bool, -} - -#[node] -impl HNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - let amount = args.expect("amount")?; - let weak = args.named("weak")?.unwrap_or(false); - Ok(Self { amount, weak }.pack()) - } -} -impl HNode { - /// Normal strong spacing. - pub fn strong(amount: impl Into<Spacing>) -> Self { - Self { amount: amount.into(), weak: false } - } - - /// User-created weak spacing. - pub fn weak(amount: impl Into<Spacing>) -> Self { - Self { amount: amount.into(), weak: true } - } + /// If true, the spacing collapses at the start or end of a paragraph. + /// Moreover, from multiple adjacent weak spacings all but the largest one + /// collapse. + /// + /// ```example + /// #h(1cm, weak: true) + /// We identified a group of + /// _weak_ specimens that fail to + /// manifest in most cases. However, + /// when #h(8pt, weak: true) + /// supported + /// #h(8pt, weak: true) on both + /// sides, they do show up. + /// ``` + #[named] + #[default(false)] + pub weak: bool, } impl Behave for HNode { fn behaviour(&self) -> Behaviour { - if self.amount.is_fractional() { + if self.amount().is_fractional() { Behaviour::Destructive - } else if self.weak { + } else if self.weak() { Behaviour::Weak(1) } else { Behaviour::Ignorant @@ -86,11 +60,10 @@ impl Behave for HNode { fn larger(&self, prev: &Content) -> bool { let Some(prev) = prev.to::<Self>() else { return false }; - self.amount > prev.amount + self.amount() > prev.amount() } } -/// # Spacing (V) /// Insert vertical spacing into a flow of blocks. /// /// The spacing can be absolute, relative, or fractional. In the last case, @@ -130,20 +103,24 @@ impl Behave for HNode { /// #v(4pt, weak: true) /// The proof is simple: /// ``` -/// ## Category -/// layout -#[func] -#[capable(Behave)] -#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd)] +/// +/// Display: Spacing (V) +/// Category: layout +#[node(Construct, Behave)] pub struct VNode { /// The amount of vertical spacing. + #[positional] + #[required] pub amount: Spacing, + /// The node's weakness level, see also [`Behaviour`]. - pub weakness: u8, + #[named] + #[skip] + #[default] + pub weakness: usize, } -#[node] -impl VNode { +impl Construct for VNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { let amount = args.expect("spacing")?; let node = if args.named("weak")?.unwrap_or(false) { @@ -158,36 +135,36 @@ impl VNode { impl VNode { /// Normal strong spacing. pub fn strong(amount: Spacing) -> Self { - Self { amount, weakness: 0 } + Self::new(amount).with_weakness(0) } /// User-created weak spacing. pub fn weak(amount: Spacing) -> Self { - Self { amount, weakness: 1 } + Self::new(amount).with_weakness(1) } /// Weak spacing with list attach weakness. pub fn list_attach(amount: Spacing) -> Self { - Self { amount, weakness: 2 } + Self::new(amount).with_weakness(2) } /// Weak spacing with BlockNode::ABOVE/BELOW weakness. pub fn block_around(amount: Spacing) -> Self { - Self { amount, weakness: 3 } + Self::new(amount).with_weakness(3) } /// Weak spacing with BlockNode::SPACING weakness. pub fn block_spacing(amount: Spacing) -> Self { - Self { amount, weakness: 4 } + Self::new(amount).with_weakness(4) } } impl Behave for VNode { fn behaviour(&self) -> Behaviour { - if self.amount.is_fractional() { + if self.amount().is_fractional() { Behaviour::Destructive - } else if self.weakness > 0 { - Behaviour::Weak(self.weakness) + } else if self.weakness() > 0 { + Behaviour::Weak(self.weakness()) } else { Behaviour::Ignorant } @@ -195,10 +172,15 @@ impl Behave for VNode { fn larger(&self, prev: &Content) -> bool { let Some(prev) = prev.to::<Self>() else { return false }; - self.amount > prev.amount + self.amount() > prev.amount() } } +cast_from_value! { + VNode, + v: Content => v.to::<Self>().cloned().ok_or("expected vnode")?, +} + /// Kinds of spacing. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Spacing { @@ -214,22 +196,6 @@ impl Spacing { pub fn is_fractional(self) -> bool { matches!(self, Self::Fr(_)) } - - /// Encode into a value. - pub fn encode(self) -> Value { - match self { - Self::Rel(rel) => { - if rel.rel.is_zero() { - Value::Length(rel.abs) - } else if rel.abs.is_zero() { - Value::Ratio(rel.rel) - } else { - Value::Relative(rel) - } - } - Self::Fr(fr) => Value::Fraction(fr), - } - } } impl From<Abs> for Spacing { @@ -244,6 +210,12 @@ impl From<Em> for Spacing { } } +impl From<Fr> for Spacing { + fn from(fr: Fr) -> Self { + Self::Fr(fr) + } +} + impl PartialOrd for Spacing { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { match (self, other) { @@ -254,8 +226,23 @@ impl PartialOrd for Spacing { } } -castable! { +cast_from_value! { Spacing, v: Rel<Length> => Self::Rel(v), v: Fr => Self::Fr(v), } + +cast_to_value! { + v: Spacing => match v { + Spacing::Rel(rel) => { + if rel.rel.is_zero() { + Value::Length(rel.abs) + } else if rel.abs.is_zero() { + Value::Ratio(rel.rel) + } else { + Value::Relative(rel) + } + } + Spacing::Fr(fr) => Value::Fraction(fr), + } +} diff --git a/library/src/layout/stack.rs b/library/src/layout/stack.rs index 864b7b42..430af715 100644 --- a/library/src/layout/stack.rs +++ b/library/src/layout/stack.rs @@ -3,7 +3,6 @@ use typst::model::StyledNode; use super::{AlignNode, Spacing}; use crate::prelude::*; -/// # Stack /// Arrange content and spacing horizontally or vertically. /// /// The stack places a list of items along an axis, with optional spacing @@ -19,45 +18,28 @@ use crate::prelude::*; /// ) /// ``` /// -/// ## Parameters -/// - items: `StackChild` (positional, variadic) -/// The items to stack along an axis. -/// -/// - dir: `Dir` (named) -/// The direction along which the items are stacked. Possible values are: -/// -/// - `{ltr}`: Left to right. -/// - `{rtl}`: Right to left. -/// - `{ttb}`: Top to bottom. -/// - `{btt}`: Bottom to top. -/// -/// - spacing: `Spacing` (named) -/// Spacing to insert between items where no explicit spacing was provided. -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Stack +/// Category: layout +#[node(Layout)] pub struct StackNode { - /// The stacking direction. - pub dir: Dir, - /// The spacing between non-spacing children. - pub spacing: Option<Spacing>, - /// The children to be stacked. + /// The childfren to stack along the axis. + #[variadic] pub children: Vec<StackChild>, -} -#[node] -impl StackNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - Ok(Self { - dir: args.named("dir")?.unwrap_or(Dir::TTB), - spacing: args.named("spacing")?, - children: args.all()?, - } - .pack()) - } + /// The direction along which the items are stacked. Possible values are: + /// + /// - `{ltr}`: Left to right. + /// - `{rtl}`: Right to left. + /// - `{ttb}`: Top to bottom. + /// - `{btt}`: Bottom to top. + #[named] + #[default(Dir::TTB)] + pub dir: Dir, + + /// Spacing to insert between items where no explicit spacing was provided. + #[named] + #[default] + pub spacing: Option<Spacing>, } impl Layout for StackNode { @@ -67,15 +49,16 @@ impl Layout for StackNode { styles: StyleChain, regions: Regions, ) -> SourceResult<Fragment> { - let mut layouter = StackLayouter::new(self.dir, regions, styles); + let mut layouter = StackLayouter::new(self.dir(), regions, styles); // Spacing to insert before the next block. + let spacing = self.spacing(); let mut deferred = None; - for child in &self.children { + for child in self.children() { match child { StackChild::Spacing(kind) => { - layouter.layout_spacing(*kind); + layouter.layout_spacing(kind); deferred = None; } StackChild::Block(block) => { @@ -83,8 +66,8 @@ impl Layout for StackNode { layouter.layout_spacing(kind); } - layouter.layout_block(vt, block, styles)?; - deferred = self.spacing; + layouter.layout_block(vt, &block, styles)?; + deferred = spacing; } } } @@ -111,10 +94,17 @@ impl Debug for StackChild { } } -castable! { +cast_from_value! { StackChild, - spacing: Spacing => Self::Spacing(spacing), - content: Content => Self::Block(content), + v: Spacing => Self::Spacing(v), + v: Content => Self::Block(v), +} + +cast_to_value! { + v: StackChild => match v { + StackChild::Spacing(spacing) => spacing.into(), + StackChild::Block(content) => content.into(), + } } /// Performs stack layout. @@ -212,9 +202,9 @@ impl<'a> StackLayouter<'a> { // Block-axis alignment of the `AlignNode` is respected // by the stack node. let aligns = if let Some(styled) = block.to::<StyledNode>() { - styles.chain(&styled.map).get(AlignNode::ALIGNS) + styles.chain(&styled.map()).get(AlignNode::ALIGNMENT) } else { - styles.get(AlignNode::ALIGNS) + styles.get(AlignNode::ALIGNMENT) }; let aligns = aligns.resolve(styles); diff --git a/library/src/layout/table.rs b/library/src/layout/table.rs index 3084c3d4..33ce9088 100644 --- a/library/src/layout/table.rs +++ b/library/src/layout/table.rs @@ -1,7 +1,6 @@ -use crate::layout::{AlignNode, GridLayouter, Sizing, TrackSizings}; +use crate::layout::{AlignNode, GridLayouter, TrackSizings}; use crate::prelude::*; -/// # Table /// A table of items. /// /// Tables are used to arrange content in cells. Cells can contain arbitrary @@ -31,47 +30,46 @@ use crate::prelude::*; /// ``` /// /// ## 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)] +/// Display: Table +/// Category: layout +#[node(Layout)] pub struct TableNode { - /// Defines sizing for content rows and columns. - pub tracks: Axes<Vec<Sizing>>, - /// Defines sizing of gutter rows and columns between content. - pub gutter: Axes<Vec<Sizing>>, - /// The content to be arranged in the table. + /// The contents of the table cells. + #[variadic] pub cells: Vec<Content>, -} -#[node] -impl TableNode { + /// Defines the column sizes. + /// See the [grid documentation]($func/grid) for more information on track + /// sizing. + #[named] + #[default] + pub columns: TrackSizings, + + /// Defines the row sizes. + /// See the [grid documentation]($func/grid) for more information on track + /// sizing. + #[named] + #[default] + pub rows: TrackSizings, + + /// Defines the gaps between columns. Takes precedence over `gutter`. + /// See the [grid documentation]($func/grid) for more information on gutters. + #[named] + #[shorthand(gutter)] + #[default] + pub column_gutter: TrackSizings, + + /// Defines the gaps between rows. Takes precedence over `gutter`. + /// See the [grid documentation]($func/grid) for more information on gutters. + #[named] + #[shorthand(gutter)] + #[default] + pub row_gutter: TrackSizings, + /// How to fill the cells. /// /// This can be a color or a function that returns a color. The function is @@ -92,58 +90,35 @@ impl TableNode { /// [Profit:], [500 €], [1000 €], [1500 €], /// ) /// ``` - #[property(referenced)] - pub const FILL: Celled<Option<Paint>> = Celled::Value(None); + #[settable] + #[default] + pub fill: Celled<Option<Paint>>, /// 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); + #[settable] + #[default] + pub align: Celled<Smart<Axes<Option<GenAlign>>>>, /// 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()); + #[settable] + #[resolve] + #[fold] + #[default(Some(PartialStroke::default()))] + pub stroke: Option<PartialStroke>, /// 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(Sizing::encode_slice(&self.tracks.x)), - "rows" => Some(Sizing::encode_slice(&self.tracks.y)), - "column-gutter" => Some(Sizing::encode_slice(&self.gutter.x)), - "row-gutter" => Some(Sizing::encode_slice(&self.gutter.y)), - "cells" => Some(Value::Array( - self.cells.iter().cloned().map(Value::Content).collect(), - )), - _ => None, - } - } + #[settable] + #[default(Abs::pt(5.0).into())] + pub inset: Rel<Length>, } impl Layout for TableNode { @@ -156,11 +131,12 @@ impl Layout for TableNode { let inset = styles.get(Self::INSET); let align = styles.get(Self::ALIGN); - let cols = self.tracks.x.len().max(1); + let tracks = Axes::new(self.columns().0, self.rows().0); + let gutter = Axes::new(self.column_gutter().0, self.row_gutter().0); + let cols = tracks.x.len().max(1); let cells: Vec<_> = self - .cells - .iter() - .cloned() + .cells() + .into_iter() .enumerate() .map(|(i, child)| { let mut child = child.padded(Sides::splat(inset)); @@ -168,7 +144,7 @@ impl Layout for TableNode { let x = i % cols; let y = i / cols; if let Smart::Custom(alignment) = align.resolve(vt, x, y)? { - child = child.styled(AlignNode::ALIGNS, alignment) + child = child.styled(AlignNode::ALIGNMENT, alignment) } Ok(child) @@ -181,8 +157,8 @@ impl Layout for TableNode { // Prepare grid layout by unifying content and gutter tracks. let layouter = GridLayouter::new( vt, - self.tracks.as_deref(), - self.gutter.as_deref(), + tracks.as_deref(), + gutter.as_deref(), &cells, regions, styles, @@ -269,6 +245,12 @@ impl<T: Cast + Clone> Celled<T> { } } +impl<T: Default> Default for Celled<T> { + fn default() -> Self { + Self::Value(T::default()) + } +} + impl<T: Cast> Cast for Celled<T> { fn is(value: &Value) -> bool { matches!(value, Value::Func(_)) || T::is(value) @@ -286,3 +268,12 @@ impl<T: Cast> Cast for Celled<T> { T::describe() + CastInfo::Type("function") } } + +impl<T: Into<Value>> From<Celled<T>> for Value { + fn from(celled: Celled<T>) -> Self { + match celled { + Celled::Value(value) => value.into(), + Celled::Func(func) => func.into(), + } + } +} diff --git a/library/src/layout/terms.rs b/library/src/layout/terms.rs index f2902b80..33b59d4d 100644 --- a/library/src/layout/terms.rs +++ b/library/src/layout/terms.rs @@ -1,8 +1,7 @@ -use crate::layout::{BlockNode, GridNode, HNode, ParNode, Sizing, Spacing}; +use crate::layout::{BlockNode, GridLayouter, HNode, ParNode, Sizing, Spacing}; 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 @@ -20,55 +19,49 @@ use crate::text::{SpaceNode, TextNode}; /// 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)] +/// Display: Term List +/// Category: layout +#[node(Layout)] pub struct TermsNode { - /// If true, the items are separated by leading instead of list spacing. + /// 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.] + /// ``` + #[variadic] + pub items: Vec<TermItem>, + + /// 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. + /// ``` + #[named] + #[default(true)] 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(); + #[settable] + #[resolve] + #[default] + pub indent: Length, /// The hanging indent of the description. /// @@ -77,31 +70,17 @@ impl TermsNode { /// / Term: This term list does not /// make use of hanging indents. /// ``` - #[property(resolve)] - pub const HANGING_INDENT: Length = Em::new(1.0).into(); + #[settable] + #[resolve] + #[default(Em::new(1.0).into())] + pub hanging_indent: Length, /// 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, - } - } + #[settable] + #[default] + pub spacing: Smart<Spacing>, } impl Layout for TermsNode { @@ -113,66 +92,63 @@ impl Layout for TermsNode { ) -> SourceResult<Fragment> { let indent = styles.get(Self::INDENT); let body_indent = styles.get(Self::HANGING_INDENT); - let gutter = if self.tight { + let gutter = if self.tight() { styles.get(ParNode::LEADING).into() } else { styles .get(Self::SPACING) - .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount) + .unwrap_or_else(|| styles.get(BlockNode::BELOW).amount()) }; let mut cells = vec![]; - for (item, map) in self.items.iter() { + for item in self.items() { let body = Content::sequence(vec![ - HNode { amount: (-body_indent).into(), weak: false }.pack(), - (item.term.clone() + TextNode::packed(':')).strong(), - SpaceNode.pack(), - item.description.clone(), + HNode::new((-body_indent).into()).pack(), + (item.term() + TextNode::packed(':')).strong(), + SpaceNode::new().pack(), + item.description(), ]); cells.push(Content::empty()); - cells.push(body.styled_with_map(map.clone())); + cells.push(body); } - GridNode { - tracks: Axes::with_x(vec![ - Sizing::Rel((indent + body_indent).into()), - Sizing::Auto, - ]), - gutter: Axes::with_y(vec![gutter.into()]), - cells, - } - .layout(vt, styles, regions) + let layouter = GridLayouter::new( + vt, + Axes::with_x(&[Sizing::Rel((indent + body_indent).into()), Sizing::Auto]), + Axes::with_y(&[gutter.into()]), + &cells, + regions, + styles, + ); + + Ok(layouter.layout()?.fragment) } } /// A term list item. -#[derive(Debug, Clone, Hash)] +#[node] pub struct TermItem { /// The term described by the list item. + #[positional] + #[required] pub term: Content, + /// The description of the term. + #[positional] + #[required] 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! { +cast_from_value! { 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("term array must contain exactly two entries")?, + _ => Err("array must contain exactly two entries")?, }; - Self { term, description } + Self::new(term, description) }, + v: Content => v.to::<Self>().cloned().ok_or("expected term item or array")?, } diff --git a/library/src/layout/transform.rs b/library/src/layout/transform.rs index 5d358e66..2ab9e5e0 100644 --- a/library/src/layout/transform.rs +++ b/library/src/layout/transform.rs @@ -2,7 +2,6 @@ use typst::geom::Transform; use crate::prelude::*; -/// # Move /// Move content without affecting layout. /// /// The `move` function allows you to move content while the layout still 'sees' @@ -22,39 +21,24 @@ use crate::prelude::*; /// )) /// ``` /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The content to move. -/// -/// - dx: `Rel<Length>` (named) -/// The horizontal displacement of the content. -/// -/// - dy: `Rel<Length>` (named) -/// The vertical displacement of the content. -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Move +/// Category: layout +#[node(Layout)] pub struct MoveNode { - /// The offset by which to move the content. - pub delta: Axes<Rel<Length>>, - /// The content that should be moved. + /// The content to move. + #[positional] + #[required] pub body: Content, -} -#[node] -impl MoveNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - let dx = args.named("dx")?.unwrap_or_default(); - let dy = args.named("dy")?.unwrap_or_default(); - Ok(Self { - delta: Axes::new(dx, dy), - body: args.expect("body")?, - } - .pack()) - } + /// The horizontal displacement of the content. + #[named] + #[default] + pub dx: Rel<Length>, + + /// The vertical displacement of the content. + #[named] + #[default] + pub dy: Rel<Length>, } impl Layout for MoveNode { @@ -65,15 +49,14 @@ impl Layout for MoveNode { regions: Regions, ) -> SourceResult<Fragment> { let pod = Regions::one(regions.base(), Axes::splat(false)); - let mut frame = self.body.layout(vt, styles, pod)?.into_frame(); - let delta = self.delta.resolve(styles); + let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); + let delta = Axes::new(self.dx(), self.dy()).resolve(styles); let delta = delta.zip(regions.base()).map(|(d, s)| d.relative_to(s)); frame.translate(delta.to_point()); Ok(Fragment::frame(frame)) } } -/// # Rotate /// Rotate content with affecting layout. /// /// Rotate an element by a given angle. The layout will act as if the element @@ -89,31 +72,26 @@ impl Layout for MoveNode { /// ) /// ``` /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The content to rotate. -/// -/// - angle: `Angle` (named) -/// The amount of rotation. -/// -/// ```example -/// #rotate(angle: -1.571rad)[Space!] -/// ``` -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Rotate +/// Category: layout +#[node(Layout)] pub struct RotateNode { - /// The angle by which to rotate the node. + /// The amount of rotation. + /// + /// ```example + /// #rotate(angle: -1.571rad)[Space!] + /// ``` + /// + #[named] + #[shorthand] + #[default] pub angle: Angle, - /// The content that should be rotated. + + /// The content to rotate. + #[positional] + #[required] pub body: Content, -} -#[node] -impl RotateNode { /// The origin of the rotation. /// /// By default, the origin is the center of the rotated element. If, @@ -130,16 +108,10 @@ impl RotateNode { /// #box(rotate(angle: 30deg, origin: top + left, square())) /// #box(rotate(angle: 30deg, origin: bottom + right, square())) /// ``` - #[property(resolve)] - pub const ORIGIN: Axes<Option<GenAlign>> = Axes::default(); - - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - Ok(Self { - angle: args.named_or_find("angle")?.unwrap_or_default(), - body: args.expect("body")?, - } - .pack()) - } + #[settable] + #[resolve] + #[default] + pub origin: Axes<Option<GenAlign>>, } impl Layout for RotateNode { @@ -150,18 +122,17 @@ impl Layout for RotateNode { regions: Regions, ) -> SourceResult<Fragment> { let pod = Regions::one(regions.base(), Axes::splat(false)); - let mut frame = self.body.layout(vt, styles, pod)?.into_frame(); + let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s)); let ts = Transform::translate(x, y) - .pre_concat(Transform::rotate(self.angle)) + .pre_concat(Transform::rotate(self.angle())) .pre_concat(Transform::translate(-x, -y)); frame.transform(ts); Ok(Fragment::frame(frame)) } } -/// # Scale /// Scale content without affecting layout. /// /// The `scale` function allows you to scale and mirror content without @@ -174,34 +145,29 @@ impl Layout for RotateNode { /// #scale(x: -100%)[This is mirrored.] /// ``` /// -/// ## Parameters -/// - body: `Content` (positional, required) -/// The content to scale. -/// -/// - x: `Ratio` (named) -/// The horizontal scaling factor. -/// -/// The body will be mirrored horizontally if the parameter is negative. -/// -/// - y: `Ratio` (named) -/// The vertical scaling factor. -/// -/// The body will be mirrored vertically if the parameter is negative. -/// -/// ## Category -/// layout -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Scale +/// Category: layout +#[node(Construct, Layout)] pub struct ScaleNode { - /// Scaling factor. - pub factor: Axes<Ratio>, - /// The content that should be scaled. + /// The content to scale. + #[positional] + #[required] pub body: Content, -} -#[node] -impl ScaleNode { + /// The horizontal scaling factor. + /// + /// The body will be mirrored horizontally if the parameter is negative. + #[named] + #[default(Ratio::one())] + pub x: Ratio, + + /// The vertical scaling factor. + /// + /// The body will be mirrored vertically if the parameter is negative. + #[named] + #[default(Ratio::one())] + pub y: Ratio, + /// The origin of the transformation. /// /// By default, the origin is the center of the scaled element. @@ -210,18 +176,18 @@ impl ScaleNode { /// A#box(scale(75%)[A])A \ /// B#box(scale(75%, origin: bottom + left)[B])B /// ``` - #[property(resolve)] - pub const ORIGIN: Axes<Option<GenAlign>> = Axes::default(); + #[settable] + #[resolve] + #[default] + pub origin: Axes<Option<GenAlign>>, +} +impl Construct for ScaleNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { let all = args.find()?; let x = args.named("x")?.or(all).unwrap_or(Ratio::one()); let y = args.named("y")?.or(all).unwrap_or(Ratio::one()); - Ok(Self { - factor: Axes::new(x, y), - body: args.expect("body")?, - } - .pack()) + Ok(Self::new(args.expect::<Content>("body")?).with_x(x).with_y(y).pack()) } } @@ -233,11 +199,11 @@ impl Layout for ScaleNode { regions: Regions, ) -> SourceResult<Fragment> { let pod = Regions::one(regions.base(), Axes::splat(false)); - let mut frame = self.body.layout(vt, styles, pod)?.into_frame(); + let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s)); let transform = Transform::translate(x, y) - .pre_concat(Transform::scale(self.factor.x, self.factor.y)) + .pre_concat(Transform::scale(self.x(), self.y())) .pre_concat(Transform::translate(-x, -y)); frame.transform(transform); Ok(Fragment::frame(frame)) |
