summaryrefslogtreecommitdiff
path: root/src/library
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-12-15 11:11:57 +0100
committerLaurenz <laurmaedje@gmail.com>2021-12-15 11:11:57 +0100
commitae38be9097bbb32142ef776e77e627ac12379000 (patch)
treef365a348d4c77d2d607d37fee3bc65a601d00a64 /src/library
parentfe21c4d399d291e75165b664762f0aa8bdc4724a (diff)
Set Rules Episode IV: A New Fold
Diffstat (limited to 'src/library')
-rw-r--r--src/library/align.rs4
-rw-r--r--src/library/deco.rs133
-rw-r--r--src/library/document.rs9
-rw-r--r--src/library/flow.rs117
-rw-r--r--src/library/image.rs6
-rw-r--r--src/library/link.rs29
-rw-r--r--src/library/mod.rs7
-rw-r--r--src/library/page.rs59
-rw-r--r--src/library/par.rs147
-rw-r--r--src/library/shape.rs24
-rw-r--r--src/library/spacing.rs22
-rw-r--r--src/library/stack.rs68
-rw-r--r--src/library/text.rs211
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