diff options
| author | Laurenz <laurmaedje@gmail.com> | 2021-12-15 11:11:57 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2021-12-15 11:11:57 +0100 |
| commit | ae38be9097bbb32142ef776e77e627ac12379000 (patch) | |
| tree | f365a348d4c77d2d607d37fee3bc65a601d00a64 | |
| parent | fe21c4d399d291e75165b664762f0aa8bdc4724a (diff) | |
Set Rules Episode IV: A New Fold
| -rw-r--r-- | src/eval/mod.rs | 11 | ||||
| -rw-r--r-- | src/eval/node.rs | 289 | ||||
| -rw-r--r-- | src/eval/styles.rs | 161 | ||||
| -rw-r--r-- | src/eval/value.rs | 10 | ||||
| -rw-r--r-- | src/font.rs | 8 | ||||
| -rw-r--r-- | src/frame.rs | 7 | ||||
| -rw-r--r-- | src/geom/em.rs | 2 | ||||
| -rw-r--r-- | src/geom/linear.rs | 15 | ||||
| -rw-r--r-- | src/layout/mod.rs | 46 | ||||
| -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 | ||||
| -rw-r--r-- | tests/typeset.rs | 24 |
23 files changed, 828 insertions, 581 deletions
diff --git a/src/eval/mod.rs b/src/eval/mod.rs index d5b33280..6dcff900 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -181,11 +181,11 @@ impl Eval for MarkupNode { Self::Linebreak => Node::Linebreak, Self::Parbreak => Node::Parbreak, Self::Strong => { - ctx.styles.set(TextNode::STRONG, !ctx.styles.get(TextNode::STRONG)); + ctx.styles.toggle(TextNode::STRONG); Node::new() } Self::Emph => { - ctx.styles.set(TextNode::EMPH, !ctx.styles.get(TextNode::EMPH)); + ctx.styles.toggle(TextNode::EMPH); Node::new() } Self::Text(text) => Node::Text(text.clone()), @@ -216,7 +216,7 @@ impl Eval for MathNode { type Output = Node; fn eval(&self, _: &mut EvalContext) -> TypResult<Self::Output> { - let text = Node::Text(self.formula.clone()).monospaced(); + let text = Node::Text(self.formula.trim().into()).monospaced(); Ok(if self.display { Node::Block(text.into_block()) } else { @@ -229,11 +229,10 @@ impl Eval for HeadingNode { type Output = Node; fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { - // TODO(set): Relative font size. let upscale = (1.6 - 0.1 * self.level() as f64).max(0.75); let mut styles = Styles::new(); styles.set(TextNode::STRONG, true); - styles.set(TextNode::SIZE, upscale * Length::pt(11.0)); + styles.set(TextNode::SIZE, Relative::new(upscale).into()); Ok(Node::Block( self.body().eval(ctx)?.into_block().styled(styles), )) @@ -266,7 +265,7 @@ fn labelled(_: &mut EvalContext, label: EcoString, body: Node) -> TypResult<Node // TODO: Switch to em units for gutter once available. Ok(Node::block(GridNode { tracks: Spec::new(vec![TrackSizing::Auto; 2], vec![]), - gutter: Spec::new(vec![TrackSizing::Linear(Length::pt(6.0).into())], vec![]), + gutter: Spec::new(vec![TrackSizing::Linear(Length::pt(5.0).into())], vec![]), children: vec![Node::Text(label).into_block(), body.into_block()], })) } diff --git a/src/eval/node.rs b/src/eval/node.rs index d3bf9806..52d9b244 100644 --- a/src/eval/node.rs +++ b/src/eval/node.rs @@ -9,8 +9,8 @@ use crate::diag::StrResult; use crate::geom::SpecAxis; use crate::layout::{Layout, PackedNode}; use crate::library::{ - Decoration, DocumentNode, FlowChild, FlowNode, PageNode, ParChild, ParNode, Spacing, - TextNode, + DocumentNode, FlowChild, FlowNode, PageNode, ParChild, ParNode, PlacedNode, + SpacingKind, SpacingNode, TextNode, }; use crate::util::EcoString; @@ -18,8 +18,7 @@ use crate::util::EcoString; /// /// A node is a composable intermediate representation that can be converted /// into a proper layout node by lifting it to a block-level or document node. -// TODO(set): Fix Debug impl leaking into user-facing repr. -#[derive(Debug, Clone)] +#[derive(Debug, PartialEq, Clone, Hash)] pub enum Node { /// A word space. Space, @@ -32,7 +31,7 @@ pub enum Node { /// Plain text. Text(EcoString), /// Spacing. - Spacing(SpecAxis, Spacing), + Spacing(SpecAxis, SpacingKind), /// An inline node. Inline(PackedNode), /// A block node. @@ -77,18 +76,12 @@ impl Node { self.styled(Styles::one(TextNode::MONOSPACE, true)) } - /// Decorate this node. - pub fn decorated(self, _: Decoration) -> Self { - // TODO(set): Actually decorate. - self - } - /// Lift to a type-erased block-level node. pub fn into_block(self) -> PackedNode { if let Node::Block(packed) = self { packed } else { - let mut packer = NodePacker::new(); + let mut packer = NodePacker::new(true); packer.walk(self, Styles::new()); packer.into_block() } @@ -96,7 +89,7 @@ impl Node { /// Lift to a document node, the root of the layout tree. pub fn into_document(self) -> DocumentNode { - let mut packer = NodePacker::new(); + let mut packer = NodePacker::new(false); packer.walk(self, Styles::new()); packer.into_document() } @@ -117,13 +110,6 @@ impl Default for Node { } } -impl PartialEq for Node { - fn eq(&self, _: &Self) -> bool { - // TODO(set): Figure out what to do here. - false - } -} - impl Add for Node { type Output = Self; @@ -141,92 +127,89 @@ impl AddAssign for Node { /// Packs a [`Node`] into a flow or whole document. struct NodePacker { + /// Whether packing should produce a block-level node. + block: bool, /// The accumulated page nodes. - document: Vec<PageNode>, - /// The common style properties of all items on the current page. - page_styles: Styles, + pages: Vec<PageNode>, /// The accumulated flow children. flow: Vec<FlowChild>, + /// The common style properties of all items on the current flow. + flow_styles: Styles, + /// The kind of thing that was last added to the current flow. + flow_last: Last<FlowChild>, /// The accumulated paragraph children. par: Vec<ParChild>, /// The common style properties of all items in the current paragraph. par_styles: Styles, /// The kind of thing that was last added to the current paragraph. - last: Last, -} - -/// The type of the last thing that was pushed into the paragraph. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -enum Last { - None, - Spacing, - Newline, - Space, - Other, + par_last: Last<ParChild>, } impl NodePacker { /// Start a new node-packing session. - fn new() -> Self { + fn new(block: bool) -> Self { Self { - document: vec![], - page_styles: Styles::new(), + block, + pages: vec![], flow: vec![], + flow_styles: Styles::new(), + flow_last: Last::None, par: vec![], par_styles: Styles::new(), - last: Last::None, + par_last: Last::None, } } /// Finish up and return the resulting flow. fn into_block(mut self) -> PackedNode { - self.parbreak(); + self.finish_par(); FlowNode(self.flow).pack() } /// Finish up and return the resulting document. fn into_document(mut self) -> DocumentNode { self.pagebreak(true); - DocumentNode(self.document) + DocumentNode(self.pages) } /// Consider a node with the given styles. fn walk(&mut self, node: Node, styles: Styles) { match node { Node::Space => { - // Only insert a space if the previous thing was actual content. - if self.last == Last::Other { - self.push_text(' '.into(), styles); - self.last = Last::Space; + if self.is_flow_compatible(&styles) && self.is_par_compatible(&styles) { + self.par_last.soft(ParChild::text(' ', styles)); } } Node::Linebreak => { - self.trim(); - self.push_text('\n'.into(), styles); - self.last = Last::Newline; + self.par_last.hard(); + self.push_inline(ParChild::text('\n', styles)); + self.par_last.hard(); } Node::Parbreak => { - self.parbreak(); + self.parbreak(Some(styles)); } Node::Pagebreak => { self.pagebreak(true); - self.page_styles = styles; + self.flow_styles = styles; } Node::Text(text) => { - self.push_text(text, styles); + self.push_inline(ParChild::text(text, styles)); } - Node::Spacing(SpecAxis::Horizontal, amount) => { - self.push_inline(ParChild::Spacing(amount), styles); - self.last = Last::Spacing; + Node::Spacing(SpecAxis::Horizontal, kind) => { + self.par_last.hard(); + self.push_inline(ParChild::Spacing(SpacingNode { kind, styles })); + self.par_last.hard(); } - Node::Spacing(SpecAxis::Vertical, amount) => { - self.push_block(FlowChild::Spacing(amount), styles); + Node::Spacing(SpecAxis::Vertical, kind) => { + self.finish_par(); + self.flow.push(FlowChild::Spacing(SpacingNode { kind, styles })); + self.flow_last.hard(); } Node::Inline(inline) => { - self.push_inline(ParChild::Node(inline), styles); + self.push_inline(ParChild::Node(inline.styled(styles))); } Node::Block(block) => { - self.push_block(FlowChild::Node(block), styles); + self.push_block(block.styled(styles)); } Node::Sequence(list) => { for (node, mut inner) in list { @@ -237,80 +220,118 @@ impl NodePacker { } } - /// Remove a trailing space. - fn trim(&mut self) { - if self.last == Last::Space { - self.par.pop(); - self.last = Last::Other; + /// Insert an inline-level element into the current paragraph. + fn push_inline(&mut self, child: ParChild) { + if let Some(child) = self.par_last.any() { + self.push_inline_impl(child); + } + + // The node must be both compatible with the current page and the + // current paragraph. + self.make_flow_compatible(child.styles()); + self.make_par_compatible(child.styles()); + self.push_inline_impl(child); + self.par_last = Last::Any; + } + + /// Push a paragraph child, coalescing text nodes with compatible styles. + fn push_inline_impl(&mut self, child: ParChild) { + if let ParChild::Text(right) = &child { + if let Some(ParChild::Text(left)) = self.par.last_mut() { + if left.styles.compatible(&right.styles, TextNode::has_property) { + left.text.push_str(&right.text); + return; + } + } + } + + self.par.push(child); + } + + /// Insert a block-level element into the current flow. + fn push_block(&mut self, node: PackedNode) { + let mut is_placed = false; + if let Some(placed) = node.downcast::<PlacedNode>() { + is_placed = true; + + // This prevents paragraph spacing after the placed node if it + // is completely out-of-flow. + if placed.out_of_flow() { + self.flow_last = Last::None; + } + } + + self.parbreak(None); + self.make_flow_compatible(&node.styles); + + if let Some(child) = self.flow_last.any() { + self.flow.push(child); + } + + self.flow.push(FlowChild::Node(node)); + self.parbreak(None); + + // This prevents paragraph spacing between the placed node and + // the paragraph below it. + if is_placed { + self.flow_last = Last::None; } } /// Advance to the next paragraph. - fn parbreak(&mut self) { - self.trim(); + fn parbreak(&mut self, break_styles: Option<Styles>) { + self.finish_par(); - let children = mem::take(&mut self.par); + // Insert paragraph spacing. + self.flow_last + .soft(FlowChild::Parbreak(break_styles.unwrap_or_default())); + } + + fn finish_par(&mut self) { + let mut children = mem::take(&mut self.par); let styles = mem::take(&mut self.par_styles); + self.par_last = Last::None; + + // No empty paragraphs. if !children.is_empty() { + // Erase any styles that will be inherited anyway. + for child in &mut children { + child.styles_mut().erase(&styles); + } + + if let Some(child) = self.flow_last.any() { + self.flow.push(child); + } + // The paragraph's children are all compatible with the page, so the // paragraph is too, meaning we don't need to check or intersect // anything here. let node = ParNode(children).pack().styled(styles); self.flow.push(FlowChild::Node(node)); } - - self.last = Last::None; } /// Advance to the next page. fn pagebreak(&mut self, keep: bool) { - self.parbreak(); - let children = mem::take(&mut self.flow); - let styles = mem::take(&mut self.page_styles); - if keep || !children.is_empty() { - let node = PageNode { node: FlowNode(children).pack(), styles }; - self.document.push(node); + if self.block { + return; } - } - - /// Insert text into the current paragraph. - fn push_text(&mut self, text: EcoString, styles: Styles) { - // TODO(set): Join compatible text nodes. Take care with space - // coalescing. - let node = TextNode { text, styles: Styles::new() }; - self.push_inline(ParChild::Text(node), styles); - } - /// Insert an inline-level element into the current paragraph. - fn push_inline(&mut self, mut child: ParChild, styles: Styles) { - match &mut child { - ParChild::Spacing(_) => {} - ParChild::Text(node) => node.styles.apply(&styles), - ParChild::Node(node) => node.styles.apply(&styles), - ParChild::Decorate(_) => {} - ParChild::Undecorate => {} - } + self.finish_par(); - // The node must be both compatible with the current page and the - // current paragraph. - self.make_page_compatible(&styles); - self.make_par_compatible(&styles); - self.par.push(child); - self.last = Last::Other; - } + let styles = mem::take(&mut self.flow_styles); + let mut children = mem::take(&mut self.flow); + self.flow_last = Last::None; - /// Insert a block-level element into the current flow. - fn push_block(&mut self, mut child: FlowChild, styles: Styles) { - self.parbreak(); + if keep || !children.is_empty() { + // Erase any styles that will be inherited anyway. + for child in &mut children { + child.styles_mut().erase(&styles); + } - match &mut child { - FlowChild::Spacing(_) => {} - FlowChild::Node(node) => node.styles.apply(&styles), + let node = PageNode { node: FlowNode(children).pack(), styles }; + self.pages.push(node); } - - // The node must be compatible with the current page. - self.make_page_compatible(&styles); - self.flow.push(child); } /// Break to a new paragraph if the `styles` contain paragraph styles that @@ -321,8 +342,8 @@ impl NodePacker { return; } - if !self.par_styles.compatible(&styles, ParNode::has_property) { - self.parbreak(); + if !self.is_par_compatible(styles) { + self.parbreak(None); self.par_styles = styles.clone(); return; } @@ -331,19 +352,55 @@ impl NodePacker { } /// Break to a new page if the `styles` contain page styles that are - /// incompatible with the current page. - fn make_page_compatible(&mut self, styles: &Styles) { + /// incompatible with the current flow. + fn make_flow_compatible(&mut self, styles: &Styles) { if self.flow.is_empty() && self.par.is_empty() { - self.page_styles = styles.clone(); + self.flow_styles = styles.clone(); return; } - if !self.page_styles.compatible(&styles, PageNode::has_property) { + if !self.is_flow_compatible(styles) { self.pagebreak(false); - self.page_styles = styles.clone(); + self.flow_styles = styles.clone(); return; } - self.page_styles.intersect(styles); + self.flow_styles.intersect(styles); + } + + /// Whether the given styles are compatible with the current page. + fn is_par_compatible(&self, styles: &Styles) -> bool { + self.par_styles.compatible(&styles, ParNode::has_property) + } + + /// Whether the given styles are compatible with the current flow. + fn is_flow_compatible(&self, styles: &Styles) -> bool { + self.block || self.flow_styles.compatible(&styles, PageNode::has_property) + } +} + +/// Finite state machine for spacing coalescing. +enum Last<N> { + None, + Any, + Soft(N), +} + +impl<N> Last<N> { + fn any(&mut self) -> Option<N> { + match mem::replace(self, Self::Any) { + Self::Soft(soft) => Some(soft), + _ => None, + } + } + + fn soft(&mut self, soft: N) { + if let Self::Any = self { + *self = Self::Soft(soft); + } + } + + fn hard(&mut self) { + *self = Self::None; } } diff --git a/src/eval/styles.rs b/src/eval/styles.rs index 9d204843..555c2a61 100644 --- a/src/eval/styles.rs +++ b/src/eval/styles.rs @@ -11,7 +11,7 @@ use std::rc::Rc; /// A map of style properties. #[derive(Default, Clone, Hash)] pub struct Styles { - pub(crate) map: Vec<(StyleId, Entry)>, + map: Vec<(StyleId, Entry)>, } impl Styles { @@ -21,10 +21,7 @@ impl Styles { } /// Create a style map with a single property-value pair. - pub fn one<P: Property>(key: P, value: P::Value) -> Self - where - P::Value: Debug + Hash + PartialEq + 'static, - { + pub fn one<P: Property>(key: P, value: P::Value) -> Self { let mut styles = Self::new(); styles.set(key, value); styles @@ -36,21 +33,31 @@ impl Styles { } /// Set the value for a style property. - pub fn set<P: Property>(&mut self, key: P, value: P::Value) - where - P::Value: Debug + Hash + PartialEq + 'static, - { + pub fn set<P: Property>(&mut self, key: P, value: P::Value) { let id = StyleId::of::<P>(); - let entry = Entry::new(key, value); - for pair in &mut self.map { if pair.0 == id { - pair.1 = entry; + let prev = pair.1.downcast::<P::Value>().unwrap(); + let folded = P::combine(value, prev.clone()); + pair.1 = Entry::new(key, folded); + return; + } + } + + self.map.push((id, Entry::new(key, value))); + } + + /// Toggle a boolean style property. + pub fn toggle<P: Property<Value = bool>>(&mut self, key: P) { + let id = StyleId::of::<P>(); + for (i, pair) in self.map.iter_mut().enumerate() { + if pair.0 == id { + self.map.swap_remove(i); return; } } - self.map.push((id, entry)); + self.map.push((id, Entry::new(key, true))); } /// Get the value of a copyable style property. @@ -84,10 +91,15 @@ impl Styles { /// /// Properties from `self` take precedence over the ones from `outer`. pub fn apply(&mut self, outer: &Self) { - for pair in &outer.map { - if self.map.iter().all(|&(id, _)| pair.0 != id) { - self.map.push(pair.clone()); + 'outer: for pair in &outer.map { + for (id, entry) in &mut self.map { + if pair.0 == *id { + entry.apply(&pair.1); + continue 'outer; + } } + + self.map.push(pair.clone()); } } @@ -105,12 +117,18 @@ impl Styles { self.map.retain(|a| other.map.iter().any(|b| a == b)); } + /// Keep only those styles that are not also in `other`. + pub fn erase(&mut self, other: &Self) { + self.map.retain(|a| other.map.iter().all(|b| a != b)); + } + /// Whether two style maps are equal when filtered down to the given /// properties. pub fn compatible<F>(&self, other: &Self, filter: F) -> bool where F: Fn(StyleId) -> bool, { + // TODO(set): Filtered length + one direction equal should suffice. let f = |e: &&(StyleId, Entry)| filter(e.0); self.map.iter().filter(f).all(|pair| other.map.contains(pair)) && other.map.iter().filter(f).all(|pair| self.map.contains(pair)) @@ -119,73 +137,88 @@ impl Styles { impl Debug for Styles { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Styles ")?; - f.debug_set().entries(self.map.iter().map(|pair| &pair.1)).finish() + if f.alternate() { + for pair in &self.map { + writeln!(f, "{:#?}", pair.1)?; + } + Ok(()) + } else { + f.write_str("Styles ")?; + f.debug_set().entries(self.map.iter().map(|pair| &pair.1)).finish() + } + } +} + +impl PartialEq for Styles { + fn eq(&self, other: &Self) -> bool { + self.compatible(other, |_| true) } } /// An entry for a single style property. #[derive(Clone)] -pub(crate) struct Entry { - #[cfg(debug_assertions)] - name: &'static str, - value: Rc<dyn Bounds>, -} +pub(crate) struct Entry(Rc<dyn Bounds>); impl Entry { - fn new<P: Property>(_: P, value: P::Value) -> Self - where - P::Value: Debug + Hash + PartialEq + 'static, - { - Self { - #[cfg(debug_assertions)] - name: P::NAME, - value: Rc::new(value), - } + fn new<P: Property>(key: P, value: P::Value) -> Self { + Self(Rc::new((key, value))) } fn downcast<T: 'static>(&self) -> Option<&T> { - self.value.as_any().downcast_ref() + self.0.as_any().downcast_ref() + } + + fn apply(&mut self, outer: &Self) { + *self = self.0.combine(outer); } } impl Debug for Entry { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - #[cfg(debug_assertions)] - write!(f, "{}: ", self.name)?; - write!(f, "{:?}", &self.value) + self.0.fmt(f) } } impl PartialEq for Entry { fn eq(&self, other: &Self) -> bool { - self.value.dyn_eq(other) + self.0.dyn_eq(other) } } impl Hash for Entry { fn hash<H: Hasher>(&self, state: &mut H) { - state.write_u64(self.value.hash64()); + state.write_u64(self.0.hash64()); } } -trait Bounds: Debug + 'static { +trait Bounds: 'static { fn as_any(&self) -> &dyn Any; + fn fmt(&self, f: &mut Formatter) -> fmt::Result; fn dyn_eq(&self, other: &Entry) -> bool; fn hash64(&self) -> u64; + fn combine(&self, outer: &Entry) -> Entry; } -impl<T> Bounds for T +impl<P> Bounds for (P, P::Value) where - T: Debug + Hash + PartialEq + 'static, + P: Property, + P::Value: Debug + Hash + PartialEq + 'static, { fn as_any(&self) -> &dyn Any { - self + &self.1 + } + + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + write!(f, "#[{} = {:?}]", P::NAME, self.1) + } else { + write!(f, "{}: {:?}", P::NAME, self.1) + } } fn dyn_eq(&self, other: &Entry) -> bool { - if let Some(other) = other.downcast::<Self>() { - self == other + if let Some(other) = other.downcast::<P::Value>() { + &self.1 == other } else { false } @@ -194,7 +227,13 @@ where fn hash64(&self) -> u64 { // No need to hash the TypeId since there's only one // valid value type per property. - fxhash::hash64(self) + fxhash::hash64(&self.1) + } + + fn combine(&self, outer: &Entry) -> Entry { + let outer = outer.downcast::<P::Value>().unwrap(); + let combined = P::combine(self.1.clone(), outer.clone()); + Entry::new(self.0, combined) } } @@ -202,14 +241,17 @@ where /// /// This trait is not intended to be implemented manually, but rather through /// the `properties!` macro. -pub trait Property: 'static { +pub trait Property: Copy + 'static { /// The type of this property, for example, this could be /// [`Length`](crate::geom::Length) for a `WIDTH` property. - type Value; + type Value: Debug + Clone + Hash + PartialEq + 'static; /// The name of the property, used for debug printing. const NAME: &'static str; + /// Combine the property with an outer value. + fn combine(inner: Self::Value, outer: Self::Value) -> Self::Value; + /// The default value of the property. fn default() -> Self::Value; @@ -235,7 +277,8 @@ impl StyleId { /// Generate the property keys for a node. macro_rules! properties { ($node:ty, $( - $(#[$attr:meta])* + $(#[doc = $doc:expr])* + $(#[fold($combine:expr)])? $name:ident: $type:ty = $default:expr ),* $(,)?) => { // TODO(set): Fix possible name clash. @@ -250,6 +293,13 @@ macro_rules! properties { pub struct Key<T>(pub PhantomData<T>); + impl<T> Copy for Key<T> {} + impl<T> Clone for Key<T> { + fn clone(&self) -> Self { + *self + } + } + impl Property for Key<$type> { type Value = $type; @@ -257,6 +307,15 @@ macro_rules! properties { stringify!($node), "::", stringify!($name) ); + #[allow(unused_mut, unused_variables)] + fn combine(mut inner: Self::Value, outer: Self::Value) -> Self::Value { + $( + let combine: fn(Self::Value, Self::Value) -> Self::Value = $combine; + inner = combine(inner, outer); + )? + inner + } + fn default() -> Self::Value { $default } @@ -275,7 +334,7 @@ macro_rules! properties { false || $(id == StyleId::of::<$name::Key<$type>>())||* } - $($(#[$attr])* pub const $name: $name::Key<$type> + $($(#[doc = $doc])* pub const $name: $name::Key<$type> = $name::Key(PhantomData);)* } } @@ -284,9 +343,9 @@ macro_rules! properties { /// Set a style property to a value if the value is `Some`. macro_rules! set { - ($ctx:expr, $target:expr => $value:expr) => { + ($styles:expr, $target:expr => $value:expr) => { if let Some(v) = $value { - $ctx.styles.set($target, v); + $styles.set($target, v); } }; } diff --git a/src/eval/value.rs b/src/eval/value.rs index c2a284eb..2cf82a26 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -26,7 +26,7 @@ pub enum Value { Float(f64), /// A length: `12pt`, `3cm`. Length(Length), - /// An angle: `1.5rad`, `90deg`. + /// An angle: `1.5rad`, `90deg`. Angle(Angle), /// A relative value: `50%`. Relative(Relative), @@ -146,7 +146,7 @@ impl Debug for Value { Self::Str(v) => Debug::fmt(v, f), Self::Array(v) => Debug::fmt(v, f), Self::Dict(v) => Debug::fmt(v, f), - Self::Node(v) => Debug::fmt(v, f), + Self::Node(_) => f.pad("<template>"), Self::Func(v) => Debug::fmt(v, f), Self::Dyn(v) => Debug::fmt(v, f), } @@ -386,13 +386,13 @@ primitive! { f64: "float", Float, Int(v) => v as f64 } primitive! { Length: "length", Length } primitive! { Angle: "angle", Angle } primitive! { Relative: "relative", Relative } -primitive! { Linear: "linear", Linear, Length(v) => v.into(), Relative(v) => v.into() } -primitive! { Fractional: "fractional", Fractional } +primitive! { Linear: "relative length", Linear, Length(v) => v.into(), Relative(v) => v.into() } +primitive! { Fractional: "fractional length", Fractional } primitive! { Color: "color", Color } primitive! { EcoString: "string", Str } primitive! { Array: "array", Array } primitive! { Dict: "dictionary", Dict } -primitive! { Node: "node", Node } +primitive! { Node: "template", Node } primitive! { Function: "function", Func } impl Cast<Value> for Value { diff --git a/src/font.rs b/src/font.rs index 5afd1aca..c2e1beac 100644 --- a/src/font.rs +++ b/src/font.rs @@ -283,11 +283,11 @@ impl Face { /// Look up a vertical metric at the given font size. pub fn vertical_metric(&self, metric: VerticalFontMetric, size: Length) -> Length { match metric { - VerticalFontMetric::Ascender => self.ascender.to_length(size), - VerticalFontMetric::CapHeight => self.cap_height.to_length(size), - VerticalFontMetric::XHeight => self.x_height.to_length(size), + VerticalFontMetric::Ascender => self.ascender.resolve(size), + VerticalFontMetric::CapHeight => self.cap_height.resolve(size), + VerticalFontMetric::XHeight => self.x_height.resolve(size), VerticalFontMetric::Baseline => Length::zero(), - VerticalFontMetric::Descender => self.descender.to_length(size), + VerticalFontMetric::Descender => self.descender.resolve(size), VerticalFontMetric::Linear(v) => v.resolve(size), } } diff --git a/src/frame.rs b/src/frame.rs index 29125962..46f23446 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -107,6 +107,11 @@ impl Frame { wrapper.push(Point::zero(), Element::Group(group)); *self = wrapper; } + + /// Link the whole frame to a resource. + pub fn link(&mut self, url: impl Into<String>) { + self.push(Point::zero(), Element::Link(url.into(), self.size)); + } } impl Debug for Frame { @@ -180,7 +185,7 @@ pub struct Text { impl Text { /// The width of the text run. pub fn width(&self) -> Length { - self.glyphs.iter().map(|g| g.x_advance.to_length(self.size)).sum() + self.glyphs.iter().map(|g| g.x_advance.resolve(self.size)).sum() } } diff --git a/src/geom/em.rs b/src/geom/em.rs index 05d1d7d5..1868222f 100644 --- a/src/geom/em.rs +++ b/src/geom/em.rs @@ -29,7 +29,7 @@ impl Em { } /// Convert to a length at the given font size. - pub fn to_length(self, font_size: Length) -> Length { + pub fn resolve(self, font_size: Length) -> Length { self.get() * font_size } diff --git a/src/geom/linear.rs b/src/geom/linear.rs index 77923c43..78602d8b 100644 --- a/src/geom/linear.rs +++ b/src/geom/linear.rs @@ -36,6 +36,16 @@ impl Linear { self.rel.resolve(length) + self.abs } + /// Compose with another linear. + /// + /// The resulting linear is (self ∘ inner)(x) = self(inner(x)). + pub fn compose(self, inner: Self) -> Self { + Self { + rel: self.rel * inner.rel, + abs: self.rel.resolve(inner.abs) + self.abs, + } + } + /// Whether both parts are zero. pub fn is_zero(self) -> bool { self.rel.is_zero() && self.abs.is_zero() @@ -155,10 +165,7 @@ impl Mul<Linear> for f64 { type Output = Linear; fn mul(self, other: Linear) -> Linear { - Linear { - rel: self * other.rel, - abs: self * other.abs, - } + other * self } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 5da0ad9a..4ede39b2 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -91,14 +91,19 @@ pub trait Layout { pub struct PackedNode { /// The type-erased node. node: Rc<dyn Bounds>, - /// The node's styles. - pub styles: Styles, + /// A precomputed hash for the node. #[cfg(feature = "layout-cache")] - /// A precomputed hash. hash: u64, + /// The node's styles. + pub styles: Styles, } impl PackedNode { + /// Check whether the contained node is a specific layout node. + pub fn is<T: 'static>(&self) -> bool { + self.node.as_any().is::<T>() + } + /// Try to downcast to a specific layout node. pub fn downcast<T>(&self) -> Option<&T> where @@ -173,7 +178,15 @@ impl Layout for PackedNode { return self.layout_impl(ctx, regions); #[cfg(feature = "layout-cache")] - ctx.layouts.get(self.hash, regions).unwrap_or_else(|| { + let hash = { + let mut state = fxhash::FxHasher64::default(); + self.hash(&mut state); + ctx.styles.hash(&mut state); + state.finish() + }; + + #[cfg(feature = "layout-cache")] + ctx.layouts.get(hash, regions).unwrap_or_else(|| { ctx.level += 1; let frames = self.layout_impl(ctx, regions); ctx.level -= 1; @@ -191,7 +204,7 @@ impl Layout for PackedNode { panic!("constraints did not match regions they were created for"); } - ctx.layouts.insert(self.hash, entry); + ctx.layouts.insert(hash, entry); frames }) } @@ -219,18 +232,31 @@ impl PackedNode { } } +impl Debug for PackedNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + self.styles.fmt(f)?; + } + self.node.fmt(f) + } +} + +impl PartialEq for PackedNode { + fn eq(&self, other: &Self) -> bool { + Rc::as_ptr(&self.node) as *const () == Rc::as_ptr(&other.node) as *const () + } +} + impl Hash for PackedNode { fn hash<H: Hasher>(&self, state: &mut H) { + // Hash the node. #[cfg(feature = "layout-cache")] state.write_u64(self.hash); #[cfg(not(feature = "layout-cache"))] state.write_u64(self.hash64()); - } -} -impl Debug for PackedNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.node.fmt(f) + // Hash the styles. + self.styles.hash(state); } } 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 diff --git a/tests/typeset.rs b/tests/typeset.rs index a36fd361..b343334d 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -71,7 +71,7 @@ fn main() { styles.set(PageNode::TOP, Smart::Custom(Length::pt(10.0).into())); styles.set(PageNode::RIGHT, Smart::Custom(Length::pt(10.0).into())); styles.set(PageNode::BOTTOM, Smart::Custom(Length::pt(10.0).into())); - styles.set(TextNode::SIZE, Length::pt(10.0)); + styles.set(TextNode::SIZE, Length::pt(10.0).into()); // Hook up an assert function into the global scope. let mut std = typst::library::new(); @@ -92,7 +92,7 @@ fn main() { let mut ctx = Context::builder().std(std).styles(styles).build(loader); // Run all the tests. - let mut ok = true; + let mut ok = 0; for src_path in filtered { let path = src_path.strip_prefix(TYP_DIR).unwrap(); let png_path = Path::new(PNG_DIR).join(path).with_extension("png"); @@ -100,16 +100,20 @@ fn main() { let pdf_path = args.pdf.then(|| Path::new(PDF_DIR).join(path).with_extension("pdf")); - ok &= test( + ok += test( &mut ctx, &src_path, &png_path, &ref_path, pdf_path.as_deref(), - ); + ) as usize; + } + + if len > 1 { + println!("{} / {} tests passed.", ok, len); } - if !ok { + if ok < len { std::process::exit(1); } } @@ -236,6 +240,7 @@ fn test_part( let mut ok = true; let (frames, mut errors) = match ctx.execute(id) { Ok(document) => { + // dbg!(&document); let mut frames = layout(ctx, &document); #[cfg(feature = "layout-cache")] @@ -321,7 +326,10 @@ fn test_incremental( } if cached != frames { - println!(" Subtest {} relayout differs from clean pass ❌", i); + println!( + " Subtest {} relayout differs from clean pass on level {} ❌", + i, level + ); ok = false; } } @@ -506,7 +514,7 @@ fn draw_text( let mut x = 0.0; for glyph in &text.glyphs { let glyph_id = GlyphId(glyph.id); - let offset = x + glyph.x_offset.to_length(text.size).to_f32(); + let offset = x + glyph.x_offset.resolve(text.size).to_f32(); let ts = ts.pre_translate(offset, 0.0); if let Some(tree) = ttf @@ -559,7 +567,7 @@ fn draw_text( } } - x += glyph.x_advance.to_length(text.size).to_f32(); + x += glyph.x_advance.resolve(text.size).to_f32(); } } |
