summaryrefslogtreecommitdiff
path: root/src/library
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-01-07 21:24:36 +0100
committerLaurenz <laurmaedje@gmail.com>2022-01-08 00:20:48 +0100
commite74ae6ce70d4c6ca006613eadf07f920951789e3 (patch)
tree0b9b2ddabf79dad8d55631780ee5d70afe7362d7 /src/library
parent0b624390906e911bde325b487b2710b67c8205c8 (diff)
Make all nodes into classes
Diffstat (limited to 'src/library')
-rw-r--r--src/library/align.rs18
-rw-r--r--src/library/columns.rs46
-rw-r--r--src/library/deco.rs79
-rw-r--r--src/library/flow.rs4
-rw-r--r--src/library/grid.rs36
-rw-r--r--src/library/heading.rs6
-rw-r--r--src/library/image.rs81
-rw-r--r--src/library/link.rs33
-rw-r--r--src/library/list.rs28
-rw-r--r--src/library/mod.rs59
-rw-r--r--src/library/pad.rs40
-rw-r--r--src/library/page.rs27
-rw-r--r--src/library/par.rs35
-rw-r--r--src/library/placed.rs46
-rw-r--r--src/library/shape.rs236
-rw-r--r--src/library/sized.rs29
-rw-r--r--src/library/spacing.rs28
-rw-r--r--src/library/stack.rs22
-rw-r--r--src/library/text.rs81
-rw-r--r--src/library/transform.rs122
-rw-r--r--src/library/utility.rs33
21 files changed, 573 insertions, 516 deletions
diff --git a/src/library/align.rs b/src/library/align.rs
index e8dfabb1..8eee116e 100644
--- a/src/library/align.rs
+++ b/src/library/align.rs
@@ -3,14 +3,7 @@
use super::prelude::*;
use super::ParNode;
-/// `align`: Configure the alignment along the layouting axes.
-pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let aligns: Spec<_> = args.find().unwrap_or_default();
- let body: PackedNode = args.expect("body")?;
- Ok(Value::block(body.aligned(aligns)))
-}
-
-/// A node that aligns its child.
+/// Align a node along the layouting axes.
#[derive(Debug, Hash)]
pub struct AlignNode {
/// How to align the node horizontally and vertically.
@@ -19,6 +12,15 @@ pub struct AlignNode {
pub child: PackedNode,
}
+#[class]
+impl AlignNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ let aligns: Spec<_> = args.find().unwrap_or_default();
+ let body: PackedNode = args.expect("body")?;
+ Ok(Node::block(body.aligned(aligns)))
+ }
+}
+
impl Layout for AlignNode {
fn layout(
&self,
diff --git a/src/library/columns.rs b/src/library/columns.rs
index ce02b508..d2dc350f 100644
--- a/src/library/columns.rs
+++ b/src/library/columns.rs
@@ -3,32 +3,34 @@
use super::prelude::*;
use super::ParNode;
-/// `columns`: Set content into multiple columns.
-pub fn columns(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- Ok(Value::block(ColumnsNode {
- columns: args.expect("column count")?,
- gutter: args.named("gutter")?.unwrap_or(Relative::new(0.04).into()),
- child: args.expect("body")?,
- }))
-}
-
-/// `colbreak`: Start a new column.
-pub fn colbreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
- Ok(Value::Node(Node::Colbreak))
-}
-
/// A node that separates a region into multiple equally sized columns.
#[derive(Debug, Hash)]
pub struct ColumnsNode {
/// How many columns there should be.
pub columns: NonZeroUsize,
- /// The size of the gutter space between each column.
- pub gutter: Linear,
/// The child to be layouted into the columns. Most likely, this should be a
/// flow or stack node.
pub child: PackedNode,
}
+#[class]
+impl ColumnsNode {
+ /// The size of the gutter space between each column.
+ pub const GUTTER: Linear = Relative::new(0.04).into();
+
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ Ok(Node::block(Self {
+ columns: args.expect("column count")?,
+ child: args.expect("body")?,
+ }))
+ }
+
+ fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
+ styles.set_opt(Self::GUTTER, args.named("gutter")?);
+ Ok(())
+ }
+}
+
impl Layout for ColumnsNode {
fn layout(
&self,
@@ -57,7 +59,7 @@ impl Layout for ColumnsNode {
.iter()
.take(1 + regions.backlog.len() + regions.last.iter().len())
{
- let gutter = self.gutter.resolve(base.x);
+ let gutter = styles.get(Self::GUTTER).resolve(base.x);
let width = (current.x - gutter * (columns - 1) as f64) / columns as f64;
let size = Size::new(width, current.y);
gutters.push(gutter);
@@ -131,3 +133,13 @@ impl Layout for ColumnsNode {
finished
}
}
+
+/// A column break.
+pub struct ColbreakNode;
+
+#[class]
+impl ColbreakNode {
+ fn construct(_: &mut EvalContext, _: &mut Args) -> TypResult<Node> {
+ Ok(Node::Colbreak)
+ }
+}
diff --git a/src/library/deco.rs b/src/library/deco.rs
new file mode 100644
index 00000000..3e91d1de
--- /dev/null
+++ b/src/library/deco.rs
@@ -0,0 +1,79 @@
+//! Text decorations.
+
+use super::prelude::*;
+use super::TextNode;
+
+/// Typeset underline, striken-through or overlined text.
+pub struct DecoNode<L: LineKind>(pub L);
+
+#[class]
+impl<L: LineKind> DecoNode<L> {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ let deco = Decoration {
+ line: L::LINE,
+ stroke: args.named("stroke")?.or_else(|| args.find()),
+ thickness: args.named::<Linear>("thickness")?.or_else(|| args.find()),
+ offset: args.named("offset")?,
+ extent: args.named("extent")?.unwrap_or_default(),
+ };
+ Ok(args.expect::<Node>("body")?.styled(TextNode::LINES, vec![deco]))
+ }
+}
+
+/// Defines a line that is positioned over, under or on top of text.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct Decoration {
+ /// Which line to draw.
+ pub line: DecoLine,
+ /// Stroke color of the line, defaults to the text color if `None`.
+ pub stroke: Option<Paint>,
+ /// Thickness of the line's strokes (dependent on scaled font size), read
+ /// from the font tables if `None`.
+ pub thickness: Option<Linear>,
+ /// Position of the line relative to the baseline (dependent on scaled font
+ /// size), read from the font tables if `None`.
+ pub offset: Option<Linear>,
+ /// Amount that the line will be longer or shorter than its associated text
+ /// (dependent on scaled font size).
+ pub extent: Linear,
+}
+
+/// The kind of decorative line.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum DecoLine {
+ /// A line under text.
+ Underline,
+ /// A line through text.
+ Strikethrough,
+ /// A line over text.
+ Overline,
+}
+
+/// Differents kinds of decorative lines for text.
+pub trait LineKind {
+ const LINE: DecoLine;
+}
+
+/// A line under text.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Underline;
+
+impl LineKind for Underline {
+ const LINE: DecoLine = DecoLine::Underline;
+}
+
+/// A line through text.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Strikethrough;
+
+impl LineKind for Strikethrough {
+ const LINE: DecoLine = DecoLine::Strikethrough;
+}
+
+/// A line over text.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Overline;
+
+impl LineKind for Overline {
+ const LINE: DecoLine = DecoLine::Overline;
+}
diff --git a/src/library/flow.rs b/src/library/flow.rs
index f274c9b6..cfcd6561 100644
--- a/src/library/flow.rs
+++ b/src/library/flow.rs
@@ -3,7 +3,7 @@
use std::fmt::{self, Debug, Formatter};
use super::prelude::*;
-use super::{AlignNode, ParNode, PlacedNode, SpacingKind, TextNode};
+use super::{AlignNode, ParNode, PlaceNode, SpacingKind, TextNode};
/// A vertical flow of content consisting of paragraphs and other layout nodes.
///
@@ -172,7 +172,7 @@ impl<'a> FlowLayouter<'a> {
) {
// Placed nodes that are out of flow produce placed items which aren't
// aligned later.
- if let Some(placed) = node.downcast::<PlacedNode>() {
+ if let Some(placed) = node.downcast::<PlaceNode>() {
if placed.out_of_flow() {
let frame = node.layout(ctx, &self.regions, styles).remove(0);
self.items.push(FlowItem::Placed(frame.item));
diff --git a/src/library/grid.rs b/src/library/grid.rs
index ffadf9c2..ee9aafe1 100644
--- a/src/library/grid.rs
+++ b/src/library/grid.rs
@@ -2,23 +2,6 @@
use super::prelude::*;
-/// `grid`: Arrange children into a grid.
-pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let columns = args.named("columns")?.unwrap_or_default();
- let rows = args.named("rows")?.unwrap_or_default();
- let base_gutter: Vec<TrackSizing> = args.named("gutter")?.unwrap_or_default();
- let column_gutter = args.named("column-gutter")?;
- let row_gutter = args.named("row-gutter")?;
- Ok(Value::block(GridNode {
- tracks: Spec::new(columns, rows),
- gutter: Spec::new(
- column_gutter.unwrap_or_else(|| base_gutter.clone()),
- row_gutter.unwrap_or(base_gutter),
- ),
- children: args.all().collect(),
- }))
-}
-
/// A node that arranges its children in a grid.
#[derive(Debug, Hash)]
pub struct GridNode {
@@ -30,6 +13,25 @@ pub struct GridNode {
pub children: Vec<PackedNode>,
}
+#[class]
+impl GridNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ let columns = args.named("columns")?.unwrap_or_default();
+ let rows = args.named("rows")?.unwrap_or_default();
+ let base_gutter: Vec<TrackSizing> = args.named("gutter")?.unwrap_or_default();
+ let column_gutter = args.named("column-gutter")?;
+ let row_gutter = args.named("row-gutter")?;
+ Ok(Node::block(Self {
+ tracks: Spec::new(columns, rows),
+ gutter: Spec::new(
+ column_gutter.unwrap_or_else(|| base_gutter.clone()),
+ row_gutter.unwrap_or(base_gutter),
+ ),
+ children: args.all().collect(),
+ }))
+ }
+}
+
impl Layout for GridNode {
fn layout(
&self,
diff --git a/src/library/heading.rs b/src/library/heading.rs
index 3591ea0c..d3beb4ee 100644
--- a/src/library/heading.rs
+++ b/src/library/heading.rs
@@ -13,25 +13,21 @@ pub struct HeadingNode {
pub child: PackedNode,
}
-#[properties]
+#[class]
impl HeadingNode {
/// The heading's font family.
pub const FAMILY: Smart<FontFamily> = Smart::Auto;
/// The fill color of text in the heading. Just the surrounding text color
/// if `auto`.
pub const FILL: Smart<Paint> = Smart::Auto;
-}
-impl Construct for HeadingNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
Ok(Node::block(Self {
child: args.expect("body")?,
level: args.named("level")?.unwrap_or(1),
}))
}
-}
-impl Set for HeadingNode {
fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
styles.set_opt(Self::FAMILY, args.named("family")?);
styles.set_opt(Self::FILL, args.named("fill")?);
diff --git a/src/library/image.rs b/src/library/image.rs
index c5cb9aeb..a5423ccb 100644
--- a/src/library/image.rs
+++ b/src/library/image.rs
@@ -1,45 +1,41 @@
//! Raster and vector graphics.
-use std::io;
-
use super::prelude::*;
+use super::TextNode;
use crate::diag::Error;
use crate::image::ImageId;
-/// `image`: An image.
-pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- // Load the image.
- let path = args.expect::<Spanned<EcoString>>("path to image file")?;
- let full = ctx.make_path(&path.v);
- let id = ctx.images.load(&full).map_err(|err| {
- Error::boxed(path.span, match err.kind() {
- io::ErrorKind::NotFound => "file not found".into(),
- _ => format!("failed to load image ({})", err),
- })
- })?;
-
- let width = args.named("width")?;
- let height = args.named("height")?;
- let fit = args.named("fit")?.unwrap_or_default();
-
- Ok(Value::inline(
- ImageNode { id, fit }.pack().sized(Spec::new(width, height)),
- ))
-}
-
/// An image node.
#[derive(Debug, Hash)]
-pub struct ImageNode {
- /// The id of the image file.
- pub id: ImageId,
- /// How the image should adjust itself to a given area.
- pub fit: ImageFit,
-}
+pub struct ImageNode(pub ImageId);
-#[properties]
+#[class]
impl ImageNode {
- /// An URL the image should link to.
- pub const LINK: Option<String> = None;
+ /// How the image should adjust itself to a given area.
+ pub const FIT: ImageFit = ImageFit::Cover;
+
+ fn construct(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ let path = args.expect::<Spanned<EcoString>>("path to image file")?;
+ let full = ctx.make_path(&path.v);
+ let id = ctx.images.load(&full).map_err(|err| {
+ Error::boxed(path.span, match err.kind() {
+ std::io::ErrorKind::NotFound => "file not found".into(),
+ _ => format!("failed to load image ({})", err),
+ })
+ })?;
+
+ let width = args.named("width")?;
+ let height = args.named("height")?;
+
+ Ok(Node::inline(
+ ImageNode(id).pack().sized(Spec::new(width, height)),
+ ))
+ }
+
+ fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
+ styles.set_opt(Self::FIT, args.named("fit")?);
+ Ok(())
+ }
}
impl Layout for ImageNode {
@@ -49,7 +45,7 @@ impl Layout for ImageNode {
regions: &Regions,
styles: StyleChain,
) -> Vec<Constrained<Rc<Frame>>> {
- let img = ctx.images.get(self.id);
+ let img = ctx.images.get(self.0);
let pxw = img.width() as f64;
let pxh = img.height() as f64;
let px_ratio = pxw / pxh;
@@ -70,10 +66,11 @@ impl Layout for ImageNode {
Size::new(Length::pt(pxw), Length::pt(pxh))
};
- // The actual size of the fitted image.
- let fitted = match self.fit {
+ // Compute the actual size of the fitted image.
+ let fit = styles.get(Self::FIT);
+ let fitted = match fit {
ImageFit::Cover | ImageFit::Contain => {
- if wide == (self.fit == ImageFit::Contain) {
+ if wide == (fit == ImageFit::Contain) {
Size::new(target.x, target.x / px_ratio)
} else {
Size::new(target.y * px_ratio, target.y)
@@ -86,16 +83,16 @@ impl Layout for ImageNode {
// the frame to the target size, center aligning the image in the
// process.
let mut frame = Frame::new(fitted);
- frame.push(Point::zero(), Element::Image(self.id, fitted));
+ frame.push(Point::zero(), Element::Image(self.0, fitted));
frame.resize(target, Align::CENTER_HORIZON);
// Create a clipping group if only part of the image should be visible.
- if self.fit == ImageFit::Cover && !target.fits(fitted) {
+ if fit == ImageFit::Cover && !target.fits(fitted) {
frame.clip();
}
// Apply link if it exists.
- if let Some(url) = styles.get_ref(Self::LINK) {
+ if let Some(url) = styles.get_ref(TextNode::LINK) {
frame.link(url);
}
@@ -114,12 +111,6 @@ pub enum ImageFit {
Stretch,
}
-impl Default for ImageFit {
- fn default() -> Self {
- Self::Cover
- }
-}
-
castable! {
ImageFit,
Expected: "string",
diff --git a/src/library/link.rs b/src/library/link.rs
index b7e3d587..dc523ffd 100644
--- a/src/library/link.rs
+++ b/src/library/link.rs
@@ -1,23 +1,24 @@
//! Hyperlinking.
use super::prelude::*;
-use super::{ImageNode, ShapeNode, TextNode};
+use super::TextNode;
use crate::util::EcoString;
-/// `link`: Link text and other elements to an URL.
-pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let url: String = args.expect::<EcoString>("url")?.into();
- let body = args.find().unwrap_or_else(|| {
- let mut text = url.as_str();
- for prefix in ["mailto:", "tel:"] {
- text = text.trim_start_matches(prefix);
- }
- Node::Text(text.into())
- });
+/// Link text and other elements to an URL.
+pub struct LinkNode;
- let mut map = StyleMap::new();
- map.set(TextNode::LINK, Some(url.clone()));
- map.set(ImageNode::LINK, Some(url.clone()));
- map.set(ShapeNode::LINK, Some(url));
- Ok(Value::Node(body.styled_with_map(map)))
+#[class]
+impl LinkNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ let url: String = args.expect::<EcoString>("url")?.into();
+ let body = args.find().unwrap_or_else(|| {
+ let mut text = url.as_str();
+ for prefix in ["mailto:", "tel:"] {
+ text = text.trim_start_matches(prefix);
+ }
+ Node::Text(text.into())
+ });
+
+ Ok(body.styled(TextNode::LINK, Some(url)))
+ }
}
diff --git a/src/library/list.rs b/src/library/list.rs
index bbdc400a..9f742a32 100644
--- a/src/library/list.rs
+++ b/src/library/list.rs
@@ -1,37 +1,31 @@
//! Unordered (bulleted) and ordered (numbered) lists.
-use std::hash::Hash;
-
use super::prelude::*;
use super::{GridNode, TextNode, TrackSizing};
/// An unordered or ordered list.
#[derive(Debug, Hash)]
-pub struct ListNode<L> {
+pub struct ListNode<L: ListKind> {
+ /// The list labelling style -- unordered or ordered.
+ pub kind: L,
/// The node that produces the item's body.
pub child: PackedNode,
- /// The list labelling style -- unordered or ordered.
- pub labelling: L,
}
-#[properties]
-impl<L: Labelling> ListNode<L> {
+#[class]
+impl<L: ListKind> ListNode<L> {
/// The indentation of each item's label.
pub const LABEL_INDENT: Linear = Relative::new(0.0).into();
/// The space between the label and the body of each item.
pub const BODY_INDENT: Linear = Relative::new(0.5).into();
-}
-impl<L: Labelling> Construct for ListNode<L> {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
Ok(args
.all()
- .map(|child: PackedNode| Node::block(Self { child, labelling: L::default() }))
+ .map(|child: PackedNode| Node::block(Self { kind: L::default(), child }))
.sum())
}
-}
-impl<L: Labelling> Set for ListNode<L> {
fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
styles.set_opt(Self::LABEL_INDENT, args.named("label-indent")?);
styles.set_opt(Self::BODY_INDENT, args.named("body-indent")?);
@@ -39,7 +33,7 @@ impl<L: Labelling> Set for ListNode<L> {
}
}
-impl<L: Labelling> Layout for ListNode<L> {
+impl<L: ListKind> Layout for ListNode<L> {
fn layout(
&self,
ctx: &mut LayoutContext,
@@ -60,7 +54,7 @@ impl<L: Labelling> Layout for ListNode<L> {
gutter: Spec::default(),
children: vec![
PackedNode::default(),
- Node::Text(self.labelling.label()).into_block(),
+ Node::Text(self.kind.label()).into_block(),
PackedNode::default(),
self.child.clone(),
],
@@ -71,7 +65,7 @@ impl<L: Labelling> Layout for ListNode<L> {
}
/// How to label a list.
-pub trait Labelling: Debug + Default + Hash + 'static {
+pub trait ListKind: Debug + Default + Hash + 'static {
/// Return the item's label.
fn label(&self) -> EcoString;
}
@@ -80,7 +74,7 @@ pub trait Labelling: Debug + Default + Hash + 'static {
#[derive(Debug, Default, Hash)]
pub struct Unordered;
-impl Labelling for Unordered {
+impl ListKind for Unordered {
fn label(&self) -> EcoString {
'•'.into()
}
@@ -90,7 +84,7 @@ impl Labelling for Unordered {
#[derive(Debug, Default, Hash)]
pub struct Ordered(pub Option<usize>);
-impl Labelling for Ordered {
+impl ListKind for Ordered {
fn label(&self) -> EcoString {
format_eco!("{}.", self.0.unwrap_or(1))
}
diff --git a/src/library/mod.rs b/src/library/mod.rs
index 461716a1..cb117702 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -5,6 +5,7 @@
pub mod align;
pub mod columns;
+pub mod deco;
pub mod flow;
pub mod grid;
pub mod heading;
@@ -26,6 +27,7 @@ pub mod utility;
pub use self::image::*;
pub use align::*;
pub use columns::*;
+pub use deco::*;
pub use flow::*;
pub use grid::*;
pub use heading::*;
@@ -56,8 +58,9 @@ prelude! {
pub use std::fmt::{self, Debug, Formatter};
pub use std::num::NonZeroUsize;
pub use std::rc::Rc;
+ pub use std::hash::Hash;
- pub use typst_macros::properties;
+ pub use typst_macros::class;
pub use crate::diag::{At, TypResult};
pub use crate::eval::{
@@ -81,41 +84,39 @@ pub fn new() -> Scope {
// Structure and semantics.
std.def_class::<PageNode>("page");
+ std.def_class::<PagebreakNode>("pagebreak");
std.def_class::<ParNode>("par");
+ std.def_class::<ParbreakNode>("parbreak");
+ std.def_class::<LinebreakNode>("linebreak");
std.def_class::<TextNode>("text");
- std.def_func("underline", underline);
- std.def_func("strike", strike);
- std.def_func("overline", overline);
- std.def_func("link", link);
+ std.def_class::<DecoNode<Underline>>("underline");
+ std.def_class::<DecoNode<Strikethrough>>("strike");
+ std.def_class::<DecoNode<Overline>>("overline");
+ std.def_class::<LinkNode>("link");
std.def_class::<HeadingNode>("heading");
std.def_class::<ListNode<Unordered>>("list");
std.def_class::<ListNode<Ordered>>("enum");
- std.def_func("image", image);
- std.def_func("rect", rect);
- std.def_func("square", square);
- std.def_func("ellipse", ellipse);
- std.def_func("circle", circle);
+ std.def_class::<ImageNode>("image");
+ std.def_class::<ShapeNode<Rect>>("rect");
+ std.def_class::<ShapeNode<Square>>("square");
+ std.def_class::<ShapeNode<Ellipse>>("ellipse");
+ std.def_class::<ShapeNode<Circle>>("circle");
// Layout.
- std.def_func("h", h);
- std.def_func("v", v);
- std.def_func("box", box_);
- std.def_func("block", block);
- std.def_func("align", align);
- std.def_func("pad", pad);
- std.def_func("place", place);
- std.def_func("move", move_);
- std.def_func("scale", scale);
- std.def_func("rotate", rotate);
- std.def_func("stack", stack);
- std.def_func("grid", grid);
- std.def_func("columns", columns);
-
- // Breaks.
- std.def_func("pagebreak", pagebreak);
- std.def_func("colbreak", colbreak);
- std.def_func("parbreak", parbreak);
- std.def_func("linebreak", linebreak);
+ std.def_class::<HNode>("h");
+ std.def_class::<VNode>("v");
+ std.def_class::<BoxNode>("box");
+ std.def_class::<BlockNode>("block");
+ std.def_class::<AlignNode>("align");
+ std.def_class::<PadNode>("pad");
+ std.def_class::<PlaceNode>("place");
+ std.def_class::<TransformNode<Move>>("move");
+ std.def_class::<TransformNode<Scale>>("scale");
+ std.def_class::<TransformNode<Rotate>>("rotate");
+ std.def_class::<StackNode>("stack");
+ std.def_class::<GridNode>("grid");
+ std.def_class::<ColumnsNode>("columns");
+ std.def_class::<ColbreakNode>("colbreak");
// Utility functions.
std.def_func("assert", assert);
diff --git a/src/library/pad.rs b/src/library/pad.rs
index e2969fd6..394d3c17 100644
--- a/src/library/pad.rs
+++ b/src/library/pad.rs
@@ -2,25 +2,7 @@
use super::prelude::*;
-/// `pad`: Pad content at the sides.
-pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let all = args.find();
- let left = args.named("left")?;
- let top = args.named("top")?;
- let right = args.named("right")?;
- let bottom = args.named("bottom")?;
- let body: PackedNode = args.expect("body")?;
- let padding = Sides::new(
- left.or(all).unwrap_or_default(),
- top.or(all).unwrap_or_default(),
- right.or(all).unwrap_or_default(),
- bottom.or(all).unwrap_or_default(),
- );
-
- Ok(Value::block(body.padded(padding)))
-}
-
-/// A node that adds padding to its child.
+/// Pad content at the sides.
#[derive(Debug, Hash)]
pub struct PadNode {
/// The amount of padding.
@@ -29,6 +11,26 @@ pub struct PadNode {
pub child: PackedNode,
}
+#[class]
+impl PadNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ let all = args.find();
+ let left = args.named("left")?;
+ let top = args.named("top")?;
+ let right = args.named("right")?;
+ let bottom = args.named("bottom")?;
+ let body: PackedNode = args.expect("body")?;
+ let padding = Sides::new(
+ left.or(all).unwrap_or_default(),
+ top.or(all).unwrap_or_default(),
+ right.or(all).unwrap_or_default(),
+ bottom.or(all).unwrap_or_default(),
+ );
+
+ Ok(Node::block(body.padded(padding)))
+ }
+}
+
impl Layout for PadNode {
fn layout(
&self,
diff --git a/src/library/page.rs b/src/library/page.rs
index 522fd3ac..e2c27a36 100644
--- a/src/library/page.rs
+++ b/src/library/page.rs
@@ -10,7 +10,7 @@ use super::ColumnsNode;
#[derive(Clone, PartialEq, Hash)]
pub struct PageNode(pub PackedNode);
-#[properties]
+#[class]
impl PageNode {
/// The unflipped width of the page.
pub const WIDTH: Smart<Length> = Smart::Custom(Paper::default().width());
@@ -32,17 +32,11 @@ impl PageNode {
pub const FILL: Option<Paint> = None;
/// How many columns the page has.
pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap();
- /// How much space is between the page's columns.
- pub const COLUMN_GUTTER: Linear = Relative::new(0.04).into();
-}
-impl Construct for PageNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
Ok(Node::Page(Self(args.expect("body")?)))
}
-}
-impl Set for PageNode {
fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) {
styles.set(Self::CLASS, paper.class());
@@ -69,7 +63,6 @@ impl Set for PageNode {
styles.set_opt(Self::FLIPPED, args.named("flipped")?);
styles.set_opt(Self::FILL, args.named("fill")?);
styles.set_opt(Self::COLUMNS, args.named("columns")?);
- styles.set_opt(Self::COLUMN_GUTTER, args.named("column-gutter")?);
Ok(())
}
@@ -102,12 +95,7 @@ impl PageNode {
// Realize columns with columns node.
let columns = styles.get(Self::COLUMNS);
if columns.get() > 1 {
- child = ColumnsNode {
- columns,
- gutter: styles.get(Self::COLUMN_GUTTER),
- child: self.0.clone(),
- }
- .pack();
+ child = ColumnsNode { columns, child: self.0.clone() }.pack();
}
// Realize margins with padding node.
@@ -142,9 +130,14 @@ impl Debug for PageNode {
}
}
-/// `pagebreak`: Start a new page.
-pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
- Ok(Value::Node(Node::Pagebreak))
+/// A page break.
+pub struct PagebreakNode;
+
+#[class]
+impl PagebreakNode {
+ fn construct(_: &mut EvalContext, _: &mut Args) -> TypResult<Node> {
+ Ok(Node::Pagebreak)
+ }
}
/// Specification of a paper.
diff --git a/src/library/par.rs b/src/library/par.rs
index 38d15097..4f711e76 100644
--- a/src/library/par.rs
+++ b/src/library/par.rs
@@ -15,7 +15,7 @@ use crate::util::{EcoString, RangeExt, RcExt, SliceExt};
#[derive(Hash)]
pub struct ParNode(pub Vec<Styled<ParChild>>);
-#[properties]
+#[class]
impl ParNode {
/// The direction for text and inline objects.
pub const DIR: Dir = Dir::LTR;
@@ -25,9 +25,7 @@ impl ParNode {
pub const LEADING: Linear = Relative::new(0.65).into();
/// The spacing between paragraphs (dependent on scaled font size).
pub const SPACING: Linear = Relative::new(1.2).into();
-}
-impl Construct for ParNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
// The paragraph constructor is special: It doesn't create a paragraph
// since that happens automatically through markup. Instead, it just
@@ -35,13 +33,8 @@ impl Construct for ParNode {
// adjacent stuff and it styles the contained paragraphs.
Ok(Node::Block(args.expect("body")?))
}
-}
-impl Set for ParNode {
fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
- let spacing = args.named("spacing")?;
- let leading = args.named("leading")?;
-
let mut dir =
args.named("lang")?
.map(|iso: EcoString| match iso.to_lowercase().as_str() {
@@ -69,8 +62,8 @@ impl Set for ParNode {
styles.set_opt(Self::DIR, dir);
styles.set_opt(Self::ALIGN, align);
- styles.set_opt(Self::LEADING, leading);
- styles.set_opt(Self::SPACING, spacing);
+ styles.set_opt(Self::LEADING, args.named("leading")?);
+ styles.set_opt(Self::SPACING, args.named("spacing")?);
Ok(())
}
@@ -166,14 +159,24 @@ impl Debug for ParChild {
}
}
-/// `parbreak`: Start a new paragraph.
-pub fn parbreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
- Ok(Value::Node(Node::Parbreak))
+/// A paragraph break.
+pub struct ParbreakNode;
+
+#[class]
+impl ParbreakNode {
+ fn construct(_: &mut EvalContext, _: &mut Args) -> TypResult<Node> {
+ Ok(Node::Parbreak)
+ }
}
-/// `linebreak`: Start a new line.
-pub fn linebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
- Ok(Value::Node(Node::Linebreak))
+/// A line break.
+pub struct LinebreakNode;
+
+#[class]
+impl LinebreakNode {
+ fn construct(_: &mut EvalContext, _: &mut Args) -> TypResult<Node> {
+ Ok(Node::Linebreak)
+ }
}
/// A paragraph representation in which children are already layouted and text
diff --git a/src/library/placed.rs b/src/library/placed.rs
index e7b17325..cee687fa 100644
--- a/src/library/placed.rs
+++ b/src/library/placed.rs
@@ -3,33 +3,24 @@
use super::prelude::*;
use super::AlignNode;
-/// `place`: Place content at an absolute position.
-pub fn place(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let aligns = args.find().unwrap_or(Spec::with_x(Some(Align::Left)));
- let tx = args.named("dx")?.unwrap_or_default();
- let ty = args.named("dy")?.unwrap_or_default();
- let body: PackedNode = args.expect("body")?;
- Ok(Value::block(PlacedNode(
- body.moved(Point::new(tx, ty)).aligned(aligns),
- )))
-}
-
-/// A node that places its child absolutely.
+/// Place content at an absolute position.
#[derive(Debug, Hash)]
-pub struct PlacedNode(pub PackedNode);
+pub struct PlaceNode(pub PackedNode);
-impl PlacedNode {
- /// Whether this node wants to be placed relative to its its parent's base
- /// origin. instead of relative to the parent's current flow/cursor
- /// position.
- pub fn out_of_flow(&self) -> bool {
- self.0
- .downcast::<AlignNode>()
- .map_or(false, |node| node.aligns.y.is_some())
+#[class]
+impl PlaceNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ let aligns = args.find().unwrap_or(Spec::with_x(Some(Align::Left)));
+ let tx = args.named("dx")?.unwrap_or_default();
+ let ty = args.named("dy")?.unwrap_or_default();
+ let body: PackedNode = args.expect("body")?;
+ Ok(Node::block(Self(
+ body.moved(Point::new(tx, ty)).aligned(aligns),
+ )))
}
}
-impl Layout for PlacedNode {
+impl Layout for PlaceNode {
fn layout(
&self,
ctx: &mut LayoutContext,
@@ -63,3 +54,14 @@ impl Layout for PlacedNode {
frames
}
}
+
+impl PlaceNode {
+ /// Whether this node wants to be placed relative to its its parent's base
+ /// origin. instead of relative to the parent's current flow/cursor
+ /// position.
+ pub fn out_of_flow(&self) -> bool {
+ self.0
+ .downcast::<AlignNode>()
+ .map_or(false, |node| node.aligns.y.is_some())
+ }
+}
diff --git a/src/library/shape.rs b/src/library/shape.rs
index c47885d2..32e39b6a 100644
--- a/src/library/shape.rs
+++ b/src/library/shape.rs
@@ -3,110 +3,64 @@
use std::f64::consts::SQRT_2;
use super::prelude::*;
-
-/// `rect`: A rectangle with optional content.
-pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let width = args.named("width")?;
- let height = args.named("height")?;
- shape_impl(args, ShapeKind::Rect, width, height)
-}
-
-/// `square`: A square with optional content.
-pub fn square(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let size = args.named::<Length>("size")?.map(Linear::from);
- let width = match size {
- None => args.named("width")?,
- size => size,
- };
- let height = match size {
- None => args.named("height")?,
- size => size,
- };
- shape_impl(args, ShapeKind::Square, width, height)
-}
-
-/// `ellipse`: An ellipse with optional content.
-pub fn ellipse(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let width = args.named("width")?;
- let height = args.named("height")?;
- shape_impl(args, ShapeKind::Ellipse, width, height)
-}
-
-/// `circle`: A circle with optional content.
-pub fn circle(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let diameter = args.named("radius")?.map(|r: Length| 2.0 * Linear::from(r));
- let width = match diameter {
- None => args.named("width")?,
- diameter => diameter,
- };
- let height = match diameter {
- None => args.named("height")?,
- diameter => diameter,
- };
- shape_impl(args, ShapeKind::Circle, width, height)
-}
-
-fn shape_impl(
- args: &mut Args,
- kind: ShapeKind,
- width: Option<Linear>,
- height: Option<Linear>,
-) -> TypResult<Value> {
- // The default appearance of a shape.
- let default = Stroke {
- paint: RgbaColor::BLACK.into(),
- thickness: Length::pt(1.0),
- };
-
- // Parse fill & stroke.
- let fill = args.named("fill")?.unwrap_or(None);
- let stroke = match (args.named("stroke")?, args.named("thickness")?) {
- (None, None) => fill.is_none().then(|| default),
- (color, thickness) => color.unwrap_or(Some(default.paint)).map(|paint| Stroke {
- paint,
- thickness: thickness.unwrap_or(default.thickness),
- }),
- };
-
- // Shorthand for padding.
- let mut padding = args.named::<Linear>("padding")?.unwrap_or_default();
-
- // Padding with this ratio ensures that a rectangular child fits
- // perfectly into a circle / an ellipse.
- if kind.is_round() {
- padding.rel += Relative::new(0.5 - SQRT_2 / 4.0);
- }
-
- // The shape's contents.
- let child = args.find().map(|body: PackedNode| body.padded(Sides::splat(padding)));
-
- Ok(Value::inline(
- ShapeNode { kind, fill, stroke, child }
- .pack()
- .sized(Spec::new(width, height)),
- ))
-}
+use super::TextNode;
/// Places its child into a sizable and fillable shape.
#[derive(Debug, Hash)]
-pub struct ShapeNode {
+pub struct ShapeNode<S: ShapeKind> {
/// Which shape to place the child into.
- pub kind: ShapeKind,
- /// How to fill the shape.
- pub fill: Option<Paint>,
- /// How the stroke the shape.
- pub stroke: Option<Stroke>,
+ pub kind: S,
/// The child node to place into the shape, if any.
pub child: Option<PackedNode>,
}
-#[properties]
-impl ShapeNode {
- /// An URL the shape should link to.
- pub const LINK: Option<String> = None;
+#[class]
+impl<S: ShapeKind> ShapeNode<S> {
+ /// How to fill the shape.
+ pub const FILL: Option<Paint> = None;
+ /// How the stroke the shape.
+ pub const STROKE: Smart<Option<Paint>> = Smart::Auto;
+ /// The stroke's thickness.
+ pub const THICKNESS: Length = Length::pt(1.0);
+ /// The How much to pad the shape's content.
+ pub const PADDING: Linear = Linear::zero();
+
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ let size = if !S::ROUND && S::QUADRATIC {
+ args.named::<Length>("size")?.map(Linear::from)
+ } else if S::ROUND && S::QUADRATIC {
+ args.named("radius")?.map(|r: Length| 2.0 * Linear::from(r))
+ } else {
+ None
+ };
+
+ let width = match size {
+ None => args.named("width")?,
+ size => size,
+ };
+
+ let height = match size {
+ None => args.named("height")?,
+ size => size,
+ };
+
+ Ok(Node::inline(
+ ShapeNode { kind: S::default(), child: args.find() }
+ .pack()
+ .sized(Spec::new(width, height)),
+ ))
+ }
+
+ fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
+ styles.set_opt(Self::FILL, args.named("fill")?);
+ styles.set_opt(Self::STROKE, args.named("stroke")?);
+ styles.set_opt(Self::THICKNESS, args.named("thickness")?);
+ styles.set_opt(Self::PADDING, args.named("padding")?);
+ Ok(())
+ }
}
-impl Layout for ShapeNode {
+impl<S: ShapeKind> Layout for ShapeNode<S> {
fn layout(
&self,
ctx: &mut LayoutContext,
@@ -115,12 +69,20 @@ impl Layout for ShapeNode {
) -> Vec<Constrained<Rc<Frame>>> {
let mut frames;
if let Some(child) = &self.child {
+ let mut padding = styles.get(Self::PADDING);
+ if S::ROUND {
+ padding.rel += Relative::new(0.5 - SQRT_2 / 4.0);
+ }
+
+ // Pad the child.
+ let child = child.clone().padded(Sides::splat(padding));
+
let mut pod = Regions::one(regions.current, regions.base, regions.expand);
frames = child.layout(ctx, &pod, styles);
// Relayout with full expansion into square region to make sure
// the result is really a square or circle.
- if self.kind.is_quadratic() {
+ if S::QUADRATIC {
let length = if regions.expand.x || regions.expand.y {
let target = regions.expand.select(regions.current, Size::zero());
target.x.max(target.y)
@@ -141,7 +103,7 @@ impl Layout for ShapeNode {
let mut size =
Size::new(Length::pt(45.0), Length::pt(30.0)).min(regions.current);
- if self.kind.is_quadratic() {
+ if S::QUADRATIC {
let length = if regions.expand.x || regions.expand.y {
let target = regions.expand.select(regions.current, Size::zero());
target.x.max(target.y)
@@ -159,23 +121,26 @@ impl Layout for ShapeNode {
let frame = Rc::make_mut(&mut frames[0].item);
// Add fill and/or stroke.
- if self.fill.is_some() || self.stroke.is_some() {
- let geometry = match self.kind {
- ShapeKind::Square | ShapeKind::Rect => Geometry::Rect(frame.size),
- ShapeKind::Circle | ShapeKind::Ellipse => Geometry::Ellipse(frame.size),
- };
-
- let shape = Shape {
- geometry,
- fill: self.fill,
- stroke: self.stroke,
+ let fill = styles.get(Self::FILL);
+ let thickness = styles.get(Self::THICKNESS);
+ let stroke = styles
+ .get(Self::STROKE)
+ .unwrap_or(fill.is_none().then(|| RgbaColor::BLACK.into()))
+ .map(|paint| Stroke { paint, thickness });
+
+ if fill.is_some() || stroke.is_some() {
+ let geometry = if S::ROUND {
+ Geometry::Ellipse(frame.size)
+ } else {
+ Geometry::Rect(frame.size)
};
+ let shape = Shape { geometry, fill, stroke };
frame.prepend(Point::zero(), Element::Shape(shape));
}
// Apply link if it exists.
- if let Some(url) = styles.get_ref(Self::LINK) {
+ if let Some(url) = styles.get_ref(TextNode::LINK) {
frame.link(url);
}
@@ -183,27 +148,44 @@ impl Layout for ShapeNode {
}
}
-/// The type of a shape.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum ShapeKind {
- /// A rectangle with equal side lengths.
- Square,
- /// A quadrilateral with four right angles.
- Rect,
- /// An ellipse with coinciding foci.
- Circle,
- /// A curve around two focal points.
- Ellipse,
+/// Categorizes shapes.
+pub trait ShapeKind: Debug + Default + Hash + 'static {
+ const ROUND: bool;
+ const QUADRATIC: bool;
}
-impl ShapeKind {
- /// Whether the shape is curved.
- pub fn is_round(self) -> bool {
- matches!(self, Self::Circle | Self::Ellipse)
- }
+/// A rectangle with equal side lengths.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Square;
- /// Whether the shape has a fixed 1-1 aspect ratio.
- pub fn is_quadratic(self) -> bool {
- matches!(self, Self::Square | Self::Circle)
- }
+impl ShapeKind for Square {
+ const ROUND: bool = false;
+ const QUADRATIC: bool = true;
+}
+
+/// A quadrilateral with four right angles.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Rect;
+
+impl ShapeKind for Rect {
+ const ROUND: bool = false;
+ const QUADRATIC: bool = false;
+}
+
+/// An ellipse with coinciding foci.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Circle;
+
+impl ShapeKind for Circle {
+ const ROUND: bool = true;
+ const QUADRATIC: bool = true;
+}
+
+/// A curve around two focal points.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Ellipse;
+
+impl ShapeKind for Ellipse {
+ const ROUND: bool = true;
+ const QUADRATIC: bool = false;
}
diff --git a/src/library/sized.rs b/src/library/sized.rs
index 2400971a..57578717 100644
--- a/src/library/sized.rs
+++ b/src/library/sized.rs
@@ -2,18 +2,27 @@
use super::prelude::*;
-/// `box`: Size content and place it into a paragraph.
-pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let width = args.named("width")?;
- let height = args.named("height")?;
- let body: PackedNode = args.find().unwrap_or_default();
- Ok(Value::inline(body.sized(Spec::new(width, height))))
+/// Size content and place it into a paragraph.
+pub struct BoxNode;
+
+#[class]
+impl BoxNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ let width = args.named("width")?;
+ let height = args.named("height")?;
+ let body: PackedNode = args.find().unwrap_or_default();
+ Ok(Node::inline(body.sized(Spec::new(width, height))))
+ }
}
-/// `block`: Place content into the flow.
-pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let body: PackedNode = args.find().unwrap_or_default();
- Ok(Value::block(body))
+/// Place content into a separate flow.
+pub struct BlockNode;
+
+#[class]
+impl BlockNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ Ok(Node::Block(args.find().unwrap_or_default()))
+ }
}
/// A node that sizes its child.
diff --git a/src/library/spacing.rs b/src/library/spacing.rs
index 1b1403e9..7c0c377c 100644
--- a/src/library/spacing.rs
+++ b/src/library/spacing.rs
@@ -2,20 +2,24 @@
use super::prelude::*;
-/// `h`: Horizontal spacing.
-pub fn h(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- Ok(Value::Node(Node::Spacing(
- SpecAxis::Horizontal,
- args.expect("spacing")?,
- )))
+/// Horizontal spacing.
+pub struct HNode;
+
+#[class]
+impl HNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ Ok(Node::Spacing(SpecAxis::Horizontal, args.expect("spacing")?))
+ }
}
-/// `v`: Vertical spacing.
-pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- Ok(Value::Node(Node::Spacing(
- SpecAxis::Vertical,
- args.expect("spacing")?,
- )))
+/// Vertical spacing.
+pub struct VNode;
+
+#[class]
+impl VNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ Ok(Node::Spacing(SpecAxis::Vertical, args.expect("spacing")?))
+ }
}
/// Kinds of spacing.
diff --git a/src/library/stack.rs b/src/library/stack.rs
index f4f7a3cf..8c8a9f60 100644
--- a/src/library/stack.rs
+++ b/src/library/stack.rs
@@ -3,16 +3,7 @@
use super::prelude::*;
use super::{AlignNode, SpacingKind};
-/// `stack`: Stack children along an axis.
-pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- Ok(Value::block(StackNode {
- dir: args.named("dir")?.unwrap_or(Dir::TTB),
- spacing: args.named("spacing")?,
- children: args.all().collect(),
- }))
-}
-
-/// A node that stacks its children.
+/// Stack children along an axis.
#[derive(Debug, Hash)]
pub struct StackNode {
/// The stacking direction.
@@ -23,6 +14,17 @@ pub struct StackNode {
pub children: Vec<StackChild>,
}
+#[class]
+impl StackNode {
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ Ok(Node::block(Self {
+ dir: args.named("dir")?.unwrap_or(Dir::TTB),
+ spacing: args.named("spacing")?,
+ children: args.all().collect(),
+ }))
+ }
+}
+
impl Layout for StackNode {
fn layout(
&self,
diff --git a/src/library/text.rs b/src/library/text.rs
index d5c87949..78de55fd 100644
--- a/src/library/text.rs
+++ b/src/library/text.rs
@@ -9,6 +9,7 @@ use rustybuzz::{Feature, UnicodeBuffer};
use ttf_parser::Tag;
use super::prelude::*;
+use super::{DecoLine, Decoration};
use crate::font::{
Face, FaceId, FontStore, FontStretch, FontStyle, FontVariant, FontWeight,
VerticalFontMetric,
@@ -20,7 +21,7 @@ use crate::util::{EcoString, SliceExt};
#[derive(Hash)]
pub struct TextNode(pub EcoString);
-#[properties]
+#[class]
impl TextNode {
/// A prioritized sequence of font families.
pub const FAMILY_LIST: Vec<FontFamily> = vec![FontFamily::SansSerif];
@@ -52,7 +53,7 @@ impl TextNode {
pub const FILL: Paint = RgbaColor::BLACK.into();
/// Decorative lines.
#[fold(|a, b| a.into_iter().chain(b).collect())]
- pub const LINES: Vec<LineDecoration> = vec![];
+ pub const LINES: Vec<Decoration> = vec![];
/// An URL the text should link to.
pub const LINK: Option<String> = None;
@@ -92,18 +93,14 @@ impl TextNode {
pub const FRACTIONS: bool = false;
/// Raw OpenType features to apply.
pub const FEATURES: Vec<(Tag, u32)> = vec![];
-}
-impl Construct for TextNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
// The text constructor is special: It doesn't create a text node.
// Instead, it leaves the passed argument structurally unchanged, but
// styles all text in it.
args.expect("body")
}
-}
-impl Set for TextNode {
fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
let list = args.named("family")?.or_else(|| {
let families: Vec<_> = args.all().collect();
@@ -382,60 +379,6 @@ castable! {
.collect(),
}
-/// `strike`: Typeset striken-through text.
-pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Strikethrough)
-}
-
-/// `underline`: Typeset underlined text.
-pub fn underline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Underline)
-}
-
-/// `overline`: Typeset text with an overline.
-pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Overline)
-}
-
-fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
- let stroke = args.named("stroke")?.or_else(|| args.find());
- let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find());
- let offset = args.named("offset")?;
- let extent = args.named("extent")?.unwrap_or_default();
- let body: Node = args.expect("body")?;
- let deco = LineDecoration { kind, stroke, thickness, offset, extent };
- Ok(Value::Node(body.styled(TextNode::LINES, vec![deco])))
-}
-
-/// Defines a line that is positioned over, under or on top of text.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct LineDecoration {
- /// The kind of line.
- pub kind: LineKind,
- /// Stroke color of the line, defaults to the text color if `None`.
- pub stroke: Option<Paint>,
- /// Thickness of the line's strokes (dependent on scaled font size), read
- /// from the font tables if `None`.
- pub thickness: Option<Linear>,
- /// Position of the line relative to the baseline (dependent on scaled font
- /// size), read from the font tables if `None`.
- pub offset: Option<Linear>,
- /// Amount that the line will be longer or shorter than its associated text
- /// (dependent on scaled font size).
- pub extent: Linear,
-}
-
-/// The kind of line decoration.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum LineKind {
- /// A line under text.
- Underline,
- /// A line through text.
- Strikethrough,
- /// A line over text.
- Overline,
-}
-
/// Shape text into [`ShapedText`].
pub fn shape<'a>(
fonts: &mut FontStore,
@@ -848,23 +791,23 @@ impl<'a> ShapedText<'a> {
frame.push(pos, Element::Text(text));
// Apply line decorations.
- for line in self.styles.get_cloned(TextNode::LINES) {
+ for deco in self.styles.get_cloned(TextNode::LINES) {
let face = fonts.get(face_id);
- let metrics = match line.kind {
- LineKind::Underline => face.underline,
- LineKind::Strikethrough => face.strikethrough,
- LineKind::Overline => face.overline,
+ let metrics = match deco.line {
+ DecoLine::Underline => face.underline,
+ DecoLine::Strikethrough => face.strikethrough,
+ DecoLine::Overline => face.overline,
};
- let extent = line.extent.resolve(size);
- let offset = line
+ let extent = deco.extent.resolve(size);
+ let offset = deco
.offset
.map(|s| s.resolve(size))
.unwrap_or(-metrics.position.resolve(size));
let stroke = Stroke {
- paint: line.stroke.unwrap_or(fill),
- thickness: line
+ paint: deco.stroke.unwrap_or(fill),
+ thickness: deco
.thickness
.map(|s| s.resolve(size))
.unwrap_or(metrics.thickness.resolve(size)),
diff --git a/src/library/transform.rs b/src/library/transform.rs
index 7392b89f..aceb4197 100644
--- a/src/library/transform.rs
+++ b/src/library/transform.rs
@@ -3,64 +3,49 @@
use super::prelude::*;
use crate::geom::Transform;
-/// `move`: Move content without affecting layout.
-pub fn move_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let tx = args.named("x")?.unwrap_or_default();
- let ty = args.named("y")?.unwrap_or_default();
- let transform = Transform::translation(tx, ty);
- transform_impl(args, transform)
-}
-
-/// `scale`: Scale content without affecting layout.
-pub fn scale(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let all = args.find();
- let sx = args.named("x")?.or(all).unwrap_or(Relative::one());
- let sy = args.named("y")?.or(all).unwrap_or(Relative::one());
- let transform = Transform::scaling(sx, sy);
- transform_impl(args, transform)
-}
-
-/// `rotate`: Rotate content without affecting layout.
-pub fn rotate(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let angle = args.named("angle")?.or_else(|| args.find()).unwrap_or_default();
- let transform = Transform::rotation(angle);
- transform_impl(args, transform)
-}
-
-fn transform_impl(args: &mut Args, transform: Transform) -> TypResult<Value> {
- let body: PackedNode = args.expect("body")?;
- let origin = args
- .named("origin")?
- .unwrap_or(Spec::splat(None))
- .unwrap_or(Align::CENTER_HORIZON);
-
- Ok(Value::inline(body.transformed(transform, origin)))
-}
-
/// A node that transforms its child without affecting layout.
#[derive(Debug, Hash)]
-pub struct TransformNode {
+pub struct TransformNode<T: TransformKind> {
/// Transformation to apply to the contents.
- pub transform: Transform,
- /// The origin of the transformation.
- pub origin: Spec<Align>,
+ pub kind: T,
/// The node whose contents should be transformed.
pub child: PackedNode,
}
-impl Layout for TransformNode {
+#[class]
+impl<T: TransformKind> TransformNode<T> {
+ /// The origin of the transformation.
+ pub const ORIGIN: Spec<Option<Align>> = Spec::default();
+
+ fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
+ Ok(Node::inline(Self {
+ kind: T::construct(args)?,
+ child: args.expect("body")?,
+ }))
+ }
+
+ fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
+ styles.set_opt(Self::ORIGIN, args.named("origin")?);
+ Ok(())
+ }
+}
+
+impl<T: TransformKind> Layout for TransformNode<T> {
fn layout(
&self,
ctx: &mut LayoutContext,
regions: &Regions,
styles: StyleChain,
) -> Vec<Constrained<Rc<Frame>>> {
+ let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON);
+ let matrix = self.kind.matrix();
+
let mut frames = self.child.layout(ctx, regions, styles);
for Constrained { item: frame, .. } in &mut frames {
- let Spec { x, y } = self.origin.zip(frame.size).map(|(o, s)| o.resolve(s));
+ let Spec { x, y } = origin.zip(frame.size).map(|(o, s)| o.resolve(s));
let transform = Transform::translation(x, y)
- .pre_concat(self.transform)
+ .pre_concat(matrix)
.pre_concat(Transform::translation(-x, -y));
Rc::make_mut(frame).transform(transform);
@@ -69,3 +54,58 @@ impl Layout for TransformNode {
frames
}
}
+
+/// Kinds of transformations.
+pub trait TransformKind: Debug + Hash + Sized + 'static {
+ fn construct(args: &mut Args) -> TypResult<Self>;
+ fn matrix(&self) -> Transform;
+}
+
+/// A translation on the X and Y axes.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Move(pub Length, pub Length);
+
+impl TransformKind for Move {
+ fn construct(args: &mut Args) -> TypResult<Self> {
+ let tx = args.named("x")?.unwrap_or_default();
+ let ty = args.named("y")?.unwrap_or_default();
+ Ok(Self(tx, ty))
+ }
+
+ fn matrix(&self) -> Transform {
+ Transform::translation(self.0, self.1)
+ }
+}
+
+/// A rotational transformation.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Rotate(pub Angle);
+
+impl TransformKind for Rotate {
+ fn construct(args: &mut Args) -> TypResult<Self> {
+ Ok(Self(
+ args.named("angle")?.or_else(|| args.find()).unwrap_or_default(),
+ ))
+ }
+
+ fn matrix(&self) -> Transform {
+ Transform::rotation(self.0)
+ }
+}
+
+/// A scale transformation.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Scale(pub Relative, pub Relative);
+
+impl TransformKind for Scale {
+ fn construct(args: &mut Args) -> TypResult<Self> {
+ let all = args.find();
+ let sx = args.named("x")?.or(all).unwrap_or(Relative::one());
+ let sy = args.named("y")?.or(all).unwrap_or(Relative::one());
+ Ok(Self(sx, sy))
+ }
+
+ fn matrix(&self) -> Transform {
+ Transform::scale(self.0, self.1)
+ }
+}
diff --git a/src/library/utility.rs b/src/library/utility.rs
index 4e4632c4..6cc17449 100644
--- a/src/library/utility.rs
+++ b/src/library/utility.rs
@@ -6,7 +6,7 @@ use std::str::FromStr;
use super::prelude::*;
use crate::eval::Array;
-/// `assert`: Ensure that a condition is fulfilled.
+/// Ensure that a condition is fulfilled.
pub fn assert(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect::<Spanned<bool>>("condition")?;
if !v {
@@ -15,18 +15,17 @@ pub fn assert(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(Value::None)
}
-/// `type`: The name of a value's type.
+/// The name of a value's type.
pub fn type_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(args.expect::<Value>("value")?.type_name().into())
}
-/// `repr`: The string representation of a value.
+/// The string representation of a value.
pub fn repr(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(args.expect::<Value>("value")?.repr().into())
}
-/// `join`: Join a sequence of values, optionally interspersing it with another
-/// value.
+/// Join a sequence of values, optionally interspersing it with another value.
pub fn join(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let span = args.span;
let sep = args.named::<Value>("sep")?.unwrap_or(Value::None);
@@ -46,7 +45,7 @@ pub fn join(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(result)
}
-/// `int`: Convert a value to a integer.
+/// Convert a value to a integer.
pub fn int(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect("value")?;
Ok(Value::Int(match v {
@@ -61,7 +60,7 @@ pub fn int(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
}))
}
-/// `float`: Convert a value to a float.
+/// Convert a value to a float.
pub fn float(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect("value")?;
Ok(Value::Float(match v {
@@ -75,7 +74,7 @@ pub fn float(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
}))
}
-/// `str`: Try to convert a value to a string.
+/// Try to convert a value to a string.
pub fn str(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect("value")?;
Ok(Value::Str(match v {
@@ -86,7 +85,7 @@ pub fn str(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
}))
}
-/// `rgb`: Create an RGB(A) color.
+/// Create an RGB(A) color.
pub fn rgb(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(Value::from(
if let Some(string) = args.find::<Spanned<EcoString>>() {
@@ -111,7 +110,7 @@ pub fn rgb(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
))
}
-/// `abs`: The absolute value of a numeric value.
+/// The absolute value of a numeric value.
pub fn abs(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect("numeric value")?;
Ok(match v {
@@ -126,12 +125,12 @@ pub fn abs(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
})
}
-/// `min`: The minimum of a sequence of values.
+/// The minimum of a sequence of values.
pub fn min(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
minmax(args, Ordering::Less)
}
-/// `max`: The maximum of a sequence of values.
+/// The maximum of a sequence of values.
pub fn max(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
minmax(args, Ordering::Greater)
}
@@ -157,7 +156,7 @@ fn minmax(args: &mut Args, goal: Ordering) -> TypResult<Value> {
Ok(extremum)
}
-/// `range`: Create a sequence of numbers.
+/// Create a sequence of numbers.
pub fn range(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let first = args.expect::<i64>("end")?;
let (start, end) = match args.eat::<i64>()? {
@@ -182,17 +181,17 @@ pub fn range(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(Value::Array(Array::from_vec(seq)))
}
-/// `lower`: Convert a string to lowercase.
+/// Convert a string to lowercase.
pub fn lower(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(args.expect::<EcoString>("string")?.to_lowercase().into())
}
-/// `upper`: Convert a string to uppercase.
+/// Convert a string to uppercase.
pub fn upper(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Ok(args.expect::<EcoString>("string")?.to_uppercase().into())
}
-/// `len`: The length of a string, an array or a dictionary.
+/// The length of a string, an array or a dictionary.
pub fn len(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect("collection")?;
Ok(Value::Int(match v {
@@ -207,7 +206,7 @@ pub fn len(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
}))
}
-/// `sorted`: The sorted version of an array.
+/// The sorted version of an array.
pub fn sorted(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect::<Spanned<Array>>("array")?;
Ok(Value::Array(v.sorted().at(span)?))