diff options
Diffstat (limited to 'src/library')
| -rw-r--r-- | src/library/align.rs | 4 | ||||
| -rw-r--r-- | src/library/deco.rs | 133 | ||||
| -rw-r--r-- | src/library/document.rs | 9 | ||||
| -rw-r--r-- | src/library/flow.rs | 117 | ||||
| -rw-r--r-- | src/library/image.rs | 6 | ||||
| -rw-r--r-- | src/library/link.rs | 29 | ||||
| -rw-r--r-- | src/library/mod.rs | 7 | ||||
| -rw-r--r-- | src/library/page.rs | 59 | ||||
| -rw-r--r-- | src/library/par.rs | 147 | ||||
| -rw-r--r-- | src/library/shape.rs | 24 | ||||
| -rw-r--r-- | src/library/spacing.rs | 22 | ||||
| -rw-r--r-- | src/library/stack.rs | 68 | ||||
| -rw-r--r-- | src/library/text.rs | 211 |
13 files changed, 461 insertions, 375 deletions
diff --git a/src/library/align.rs b/src/library/align.rs index 96a1c6c5..c16277f6 100644 --- a/src/library/align.rs +++ b/src/library/align.rs @@ -3,8 +3,8 @@ use super::ParNode; /// `align`: Configure the alignment along the layouting axes. pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - let aligns = args.expect::<Spec<_>>("alignment")?; - let body = args.expect::<Node>("body")?; + let aligns: Spec<_> = args.expect("alignment")?; + let body: Node = args.expect("body")?; let mut styles = Styles::new(); if let Some(align) = aligns.x { diff --git a/src/library/deco.rs b/src/library/deco.rs deleted file mode 100644 index d12f60b0..00000000 --- a/src/library/deco.rs +++ /dev/null @@ -1,133 +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: Node = args.expect("body")?; - Ok(Value::Node(body.decorated(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 text = url.as_str(); - for prefix in ["mailto:", "tel:"] { - text = text.trim_start_matches(prefix); - } - Node::Text(text.into()) - }); - Ok(Value::Node(body.decorated(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 index b9a00f9b..84673270 100644 --- a/src/library/document.rs +++ b/src/library/document.rs @@ -2,7 +2,7 @@ use super::prelude::*; use super::PageNode; /// The root layout node, a document consisting of top-level page runs. -#[derive(Debug, Hash)] +#[derive(Hash)] pub struct DocumentNode(pub Vec<PageNode>); impl DocumentNode { @@ -11,3 +11,10 @@ impl DocumentNode { self.0.iter().flat_map(|node| node.layout(ctx)).collect() } } + +impl Debug for DocumentNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Document ")?; + f.debug_list().entries(&self.0).finish() + } +} diff --git a/src/library/flow.rs b/src/library/flow.rs index 41760e51..eaa1811c 100644 --- a/src/library/flow.rs +++ b/src/library/flow.rs @@ -1,41 +1,13 @@ 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(Node), - } - - 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::Node(v) => Self::Any(v), - } - - let children = args - .all() - .map(|child| match child { - Child::Spacing(spacing) => FlowChild::Spacing(spacing), - Child::Any(node) => FlowChild::Node(node.into_block()), - }) - .collect(); - - Ok(Value::block(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)] +#[derive(Hash)] pub struct FlowNode(pub Vec<FlowChild>); impl Layout for FlowNode { @@ -48,20 +20,55 @@ 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 { /// Vertical spacing between other children. - Spacing(Spacing), + Spacing(SpacingNode), /// An arbitrary node. Node(PackedNode), + /// A paragraph break. + Parbreak(Styles), +} + +impl FlowChild { + /// A reference to the child's styles. + pub fn styles(&self) -> &Styles { + match self { + Self::Spacing(node) => &node.styles, + Self::Node(node) => &node.styles, + Self::Parbreak(styles) => 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::Node(node) => &mut node.styles, + Self::Parbreak(styles) => styles, + } + } } impl Debug for FlowChild { 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), + Self::Parbreak(styles) => { + if f.alternate() { + styles.fmt(f)?; + } + write!(f, "Parbreak") + } } } } @@ -124,21 +131,28 @@ 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; - } - FlowChild::Node(ref node) => { + match child { + 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(); } self.layout_node(ctx, node); } + FlowChild::Parbreak(styles) => { + let chain = styles.chain(&ctx.styles); + let amount = chain + .get(ParNode::SPACING) + .resolve(chain.get(TextNode::SIZE).abs); + self.layout_absolute(amount.into()); + } } } @@ -158,24 +172,25 @@ impl<'a> FlowLayouter<'a> { /// Layout a node. fn layout_node(&mut self, ctx: &mut LayoutContext, node: &PackedNode) { - // Add paragraph spacing. - // TODO(set): Handle edge cases. - if !self.items.is_empty() { - let spacing = node.styles.chain(&ctx.styles).get(ParNode::SPACING); - self.layout_absolute(spacing.into()); - } - + // 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( - // TODO(set): Align paragraph according to its internal alignment. - Align::Left, + // For non-expanding paragraphs it is crucial that we align the + // whole paragraph according to its internal alignment. + 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) diff --git a/src/library/image.rs b/src/library/image.rs index 562574f9..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; @@ -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..114d25a1 --- /dev/null +++ b/src/library/link.rs @@ -0,0 +1,29 @@ +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! { + LinkNode, + + /// An URL to link to. + URL: Option<String> = None, +} diff --git a/src/library/mod.rs b/src/library/mod.rs index 7f7f95e3..9b6da6a9 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -4,11 +4,11 @@ //! definitions. mod align; -mod deco; mod document; mod flow; mod grid; mod image; +mod link; mod pad; mod page; mod par; @@ -23,6 +23,7 @@ mod utility; /// Helpful imports for creating library functionality. mod prelude { + pub use std::fmt::{self, Debug, Formatter}; pub use std::rc::Rc; pub use crate::diag::{At, TypResult}; @@ -36,10 +37,10 @@ mod prelude { pub use self::image::*; pub use align::*; -pub use deco::*; pub use document::*; pub use flow::*; pub use grid::*; +pub use link::*; pub use pad::*; pub use page::*; pub use par::*; @@ -62,6 +63,7 @@ pub fn new() -> Scope { // Text. std.def_func("font", font); std.def_func("par", par); + std.def_func("parbreak", parbreak); std.def_func("strike", strike); std.def_func("underline", underline); std.def_func("overline", overline); @@ -74,7 +76,6 @@ pub fn new() -> Scope { std.def_func("v", v); 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); diff --git a/src/library/page.rs b/src/library/page.rs index 490eef66..8905ffb6 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -14,32 +14,43 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { Value::Str(string) => Paper::from_str(&string).map_err(|e| e.to_string())?, } + let body: Option<Node> = args.find(); + + let mut map = Styles::new(); + let styles = match body { + Some(_) => &mut map, + None => &mut ctx.styles, + }; + if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) { - ctx.styles.set(PageNode::CLASS, paper.class()); - ctx.styles.set(PageNode::WIDTH, Smart::Custom(paper.width())); - ctx.styles.set(PageNode::HEIGHT, Smart::Custom(paper.height())); + styles.set(PageNode::CLASS, paper.class()); + styles.set(PageNode::WIDTH, Smart::Custom(paper.width())); + styles.set(PageNode::HEIGHT, Smart::Custom(paper.height())); } if let Some(width) = args.named("width")? { - ctx.styles.set(PageNode::CLASS, PaperClass::Custom); - ctx.styles.set(PageNode::WIDTH, width); + styles.set(PageNode::CLASS, PaperClass::Custom); + styles.set(PageNode::WIDTH, width); } if let Some(height) = args.named("height")? { - ctx.styles.set(PageNode::CLASS, PaperClass::Custom); - ctx.styles.set(PageNode::HEIGHT, height); + styles.set(PageNode::CLASS, PaperClass::Custom); + styles.set(PageNode::HEIGHT, height); } let margins = args.named("margins")?; - set!(ctx, PageNode::FLIPPED => args.named("flipped")?); - set!(ctx, PageNode::LEFT => args.named("left")?.or(margins)); - set!(ctx, PageNode::TOP => args.named("top")?.or(margins)); - set!(ctx, PageNode::RIGHT => args.named("right")?.or(margins)); - set!(ctx, PageNode::BOTTOM => args.named("bottom")?.or(margins)); - set!(ctx, PageNode::FILL => args.named("fill")?); - - Ok(Value::None) + set!(styles, PageNode::FLIPPED => args.named("flipped")?); + set!(styles, PageNode::LEFT => args.named("left")?.or(margins)); + set!(styles, PageNode::TOP => args.named("top")?.or(margins)); + set!(styles, PageNode::RIGHT => args.named("right")?.or(margins)); + set!(styles, PageNode::BOTTOM => args.named("bottom")?.or(margins)); + set!(styles, PageNode::FILL => args.named("fill")?); + + Ok(match body { + Some(body) => Value::block(body.into_block().styled(map)), + None => Value::None, + }) } /// `pagebreak`: Start a new page. @@ -48,7 +59,7 @@ pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { } /// Layouts its child onto one or multiple pages. -#[derive(Debug, Hash)] +#[derive(Hash)] pub struct PageNode { /// The node producing the content. pub node: PackedNode, @@ -82,9 +93,8 @@ properties! { impl PageNode { /// Layout the page run into a sequence of frames, one per page. pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> { - // TODO(set): Use chaining. - let prev = std::mem::replace(&mut ctx.styles, self.styles.clone()); - ctx.styles.apply(&prev); + 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. @@ -127,6 +137,17 @@ impl PageNode { } } +impl Debug for PageNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + self.styles.fmt(f)?; + } + f.write_str("Page(")?; + self.node.fmt(f)?; + f.write_str(")") + } +} + /// Specification of a paper. #[derive(Debug, Copy, Clone)] pub struct Paper { diff --git a/src/library/par.rs b/src/library/par.rs index e7433e3e..6ea960bf 100644 --- a/src/library/par.rs +++ b/src/library/par.rs @@ -6,7 +6,7 @@ use unicode_bidi::{BidiInfo, Level}; use xi_unicode::LineBreakIterator; use super::prelude::*; -use super::{shape, Decoration, ShapedText, Spacing, TextNode}; +use super::{shape, ShapedText, SpacingKind, SpacingNode, TextNode}; use crate::util::{EcoString, RangeExt, RcExt, SliceExt}; /// `par`: Configure paragraphs. @@ -41,16 +41,21 @@ pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right }); } - set!(ctx, ParNode::DIR => dir); - set!(ctx, ParNode::ALIGN => align); - set!(ctx, ParNode::LEADING => leading); - set!(ctx, ParNode::SPACING => spacing); + set!(ctx.styles, ParNode::DIR => dir); + set!(ctx.styles, ParNode::ALIGN => align); + set!(ctx.styles, ParNode::LEADING => leading); + set!(ctx.styles, ParNode::SPACING => spacing); Ok(Value::None) } +/// `parbreak`: Start a new paragraph. +pub fn parbreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { + Ok(Value::Node(Node::Parbreak)) +} + /// A node that arranges its children into a paragraph. -#[derive(Debug, Hash)] +#[derive(Hash)] pub struct ParNode(pub Vec<ParChild>); properties! { @@ -60,11 +65,10 @@ properties! { DIR: Dir = Dir::LTR, /// How to align text and inline objects in their line. ALIGN: Align = Align::Left, - // TODO(set): Make relative to font size. /// The spacing between lines (dependent on scaled font size). - LEADING: Length = Length::pt(6.5), + LEADING: Linear = Relative::new(0.65).into(), /// The spacing between paragraphs (dependent on scaled font size). - SPACING: Length = Length::pt(12.0), + SPACING: Linear = Relative::new(1.2).into(), } impl Layout for ParNode { @@ -118,34 +122,59 @@ impl ParNode { ParChild::Spacing(_) => " ", ParChild::Text(ref node) => &node.text, ParChild::Node(_) => "\u{FFFC}", - ParChild::Decorate(_) | ParChild::Undecorate => "", }) } } +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(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(node) => write!(f, "Text({:?})", node.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"), } } } @@ -163,8 +192,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. @@ -192,22 +219,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.0) { - 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 node) => { + 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) { @@ -221,36 +248,23 @@ impl<'a> ParLayouter<'a> { 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 align = ctx.styles.get(ParNode::ALIGN); + let leading = ctx + .styles + .get(ParNode::LEADING) + .resolve(ctx.styles.get(TextNode::SIZE).abs); - Self { - align: ctx.styles.get(ParNode::ALIGN), - leading: ctx.styles.get(ParNode::LEADING), - bidi, - items, - ranges, - decos, - } + Self { align, leading, bidi, items, ranges } } /// Find first-fit line breaks and build the paragraph. @@ -496,28 +510,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()), } } @@ -525,7 +530,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. @@ -557,7 +562,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/shape.rs b/src/library/shape.rs index c83a1c49..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,17 +77,14 @@ fn shape_impl( } // The shape's contents. - let body = args.find::<Node>(); + let child = args + .find() + .map(|body: Node| body.into_block().padded(Sides::splat(padding))); Ok(Value::inline( - ShapeNode { - kind, - fill, - stroke, - child: body.map(|body| body.into_block().padded(Sides::splat(padding))), - } - .pack() - .sized(Spec::new(width, height)), + ShapeNode { kind, fill, stroke, child } + .pack() + .sized(Spec::new(width, height)), )) } @@ -152,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), @@ -169,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/spacing.rs b/src/library/spacing.rs index f5de8359..4c6c2017 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -16,9 +16,27 @@ pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { ))) } +/// A single run of text with the same style. +#[derive(Hash)] +pub struct SpacingNode { + /// The kind of spacing. + pub kind: SpacingKind, + /// The rspacing'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. @@ -26,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 606632af..285ab9d5 100644 --- a/src/library/stack.rs +++ b/src/library/stack.rs @@ -1,25 +1,10 @@ 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(Node), - } - - 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::Node(v) => Self::Any(v), - } - let dir = args.named("dir")?.unwrap_or(Dir::TTB); let spacing = args.named("spacing")?; @@ -29,19 +14,15 @@ pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { // Build the list of stack children. for child in args.all() { match child { - Child::Spacing(v) => { - children.push(StackChild::Spacing(v)); - delayed = None; - } - Child::Any(child) => { + StackChild::Spacing(_) => delayed = None, + StackChild::Node(_) => { if let Some(v) = delayed { - children.push(StackChild::Spacing(v)); + children.push(StackChild::spacing(v)); } - - children.push(StackChild::Node(child.into_block())); delayed = spacing; } } + children.push(child); } Ok(Value::block(StackNode { dir, children })) @@ -70,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. @@ -144,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 e8bb6093..4ab378c2 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -1,12 +1,13 @@ use std::borrow::Cow; use std::convert::TryInto; use std::fmt::{self, Debug, Formatter}; -use std::ops::Range; +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, @@ -16,43 +17,81 @@ use crate::util::{EcoString, SliceExt}; /// `font`: Configure the font. pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { + let body = args.find::<Node>(); + + let mut map = Styles::new(); + let styles = match body { + Some(_) => &mut map, + None => &mut ctx.styles, + }; + let list = args.named("family")?.or_else(|| { let families: Vec<_> = args.all().collect(); (!families.is_empty()).then(|| families) }); - set!(ctx, TextNode::FAMILY_LIST => list); - set!(ctx, TextNode::SERIF_LIST => args.named("serif")?); - set!(ctx, TextNode::SANS_SERIF_LIST => args.named("sans-serif")?); - set!(ctx, TextNode::MONOSPACE_LIST => args.named("monospace")?); - set!(ctx, TextNode::FALLBACK => args.named("fallback")?); - set!(ctx, TextNode::STYLE => args.named("style")?); - set!(ctx, TextNode::WEIGHT => args.named("weight")?); - set!(ctx, TextNode::STRETCH => args.named("stretch")?); - set!(ctx, TextNode::FILL => args.named("fill")?.or_else(|| args.find())); - set!(ctx, TextNode::SIZE => args.named("size")?.or_else(|| args.find())); - set!(ctx, TextNode::TRACKING => args.named("tracking")?.map(Em::new)); - set!(ctx, TextNode::TOP_EDGE => args.named("top-edge")?); - set!(ctx, TextNode::BOTTOM_EDGE => args.named("bottom-edge")?); - set!(ctx, TextNode::KERNING => args.named("kerning")?); - set!(ctx, TextNode::SMALLCAPS => args.named("smallcaps")?); - set!(ctx, TextNode::ALTERNATES => args.named("alternates")?); - set!(ctx, TextNode::STYLISTIC_SET => args.named("stylistic-set")?); - set!(ctx, TextNode::LIGATURES => args.named("ligatures")?); - set!(ctx, TextNode::DISCRETIONARY_LIGATURES => args.named("discretionary-ligatures")?); - set!(ctx, TextNode::HISTORICAL_LIGATURES => args.named("historical-ligatures")?); - set!(ctx, TextNode::NUMBER_TYPE => args.named("number-type")?); - set!(ctx, TextNode::NUMBER_WIDTH => args.named("number-width")?); - set!(ctx, TextNode::NUMBER_POSITION => args.named("number-position")?); - set!(ctx, TextNode::SLASHED_ZERO => args.named("slashed-zero")?); - set!(ctx, TextNode::FRACTIONS => args.named("fractions")?); - set!(ctx, TextNode::FEATURES => args.named("features")?); - - Ok(Value::None) + set!(styles, TextNode::FAMILY_LIST => list); + set!(styles, TextNode::SERIF_LIST => args.named("serif")?); + set!(styles, TextNode::SANS_SERIF_LIST => args.named("sans-serif")?); + set!(styles, TextNode::MONOSPACE_LIST => args.named("monospace")?); + set!(styles, TextNode::FALLBACK => args.named("fallback")?); + set!(styles, TextNode::STYLE => args.named("style")?); + set!(styles, TextNode::WEIGHT => args.named("weight")?); + set!(styles, TextNode::STRETCH => args.named("stretch")?); + set!(styles, TextNode::FILL => args.named("fill")?.or_else(|| args.find())); + set!(styles, TextNode::SIZE => args.named("size")?.or_else(|| args.find())); + set!(styles, TextNode::TRACKING => args.named("tracking")?.map(Em::new)); + set!(styles, TextNode::TOP_EDGE => args.named("top-edge")?); + set!(styles, TextNode::BOTTOM_EDGE => args.named("bottom-edge")?); + set!(styles, TextNode::KERNING => args.named("kerning")?); + set!(styles, TextNode::SMALLCAPS => args.named("smallcaps")?); + set!(styles, TextNode::ALTERNATES => args.named("alternates")?); + set!(styles, TextNode::STYLISTIC_SET => args.named("stylistic-set")?); + set!(styles, TextNode::LIGATURES => args.named("ligatures")?); + set!(styles, TextNode::DISCRETIONARY_LIGATURES => args.named("discretionary-ligatures")?); + set!(styles, TextNode::HISTORICAL_LIGATURES => args.named("historical-ligatures")?); + set!(styles, TextNode::NUMBER_TYPE => args.named("number-type")?); + set!(styles, TextNode::NUMBER_WIDTH => args.named("number-width")?); + set!(styles, TextNode::NUMBER_POSITION => args.named("number-position")?); + set!(styles, TextNode::SLASHED_ZERO => args.named("slashed-zero")?); + set!(styles, TextNode::FRACTIONS => args.named("fractions")?); + set!(styles, TextNode::FEATURES => args.named("features")?); + + Ok(match body { + Some(body) => Value::Node(body.styled(map)), + None => Value::None, + }) +} + +/// `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(Debug, Hash)] +#[derive(Hash)] pub struct TextNode { /// The run's text. pub text: EcoString, @@ -82,17 +121,22 @@ properties! { /// The width of the glyphs. STRETCH: FontStretch = FontStretch::NORMAL, /// Whether the font weight should be increased by 300. + #[fold(bool::bitxor)] STRONG: bool = false, /// Whether the the font style should be inverted. + #[fold(bool::bitxor)] EMPH: bool = false, /// Whether a monospace font should be preferred. MONOSPACE: bool = false, /// The glyph fill color. FILL: Paint = RgbaColor::BLACK.into(), + /// Decorative lines. + #[fold(|a, b| a.into_iter().chain(b).collect())] + LINES: Vec<LineDecoration> = vec![], /// The size of the glyphs. - // TODO(set): Resolve relative to outer font size. - SIZE: Length = Length::pt(11.0), + #[fold(Linear::compose)] + SIZE: Linear = Length::pt(11.0).into(), /// The amount of space that should be added between characters. TRACKING: Em = Em::zero(), /// The top end of the text bounding box. @@ -128,6 +172,15 @@ properties! { FEATURES: Vec<(Tag, u32)> = vec![], } +impl Debug for TextNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + self.styles.fmt(f)?; + } + write!(f, "Text({:?})", self.text) + } +} + /// A generic or named font family. #[derive(Clone, Eq, PartialEq, Hash)] pub enum FontFamily { @@ -332,6 +385,35 @@ castable! { .collect(), } +/// Defines a line that is positioned over, under or on top of text. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct LineDecoration { + /// The kind of line. + pub kind: LineKind, + /// Stroke color of the line, defaults to the text color if `None`. + pub stroke: Option<Paint>, + /// Thickness of the line's strokes (dependent on scaled font size), read + /// from the font tables if `None`. + pub thickness: Option<Linear>, + /// Position of the line relative to the baseline (dependent on scaled font + /// size), read from the font tables if `None`. + pub offset: Option<Linear>, + /// Amount that the line will be longer or shorter than its associated text + /// (dependent on scaled font size). + pub extent: Linear, +} + +/// The kind of line decoration. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum LineKind { + /// A line under text. + Underline, + /// A line through text. + Strikethrough, + /// A line over text. + Overline, +} + /// Shape text into [`ShapedText`]. pub fn shape<'a>( fonts: &mut FontStore, @@ -520,7 +602,7 @@ fn measure( let mut top = Length::zero(); let mut bottom = Length::zero(); - let size = styles.get(TextNode::SIZE); + let size = styles.get(TextNode::SIZE).abs; let top_edge = styles.get(TextNode::TOP_EDGE); let bottom_edge = styles.get(TextNode::BOTTOM_EDGE); @@ -545,7 +627,7 @@ fn measure( expand(face); for glyph in group { - width += glyph.x_advance.to_length(size); + width += glyph.x_advance.resolve(size); } } } @@ -553,7 +635,7 @@ fn measure( (Size::new(width, top + bottom), top) } -/// Resolved the font variant with `STRONG` and `EMPH` factored in. +/// Resolve the font variant with `STRONG` and `EMPH` factored in. fn variant(styles: &Styles) -> FontVariant { let mut variant = FontVariant::new( styles.get(TextNode::STYLE), @@ -721,7 +803,7 @@ pub struct ShapedGlyph { impl<'a> ShapedText<'a> { /// Build the shaped text's frame. - pub fn build(&self) -> 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); @@ -729,23 +811,56 @@ impl<'a> ShapedText<'a> { for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) { let pos = Point::new(offset, self.baseline); - let mut text = Text { - face_id, - size: self.styles.get(TextNode::SIZE), - fill: self.styles.get(TextNode::FILL), - glyphs: vec![], - }; - - for glyph in group { - text.glyphs.push(Glyph { + 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(); - offset += text.width(); + 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 |
