diff options
| author | Martin <mhaug@live.de> | 2021-12-22 20:37:34 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-12-22 20:37:34 +0100 |
| commit | f6c7a8292dc1ab0560408fca9d74505e9d7cf13a (patch) | |
| tree | badd3076f6146cec34c55764600df5124c408521 /src/library | |
| parent | 738ff7e1f573bef678932b313be9969a17af8d22 (diff) | |
| parent | 438255519e88bb790480306b9a9b452aaf054519 (diff) | |
Merge pull request #51 from typst/set-rules
Set rules
Diffstat (limited to 'src/library')
| -rw-r--r-- | src/library/align.rs | 44 | ||||
| -rw-r--r-- | src/library/deco.rs | 132 | ||||
| -rw-r--r-- | src/library/document.rs | 16 | ||||
| -rw-r--r-- | src/library/flow.rs | 118 | ||||
| -rw-r--r-- | src/library/grid.rs | 16 | ||||
| -rw-r--r-- | src/library/heading.rs | 63 | ||||
| -rw-r--r-- | src/library/image.rs | 12 | ||||
| -rw-r--r-- | src/library/link.rs | 28 | ||||
| -rw-r--r-- | src/library/list.rs | 102 | ||||
| -rw-r--r-- | src/library/mod.rs | 69 | ||||
| -rw-r--r-- | src/library/pad.rs | 6 | ||||
| -rw-r--r-- | src/library/page.rs | 450 | ||||
| -rw-r--r-- | src/library/par.rs | 262 | ||||
| -rw-r--r-- | src/library/placed.rs | 26 | ||||
| -rw-r--r-- | src/library/shape.rs | 32 | ||||
| -rw-r--r-- | src/library/sized.rs | 14 | ||||
| -rw-r--r-- | src/library/spacing.rs | 36 | ||||
| -rw-r--r-- | src/library/stack.rs | 90 | ||||
| -rw-r--r-- | src/library/text.rs | 1101 | ||||
| -rw-r--r-- | src/library/transform.rs | 8 |
20 files changed, 1658 insertions, 967 deletions
diff --git a/src/library/align.rs b/src/library/align.rs index 18920369..c16277f6 100644 --- a/src/library/align.rs +++ b/src/library/align.rs @@ -1,29 +1,19 @@ use super::prelude::*; +use super::ParNode; /// `align`: Configure the alignment along the layouting axes. pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - castable! { - Spec<Option<Align>>, - Expected: "1d or 2d alignment", - @align: Align => { - let mut aligns = Spec::default(); - aligns.set(align.axis(), Some(*align)); - aligns - }, - @aligns: Spec<Align> => aligns.map(Some), + let aligns: Spec<_> = args.expect("alignment")?; + let body: Node = args.expect("body")?; + let mut styles = Styles::new(); + if let Some(align) = aligns.x { + styles.set(ParNode::ALIGN, align); } - let aligns = args.expect::<Spec<_>>("alignment")?; - let body = args.expect::<Template>("body")?; - Ok(Value::Template(Template::from_block(move |style| { - let mut style = style.clone(); - if let Some(x) = aligns.x { - style.par_mut().align = x; - } - - body.pack(&style).aligned(aligns) - }))) + Ok(Value::block( + body.into_block().styled(styles).aligned(aligns), + )) } /// A node that aligns its child. @@ -67,3 +57,19 @@ impl Layout for AlignNode { frames } } + +dynamic! { + Align: "alignment", +} + +castable! { + Spec<Option<Align>>, + Expected: "1d or 2d alignment", + @align: Align => { + let mut aligns = Spec::default(); + aligns.set(align.axis(), Some(*align)); + aligns + }, + @aligns: Spec<Align> => aligns.map(Some), + +} diff --git a/src/library/deco.rs b/src/library/deco.rs deleted file mode 100644 index cb065689..00000000 --- a/src/library/deco.rs +++ /dev/null @@ -1,132 +0,0 @@ -use super::prelude::*; -use crate::util::EcoString; - -/// `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: Template = args.expect("body")?; - Ok(Value::Template(body.decorate(Decoration::Line( - LineDecoration { kind, stroke, thickness, offset, extent }, - )))) -} - -/// `link`: Typeset text as a link. -pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - let url = args.expect::<EcoString>("url")?; - let body = args.find().unwrap_or_else(|| { - let mut template = Template::new(); - template.text(url.trim_start_matches("mailto:").trim_start_matches("tel:")); - template - }); - - Ok(Value::Template(body.decorate(Decoration::Link(url)))) -} - -/// A decoration for a frame. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum Decoration { - /// A link to an external resource. - Link(EcoString), - /// An underline/strikethrough/overline decoration. - Line(LineDecoration), -} - -impl Decoration { - /// Apply a decoration to a child's frame. - pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) { - match self { - Decoration::Link(href) => { - let link = Element::Link(href.to_string(), frame.size); - frame.push(Point::zero(), link); - } - Decoration::Line(line) => { - line.apply(ctx, frame); - } - } - } -} - -/// 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, -} - -impl LineDecoration { - /// Apply a line decoration to a all text elements in a frame. - pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) { - for i in 0 .. frame.elements.len() { - let (pos, child) = &frame.elements[i]; - if let Element::Text(text) = child { - let face = ctx.fonts.get(text.face_id); - let metrics = match self.kind { - LineKind::Underline => face.underline, - LineKind::Strikethrough => face.strikethrough, - LineKind::Overline => face.overline, - }; - - let thickness = self - .thickness - .map(|s| s.resolve(text.size)) - .unwrap_or(metrics.thickness.to_length(text.size)); - - let stroke = Stroke { - paint: self.stroke.unwrap_or(text.fill), - thickness, - }; - - let offset = self - .offset - .map(|s| s.resolve(text.size)) - .unwrap_or(-metrics.position.to_length(text.size)); - - let extent = self.extent.resolve(text.size); - - let subpos = Point::new(pos.x - extent, pos.y + offset); - let target = Point::new(text.width + 2.0 * extent, Length::zero()); - let shape = Shape::stroked(Geometry::Line(target), stroke); - frame.push(subpos, Element::Shape(shape)); - } - } - } -} diff --git a/src/library/document.rs b/src/library/document.rs deleted file mode 100644 index fe01d2df..00000000 --- a/src/library/document.rs +++ /dev/null @@ -1,16 +0,0 @@ -use super::prelude::*; -use super::PageNode; - -/// The root layout node, a document consisting of top-level page runs. -#[derive(Debug, Hash)] -pub struct DocumentNode { - /// The page runs. - pub pages: Vec<PageNode>, -} - -impl DocumentNode { - /// Layout the document into a sequence of frames, one per page. - pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> { - self.pages.iter().flat_map(|node| node.layout(ctx)).collect() - } -} diff --git a/src/library/flow.rs b/src/library/flow.rs index 98b518b7..cfa761b6 100644 --- a/src/library/flow.rs +++ b/src/library/flow.rs @@ -1,50 +1,14 @@ use std::fmt::{self, Debug, Formatter}; use super::prelude::*; -use super::{AlignNode, ParNode, PlacedNode, Spacing}; - -/// `flow`: A vertical flow of paragraphs and other layout nodes. -pub fn flow(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - enum Child { - Spacing(Spacing), - Any(Template), - } - - castable! { - Child, - Expected: "linear, fractional or template", - Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())), - Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())), - Value::Linear(v) => Self::Spacing(Spacing::Linear(v)), - Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)), - Value::Template(v) => Self::Any(v), - } - - let children: Vec<Child> = args.all().collect(); - - Ok(Value::Template(Template::from_block(move |style| { - let children = children - .iter() - .map(|child| match child { - Child::Spacing(spacing) => FlowChild::Spacing(*spacing), - Child::Any(node) => FlowChild::Node(node.pack(style)), - }) - .collect(); - - FlowNode { children } - }))) -} +use super::{AlignNode, ParNode, PlacedNode, SpacingKind, SpacingNode, TextNode}; /// A vertical flow of content consisting of paragraphs and other layout nodes. /// /// This node is reponsible for layouting both the top-level content flow and /// the contents of boxes. -#[derive(Debug, Hash)] -pub struct FlowNode { - /// The children that compose the flow. There are different kinds of - /// children for different purposes. - pub children: Vec<FlowChild>, -} +#[derive(Hash)] +pub struct FlowNode(pub Vec<FlowChild>); impl Layout for FlowNode { fn layout( @@ -56,19 +20,54 @@ impl Layout for FlowNode { } } +impl Debug for FlowNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Flow ")?; + f.debug_list().entries(&self.0).finish() + } +} + /// A child of a flow node. #[derive(Hash)] pub enum FlowChild { + /// A paragraph/block break. + Break(Styles), /// Vertical spacing between other children. - Spacing(Spacing), + Spacing(SpacingNode), /// An arbitrary node. Node(PackedNode), } +impl FlowChild { + /// A reference to the child's styles. + pub fn styles(&self) -> &Styles { + match self { + Self::Break(styles) => styles, + Self::Spacing(node) => &node.styles, + Self::Node(node) => &node.styles, + } + } + + /// A mutable reference to the child's styles. + pub fn styles_mut(&mut self) -> &mut Styles { + match self { + Self::Break(styles) => styles, + Self::Spacing(node) => &mut node.styles, + Self::Node(node) => &mut node.styles, + } + } +} + impl Debug for FlowChild { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - Self::Spacing(spacing) => spacing.fmt(f), + Self::Break(styles) => { + if f.alternate() { + styles.fmt(f)?; + } + write!(f, "Break") + } + Self::Spacing(node) => node.fmt(f), Self::Node(node) => node.fmt(f), } } @@ -118,7 +117,7 @@ impl<'a> FlowLayouter<'a> { regions.expand.y = false; Self { - children: &flow.children, + children: &flow.0, expand, full, regions, @@ -132,15 +131,21 @@ impl<'a> FlowLayouter<'a> { /// Layout all children. fn layout(mut self, ctx: &mut LayoutContext) -> Vec<Constrained<Rc<Frame>>> { for child in self.children { - match *child { - FlowChild::Spacing(Spacing::Linear(v)) => { - self.layout_absolute(v); - } - FlowChild::Spacing(Spacing::Fractional(v)) => { - self.items.push(FlowItem::Fractional(v)); - self.fr += v; + match child { + FlowChild::Break(styles) => { + let chain = styles.chain(&ctx.styles); + let em = chain.get(TextNode::SIZE).abs; + let amount = chain.get(ParNode::SPACING).resolve(em); + self.layout_absolute(amount.into()); } - FlowChild::Node(ref node) => { + FlowChild::Spacing(node) => match node.kind { + SpacingKind::Linear(v) => self.layout_absolute(v), + SpacingKind::Fractional(v) => { + self.items.push(FlowItem::Fractional(v)); + self.fr += v; + } + }, + FlowChild::Node(node) => { if self.regions.is_full() { self.finish_region(); } @@ -166,18 +171,25 @@ impl<'a> FlowLayouter<'a> { /// Layout a node. fn layout_node(&mut self, ctx: &mut LayoutContext, node: &PackedNode) { + // Placed nodes that are out of flow produce placed items which aren't + // aligned later. if let Some(placed) = node.downcast::<PlacedNode>() { - let frame = node.layout(ctx, &self.regions).remove(0); if placed.out_of_flow() { + let frame = node.layout(ctx, &self.regions).remove(0); self.items.push(FlowItem::Placed(frame.item)); return; } } + // How to align the node. let aligns = Spec::new( // For non-expanding paragraphs it is crucial that we align the // whole paragraph according to its internal alignment. - node.downcast::<ParNode>().map_or(Align::Left, |par| par.align), + if node.is::<ParNode>() { + node.styles.chain(&ctx.styles).get(ParNode::ALIGN) + } else { + Align::Left + }, // Vertical align node alignment is respected by the flow node. node.downcast::<AlignNode>() .and_then(|aligned| aligned.aligns.y) @@ -235,7 +247,7 @@ impl<'a> FlowLayouter<'a> { output.push_frame(pos, frame); } FlowItem::Placed(frame) => { - output.push_frame(Point::with_y(offset), frame); + output.push_frame(Point::zero(), frame); } } } diff --git a/src/library/grid.rs b/src/library/grid.rs index 7a9d88c3..d341cf5b 100644 --- a/src/library/grid.rs +++ b/src/library/grid.rs @@ -10,7 +10,7 @@ pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { Value::Relative(v) => vec![TrackSizing::Linear(v.into())], Value::Linear(v) => vec![TrackSizing::Linear(v)], Value::Fractional(v) => vec![TrackSizing::Fractional(v)], - Value::Int(count) => vec![TrackSizing::Auto; count.max(0) as usize], + Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast()?], Value::Array(values) => values .into_iter() .filter_map(|v| v.cast().ok()) @@ -39,15 +39,11 @@ pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { row_gutter.unwrap_or(base_gutter), ); - let children: Vec<Template> = args.all().collect(); - - Ok(Value::Template(Template::from_block(move |style| { - GridNode { - tracks: tracks.clone(), - gutter: gutter.clone(), - children: children.iter().map(|child| child.pack(style)).collect(), - } - }))) + Ok(Value::block(GridNode { + tracks, + gutter, + children: args.all().map(Node::into_block).collect(), + })) } /// A node that arranges its children in a grid. diff --git a/src/library/heading.rs b/src/library/heading.rs new file mode 100644 index 00000000..96ff2688 --- /dev/null +++ b/src/library/heading.rs @@ -0,0 +1,63 @@ +use super::prelude::*; +use super::{FontFamily, TextNode}; + +/// A section heading. +#[derive(Debug, Hash)] +pub struct HeadingNode { + /// The node that produces the heading's contents. + pub child: PackedNode, + /// The logical nesting depth of the section, starting from one. In the + /// default style, this controls the text size of the heading. + pub level: usize, +} + +#[properties] +impl HeadingNode { + /// The heading's font family. + pub const FAMILY: Smart<String> = Smart::Auto; + /// The fill color of heading in the text. 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::<Node>("body")?.into_block(), + level: args.named("level")?.unwrap_or(1), + })) + } +} + +impl Set for HeadingNode { + fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> { + styles.set_opt(Self::FAMILY, args.named("family")?); + styles.set_opt(Self::FILL, args.named("fill")?); + Ok(()) + } +} + +impl Layout for HeadingNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec<Constrained<Rc<Frame>>> { + let upscale = (1.6 - 0.1 * self.level as f64).max(0.75); + ctx.styles.set(TextNode::STRONG, true); + ctx.styles.set(TextNode::SIZE, Relative::new(upscale).into()); + + if let Smart::Custom(family) = ctx.styles.get_ref(Self::FAMILY) { + let list: Vec<_> = std::iter::once(FontFamily::named(family)) + .chain(ctx.styles.get_ref(TextNode::FAMILY_LIST).iter().cloned()) + .collect(); + ctx.styles.set(TextNode::FAMILY_LIST, list); + } + + if let Smart::Custom(fill) = ctx.styles.get(Self::FILL) { + ctx.styles.set(TextNode::FILL, fill); + } + + self.child.layout(ctx, regions) + } +} diff --git a/src/library/image.rs b/src/library/image.rs index 08ed5069..efb246a1 100644 --- a/src/library/image.rs +++ b/src/library/image.rs @@ -1,6 +1,7 @@ use std::io; use super::prelude::*; +use super::LinkNode; use crate::diag::Error; use crate::image::ImageId; @@ -20,9 +21,9 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { }) })?; - Ok(Value::Template(Template::from_inline(move |_| { - ImageNode { id, fit }.pack().sized(Spec::new(width, height)) - }))) + Ok(Value::inline( + ImageNode { id, fit }.pack().sized(Spec::new(width, height)), + )) } /// An image node. @@ -85,6 +86,11 @@ impl Layout for ImageNode { frame.clip(); } + // Apply link if it exists. + if let Some(url) = ctx.styles.get_ref(LinkNode::URL) { + frame.link(url); + } + vec![frame.constrain(Constraints::tight(regions))] } } diff --git a/src/library/link.rs b/src/library/link.rs new file mode 100644 index 00000000..40604a62 --- /dev/null +++ b/src/library/link.rs @@ -0,0 +1,28 @@ +use super::prelude::*; +use crate::util::EcoString; + +/// `link`: Link text or other elements. +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()) + }); + + Ok(Value::Node( + body.styled(Styles::one(LinkNode::URL, Some(url))), + )) +} + +/// Host for link styles. +#[derive(Debug, Hash)] +pub struct LinkNode; + +#[properties] +impl LinkNode { + /// An URL to link to. + pub const URL: Option<String> = None; +} diff --git a/src/library/list.rs b/src/library/list.rs new file mode 100644 index 00000000..25eb3600 --- /dev/null +++ b/src/library/list.rs @@ -0,0 +1,102 @@ +use std::hash::Hash; + +use super::prelude::*; +use super::{GridNode, TextNode, TrackSizing}; + +/// An unordered or ordered list. +#[derive(Debug, Hash)] +pub struct ListNode<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> { + /// 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(|node: Node| { + Node::block(Self { + child: node.into_block(), + labelling: L::default(), + }) + }) + .sum()) + } +} + +impl<L: Labelling> Set for ListNode<L> { + fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> { + styles.set_opt(Self::LABEL_INDENT, args.named("label-indent")?); + styles.set_opt(Self::BODY_INDENT, args.named("body-indent")?); + Ok(()) + } +} + +impl<L: Labelling> Layout for ListNode<L> { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec<Constrained<Rc<Frame>>> { + let em = ctx.styles.get(TextNode::SIZE).abs; + let label_indent = ctx.styles.get(Self::LABEL_INDENT).resolve(em); + let body_indent = ctx.styles.get(Self::BODY_INDENT).resolve(em); + + let columns = vec![ + TrackSizing::Linear(label_indent.into()), + TrackSizing::Auto, + TrackSizing::Linear(body_indent.into()), + TrackSizing::Auto, + ]; + + let children = vec![ + PackedNode::default(), + Node::Text(self.labelling.label()).into_block(), + PackedNode::default(), + self.child.clone(), + ]; + + GridNode { + tracks: Spec::new(columns, vec![]), + gutter: Spec::default(), + children, + } + .layout(ctx, regions) + } +} + +/// How to label a list. +pub trait Labelling: Debug + Default + Hash + 'static { + /// Return the item's label. + fn label(&self) -> EcoString; +} + +/// Unordered list labelling style. +#[derive(Debug, Default, Hash)] +pub struct Unordered; + +impl Labelling for Unordered { + fn label(&self) -> EcoString { + '•'.into() + } +} + +/// Ordered list labelling style. +#[derive(Debug, Default, Hash)] +pub struct Ordered(pub Option<usize>); + +impl Labelling 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 d60a13ea..b2dd0dbe 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -4,11 +4,12 @@ //! definitions. mod align; -mod deco; -mod document; mod flow; mod grid; +mod heading; mod image; +mod link; +mod list; mod pad; mod page; mod par; @@ -23,10 +24,15 @@ mod utility; /// Helpful imports for creating library functionality. mod prelude { + pub use std::fmt::{self, Debug, Formatter}; pub use std::rc::Rc; + pub use typst_macros::properties; + pub use crate::diag::{At, TypResult}; - pub use crate::eval::{Args, EvalContext, Smart, Template, Value}; + pub use crate::eval::{ + Args, Construct, EvalContext, Node, Property, Set, Smart, Styles, Value, + }; pub use crate::frame::*; pub use crate::geom::*; pub use crate::layout::*; @@ -36,10 +42,11 @@ mod prelude { pub use self::image::*; pub use align::*; -pub use deco::*; -pub use document::*; pub use flow::*; pub use grid::*; +pub use heading::*; +pub use link::*; +pub use list::*; pub use pad::*; pub use page::*; pub use par::*; @@ -54,28 +61,38 @@ pub use utility::*; use crate::eval::{Scope, Value}; use crate::geom::*; -use crate::style::FontFamily; /// Construct a scope containing all standard library definitions. pub fn new() -> Scope { let mut std = Scope::new(); - // Text. - std.def_func("font", font); - std.def_func("par", par); + // Classes. + std.def_class::<PageNode>("page"); + std.def_class::<ParNode>("par"); + std.def_class::<TextNode>("text"); + std.def_class::<HeadingNode>("heading"); + std.def_class::<ListNode<Unordered>>("list"); + std.def_class::<ListNode<Ordered>>("enum"); + + // Text functions. + // TODO(style): These should be classes, once that works for inline nodes. std.def_func("strike", strike); std.def_func("underline", underline); std.def_func("overline", overline); std.def_func("link", link); - // Layout. - std.def_func("page", page); + // Break and spacing functions. std.def_func("pagebreak", pagebreak); + std.def_func("parbreak", parbreak); + std.def_func("linebreak", linebreak); std.def_func("h", h); std.def_func("v", v); + + // Layout functions. + // TODO(style): Decide which of these should be classes + // (and which of their properties should be settable). std.def_func("box", box_); std.def_func("block", block); - std.def_func("flow", flow); std.def_func("stack", stack); std.def_func("grid", grid); std.def_func("pad", pad); @@ -84,15 +101,13 @@ pub fn new() -> Scope { std.def_func("move", move_); std.def_func("scale", scale); std.def_func("rotate", rotate); - - // Elements. 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); - // Utility. + // Utility functions. std.def_func("assert", assert); std.def_func("type", type_); std.def_func("repr", repr); @@ -110,14 +125,15 @@ pub fn new() -> Scope { std.def_func("len", len); std.def_func("sorted", sorted); - // Colors. + // Predefined colors. + // TODO: More colors. std.def_const("white", RgbaColor::WHITE); std.def_const("black", RgbaColor::BLACK); std.def_const("eastern", RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); std.def_const("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); std.def_const("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); - // Arbitrary constants. + // Other constants. std.def_const("ltr", Dir::LTR); std.def_const("rtl", Dir::RTL); std.def_const("ttb", Dir::TTB); @@ -139,17 +155,20 @@ dynamic! { Dir: "direction", } -dynamic! { - Align: "alignment", +castable! { + Paint, + Expected: "color", + Value::Color(color) => Paint::Solid(color), } -dynamic! { - FontFamily: "font family", - Value::Str(string) => Self::Named(string.to_lowercase()), +castable! { + usize, + Expected: "non-negative integer", + Value::Int(int) => int.try_into().map_err(|_| "must be at least zero")?, } castable! { - Paint, - Expected: "color", - Value::Color(color) => Paint::Solid(color), + String, + Expected: "string", + Value::Str(string) => string.into(), } diff --git a/src/library/pad.rs b/src/library/pad.rs index 681da73e..75fea2e5 100644 --- a/src/library/pad.rs +++ b/src/library/pad.rs @@ -7,7 +7,7 @@ pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { let top = args.named("top")?; let right = args.named("right")?; let bottom = args.named("bottom")?; - let body: Template = args.expect("body")?; + let body: Node = args.expect("body")?; let padding = Sides::new( left.or(all).unwrap_or_default(), top.or(all).unwrap_or_default(), @@ -15,9 +15,7 @@ pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { bottom.or(all).unwrap_or_default(), ); - Ok(Value::Template(Template::from_block(move |style| { - body.pack(style).padded(padding) - }))) + Ok(Value::block(body.into_block().padded(padding))) } /// A node that adds padding to its child. diff --git a/src/library/page.rs b/src/library/page.rs index 0d29ddb6..0e690770 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -1,115 +1,405 @@ -use super::prelude::*; -use crate::style::{Paper, PaperClass}; - -/// `page`: Configure pages. -pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - castable! { - Paper, - Expected: "string", - Value::Str(string) => Paper::from_name(&string).ok_or("unknown paper")?, - } - - let paper = args.named::<Paper>("paper")?.or_else(|| args.find()); - let width = args.named::<Smart<_>>("width")?; - let height = args.named::<Smart<_>>("height")?; - let flip = args.named("flip")?; - let margins = args.named("margins")?; - let left = args.named("left")?; - let top = args.named("top")?; - let right = args.named("right")?; - let bottom = args.named("bottom")?; - let fill = args.named("fill")?; - - ctx.template.modify(move |style| { - let page = style.page_mut(); - - if let Some(paper) = paper { - page.class = paper.class(); - page.size = paper.size(); - } +#![allow(unused)] - if let Some(width) = width { - page.class = PaperClass::Custom; - page.size.x = width.unwrap_or(Length::inf()); - } +use std::fmt::{self, Display, Formatter}; +use std::str::FromStr; - if let Some(height) = height { - page.class = PaperClass::Custom; - page.size.y = height.unwrap_or(Length::inf()); - } +use super::prelude::*; +use super::PadNode; - if flip.unwrap_or(false) { - std::mem::swap(&mut page.size.x, &mut page.size.y); - } +/// `pagebreak`: Start a new page. +pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { + Ok(Value::Node(Node::Pagebreak)) +} - if let Some(margins) = margins { - page.margins = Sides::splat(margins); - } +/// Layouts its child onto one or multiple pages. +#[derive(Clone, PartialEq, Hash)] +pub struct PageNode { + /// The node producing the content. + pub child: PackedNode, + /// The page's styles. + pub styles: Styles, +} - if let Some(left) = left { - page.margins.left = left; - } +#[properties] +impl PageNode { + /// The unflipped width of the page. + pub const WIDTH: Smart<Length> = Smart::Custom(Paper::default().width()); + /// The unflipped height of the page. + pub const HEIGHT: Smart<Length> = Smart::Custom(Paper::default().height()); + /// The class of paper. Defines the default margins. + pub const CLASS: PaperClass = Paper::default().class(); + /// Whether the page is flipped into landscape orientation. + pub const FLIPPED: bool = false; + /// The left margin. + pub const LEFT: Smart<Linear> = Smart::Auto; + /// The right margin. + pub const RIGHT: Smart<Linear> = Smart::Auto; + /// The top margin. + pub const TOP: Smart<Linear> = Smart::Auto; + /// The bottom margin. + pub const BOTTOM: Smart<Linear> = Smart::Auto; + /// The page's background color. + pub const FILL: Option<Paint> = None; +} - if let Some(top) = top { - page.margins.top = top; - } +impl Construct for PageNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { + Ok(Node::Page(Self { + child: args.expect::<Node>("body")?.into_block(), + styles: Styles::new(), + })) + } +} - if let Some(right) = right { - page.margins.right = right; +impl Set for PageNode { + fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> { + if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) { + styles.set(Self::CLASS, paper.class()); + styles.set(Self::WIDTH, Smart::Custom(paper.width())); + styles.set(Self::HEIGHT, Smart::Custom(paper.height())); } - if let Some(bottom) = bottom { - page.margins.bottom = bottom; + if let Some(width) = args.named("width")? { + styles.set(Self::CLASS, PaperClass::Custom); + styles.set(Self::WIDTH, width); } - if let Some(fill) = fill { - page.fill = fill; + if let Some(height) = args.named("height")? { + styles.set(Self::CLASS, PaperClass::Custom); + styles.set(Self::HEIGHT, height); } - }); - - ctx.template.pagebreak(false); - - Ok(Value::None) -} -/// `pagebreak`: Start a new page. -pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { - let mut template = Template::new(); - template.pagebreak(true); - Ok(Value::Template(template)) -} + let margins = args.named("margins")?; + styles.set_opt(Self::FLIPPED, args.named("flipped")?); + styles.set_opt(Self::LEFT, args.named("left")?.or(margins)); + styles.set_opt(Self::TOP, args.named("top")?.or(margins)); + styles.set_opt(Self::RIGHT, args.named("right")?.or(margins)); + styles.set_opt(Self::BOTTOM, args.named("bottom")?.or(margins)); + styles.set_opt(Self::FILL, args.named("fill")?); -/// Layouts its children onto one or multiple pages. -#[derive(Debug, Hash)] -pub struct PageNode { - /// The size of the page. - pub size: Size, - /// The background fill. - pub fill: Option<Paint>, - /// The node that produces the actual pages. - pub child: PackedNode, + Ok(()) + } } impl PageNode { + /// Style the node with styles from a style map. + pub fn styled(mut self, styles: Styles) -> Self { + self.styles.apply(&styles); + self + } + /// Layout the page run into a sequence of frames, one per page. pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> { + let prev = ctx.styles.clone(); + ctx.styles = self.styles.chain(&ctx.styles); + // When one of the lengths is infinite the page fits its content along // that axis. - let expand = self.size.map(Length::is_finite); - let regions = Regions::repeat(self.size, self.size, expand); + let width = ctx.styles.get(Self::WIDTH).unwrap_or(Length::inf()); + let height = ctx.styles.get(Self::HEIGHT).unwrap_or(Length::inf()); + let mut size = Size::new(width, height); + if ctx.styles.get(Self::FLIPPED) { + std::mem::swap(&mut size.x, &mut size.y); + } + + // Determine the margins. + let class = ctx.styles.get(Self::CLASS); + let default = class.default_margins(); + let padding = Sides { + left: ctx.styles.get(Self::LEFT).unwrap_or(default.left), + right: ctx.styles.get(Self::RIGHT).unwrap_or(default.right), + top: ctx.styles.get(Self::TOP).unwrap_or(default.top), + bottom: ctx.styles.get(Self::BOTTOM).unwrap_or(default.bottom), + }; + + // Pad the child. + let padded = PadNode { child: self.child.clone(), padding }.pack(); // Layout the child. + let expand = size.map(Length::is_finite); + let regions = Regions::repeat(size, size, expand); let mut frames: Vec<_> = - self.child.layout(ctx, ®ions).into_iter().map(|c| c.item).collect(); + padded.layout(ctx, ®ions).into_iter().map(|c| c.item).collect(); // Add background fill if requested. - if let Some(fill) = self.fill { + if let Some(fill) = ctx.styles.get(Self::FILL) { for frame in &mut frames { let shape = Shape::filled(Geometry::Rect(frame.size), fill); Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); } } + ctx.styles = prev; frames } } + +impl Debug for PageNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + self.styles.fmt(f)?; + } + f.write_str("Page(")?; + self.child.fmt(f)?; + f.write_str(")") + } +} + +/// Specification of a paper. +#[derive(Debug, Copy, Clone)] +pub struct Paper { + /// The broad class this paper belongs to. + class: PaperClass, + /// The width of the paper in millimeters. + width: f64, + /// The height of the paper in millimeters. + height: f64, +} + +impl Paper { + /// The class of the paper. + pub fn class(self) -> PaperClass { + self.class + } + + /// The width of the paper. + pub fn width(self) -> Length { + Length::mm(self.width) + } + + /// The height of the paper. + pub fn height(self) -> Length { + Length::mm(self.height) + } +} + +impl Default for Paper { + fn default() -> Self { + Paper::A4 + } +} + +castable! { + Paper, + Expected: "string", + Value::Str(string) => Paper::from_str(&string).map_err(|e| e.to_string())?, +} + +/// Defines default margins for a class of related papers. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum PaperClass { + Custom, + Base, + US, + Newspaper, + Book, +} + +impl PaperClass { + /// The default margins for this page class. + fn default_margins(self) -> Sides<Linear> { + let f = |r| Relative::new(r).into(); + let s = |l, t, r, b| Sides::new(f(l), f(t), f(r), f(b)); + match self { + Self::Custom => s(0.1190, 0.0842, 0.1190, 0.0842), + Self::Base => s(0.1190, 0.0842, 0.1190, 0.0842), + Self::US => s(0.1760, 0.1092, 0.1760, 0.0910), + Self::Newspaper => s(0.0455, 0.0587, 0.0455, 0.0294), + Self::Book => s(0.1200, 0.0852, 0.1500, 0.0965), + } + } +} + +/// Defines paper constants and a paper parsing implementation. +macro_rules! papers { + ($(($var:ident: $class:ident, $width:expr, $height: expr, $($pats:tt)*))*) => { + /// Predefined papers. + /// + /// Each paper is parsable from its name in kebab-case. + impl Paper { + $(pub const $var: Self = Self { + class: PaperClass::$class, + width: $width, + height: $height, + };)* + } + + impl FromStr for Paper { + type Err = ParsePaperError; + + fn from_str(name: &str) -> Result<Self, Self::Err> { + match name.to_lowercase().as_str() { + $($($pats)* => Ok(Self::$var),)* + _ => Err(ParsePaperError), + } + } + } + + /// The error when parsing a [`Paper`] from a string fails. + #[derive(Debug, Copy, Clone, Eq, PartialEq)] + pub struct ParsePaperError; + + impl Display for ParsePaperError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("invalid paper name") + } + } + + impl std::error::Error for ParsePaperError {} + }; +} + +// All paper sizes in mm. +// +// Resources: +// - https://papersizes.io/ +// - https://en.wikipedia.org/wiki/Paper_size +// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm +// - https://vintagepaper.co/blogs/news/traditional-paper-sizes +papers! { + // ---------------------------------------------------------------------- // + // ISO 216 A Series + (A0: Base, 841.0, 1189.0, "a0") + (A1: Base, 594.0, 841.0, "a1") + (A2: Base, 420.0, 594.0, "a2") + (A3: Base, 297.0, 420.0, "a3") + (A4: Base, 210.0, 297.0, "a4") + (A5: Base, 148.0, 210.0, "a5") + (A6: Book, 105.0, 148.0, "a6") + (A7: Base, 74.0, 105.0, "a7") + (A8: Base, 52.0, 74.0, "a8") + (A9: Base, 37.0, 52.0, "a9") + (A10: Base, 26.0, 37.0, "a10") + (A11: Base, 18.0, 26.0, "a11") + + // ISO 216 B Series + (ISO_B1: Base, 707.0, 1000.0, "iso-b1") + (ISO_B2: Base, 500.0, 707.0, "iso-b2") + (ISO_B3: Base, 353.0, 500.0, "iso-b3") + (ISO_B4: Base, 250.0, 353.0, "iso-b4") + (ISO_B5: Book, 176.0, 250.0, "iso-b5") + (ISO_B6: Book, 125.0, 176.0, "iso-b6") + (ISO_B7: Base, 88.0, 125.0, "iso-b7") + (ISO_B8: Base, 62.0, 88.0, "iso-b8") + + // ISO 216 C Series + (ISO_C3: Base, 324.0, 458.0, "iso-c3") + (ISO_C4: Base, 229.0, 324.0, "iso-c4") + (ISO_C5: Base, 162.0, 229.0, "iso-c5") + (ISO_C6: Base, 114.0, 162.0, "iso-c6") + (ISO_C7: Base, 81.0, 114.0, "iso-c7") + (ISO_C8: Base, 57.0, 81.0, "iso-c8") + + // DIN D Series (extension to ISO) + (DIN_D3: Base, 272.0, 385.0, "din-d3") + (DIN_D4: Base, 192.0, 272.0, "din-d4") + (DIN_D5: Base, 136.0, 192.0, "din-d5") + (DIN_D6: Base, 96.0, 136.0, "din-d6") + (DIN_D7: Base, 68.0, 96.0, "din-d7") + (DIN_D8: Base, 48.0, 68.0, "din-d8") + + // SIS (used in academia) + (SIS_G5: Base, 169.0, 239.0, "sis-g5") + (SIS_E5: Base, 115.0, 220.0, "sis-e5") + + // ANSI Extensions + (ANSI_A: Base, 216.0, 279.0, "ansi-a") + (ANSI_B: Base, 279.0, 432.0, "ansi-b") + (ANSI_C: Base, 432.0, 559.0, "ansi-c") + (ANSI_D: Base, 559.0, 864.0, "ansi-d") + (ANSI_E: Base, 864.0, 1118.0, "ansi-e") + + // ANSI Architectural Paper + (ARCH_A: Base, 229.0, 305.0, "arch-a") + (ARCH_B: Base, 305.0, 457.0, "arch-b") + (ARCH_C: Base, 457.0, 610.0, "arch-c") + (ARCH_D: Base, 610.0, 914.0, "arch-d") + (ARCH_E1: Base, 762.0, 1067.0, "arch-e1") + (ARCH_E: Base, 914.0, 1219.0, "arch-e") + + // JIS B Series + (JIS_B0: Base, 1030.0, 1456.0, "jis-b0") + (JIS_B1: Base, 728.0, 1030.0, "jis-b1") + (JIS_B2: Base, 515.0, 728.0, "jis-b2") + (JIS_B3: Base, 364.0, 515.0, "jis-b3") + (JIS_B4: Base, 257.0, 364.0, "jis-b4") + (JIS_B5: Base, 182.0, 257.0, "jis-b5") + (JIS_B6: Base, 128.0, 182.0, "jis-b6") + (JIS_B7: Base, 91.0, 128.0, "jis-b7") + (JIS_B8: Base, 64.0, 91.0, "jis-b8") + (JIS_B9: Base, 45.0, 64.0, "jis-b9") + (JIS_B10: Base, 32.0, 45.0, "jis-b10") + (JIS_B11: Base, 22.0, 32.0, "jis-b11") + + // SAC D Series + (SAC_D0: Base, 764.0, 1064.0, "sac-d0") + (SAC_D1: Base, 532.0, 760.0, "sac-d1") + (SAC_D2: Base, 380.0, 528.0, "sac-d2") + (SAC_D3: Base, 264.0, 376.0, "sac-d3") + (SAC_D4: Base, 188.0, 260.0, "sac-d4") + (SAC_D5: Base, 130.0, 184.0, "sac-d5") + (SAC_D6: Base, 92.0, 126.0, "sac-d6") + + // ISO 7810 ID + (ISO_ID_1: Base, 85.6, 53.98, "iso-id-1") + (ISO_ID_2: Base, 74.0, 105.0, "iso-id-2") + (ISO_ID_3: Base, 88.0, 125.0, "iso-id-3") + + // ---------------------------------------------------------------------- // + // Asia + (ASIA_F4: Base, 210.0, 330.0, "asia-f4") + + // Japan + (JP_SHIROKU_BAN_4: Base, 264.0, 379.0, "jp-shiroku-ban-4") + (JP_SHIROKU_BAN_5: Base, 189.0, 262.0, "jp-shiroku-ban-5") + (JP_SHIROKU_BAN_6: Base, 127.0, 188.0, "jp-shiroku-ban-6") + (JP_KIKU_4: Base, 227.0, 306.0, "jp-kiku-4") + (JP_KIKU_5: Base, 151.0, 227.0, "jp-kiku-5") + (JP_BUSINESS_CARD: Base, 91.0, 55.0, "jp-business-card") + + // China + (CN_BUSINESS_CARD: Base, 90.0, 54.0, "cn-business-card") + + // Europe + (EU_BUSINESS_CARD: Base, 85.0, 55.0, "eu-business-card") + + // French Traditional (AFNOR) + (FR_TELLIERE: Base, 340.0, 440.0, "fr-tellière") + (FR_COURONNE_ECRITURE: Base, 360.0, 460.0, "fr-couronne-écriture") + (FR_COURONNE_EDITION: Base, 370.0, 470.0, "fr-couronne-édition") + (FR_RAISIN: Base, 500.0, 650.0, "fr-raisin") + (FR_CARRE: Base, 450.0, 560.0, "fr-carré") + (FR_JESUS: Base, 560.0, 760.0, "fr-jésus") + + // United Kingdom Imperial + (UK_BRIEF: Base, 406.4, 342.9, "uk-brief") + (UK_DRAFT: Base, 254.0, 406.4, "uk-draft") + (UK_FOOLSCAP: Base, 203.2, 330.2, "uk-foolscap") + (UK_QUARTO: Base, 203.2, 254.0, "uk-quarto") + (UK_CROWN: Base, 508.0, 381.0, "uk-crown") + (UK_BOOK_A: Book, 111.0, 178.0, "uk-book-a") + (UK_BOOK_B: Book, 129.0, 198.0, "uk-book-b") + + // Unites States + (US_LETTER: US, 215.9, 279.4, "us-letter") + (US_LEGAL: US, 215.9, 355.6, "us-legal") + (US_TABLOID: US, 279.4, 431.8, "us-tabloid") + (US_EXECUTIVE: US, 184.15, 266.7, "us-executive") + (US_FOOLSCAP_FOLIO: US, 215.9, 342.9, "us-foolscap-folio") + (US_STATEMENT: US, 139.7, 215.9, "us-statement") + (US_LEDGER: US, 431.8, 279.4, "us-ledger") + (US_OFICIO: US, 215.9, 340.36, "us-oficio") + (US_GOV_LETTER: US, 203.2, 266.7, "us-gov-letter") + (US_GOV_LEGAL: US, 215.9, 330.2, "us-gov-legal") + (US_BUSINESS_CARD: Base, 88.9, 50.8, "us-business-card") + (US_DIGEST: Book, 139.7, 215.9, "us-digest") + (US_TRADE: Book, 152.4, 228.6, "us-trade") + + // ---------------------------------------------------------------------- // + // Other + (NEWSPAPER_COMPACT: Newspaper, 280.0, 430.0, "newspaper-compact") + (NEWSPAPER_BERLINER: Newspaper, 315.0, 470.0, "newspaper-berliner") + (NEWSPAPER_BROADSHEET: Newspaper, 381.0, 578.0, "newspaper-broadsheet") + (PRESENTATION_16_9: Base, 297.0, 167.0625, "presentation-16-9") + (PRESENTATION_4_3: Base, 280.0, 210.0, "presentation-4-3") +} diff --git a/src/library/par.rs b/src/library/par.rs index 6abfa7af..26280d8e 100644 --- a/src/library/par.rs +++ b/src/library/par.rs @@ -6,75 +6,81 @@ use unicode_bidi::{BidiInfo, Level}; use xi_unicode::LineBreakIterator; use super::prelude::*; -use super::{shape, Decoration, ShapedText, Spacing}; -use crate::style::TextStyle; +use super::{shape, ShapedText, SpacingKind, SpacingNode, TextNode}; use crate::util::{EcoString, RangeExt, RcExt, SliceExt}; -/// `par`: Configure paragraphs. -pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - 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() { - "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL, - "en" | "fr" | "de" => Dir::LTR, - _ => Dir::LTR, - }); - - if let Some(Spanned { v, span }) = args.named::<Spanned<Dir>>("dir")? { - if v.axis() != SpecAxis::Horizontal { - bail!(span, "must be horizontal"); - } - dir = Some(v); - } +/// `parbreak`: Start a new paragraph. +pub fn parbreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { + Ok(Value::Node(Node::Parbreak)) +} - let mut align = None; - if let Some(Spanned { v, span }) = args.named::<Spanned<Align>>("align")? { - if v.axis() != SpecAxis::Horizontal { - bail!(span, "must be horizontal"); - } - align = Some(v); - } +/// `linebreak`: Start a new line. +pub fn linebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { + Ok(Value::Node(Node::Linebreak)) +} - ctx.template.modify(move |style| { - let par = style.par_mut(); +/// A node that arranges its children into a paragraph. +#[derive(Hash)] +pub struct ParNode(pub Vec<ParChild>); - if let Some(dir) = dir { - par.dir = dir; - par.align = if dir == Dir::LTR { Align::Left } else { Align::Right }; - } +#[properties] +impl ParNode { + /// The direction for text and inline objects. + pub const DIR: Dir = Dir::LTR; + /// How to align text and inline objects in their line. + pub const ALIGN: Align = Align::Left; + /// The spacing between lines (dependent on scaled font size). + 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(); +} - if let Some(align) = align { - par.align = align; - } +impl Construct for ParNode { + fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> { + // Lift to a block so that it doesn't merge with adjacent stuff. + Ok(Node::Block(args.expect::<Node>("body")?.into_block())) + } +} - if let Some(leading) = leading { - par.leading = leading; +impl Set for ParNode { + fn set(args: &mut Args, styles: &mut Styles) -> 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() { + "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL, + "en" | "fr" | "de" => Dir::LTR, + _ => Dir::LTR, + }); + + if let Some(Spanned { v, span }) = args.named::<Spanned<Dir>>("dir")? { + if v.axis() != SpecAxis::Horizontal { + bail!(span, "must be horizontal"); + } + dir = Some(v); } - if let Some(spacing) = spacing { - par.spacing = spacing; + let mut align = None; + if let Some(Spanned { v, span }) = args.named::<Spanned<Align>>("align")? { + if v.axis() != SpecAxis::Horizontal { + bail!(span, "must be horizontal"); + } + align = Some(v); } - }); - ctx.template.parbreak(); + if let (Some(dir), None) = (dir, align) { + align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right }); + } - Ok(Value::None) -} + styles.set_opt(Self::DIR, dir); + styles.set_opt(Self::ALIGN, align); + styles.set_opt(Self::LEADING, leading); + styles.set_opt(Self::SPACING, spacing); -/// A node that arranges its children into a paragraph. -#[derive(Debug, Hash)] -pub struct ParNode { - /// The text direction (either LTR or RTL). - pub dir: Dir, - /// How to align text in its line. - pub align: Align, - /// The spacing to insert between each line. - pub leading: Length, - /// The children to be arranged in a paragraph. - pub children: Vec<ParChild>, + Ok(()) + } } impl Layout for ParNode { @@ -87,7 +93,7 @@ impl Layout for ParNode { let text = self.collect_text(); // Find out the BiDi embedding levels. - let bidi = BidiInfo::new(&text, Level::from_dir(self.dir)); + let bidi = BidiInfo::new(&text, Level::from_dir(ctx.styles.get(Self::DIR))); // Prepare paragraph layout by building a representation on which we can // do line breaking without layouting each and every line from scratch. @@ -123,38 +129,63 @@ impl ParNode { /// The string representation of each child. fn strings(&self) -> impl Iterator<Item = &str> { - self.children.iter().map(|child| match child { + self.0.iter().map(|child| match child { ParChild::Spacing(_) => " ", - ParChild::Text(ref piece, ..) => piece, - ParChild::Node(..) => "\u{FFFC}", - ParChild::Decorate(_) | ParChild::Undecorate => "", + ParChild::Text(ref node) => &node.text, + ParChild::Node(_) => "\u{FFFC}", }) } } +impl Debug for ParNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Par ")?; + f.debug_list().entries(&self.0).finish() + } +} + /// A child of a paragraph node. #[derive(Hash)] pub enum ParChild { /// Spacing between other nodes. - Spacing(Spacing), + Spacing(SpacingNode), /// A run of text and how to align it in its line. - Text(EcoString, Rc<TextStyle>), + Text(TextNode), /// Any child node and how to align it in its line. Node(PackedNode), - /// A decoration that applies until a matching `Undecorate`. - Decorate(Decoration), - /// The end of a decoration. - Undecorate, +} + +impl ParChild { + /// Create a text child. + pub fn text(text: impl Into<EcoString>, styles: Styles) -> Self { + Self::Text(TextNode { text: text.into(), styles }) + } + + /// A reference to the child's styles. + pub fn styles(&self) -> &Styles { + match self { + Self::Spacing(node) => &node.styles, + Self::Text(node) => &node.styles, + Self::Node(node) => &node.styles, + } + } + + /// A mutable reference to the child's styles. + pub fn styles_mut(&mut self) -> &mut Styles { + match self { + Self::Spacing(node) => &mut node.styles, + Self::Text(node) => &mut node.styles, + Self::Node(node) => &mut node.styles, + } + } } impl Debug for ParChild { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - Self::Spacing(v) => write!(f, "Spacing({:?})", v), - Self::Text(text, _) => write!(f, "Text({:?})", text), + Self::Spacing(node) => node.fmt(f), + Self::Text(node) => node.fmt(f), Self::Node(node) => node.fmt(f), - Self::Decorate(deco) => write!(f, "Decorate({:?})", deco), - Self::Undecorate => write!(f, "Undecorate"), } } } @@ -172,8 +203,6 @@ struct ParLayouter<'a> { items: Vec<ParItem<'a>>, /// The ranges of the items in `bidi.text`. ranges: Vec<Range>, - /// The decorations and the ranges they span. - decos: Vec<(Range, &'a Decoration)>, } /// Range of a substring of text. @@ -201,22 +230,22 @@ impl<'a> ParLayouter<'a> { ) -> Self { let mut items = vec![]; let mut ranges = vec![]; - let mut starts = vec![]; - let mut decos = vec![]; // Layout the children and collect them into items. - for (range, child) in par.ranges().zip(&par.children) { - match *child { - ParChild::Spacing(Spacing::Linear(v)) => { - let resolved = v.resolve(regions.current.x); - items.push(ParItem::Absolute(resolved)); - ranges.push(range); - } - ParChild::Spacing(Spacing::Fractional(v)) => { - items.push(ParItem::Fractional(v)); - ranges.push(range); - } - ParChild::Text(_, ref style) => { + for (range, child) in par.ranges().zip(&par.0) { + match child { + ParChild::Spacing(node) => match node.kind { + SpacingKind::Linear(v) => { + let resolved = v.resolve(regions.current.x); + items.push(ParItem::Absolute(resolved)); + ranges.push(range); + } + SpacingKind::Fractional(v) => { + items.push(ParItem::Fractional(v)); + ranges.push(range); + } + }, + ParChild::Text(node) => { // TODO: Also split by language and script. let mut cursor = range.start; for (level, group) in bidi.levels[range].group_by_key(|&lvl| lvl) { @@ -224,41 +253,27 @@ impl<'a> ParLayouter<'a> { cursor += group.len(); let subrange = start .. cursor; let text = &bidi.text[subrange.clone()]; - let shaped = shape(ctx, text, style, level.dir()); + let styles = node.styles.chain(&ctx.styles); + let shaped = shape(&mut ctx.fonts, text, styles, level.dir()); items.push(ParItem::Text(shaped)); ranges.push(subrange); } } - ParChild::Node(ref node) => { + ParChild::Node(node) => { let size = Size::new(regions.current.x, regions.base.y); let pod = Regions::one(size, regions.base, Spec::splat(false)); let frame = node.layout(ctx, &pod).remove(0); items.push(ParItem::Frame(Rc::take(frame.item))); ranges.push(range); } - ParChild::Decorate(ref deco) => { - starts.push((range.start, deco)); - } - ParChild::Undecorate => { - if let Some((start, deco)) = starts.pop() { - decos.push((start .. range.end, deco)); - } - } } } - for (start, deco) in starts { - decos.push((start .. bidi.text.len(), deco)); - } + let em = ctx.styles.get(TextNode::SIZE).abs; + let align = ctx.styles.get(ParNode::ALIGN); + let leading = ctx.styles.get(ParNode::LEADING).resolve(em); - Self { - align: par.align, - leading: par.leading, - bidi, - items, - ranges, - decos, - } + Self { align, leading, bidi, items, ranges } } /// Find first-fit line breaks and build the paragraph. @@ -430,7 +445,7 @@ impl<'a> LineLayout<'a> { // empty string. if !range.is_empty() || rest.is_empty() { // Reshape that part. - let reshaped = shaped.reshape(ctx, range); + let reshaped = shaped.reshape(&mut ctx.fonts, range); last = Some(ParItem::Text(reshaped)); } @@ -451,7 +466,7 @@ impl<'a> LineLayout<'a> { // Reshape if necessary. if range.len() < shaped.text.len() { if !range.is_empty() { - let reshaped = shaped.reshape(ctx, range); + let reshaped = shaped.reshape(&mut ctx.fonts, range); first = Some(ParItem::Text(reshaped)); } @@ -504,28 +519,19 @@ impl<'a> LineLayout<'a> { let mut output = Frame::new(size); output.baseline = Some(self.baseline); - for (range, item) in self.reordered() { - let mut position = |mut frame: Frame| { - // Decorate. - for (deco_range, deco) in &self.par.decos { - if deco_range.contains(&range.start) { - deco.apply(ctx, &mut frame); - } - } - + for item in self.reordered() { + let mut position = |frame: Frame| { let x = offset + self.par.align.resolve(remaining); let y = self.baseline - frame.baseline(); offset += frame.size.x; - - // Add to the line's frame. output.merge_frame(Point::new(x, y), frame); }; - match *item { - ParItem::Absolute(v) => offset += v, + match item { + ParItem::Absolute(v) => offset += *v, ParItem::Fractional(v) => offset += v.resolve(self.fr, remaining), - ParItem::Text(ref shaped) => position(shaped.build()), - ParItem::Frame(ref frame) => position(frame.clone()), + ParItem::Text(shaped) => position(shaped.build(&ctx.fonts)), + ParItem::Frame(frame) => position(frame.clone()), } } @@ -533,7 +539,7 @@ impl<'a> LineLayout<'a> { } /// Iterate through the line's items in visual order. - fn reordered(&self) -> impl Iterator<Item = (Range, &ParItem<'a>)> { + fn reordered(&self) -> impl Iterator<Item = &ParItem<'a>> { // The bidi crate doesn't like empty lines. let (levels, runs) = if !self.line.is_empty() { // Find the paragraph that contains the line. @@ -548,7 +554,7 @@ impl<'a> LineLayout<'a> { // Compute the reordered ranges in visual order (left to right). self.par.bidi.visual_runs(para, self.line.clone()) } else { - <_>::default() + (vec![], vec![]) }; runs.into_iter() @@ -565,7 +571,7 @@ impl<'a> LineLayout<'a> { Either::Right(range.rev()) } }) - .map(move |idx| (self.ranges[idx].clone(), self.get(idx).unwrap())) + .map(move |idx| self.get(idx).unwrap()) } /// Find the index of the item whose range contains the `text_offset`. diff --git a/src/library/placed.rs b/src/library/placed.rs index 722e0035..589a299b 100644 --- a/src/library/placed.rs +++ b/src/library/placed.rs @@ -6,27 +6,22 @@ pub fn place(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { let aligns = args.find().unwrap_or(Spec::new(Some(Align::Left), None)); let tx = args.named("dx")?.unwrap_or_default(); let ty = args.named("dy")?.unwrap_or_default(); - let body: Template = args.expect("body")?; - Ok(Value::Template(Template::from_block(move |style| { - PlacedNode { - child: body.pack(style).moved(Point::new(tx, ty)).aligned(aligns), - } - }))) + let body: Node = args.expect("body")?; + Ok(Value::block(PlacedNode( + body.into_block().moved(Point::new(tx, ty)).aligned(aligns), + ))) } /// A node that places its child absolutely. #[derive(Debug, Hash)] -pub struct PlacedNode { - /// The node to be placed. - pub child: PackedNode, -} +pub struct PlacedNode(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.child + self.0 .downcast::<AlignNode>() .map_or(false, |node| node.aligns.y.is_some()) } @@ -48,7 +43,7 @@ impl Layout for PlacedNode { Regions::one(regions.base, regions.base, expand) }; - let mut frames = self.child.layout(ctx, &pod); + let mut frames = self.0.layout(ctx, &pod); let Constrained { item: frame, cts } = &mut frames[0]; // If expansion is off, zero all sizes so that we don't take up any @@ -56,13 +51,6 @@ impl Layout for PlacedNode { let target = regions.expand.select(regions.current, Size::zero()); Rc::make_mut(frame).resize(target, Align::LEFT_TOP); - // Place relative to parent's base origin by offsetting our elements by - // the negative cursor position. - if out_of_flow { - let offset = (regions.current - regions.base).to_point(); - Rc::make_mut(frame).translate(offset); - } - // Set base constraint because our pod size is base and exact // constraints if we needed to expand or offset. *cts = Constraints::new(regions.expand); diff --git a/src/library/shape.rs b/src/library/shape.rs index 61c0d6e3..a9c9b333 100644 --- a/src/library/shape.rs +++ b/src/library/shape.rs @@ -1,6 +1,7 @@ use std::f64::consts::SQRT_2; use super::prelude::*; +use super::LinkNode; /// `rect`: A rectangle with optional content. pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { @@ -76,20 +77,15 @@ fn shape_impl( } // The shape's contents. - let body = args.find::<Template>(); - - Ok(Value::Template(Template::from_inline(move |style| { - ShapeNode { - kind, - fill, - stroke, - child: body - .as_ref() - .map(|body| body.pack(style).padded(Sides::splat(padding))), - } - .pack() - .sized(Spec::new(width, height)) - }))) + let child = args + .find() + .map(|body: Node| body.into_block().padded(Sides::splat(padding))); + + Ok(Value::inline( + ShapeNode { kind, fill, stroke, child } + .pack() + .sized(Spec::new(width, height)), + )) } /// Places its child into a sizable and fillable shape. @@ -154,9 +150,10 @@ impl Layout for ShapeNode { frames = vec![Frame::new(size).constrain(Constraints::tight(regions))]; } + let frame = Rc::make_mut(&mut frames[0].item); + // Add fill and/or stroke. if self.fill.is_some() || self.stroke.is_some() { - let frame = Rc::make_mut(&mut frames[0].item); let geometry = match self.kind { ShapeKind::Square | ShapeKind::Rect => Geometry::Rect(frame.size), ShapeKind::Circle | ShapeKind::Ellipse => Geometry::Ellipse(frame.size), @@ -171,6 +168,11 @@ impl Layout for ShapeNode { frame.prepend(Point::zero(), Element::Shape(shape)); } + // Apply link if it exists. + if let Some(url) = ctx.styles.get_ref(LinkNode::URL) { + frame.link(url); + } + frames } } diff --git a/src/library/sized.rs b/src/library/sized.rs index dfdc721d..6d677ca8 100644 --- a/src/library/sized.rs +++ b/src/library/sized.rs @@ -4,18 +4,16 @@ use super::prelude::*; pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { let width = args.named("width")?; let height = args.named("height")?; - let body: Template = args.find().unwrap_or_default(); - Ok(Value::Template(Template::from_inline(move |style| { - body.pack(style).sized(Spec::new(width, height)) - }))) + let body: Node = args.find().unwrap_or_default(); + Ok(Value::inline( + body.into_block().sized(Spec::new(width, height)), + )) } /// `block`: Place content into the flow. pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - let body: Template = args.find().unwrap_or_default(); - Ok(Value::Template(Template::from_block(move |style| { - body.pack(style) - }))) + let body: Node = args.find().unwrap_or_default(); + Ok(Value::block(body.into_block())) } /// A node that sizes its child. diff --git a/src/library/spacing.rs b/src/library/spacing.rs index 59911dc7..b5ecce69 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -2,21 +2,41 @@ use super::prelude::*; /// `h`: Horizontal spacing. pub fn h(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - let mut template = Template::new(); - template.spacing(SpecAxis::Horizontal, args.expect("spacing")?); - Ok(Value::Template(template)) + Ok(Value::Node(Node::Spacing( + SpecAxis::Horizontal, + args.expect("spacing")?, + ))) } /// `v`: Vertical spacing. pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - let mut template = Template::new(); - template.spacing(SpecAxis::Vertical, args.expect("spacing")?); - Ok(Value::Template(template)) + Ok(Value::Node(Node::Spacing( + SpecAxis::Vertical, + args.expect("spacing")?, + ))) +} + +/// Explicit spacing in a flow or paragraph. +#[derive(Hash)] +pub struct SpacingNode { + /// The kind of spacing. + pub kind: SpacingKind, + /// The spacing's styles. + pub styles: Styles, +} + +impl Debug for SpacingNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + self.styles.fmt(f)?; + } + write!(f, "{:?}", self.kind) + } } /// Kinds of spacing. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Spacing { +pub enum SpacingKind { /// A length stated in absolute values and/or relative to the parent's size. Linear(Linear), /// A length that is the fraction of the remaining free space in the parent. @@ -24,7 +44,7 @@ pub enum Spacing { } castable! { - Spacing, + SpacingKind, Expected: "linear or fractional", Value::Length(v) => Self::Linear(v.into()), Value::Relative(v) => Self::Linear(v.into()), diff --git a/src/library/stack.rs b/src/library/stack.rs index 2b1371ab..8a1f0fd5 100644 --- a/src/library/stack.rs +++ b/src/library/stack.rs @@ -1,54 +1,31 @@ use std::fmt::{self, Debug, Formatter}; use super::prelude::*; -use super::{AlignNode, Spacing}; +use super::{AlignNode, SpacingKind, SpacingNode}; /// `stack`: Stack children along an axis. pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - enum Child { - Spacing(Spacing), - Any(Template), - } - - castable! { - Child, - Expected: "linear, fractional or template", - Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())), - Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())), - Value::Linear(v) => Self::Spacing(Spacing::Linear(v)), - Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)), - Value::Template(v) => Self::Any(v), - } - let dir = args.named("dir")?.unwrap_or(Dir::TTB); let spacing = args.named("spacing")?; - let list: Vec<Child> = args.all().collect(); - Ok(Value::Template(Template::from_block(move |style| { - let mut children = vec![]; - let mut delayed = None; + let mut children = vec![]; + let mut deferred = None; - // Build the list of stack children. - for child in &list { - match child { - Child::Spacing(v) => { - children.push(StackChild::Spacing(*v)); - delayed = None; - } - Child::Any(child) => { - if let Some(v) = delayed { - children.push(StackChild::Spacing(v)); - } - - let node = child.pack(style); - children.push(StackChild::Node(node)); - delayed = spacing; + // Build the list of stack children. + for child in args.all() { + match child { + StackChild::Spacing(_) => deferred = None, + StackChild::Node(_) => { + if let Some(v) = deferred { + children.push(StackChild::spacing(v)); } + deferred = spacing; } } + children.push(child); + } - StackNode { dir, children } - }))) + Ok(Value::block(StackNode { dir, children })) } /// A node that stacks its children. @@ -74,20 +51,37 @@ impl Layout for StackNode { #[derive(Hash)] pub enum StackChild { /// Spacing between other nodes. - Spacing(Spacing), + Spacing(SpacingNode), /// An arbitrary node. Node(PackedNode), } +impl StackChild { + /// Create a spacing node from a spacing kind. + pub fn spacing(kind: SpacingKind) -> Self { + Self::Spacing(SpacingNode { kind, styles: Styles::new() }) + } +} + impl Debug for StackChild { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - Self::Spacing(spacing) => spacing.fmt(f), + Self::Spacing(node) => node.fmt(f), Self::Node(node) => node.fmt(f), } } } +castable! { + StackChild, + Expected: "linear, fractional or template", + Value::Length(v) => Self::spacing(SpacingKind::Linear(v.into())), + Value::Relative(v) => Self::spacing(SpacingKind::Linear(v.into())), + Value::Linear(v) => Self::spacing(SpacingKind::Linear(v)), + Value::Fractional(v) => Self::spacing(SpacingKind::Fractional(v)), + Value::Node(v) => Self::Node(v.into_block()), +} + /// Performs stack layout. struct StackLayouter<'a> { /// The stack node to layout. @@ -148,15 +142,15 @@ impl<'a> StackLayouter<'a> { /// Layout all children. fn layout(mut self, ctx: &mut LayoutContext) -> Vec<Constrained<Rc<Frame>>> { for child in &self.stack.children { - match *child { - StackChild::Spacing(Spacing::Linear(v)) => { - self.layout_absolute(v); - } - StackChild::Spacing(Spacing::Fractional(v)) => { - self.items.push(StackItem::Fractional(v)); - self.fr += v; - } - StackChild::Node(ref node) => { + match child { + StackChild::Spacing(node) => match node.kind { + SpacingKind::Linear(v) => self.layout_absolute(v), + SpacingKind::Fractional(v) => { + self.items.push(StackItem::Fractional(v)); + self.fr += v; + } + }, + StackChild::Node(node) => { if self.regions.is_full() { self.finish_region(); } diff --git a/src/library/text.rs b/src/library/text.rs index 0790196d..99c68f79 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -1,426 +1,488 @@ use std::borrow::Cow; use std::convert::TryInto; -use std::ops::Range; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{BitXor, Range}; use rustybuzz::{Feature, UnicodeBuffer}; use ttf_parser::Tag; use super::prelude::*; +use super::LinkNode; use crate::font::{ Face, FaceId, FontStore, FontStretch, FontStyle, FontVariant, FontWeight, VerticalFontMetric, }; use crate::geom::{Dir, Em, Length, Point, Size}; -use crate::style::{ - FontFamily, FontFeatures, NumberPosition, NumberType, NumberWidth, Style, - StylisticSet, TextStyle, -}; use crate::util::{EcoString, SliceExt}; -/// `font`: Configure the font. -pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - castable! { - Vec<FontFamily>, - Expected: "string, generic family or array thereof", - Value::Str(string) => vec![FontFamily::Named(string.to_lowercase())], - Value::Array(values) => { - values.into_iter().filter_map(|v| v.cast().ok()).collect() - }, - @family: FontFamily => vec![family.clone()], - } - - castable! { - Vec<EcoString>, - Expected: "string or array of strings", - Value::Str(string) => vec![string.to_lowercase()], - Value::Array(values) => values - .into_iter() - .filter_map(|v| v.cast().ok()) - .map(|string: EcoString| string.to_lowercase()) - .collect(), - } - - castable! { - FontStyle, - Expected: "string", - Value::Str(string) => match string.as_str() { - "normal" => Self::Normal, - "italic" => Self::Italic, - "oblique" => Self::Oblique, - _ => Err(r#"expected "normal", "italic" or "oblique""#)?, - }, - } - - castable! { - FontWeight, - Expected: "integer or string", - Value::Int(v) => v.try_into().map_or(Self::BLACK, Self::from_number), - Value::Str(string) => match string.as_str() { - "thin" => Self::THIN, - "extralight" => Self::EXTRALIGHT, - "light" => Self::LIGHT, - "regular" => Self::REGULAR, - "medium" => Self::MEDIUM, - "semibold" => Self::SEMIBOLD, - "bold" => Self::BOLD, - "extrabold" => Self::EXTRABOLD, - "black" => Self::BLACK, - _ => Err("unknown font weight")?, - }, - } - - castable! { - FontStretch, - Expected: "relative", - Value::Relative(v) => Self::from_ratio(v.get() as f32), - } - - castable! { - VerticalFontMetric, - Expected: "linear or string", - Value::Length(v) => Self::Linear(v.into()), - Value::Relative(v) => Self::Linear(v.into()), - Value::Linear(v) => Self::Linear(v), - Value::Str(string) => match string.as_str() { - "ascender" => Self::Ascender, - "cap-height" => Self::CapHeight, - "x-height" => Self::XHeight, - "baseline" => Self::Baseline, - "descender" => Self::Descender, - _ => Err("unknown font metric")?, - }, - } - - castable! { - StylisticSet, - Expected: "integer", - Value::Int(v) => match v { - 1 ..= 20 => Self::new(v as u8), - _ => Err("must be between 1 and 20")?, - }, - } - - castable! { - NumberType, - Expected: "string", - Value::Str(string) => match string.as_str() { - "lining" => Self::Lining, - "old-style" => Self::OldStyle, - _ => Err(r#"expected "lining" or "old-style""#)?, - }, - } - - castable! { - NumberWidth, - Expected: "string", - Value::Str(string) => match string.as_str() { - "proportional" => Self::Proportional, - "tabular" => Self::Tabular, - _ => Err(r#"expected "proportional" or "tabular""#)?, - }, - } - - castable! { - NumberPosition, - Expected: "string", - Value::Str(string) => match string.as_str() { - "normal" => Self::Normal, - "subscript" => Self::Subscript, - "superscript" => Self::Superscript, - _ => Err(r#"expected "normal", "subscript" or "superscript""#)?, - }, - } - - castable! { - Vec<(Tag, u32)>, - Expected: "array of strings or dictionary mapping tags to integers", - Value::Array(values) => values - .into_iter() - .filter_map(|v| v.cast().ok()) - .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1)) - .collect(), - Value::Dict(values) => values - .into_iter() - .filter_map(|(k, v)| { - let tag = Tag::from_bytes_lossy(k.as_bytes()); - let num = v.cast::<i64>().ok()?.try_into().ok()?; - Some((tag, num)) - }) - .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(Styles::one(TextNode::LINES, vec![deco])), + )) +} + +/// A single run of text with the same style. +#[derive(Hash)] +pub struct TextNode { + /// The run's text. + pub text: EcoString, + /// The run's styles. + pub styles: Styles, +} + +#[properties] +impl TextNode { + /// A prioritized sequence of font families. + pub const FAMILY_LIST: Vec<FontFamily> = vec![FontFamily::SansSerif]; + /// The serif font family/families. + pub const SERIF_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Serif")]; + /// The sans-serif font family/families. + pub const SANS_SERIF_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Sans")]; + /// The monospace font family/families. + pub const MONOSPACE_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Mono")]; + /// Whether to allow font fallback when the primary font list contains no + /// match. + pub const FALLBACK: bool = true; + + /// How the font is styled. + pub const STYLE: FontStyle = FontStyle::Normal; + /// The boldness / thickness of the font's glyphs. + pub const WEIGHT: FontWeight = FontWeight::REGULAR; + /// The width of the glyphs. + pub const STRETCH: FontStretch = FontStretch::NORMAL; + /// Whether the font weight should be increased by 300. + #[fold(bool::bitxor)] + pub const STRONG: bool = false; + /// Whether the the font style should be inverted. + #[fold(bool::bitxor)] + pub const EMPH: bool = false; + /// Whether a monospace font should be preferred. + pub const MONOSPACE: bool = false; + /// The glyph fill color. + pub const FILL: Paint = RgbaColor::BLACK.into(); + /// Decorative lines. + #[fold(|a, b| a.into_iter().chain(b).collect())] + pub const LINES: Vec<LineDecoration> = vec![]; + + /// The size of the glyphs. + #[fold(Linear::compose)] + pub const SIZE: Linear = Length::pt(11.0).into(); + /// The amount of space that should be added between characters. + pub const TRACKING: Em = Em::zero(); + /// The top end of the text bounding box. + pub const TOP_EDGE: VerticalFontMetric = VerticalFontMetric::CapHeight; + /// The bottom end of the text bounding box. + pub const BOTTOM_EDGE: VerticalFontMetric = VerticalFontMetric::Baseline; + + /// Whether to apply kerning ("kern"). + pub const KERNING: bool = true; + /// Whether small capital glyphs should be used. ("smcp") + pub const SMALLCAPS: bool = false; + /// Whether to apply stylistic alternates. ("salt") + pub const ALTERNATES: bool = false; + /// Which stylistic set to apply. ("ss01" - "ss20") + pub const STYLISTIC_SET: Option<StylisticSet> = None; + /// Whether standard ligatures are active. ("liga", "clig") + pub const LIGATURES: bool = true; + /// Whether ligatures that should be used sparingly are active. ("dlig") + pub const DISCRETIONARY_LIGATURES: bool = false; + /// Whether historical ligatures are active. ("hlig") + pub const HISTORICAL_LIGATURES: bool = false; + /// Which kind of numbers / figures to select. + pub const NUMBER_TYPE: Smart<NumberType> = Smart::Auto; + /// The width of numbers / figures. + pub const NUMBER_WIDTH: Smart<NumberWidth> = Smart::Auto; + /// How to position numbers. + pub const NUMBER_POSITION: NumberPosition = NumberPosition::Normal; + /// Whether to have a slash through the zero glyph. ("zero") + pub const SLASHED_ZERO: bool = false; + /// Whether to convert fractions. ("frac") + 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> { + // We don't need to do anything more here because the whole point of the + // text constructor is to apply the styles and that happens + // automatically during class construction. + args.expect::<Node>("body") } +} - let list = args.named("family")?.or_else(|| { - let families: Vec<_> = args.all().collect(); - (!families.is_empty()).then(|| families) - }); +impl Set for TextNode { + fn set(args: &mut Args, styles: &mut Styles) -> TypResult<()> { + let list = args.named("family")?.or_else(|| { + let families: Vec<_> = args.all().collect(); + (!families.is_empty()).then(|| families) + }); + + styles.set_opt(Self::FAMILY_LIST, list); + styles.set_opt(Self::SERIF_LIST, args.named("serif")?); + styles.set_opt(Self::SANS_SERIF_LIST, args.named("sans-serif")?); + styles.set_opt(Self::MONOSPACE_LIST, args.named("monospace")?); + styles.set_opt(Self::FALLBACK, args.named("fallback")?); + styles.set_opt(Self::STYLE, args.named("style")?); + styles.set_opt(Self::WEIGHT, args.named("weight")?); + styles.set_opt(Self::STRETCH, args.named("stretch")?); + styles.set_opt(Self::FILL, args.named("fill")?.or_else(|| args.find())); + styles.set_opt(Self::SIZE, args.named("size")?.or_else(|| args.find())); + styles.set_opt(Self::TRACKING, args.named("tracking")?.map(Em::new)); + styles.set_opt(Self::TOP_EDGE, args.named("top-edge")?); + styles.set_opt(Self::BOTTOM_EDGE, args.named("bottom-edge")?); + styles.set_opt(Self::KERNING, args.named("kerning")?); + styles.set_opt(Self::SMALLCAPS, args.named("smallcaps")?); + styles.set_opt(Self::ALTERNATES, args.named("alternates")?); + styles.set_opt(Self::STYLISTIC_SET, args.named("stylistic-set")?); + styles.set_opt(Self::LIGATURES, args.named("ligatures")?); + styles.set_opt( + Self::DISCRETIONARY_LIGATURES, + args.named("discretionary-ligatures")?, + ); + styles.set_opt( + Self::HISTORICAL_LIGATURES, + args.named("historical-ligatures")?, + ); + styles.set_opt(Self::NUMBER_TYPE, args.named("number-type")?); + styles.set_opt(Self::NUMBER_WIDTH, args.named("number-width")?); + styles.set_opt(Self::NUMBER_POSITION, args.named("number-position")?); + styles.set_opt(Self::SLASHED_ZERO, args.named("slashed-zero")?); + styles.set_opt(Self::FRACTIONS, args.named("fractions")?); + styles.set_opt(Self::FEATURES, args.named("features")?); - let serif = args.named("serif")?; - let sans_serif = args.named("sans-serif")?; - let monospace = args.named("monospace")?; - let fallback = args.named("fallback")?; - let style = args.named("style")?; - let weight = args.named("weight")?; - let stretch = args.named("stretch")?; - let size = args.named::<Linear>("size")?.or_else(|| args.find()); - let tracking = args.named("tracking")?.map(Em::new); - let top_edge = args.named("top-edge")?; - let bottom_edge = args.named("bottom-edge")?; - let fill = args.named("fill")?.or_else(|| args.find()); - let kerning = args.named("kerning")?; - let smallcaps = args.named("smallcaps")?; - let alternates = args.named("alternates")?; - let stylistic_set = args.named("stylistic-set")?; - let ligatures = args.named("ligatures")?; - let discretionary_ligatures = args.named("discretionary-ligatures")?; - let historical_ligatures = args.named("historical-ligatures")?; - let number_type = args.named("number-type")?; - let number_width = args.named("number-width")?; - let number_position = args.named("number-position")?; - let slashed_zero = args.named("slashed-zero")?; - let fractions = args.named("fractions")?; - let features = args.named("features")?; - let body = args.find::<Template>(); - - macro_rules! set { - ($target:expr => $source:expr) => { - if let Some(v) = $source { - $target = v; - } - }; + Ok(()) } +} - let f = move |style_: &mut Style| { - let text = style_.text_mut(); - set!(text.families_mut().list => list.clone()); - set!(text.families_mut().serif => serif.clone()); - set!(text.families_mut().sans_serif => sans_serif.clone()); - set!(text.families_mut().monospace => monospace.clone()); - set!(text.fallback => fallback); - set!(text.variant.style => style); - set!(text.variant.weight => weight); - set!(text.variant.stretch => stretch); - set!(text.size => size.map(|v| v.resolve(text.size))); - set!(text.tracking => tracking); - set!(text.top_edge => top_edge); - set!(text.bottom_edge => bottom_edge); - set!(text.fill => fill); - set!(text.features_mut().kerning => kerning); - set!(text.features_mut().smallcaps => smallcaps); - set!(text.features_mut().alternates => alternates); - set!(text.features_mut().stylistic_set => stylistic_set); - set!(text.features_mut().ligatures.standard => ligatures); - set!(text.features_mut().ligatures.discretionary => discretionary_ligatures); - set!(text.features_mut().ligatures.historical => historical_ligatures); - set!(text.features_mut().numbers.type_ => number_type); - set!(text.features_mut().numbers.width => number_width); - set!(text.features_mut().numbers.position => number_position); - set!(text.features_mut().numbers.slashed_zero => slashed_zero); - set!(text.features_mut().numbers.fractions => fractions); - set!(text.features_mut().raw => features.clone()); - }; +impl Debug for TextNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + self.styles.fmt(f)?; + } + write!(f, "Text({:?})", self.text) + } +} - Ok(if let Some(body) = body { - Value::Template(body.modified(f)) - } else { - ctx.template.modify(f); - Value::None - }) +/// A generic or named font family. +#[derive(Clone, Eq, PartialEq, Hash)] +pub enum FontFamily { + /// A family that has "serifs", small strokes attached to letters. + Serif, + /// A family in which glyphs do not have "serifs", small attached strokes. + SansSerif, + /// A family in which (almost) all glyphs are of equal width. + Monospace, + /// A specific font family like "Arial". + Named(NamedFamily), } -/// Shape text into [`ShapedText`]. -pub fn shape<'a>( - ctx: &mut LayoutContext, - text: &'a str, - style: &'a TextStyle, - dir: Dir, -) -> ShapedText<'a> { - let mut glyphs = vec![]; - if !text.is_empty() { - shape_segment( - ctx.fonts, - &mut glyphs, - 0, - text, - style.variant(), - style.families(), - None, - dir, - &tags(&style.features), - ); +impl FontFamily { + /// Create a named font family variant, directly from a string. + pub fn named(string: &str) -> Self { + Self::Named(NamedFamily::new(string)) } +} - track(&mut glyphs, style.tracking); +impl Debug for FontFamily { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Serif => f.pad("serif"), + Self::SansSerif => f.pad("sans-serif"), + Self::Monospace => f.pad("monospace"), + Self::Named(s) => s.fmt(f), + } + } +} - let (size, baseline) = measure(ctx, &glyphs, style); - ShapedText { - text, - dir, - style, - size, - baseline, - glyphs: Cow::Owned(glyphs), +/// A specific font family like "Arial". +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct NamedFamily(String); + +impl NamedFamily { + /// Create a named font family variant. + pub fn new(string: &str) -> Self { + Self(string.to_lowercase()) + } + + /// The lowercased family name. + pub fn as_str(&self) -> &str { + &self.0 } } -/// The result of shaping text. -/// -/// This type contains owned or borrowed shaped text runs, which can be -/// measured, used to reshape substrings more quickly and converted into a -/// frame. -#[derive(Debug, Clone)] -pub struct ShapedText<'a> { - /// The text that was shaped. - pub text: &'a str, - /// The text direction. - pub dir: Dir, - /// The properties used for font selection. - pub style: &'a TextStyle, - /// The font size. - pub size: Size, - /// The baseline from the top of the frame. - pub baseline: Length, - /// The shaped glyphs. - pub glyphs: Cow<'a, [ShapedGlyph]>, +impl Debug for NamedFamily { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } } -/// A single glyph resulting from shaping. -#[derive(Debug, Copy, Clone)] -pub struct ShapedGlyph { - /// The font face the glyph is contained in. - pub face_id: FaceId, - /// The glyph's index in the face. - pub glyph_id: u16, - /// The advance width of the glyph. - pub x_advance: Em, - /// The horizontal offset of the glyph. - pub x_offset: Em, - /// The start index of the glyph in the source text. - pub text_index: usize, - /// Whether splitting the shaping result before this glyph would yield the - /// same results as shaping the parts to both sides of `text_index` - /// separately. - pub safe_to_break: bool, +dynamic! { + FontFamily: "font family", + Value::Str(string) => Self::named(&string), } -impl<'a> ShapedText<'a> { - /// Build the shaped text's frame. - pub fn build(&self) -> Frame { - let mut offset = Length::zero(); - let mut frame = Frame::new(self.size); - frame.baseline = Some(self.baseline); +castable! { + Vec<FontFamily>, + Expected: "string, generic family or array thereof", + Value::Str(string) => vec![FontFamily::named(&string)], + Value::Array(values) => { + values.into_iter().filter_map(|v| v.cast().ok()).collect() + }, + @family: FontFamily => vec![family.clone()], +} - for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) { - let pos = Point::new(offset, self.baseline); +castable! { + Vec<NamedFamily>, + Expected: "string or array of strings", + Value::Str(string) => vec![NamedFamily::new(&string)], + Value::Array(values) => values + .into_iter() + .filter_map(|v| v.cast().ok()) + .map(|string: EcoString| NamedFamily::new(&string)) + .collect(), +} - let mut text = Text { - face_id, - size: self.style.size, - width: Length::zero(), - fill: self.style.fill, - glyphs: vec![], - }; +castable! { + FontStyle, + Expected: "string", + Value::Str(string) => match string.as_str() { + "normal" => Self::Normal, + "italic" => Self::Italic, + "oblique" => Self::Oblique, + _ => Err(r#"expected "normal", "italic" or "oblique""#)?, + }, +} - for glyph in group { - text.glyphs.push(Glyph { - id: glyph.glyph_id, - x_advance: glyph.x_advance, - x_offset: glyph.x_offset, - }); - text.width += glyph.x_advance.to_length(text.size); - } +castable! { + FontWeight, + Expected: "integer or string", + Value::Int(v) => Value::Int(v) + .cast::<usize>()? + .try_into() + .map_or(Self::BLACK, Self::from_number), + Value::Str(string) => match string.as_str() { + "thin" => Self::THIN, + "extralight" => Self::EXTRALIGHT, + "light" => Self::LIGHT, + "regular" => Self::REGULAR, + "medium" => Self::MEDIUM, + "semibold" => Self::SEMIBOLD, + "bold" => Self::BOLD, + "extrabold" => Self::EXTRABOLD, + "black" => Self::BLACK, + _ => Err("unknown font weight")?, + }, +} - offset += text.width; - frame.push(pos, Element::Text(text)); - } +castable! { + FontStretch, + Expected: "relative", + Value::Relative(v) => Self::from_ratio(v.get() as f32), +} - frame +castable! { + VerticalFontMetric, + Expected: "linear or string", + Value::Length(v) => Self::Linear(v.into()), + Value::Relative(v) => Self::Linear(v.into()), + Value::Linear(v) => Self::Linear(v), + Value::Str(string) => match string.as_str() { + "ascender" => Self::Ascender, + "cap-height" => Self::CapHeight, + "x-height" => Self::XHeight, + "baseline" => Self::Baseline, + "descender" => Self::Descender, + _ => Err("unknown font metric")?, + }, +} + +/// A stylistic set in a font face. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct StylisticSet(u8); + +impl StylisticSet { + /// Creates a new set, clamping to 1-20. + pub fn new(index: u8) -> Self { + Self(index.clamp(1, 20)) } - /// Reshape a range of the shaped text, reusing information from this - /// shaping process if possible. - pub fn reshape( - &'a self, - ctx: &mut LayoutContext, - text_range: Range<usize>, - ) -> ShapedText<'a> { - if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { - let (size, baseline) = measure(ctx, glyphs, self.style); - Self { - text: &self.text[text_range], - dir: self.dir, - style: self.style, - size, - baseline, - glyphs: Cow::Borrowed(glyphs), - } - } else { - shape(ctx, &self.text[text_range], self.style, self.dir) - } + /// Get the value, guaranteed to be 1-20. + pub fn get(self) -> u8 { + self.0 } +} - /// Find the subslice of glyphs that represent the given text range if both - /// sides are safe to break. - fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> { - let Range { mut start, mut end } = text_range; - if !self.dir.is_positive() { - std::mem::swap(&mut start, &mut end); - } +castable! { + StylisticSet, + Expected: "integer", + Value::Int(v) => match v { + 1 ..= 20 => Self::new(v as u8), + _ => Err("must be between 1 and 20")?, + }, +} - let left = self.find_safe_to_break(start, Side::Left)?; - let right = self.find_safe_to_break(end, Side::Right)?; - Some(&self.glyphs[left .. right]) - } +/// Which kind of numbers / figures to select. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberType { + /// Numbers that fit well with capital text. ("lnum") + Lining, + /// Numbers that fit well into flow of upper- and lowercase text. ("onum") + OldStyle, +} - /// Find the glyph offset matching the text index that is most towards the - /// given side and safe-to-break. - fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> { - let ltr = self.dir.is_positive(); +castable! { + NumberType, + Expected: "string", + Value::Str(string) => match string.as_str() { + "lining" => Self::Lining, + "old-style" => Self::OldStyle, + _ => Err(r#"expected "lining" or "old-style""#)?, + }, +} - // Handle edge cases. - let len = self.glyphs.len(); - if text_index == 0 { - return Some(if ltr { 0 } else { len }); - } else if text_index == self.text.len() { - return Some(if ltr { len } else { 0 }); - } +/// The width of numbers / figures. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberWidth { + /// Number widths are glyph specific. ("pnum") + Proportional, + /// All numbers are of equal width / monospaced. ("tnum") + Tabular, +} - // Find any glyph with the text index. - let mut idx = self - .glyphs - .binary_search_by(|g| { - let ordering = g.text_index.cmp(&text_index); - if ltr { ordering } else { ordering.reverse() } - }) - .ok()?; +castable! { + NumberWidth, + Expected: "string", + Value::Str(string) => match string.as_str() { + "proportional" => Self::Proportional, + "tabular" => Self::Tabular, + _ => Err(r#"expected "proportional" or "tabular""#)?, + }, +} - let next = match towards { - Side::Left => usize::checked_sub, - Side::Right => usize::checked_add, - }; +/// How to position numbers. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberPosition { + /// Numbers are positioned on the same baseline as text. + Normal, + /// Numbers are smaller and placed at the bottom. ("subs") + Subscript, + /// Numbers are smaller and placed at the top. ("sups") + Superscript, +} - // Search for the outermost glyph with the text index. - while let Some(next) = next(idx, 1) { - if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) { - break; - } - idx = next; - } +castable! { + NumberPosition, + Expected: "string", + Value::Str(string) => match string.as_str() { + "normal" => Self::Normal, + "subscript" => Self::Subscript, + "superscript" => Self::Superscript, + _ => Err(r#"expected "normal", "subscript" or "superscript""#)?, + }, +} - // RTL needs offset one because the left side of the range should be - // exclusive and the right side inclusive, contrary to the normal - // behaviour of ranges. - if !ltr { - idx += 1; - } +castable! { + Vec<(Tag, u32)>, + Expected: "array of strings or dictionary mapping tags to integers", + Value::Array(values) => values + .into_iter() + .filter_map(|v| v.cast().ok()) + .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1)) + .collect(), + Value::Dict(values) => values + .into_iter() + .filter_map(|(k, v)| { + let tag = Tag::from_bytes_lossy(k.as_bytes()); + let num = v.cast::<i64>().ok()?.try_into().ok()?; + Some((tag, num)) + }) + .collect(), +} - self.glyphs[idx].safe_to_break.then(|| idx) - } +/// 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, } -/// A visual side. -enum Side { - Left, - Right, +/// 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, + text: &'a str, + styles: Styles, + dir: Dir, +) -> ShapedText<'a> { + let mut glyphs = vec![]; + if !text.is_empty() { + shape_segment( + fonts, + &mut glyphs, + 0, + text, + variant(&styles), + families(&styles), + None, + dir, + &tags(&styles), + ); + } + + track(&mut glyphs, styles.get(TextNode::TRACKING)); + let (size, baseline) = measure(fonts, &glyphs, &styles); + + ShapedText { + text, + dir, + styles, + size, + baseline, + glyphs: Cow::Owned(glyphs), + } } /// Shape text with font fallback using the `families` iterator. @@ -568,36 +630,40 @@ fn track(glyphs: &mut [ShapedGlyph], tracking: Em) { /// Measure the size and baseline of a run of shaped glyphs with the given /// properties. fn measure( - ctx: &mut LayoutContext, + fonts: &mut FontStore, glyphs: &[ShapedGlyph], - style: &TextStyle, + styles: &Styles, ) -> (Size, Length) { let mut width = Length::zero(); let mut top = Length::zero(); let mut bottom = Length::zero(); + let size = styles.get(TextNode::SIZE).abs; + let top_edge = styles.get(TextNode::TOP_EDGE); + let bottom_edge = styles.get(TextNode::BOTTOM_EDGE); + // Expand top and bottom by reading the face's vertical metrics. let mut expand = |face: &Face| { - top.set_max(face.vertical_metric(style.top_edge, style.size)); - bottom.set_max(-face.vertical_metric(style.bottom_edge, style.size)); + top.set_max(face.vertical_metric(top_edge, size)); + bottom.set_max(-face.vertical_metric(bottom_edge, size)); }; if glyphs.is_empty() { // When there are no glyphs, we just use the vertical metrics of the // first available font. - for family in style.families() { - if let Some(face_id) = ctx.fonts.select(family, style.variant) { - expand(ctx.fonts.get(face_id)); + for family in families(styles) { + if let Some(face_id) = fonts.select(family, variant(styles)) { + expand(fonts.get(face_id)); break; } } } else { for (face_id, group) in glyphs.group_by_key(|g| g.face_id) { - let face = ctx.fonts.get(face_id); + let face = fonts.get(face_id); expand(face); for glyph in group { - width += glyph.x_advance.to_length(style.size); + width += glyph.x_advance.resolve(size); } } } @@ -605,75 +671,320 @@ fn measure( (Size::new(width, top + bottom), top) } +/// Resolve the font variant with `STRONG` and `EMPH` factored in. +fn variant(styles: &Styles) -> FontVariant { + let mut variant = FontVariant::new( + styles.get(TextNode::STYLE), + styles.get(TextNode::WEIGHT), + styles.get(TextNode::STRETCH), + ); + + if styles.get(TextNode::STRONG) { + variant.weight = variant.weight.thicken(300); + } + + if styles.get(TextNode::EMPH) { + variant.style = match variant.style { + FontStyle::Normal => FontStyle::Italic, + FontStyle::Italic => FontStyle::Normal, + FontStyle::Oblique => FontStyle::Normal, + } + } + + variant +} + +/// Resolve a prioritized iterator over the font families. +fn families(styles: &Styles) -> impl Iterator<Item = &str> + Clone { + let head = if styles.get(TextNode::MONOSPACE) { + styles.get_ref(TextNode::MONOSPACE_LIST).as_slice() + } else { + &[] + }; + + let core = styles.get_ref(TextNode::FAMILY_LIST).iter().flat_map(move |family| { + match family { + FontFamily::Named(name) => std::slice::from_ref(name), + FontFamily::Serif => styles.get_ref(TextNode::SERIF_LIST), + FontFamily::SansSerif => styles.get_ref(TextNode::SANS_SERIF_LIST), + FontFamily::Monospace => styles.get_ref(TextNode::MONOSPACE_LIST), + } + }); + + let tail: &[&str] = if styles.get(TextNode::FALLBACK) { + &["ibm plex sans", "latin modern math", "twitter color emoji"] + } else { + &[] + }; + + head.iter() + .chain(core) + .map(|named| named.as_str()) + .chain(tail.iter().copied()) +} + /// Collect the tags of the OpenType features to apply. -fn tags(features: &FontFeatures) -> Vec<Feature> { +fn tags(styles: &Styles) -> Vec<Feature> { let mut tags = vec![]; let mut feat = |tag, value| { tags.push(Feature::new(Tag::from_bytes(tag), value, ..)); }; // Features that are on by default in Harfbuzz are only added if disabled. - if !features.kerning { + if !styles.get(TextNode::KERNING) { feat(b"kern", 0); } // Features that are off by default in Harfbuzz are only added if enabled. - if features.smallcaps { + if styles.get(TextNode::SMALLCAPS) { feat(b"smcp", 1); } - if features.alternates { + if styles.get(TextNode::ALTERNATES) { feat(b"salt", 1); } let storage; - if let Some(set) = features.stylistic_set { + if let Some(set) = styles.get(TextNode::STYLISTIC_SET) { storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10]; feat(&storage, 1); } - if !features.ligatures.standard { + if !styles.get(TextNode::LIGATURES) { feat(b"liga", 0); feat(b"clig", 0); } - if features.ligatures.discretionary { + if styles.get(TextNode::DISCRETIONARY_LIGATURES) { feat(b"dlig", 1); } - if features.ligatures.historical { + if styles.get(TextNode::HISTORICAL_LIGATURES) { feat(b"hilg", 1); } - match features.numbers.type_ { + match styles.get(TextNode::NUMBER_TYPE) { Smart::Auto => {} Smart::Custom(NumberType::Lining) => feat(b"lnum", 1), Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1), } - match features.numbers.width { + match styles.get(TextNode::NUMBER_WIDTH) { Smart::Auto => {} Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1), Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1), } - match features.numbers.position { + match styles.get(TextNode::NUMBER_POSITION) { NumberPosition::Normal => {} NumberPosition::Subscript => feat(b"subs", 1), NumberPosition::Superscript => feat(b"sups", 1), } - if features.numbers.slashed_zero { + if styles.get(TextNode::SLASHED_ZERO) { feat(b"zero", 1); } - if features.numbers.fractions { + if styles.get(TextNode::FRACTIONS) { feat(b"frac", 1); } - for &(tag, value) in features.raw.iter() { + for &(tag, value) in styles.get_ref(TextNode::FEATURES).iter() { tags.push(Feature::new(tag, value, ..)) } tags } + +/// The result of shaping text. +/// +/// This type contains owned or borrowed shaped text runs, which can be +/// measured, used to reshape substrings more quickly and converted into a +/// frame. +#[derive(Debug, Clone)] +pub struct ShapedText<'a> { + /// The text that was shaped. + pub text: &'a str, + /// The text direction. + pub dir: Dir, + /// The text's style properties. + // TODO(style): Go back to reference. + pub styles: Styles, + /// The font size. + pub size: Size, + /// The baseline from the top of the frame. + pub baseline: Length, + /// The shaped glyphs. + pub glyphs: Cow<'a, [ShapedGlyph]>, +} + +/// A single glyph resulting from shaping. +#[derive(Debug, Copy, Clone)] +pub struct ShapedGlyph { + /// The font face the glyph is contained in. + pub face_id: FaceId, + /// The glyph's index in the face. + pub glyph_id: u16, + /// The advance width of the glyph. + pub x_advance: Em, + /// The horizontal offset of the glyph. + pub x_offset: Em, + /// The start index of the glyph in the source text. + pub text_index: usize, + /// Whether splitting the shaping result before this glyph would yield the + /// same results as shaping the parts to both sides of `text_index` + /// separately. + pub safe_to_break: bool, +} + +impl<'a> ShapedText<'a> { + /// Build the shaped text's frame. + pub fn build(&self, fonts: &FontStore) -> Frame { + let mut offset = Length::zero(); + let mut frame = Frame::new(self.size); + frame.baseline = Some(self.baseline); + + for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) { + let pos = Point::new(offset, self.baseline); + + let size = self.styles.get(TextNode::SIZE).abs; + let fill = self.styles.get(TextNode::FILL); + let glyphs = group + .iter() + .map(|glyph| Glyph { + id: glyph.glyph_id, + x_advance: glyph.x_advance, + x_offset: glyph.x_offset, + }) + .collect(); + + let text = Text { face_id, size, fill, glyphs }; + let width = text.width(); + frame.push(pos, Element::Text(text)); + + // Apply line decorations. + for line in self.styles.get_ref(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 extent = line.extent.resolve(size); + let offset = line + .offset + .map(|s| s.resolve(size)) + .unwrap_or(-metrics.position.resolve(size)); + + let stroke = Stroke { + paint: line.stroke.unwrap_or(fill), + thickness: line + .thickness + .map(|s| s.resolve(size)) + .unwrap_or(metrics.thickness.resolve(size)), + }; + + let subpos = Point::new(pos.x - extent, pos.y + offset); + let target = Point::new(width + 2.0 * extent, Length::zero()); + let shape = Shape::stroked(Geometry::Line(target), stroke); + frame.push(subpos, Element::Shape(shape)); + } + + offset += width; + } + + // Apply link if it exists. + if let Some(url) = self.styles.get_ref(LinkNode::URL) { + frame.link(url); + } + + frame + } + + /// Reshape a range of the shaped text, reusing information from this + /// shaping process if possible. + pub fn reshape( + &'a self, + fonts: &mut FontStore, + text_range: Range<usize>, + ) -> ShapedText<'a> { + if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { + let (size, baseline) = measure(fonts, glyphs, &self.styles); + Self { + text: &self.text[text_range], + dir: self.dir, + styles: self.styles.clone(), + size, + baseline, + glyphs: Cow::Borrowed(glyphs), + } + } else { + shape(fonts, &self.text[text_range], self.styles.clone(), self.dir) + } + } + + /// Find the subslice of glyphs that represent the given text range if both + /// sides are safe to break. + fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> { + let Range { mut start, mut end } = text_range; + if !self.dir.is_positive() { + std::mem::swap(&mut start, &mut end); + } + + let left = self.find_safe_to_break(start, Side::Left)?; + let right = self.find_safe_to_break(end, Side::Right)?; + Some(&self.glyphs[left .. right]) + } + + /// Find the glyph offset matching the text index that is most towards the + /// given side and safe-to-break. + fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> { + let ltr = self.dir.is_positive(); + + // Handle edge cases. + let len = self.glyphs.len(); + if text_index == 0 { + return Some(if ltr { 0 } else { len }); + } else if text_index == self.text.len() { + return Some(if ltr { len } else { 0 }); + } + + // Find any glyph with the text index. + let mut idx = self + .glyphs + .binary_search_by(|g| { + let ordering = g.text_index.cmp(&text_index); + if ltr { ordering } else { ordering.reverse() } + }) + .ok()?; + + let next = match towards { + Side::Left => usize::checked_sub, + Side::Right => usize::checked_add, + }; + + // Search for the outermost glyph with the text index. + while let Some(next) = next(idx, 1) { + if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) { + break; + } + idx = next; + } + + // RTL needs offset one because the left side of the range should be + // exclusive and the right side inclusive, contrary to the normal + // behaviour of ranges. + if !ltr { + idx += 1; + } + + self.glyphs[idx].safe_to_break.then(|| idx) + } +} + +/// A visual side. +enum Side { + Left, + Right, +} diff --git a/src/library/transform.rs b/src/library/transform.rs index 9ba71ecf..6a6b4034 100644 --- a/src/library/transform.rs +++ b/src/library/transform.rs @@ -26,15 +26,15 @@ pub fn rotate(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { } fn transform_impl(args: &mut Args, transform: Transform) -> TypResult<Value> { - let body: Template = args.expect("body")?; + let body: Node = args.expect("body")?; let origin = args .named("origin")? .unwrap_or(Spec::splat(None)) .unwrap_or(Align::CENTER_HORIZON); - Ok(Value::Template(Template::from_inline(move |style| { - body.pack(style).transformed(transform, origin) - }))) + Ok(Value::inline( + body.into_block().transformed(transform, origin), + )) } /// A node that transforms its child without affecting layout. |
