summaryrefslogtreecommitdiff
path: root/library/src/layout
diff options
context:
space:
mode:
Diffstat (limited to 'library/src/layout')
-rw-r--r--library/src/layout/align.rs109
-rw-r--r--library/src/layout/columns.rs79
-rw-r--r--library/src/layout/container.rs347
-rw-r--r--library/src/layout/enum.rs175
-rw-r--r--library/src/layout/flow.rs55
-rw-r--r--library/src/layout/grid.rs107
-rw-r--r--library/src/layout/hide.rs27
-rw-r--r--library/src/layout/list.rs191
-rw-r--r--library/src/layout/mod.rs165
-rw-r--r--library/src/layout/pad.rs66
-rw-r--r--library/src/layout/page.rs171
-rw-r--r--library/src/layout/par.rs155
-rw-r--r--library/src/layout/place.rs85
-rw-r--r--library/src/layout/repeat.rs29
-rw-r--r--library/src/layout/spacing.rs161
-rw-r--r--library/src/layout/stack.rs84
-rw-r--r--library/src/layout/table.rs151
-rw-r--r--library/src/layout/terms.rs174
-rw-r--r--library/src/layout/transform.rs170
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(&quotes, node.double, peeked));
+ full.push_str(quoter.quote(&quotes, 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))