diff --git a/src/eval/mod.rs b/src/eval/mod.rs index c1f0b0248..d5b332806 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -117,14 +117,16 @@ impl<'a> EvalContext<'a> { // Prepare the new context. let new_scopes = Scopes::new(self.scopes.base); - let old_scopes = mem::replace(&mut self.scopes, new_scopes); + let prev_scopes = mem::replace(&mut self.scopes, new_scopes); + let prev_styles = mem::take(&mut self.styles); self.route.push(id); // Evaluate the module. let template = ast.eval(self).trace(|| Tracepoint::Import, span)?; // Restore the old context. - let new_scopes = mem::replace(&mut self.scopes, old_scopes); + let new_scopes = mem::replace(&mut self.scopes, prev_scopes); + self.styles = prev_styles; self.route.pop().unwrap(); // Save the evaluated module. @@ -160,11 +162,13 @@ impl Eval for Markup { type Output = Node; fn eval(&self, ctx: &mut EvalContext) -> TypResult { - let mut result = Node::new(); + let prev = mem::take(&mut ctx.styles); + let mut seq = vec![]; for piece in self.nodes() { - result += piece.eval(ctx)?; + seq.push((piece.eval(ctx)?, ctx.styles.clone())); } - Ok(result) + ctx.styles = prev; + Ok(Node::Sequence(seq)) } } @@ -190,7 +194,7 @@ impl Eval for MarkupNode { Self::Heading(heading) => heading.eval(ctx)?, Self::List(list) => list.eval(ctx)?, Self::Enum(enum_) => enum_.eval(ctx)?, - Self::Expr(expr) => expr.eval(ctx)?.display(), + Self::Expr(expr) => expr.eval(ctx)?.show(), }) } } @@ -199,8 +203,7 @@ impl Eval for RawNode { type Output = Node; fn eval(&self, _: &mut EvalContext) -> TypResult { - // TODO(set): Styled in monospace. - let text = Node::Text(self.text.clone()); + let text = Node::Text(self.text.clone()).monospaced(); Ok(if self.block { Node::Block(text.into_block()) } else { @@ -213,8 +216,7 @@ impl Eval for MathNode { type Output = Node; fn eval(&self, _: &mut EvalContext) -> TypResult { - // TODO(set): Styled in monospace. - let text = Node::Text(self.formula.clone()); + let text = Node::Text(self.formula.clone()).monospaced(); Ok(if self.display { Node::Block(text.into_block()) } else { @@ -227,8 +229,14 @@ impl Eval for HeadingNode { type Output = Node; fn eval(&self, ctx: &mut EvalContext) -> TypResult { - // TODO(set): Styled appropriately. - Ok(Node::Block(self.body().eval(ctx)?.into_block())) + // 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)); + Ok(Node::Block( + self.body().eval(ctx)?.into_block().styled(styles), + )) } } diff --git a/src/eval/node.rs b/src/eval/node.rs index 4d259e2d0..d3bf98067 100644 --- a/src/eval/node.rs +++ b/src/eval/node.rs @@ -1,9 +1,10 @@ use std::convert::TryFrom; -use std::fmt::{self, Debug, Formatter}; +use std::fmt::Debug; use std::hash::Hash; use std::mem; use std::ops::{Add, AddAssign}; +use super::Styles; use crate::diag::StrResult; use crate::geom::SpecAxis; use crate::layout::{Layout, PackedNode}; @@ -16,8 +17,9 @@ use crate::util::EcoString; /// A partial representation of a layout node. /// /// A node is a composable intermediate representation that can be converted -/// into a proper layout node by lifting it to the block or page level. -#[derive(Clone)] +/// 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)] pub enum Node { /// A word space. Space, @@ -36,13 +38,13 @@ pub enum Node { /// A block node. Block(PackedNode), /// A sequence of nodes (which may themselves contain sequences). - Seq(Vec), + Sequence(Vec<(Self, Styles)>), } impl Node { /// Create an empty node. pub fn new() -> Self { - Self::Seq(vec![]) + Self::Sequence(vec![]) } /// Create an inline-level node. @@ -61,8 +63,22 @@ impl Node { Self::Block(node.pack()) } - /// Decoration this node. - pub fn decorate(self, _: Decoration) -> Self { + /// Style this node. + pub fn styled(self, styles: Styles) -> Self { + match self { + Self::Inline(inline) => Self::Inline(inline.styled(styles)), + Self::Block(block) => Self::Block(block.styled(styles)), + other => Self::Sequence(vec![(other, styles)]), + } + } + + /// Style this node in monospace. + pub fn monospaced(self) -> Self { + self.styled(Styles::one(TextNode::MONOSPACE, true)) + } + + /// Decorate this node. + pub fn decorated(self, _: Decoration) -> Self { // TODO(set): Actually decorate. self } @@ -73,7 +89,7 @@ impl Node { packed } else { let mut packer = NodePacker::new(); - packer.walk(self); + packer.walk(self, Styles::new()); packer.into_block() } } @@ -81,7 +97,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(); - packer.walk(self); + packer.walk(self, Styles::new()); packer.into_document() } @@ -91,13 +107,7 @@ impl Node { .map_err(|_| format!("cannot repeat this template {} times", n))?; // TODO(set): Make more efficient. - Ok(Self::Seq(vec![self.clone(); count])) - } -} - -impl Debug for Node { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad("") + Ok(Self::Sequence(vec![(self.clone(), Styles::new()); count])) } } @@ -119,7 +129,7 @@ impl Add for Node { fn add(self, rhs: Self) -> Self::Output { // TODO(set): Make more efficient. - Self::Seq(vec![self, rhs]) + Self::Sequence(vec![(self, Styles::new()), (rhs, Styles::new())]) } } @@ -129,86 +139,211 @@ impl AddAssign for Node { } } -/// Packs a `Node` into a flow or whole document. +/// Packs a [`Node`] into a flow or whole document. struct NodePacker { + /// The accumulated page nodes. document: Vec, + /// The common style properties of all items on the current page. + page_styles: Styles, + /// The accumulated flow children. flow: Vec, + /// The accumulated paragraph children. par: Vec, + /// 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, } impl NodePacker { + /// Start a new node-packing session. fn new() -> Self { Self { document: vec![], + page_styles: Styles::new(), flow: vec![], par: vec![], + par_styles: Styles::new(), + last: Last::None, } } + /// Finish up and return the resulting flow. fn into_block(mut self) -> PackedNode { self.parbreak(); FlowNode(self.flow).pack() } + /// Finish up and return the resulting document. fn into_document(mut self) -> DocumentNode { - self.parbreak(); - self.pagebreak(); + self.pagebreak(true); DocumentNode(self.document) } - fn walk(&mut self, node: Node) { + /// Consider a node with the given styles. + fn walk(&mut self, node: Node, styles: Styles) { match node { Node::Space => { - self.push_inline(ParChild::Text(TextNode(' '.into()))); + // 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; + } } Node::Linebreak => { - self.push_inline(ParChild::Text(TextNode('\n'.into()))); + self.trim(); + self.push_text('\n'.into(), styles); + self.last = Last::Newline; } Node::Parbreak => { self.parbreak(); } Node::Pagebreak => { - self.pagebreak(); + self.pagebreak(true); + self.page_styles = styles; } Node::Text(text) => { - self.push_inline(ParChild::Text(TextNode(text))); + self.push_text(text, styles); + } + Node::Spacing(SpecAxis::Horizontal, amount) => { + self.push_inline(ParChild::Spacing(amount), styles); + self.last = Last::Spacing; + } + Node::Spacing(SpecAxis::Vertical, amount) => { + self.push_block(FlowChild::Spacing(amount), styles); } - Node::Spacing(axis, amount) => match axis { - SpecAxis::Horizontal => self.push_inline(ParChild::Spacing(amount)), - SpecAxis::Vertical => self.push_block(FlowChild::Spacing(amount)), - }, Node::Inline(inline) => { - self.push_inline(ParChild::Node(inline)); + self.push_inline(ParChild::Node(inline), styles); } Node::Block(block) => { - self.push_block(FlowChild::Node(block)); + self.push_block(FlowChild::Node(block), styles); } - Node::Seq(list) => { - for node in list { - self.walk(node); + Node::Sequence(list) => { + for (node, mut inner) in list { + inner.apply(&styles); + self.walk(node, inner); } } } } - fn parbreak(&mut self) { - let children = mem::take(&mut self.par); - if !children.is_empty() { - self.flow.push(FlowChild::Node(ParNode(children).pack())); + /// Remove a trailing space. + fn trim(&mut self) { + if self.last == Last::Space { + self.par.pop(); + self.last = Last::Other; } } - fn pagebreak(&mut self) { - let children = mem::take(&mut self.flow); - self.document.push(PageNode(FlowNode(children).pack())); + /// Advance to the next paragraph. + fn parbreak(&mut self) { + self.trim(); + + let children = mem::take(&mut self.par); + let styles = mem::take(&mut self.par_styles); + if !children.is_empty() { + // 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; } - fn push_inline(&mut self, child: ParChild) { - self.par.push(child); - } - - fn push_block(&mut self, child: FlowChild) { + /// 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); + } + } + + /// 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 => {} + } + + // 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; + } + + /// Insert a block-level element into the current flow. + fn push_block(&mut self, mut child: FlowChild, styles: Styles) { + self.parbreak(); + + match &mut child { + FlowChild::Spacing(_) => {} + FlowChild::Node(node) => node.styles.apply(&styles), + } + + // 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 + /// are incompatible with the current paragraph. + fn make_par_compatible(&mut self, styles: &Styles) { + if self.par.is_empty() { + self.par_styles = styles.clone(); + return; + } + + if !self.par_styles.compatible(&styles, ParNode::has_property) { + self.parbreak(); + self.par_styles = styles.clone(); + return; + } + + self.par_styles.intersect(&styles); + } + + /// 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) { + if self.flow.is_empty() && self.par.is_empty() { + self.page_styles = styles.clone(); + return; + } + + if !self.page_styles.compatible(&styles, PageNode::has_property) { + self.pagebreak(false); + self.page_styles = styles.clone(); + return; + } + + self.page_styles.intersect(styles); + } } diff --git a/src/eval/scope.rs b/src/eval/scope.rs index 2290affdb..ffe2d63e3 100644 --- a/src/eval/scope.rs +++ b/src/eval/scope.rs @@ -120,6 +120,7 @@ impl Scope { impl Debug for Scope { fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Scope ")?; f.debug_map() .entries(self.values.iter().map(|(k, v)| (k, v.borrow()))) .finish() diff --git a/src/eval/styles.rs b/src/eval/styles.rs index 30822edbe..9d2048436 100644 --- a/src/eval/styles.rs +++ b/src/eval/styles.rs @@ -1,6 +1,6 @@ use std::any::{Any, TypeId}; -use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; use std::rc::Rc; // Possible optimizations: @@ -9,20 +9,48 @@ use std::rc::Rc; // - Store small properties inline /// A map of style properties. -#[derive(Default, Clone)] +#[derive(Default, Clone, Hash)] pub struct Styles { - map: HashMap>, + pub(crate) map: Vec<(StyleId, Entry)>, } impl Styles { /// Create a new, empty style map. pub fn new() -> Self { - Self { map: HashMap::new() } + Self { map: vec![] } + } + + /// Create a style map with a single property-value pair. + pub fn one(key: P, value: P::Value) -> Self + where + P::Value: Debug + Hash + PartialEq + 'static, + { + let mut styles = Self::new(); + styles.set(key, value); + styles + } + + /// Whether this map contains no styles. + pub fn is_empty(&self) -> bool { + self.map.is_empty() } /// Set the value for a style property. - pub fn set(&mut self, _: P, value: P::Value) { - self.map.insert(TypeId::of::

(), Rc::new(value)); + pub fn set(&mut self, key: P, value: P::Value) + where + P::Value: Debug + Hash + PartialEq + 'static, + { + let id = StyleId::of::

(); + let entry = Entry::new(key, value); + + for pair in &mut self.map { + if pair.0 == id { + pair.1 = entry; + return; + } + } + + self.map.push((id, entry)); } /// Get the value of a copyable style property. @@ -33,7 +61,7 @@ impl Styles { where P::Value: Copy, { - self.get_inner(key).copied().unwrap_or_else(P::default) + self.get_direct(key).copied().unwrap_or_else(P::default) } /// Get a reference to a style property. @@ -41,30 +69,147 @@ impl Styles { /// Returns a reference to the property's default value if the map does not /// contain an entry for it. pub fn get_ref(&self, key: P) -> &P::Value { - self.get_inner(key).unwrap_or_else(|| P::default_ref()) + self.get_direct(key).unwrap_or_else(|| P::default_ref()) } - /// Get a reference to a style directly in this map. - fn get_inner(&self, _: P) -> Option<&P::Value> { + /// Get a reference to a style directly in this map (no default value). + pub fn get_direct(&self, _: P) -> Option<&P::Value> { self.map - .get(&TypeId::of::

()) - .and_then(|boxed| boxed.downcast_ref()) + .iter() + .find(|pair| pair.0 == StyleId::of::

()) + .and_then(|pair| pair.1.downcast()) + } + + /// Apply styles from `outer` in-place. + /// + /// 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()); + } + } + } + + /// Create new styles combining `self` with `outer`. + /// + /// Properties from `self` take precedence over the ones from `outer`. + pub fn chain(&self, outer: &Self) -> Self { + let mut styles = self.clone(); + styles.apply(outer); + styles + } + + /// Keep only those styles that are also in `other`. + pub fn intersect(&mut self, other: &Self) { + self.map.retain(|a| other.map.iter().any(|b| a == b)); + } + + /// Whether two style maps are equal when filtered down to the given + /// properties. + pub fn compatible(&self, other: &Self, filter: F) -> bool + where + F: Fn(StyleId) -> bool, + { + 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)) } } impl Debug for Styles { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - // TODO(set): Better debug printing possible? - f.pad("Styles(..)") + f.write_str("Styles ")?; + f.debug_set().entries(self.map.iter().map(|pair| &pair.1)).finish() } } -/// Stylistic property keys. +/// An entry for a single style property. +#[derive(Clone)] +pub(crate) struct Entry { + #[cfg(debug_assertions)] + name: &'static str, + value: Rc, +} + +impl Entry { + fn new(_: P, value: P::Value) -> Self + where + P::Value: Debug + Hash + PartialEq + 'static, + { + Self { + #[cfg(debug_assertions)] + name: P::NAME, + value: Rc::new(value), + } + } + + fn downcast(&self) -> Option<&T> { + self.value.as_any().downcast_ref() + } +} + +impl Debug for Entry { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + #[cfg(debug_assertions)] + write!(f, "{}: ", self.name)?; + write!(f, "{:?}", &self.value) + } +} + +impl PartialEq for Entry { + fn eq(&self, other: &Self) -> bool { + self.value.dyn_eq(other) + } +} + +impl Hash for Entry { + fn hash(&self, state: &mut H) { + state.write_u64(self.value.hash64()); + } +} + +trait Bounds: Debug + 'static { + fn as_any(&self) -> &dyn Any; + fn dyn_eq(&self, other: &Entry) -> bool; + fn hash64(&self) -> u64; +} + +impl Bounds for T +where + T: Debug + Hash + PartialEq + 'static, +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn dyn_eq(&self, other: &Entry) -> bool { + if let Some(other) = other.downcast::() { + self == other + } else { + false + } + } + + fn hash64(&self) -> u64 { + // No need to hash the TypeId since there's only one + // valid value type per property. + fxhash::hash64(self) + } +} + +/// Style property keys. +/// +/// This trait is not intended to be implemented manually, but rather through +/// the `properties!` macro. pub trait Property: 'static { /// The type of this property, for example, this could be /// [`Length`](crate::geom::Length) for a `WIDTH` property. type Value; + /// The name of the property, used for debug printing. + const NAME: &'static str; + /// The default value of the property. fn default() -> Self::Value; @@ -76,14 +221,18 @@ pub trait Property: 'static { fn default_ref() -> &'static Self::Value; } -macro_rules! set { - ($ctx:expr, $target:expr => $source:expr) => { - if let Some(v) = $source { - $ctx.styles.set($target, v); - } - }; +/// A unique identifier for a style property. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct StyleId(TypeId); + +impl StyleId { + /// The style id of the property. + pub fn of() -> Self { + Self(TypeId::of::

()) + } } +/// Generate the property keys for a node. macro_rules! properties { ($node:ty, $( $(#[$attr:meta])* @@ -92,10 +241,10 @@ macro_rules! properties { // TODO(set): Fix possible name clash. mod properties { use std::marker::PhantomData; + use $crate::eval::{Property, StyleId}; use super::*; $(#[allow(non_snake_case)] mod $name { - use $crate::eval::Property; use once_cell::sync::Lazy; use super::*; @@ -104,6 +253,10 @@ macro_rules! properties { impl Property for Key<$type> { type Value = $type; + const NAME: &'static str = concat!( + stringify!($node), "::", stringify!($name) + ); + fn default() -> Self::Value { $default } @@ -116,9 +269,24 @@ macro_rules! properties { })* impl $node { + /// Check whether the property with the given type id belongs to + /// `Self`. + pub fn has_property(id: StyleId) -> bool { + false || $(id == StyleId::of::<$name::Key<$type>>())||* + } + $($(#[$attr])* pub const $name: $name::Key<$type> = $name::Key(PhantomData);)* } } }; } + +/// Set a style property to a value if the value is `Some`. +macro_rules! set { + ($ctx:expr, $target:expr => $value:expr) => { + if let Some(v) = $value { + $ctx.styles.set($target, v); + } + }; +} diff --git a/src/eval/value.rs b/src/eval/value.rs index a62309563..c2a284eb9 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -108,8 +108,8 @@ impl Value { format_eco!("{:?}", self) } - /// Return the display representation of a value in form of a node. - pub fn display(self) -> Node { + /// Return the display representation of the value. + pub fn show(self) -> Node { match self { Value::None => Node::new(), Value::Int(v) => Node::Text(format_eco!("{}", v)), @@ -118,8 +118,7 @@ impl Value { Value::Node(v) => v, // For values which can't be shown "naturally", we print the // representation in monospace. - // TODO(set): Styled in monospace. - v => Node::Text(v.repr()), + v => Node::Text(v.repr()).monospaced(), } } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 9c57152aa..5da0ad9af 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -15,6 +15,7 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::rc::Rc; +use crate::eval::Styles; use crate::font::FontStore; use crate::frame::Frame; use crate::geom::{Align, Linear, Point, Sides, Spec, Transform}; @@ -37,6 +38,8 @@ pub struct LayoutContext<'a> { /// Caches layouting artifacts. #[cfg(feature = "layout-cache")] pub layouts: &'a mut LayoutCache, + /// The inherited style properties. + pub styles: Styles, /// How deeply nested the current layout tree position is. #[cfg(feature = "layout-cache")] pub level: usize, @@ -50,6 +53,7 @@ impl<'a> LayoutContext<'a> { images: &mut ctx.images, #[cfg(feature = "layout-cache")] layouts: &mut ctx.layouts, + styles: ctx.styles.clone(), #[cfg(feature = "layout-cache")] level: 0, } @@ -75,13 +79,9 @@ pub trait Layout { { PackedNode { #[cfg(feature = "layout-cache")] - hash: { - let mut state = fxhash::FxHasher64::default(); - self.type_id().hash(&mut state); - self.hash(&mut state); - state.finish() - }, + hash: self.hash64(), node: Rc::new(self), + styles: Styles::new(), } } } @@ -89,8 +89,12 @@ pub trait Layout { /// A packed layouting node with precomputed hash. #[derive(Clone)] pub struct PackedNode { + /// The type-erased node. node: Rc, + /// The node's styles. + pub styles: Styles, #[cfg(feature = "layout-cache")] + /// A precomputed hash. hash: u64, } @@ -103,6 +107,16 @@ impl PackedNode { self.node.as_any().downcast_ref() } + /// Style the node with styles from a style map. + pub fn styled(mut self, styles: Styles) -> Self { + if self.styles.is_empty() { + self.styles = styles; + } else { + self.styles.apply(&styles); + } + self + } + /// Force a size for this node. pub fn sized(self, sizing: Spec>) -> Self { if sizing.any(Option::is_some) { @@ -156,12 +170,12 @@ impl Layout for PackedNode { regions: &Regions, ) -> Vec>> { #[cfg(not(feature = "layout-cache"))] - return self.node.layout(ctx, regions); + return self.layout_impl(ctx, regions); #[cfg(feature = "layout-cache")] ctx.layouts.get(self.hash, regions).unwrap_or_else(|| { ctx.level += 1; - let frames = self.node.layout(ctx, regions); + let frames = self.layout_impl(ctx, regions); ctx.level -= 1; let entry = FramesEntry::new(frames.clone(), ctx.level); @@ -190,12 +204,27 @@ impl Layout for PackedNode { } } +impl PackedNode { + /// Layout the node without checking the cache. + fn layout_impl( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec>> { + let new = self.styles.chain(&ctx.styles); + let prev = std::mem::replace(&mut ctx.styles, new); + let frames = self.node.layout(ctx, regions); + ctx.styles = prev; + frames + } +} + impl Hash for PackedNode { - fn hash(&self, _state: &mut H) { + fn hash(&self, state: &mut H) { #[cfg(feature = "layout-cache")] - _state.write_u64(self.hash); + state.write_u64(self.hash); #[cfg(not(feature = "layout-cache"))] - unimplemented!() + state.write_u64(self.hash64()); } } @@ -207,13 +236,23 @@ impl Debug for PackedNode { trait Bounds: Layout + Debug + 'static { fn as_any(&self) -> &dyn Any; + fn hash64(&self) -> u64; } impl Bounds for T where - T: Layout + Debug + 'static, + T: Layout + Hash + Debug + 'static, { fn as_any(&self) -> &dyn Any { self } + + fn hash64(&self) -> u64 { + // Also hash the TypeId since nodes with different types but + // equal data should be different. + let mut state = fxhash::FxHasher64::default(); + self.type_id().hash(&mut state); + self.hash(&mut state); + state.finish() + } } diff --git a/src/library/align.rs b/src/library/align.rs index 76db7fc42..96a1c6c5f 100644 --- a/src/library/align.rs +++ b/src/library/align.rs @@ -1,12 +1,19 @@ use super::prelude::*; +use super::ParNode; /// `align`: Configure the alignment along the layouting axes. pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult { let aligns = args.expect::>("alignment")?; let body = args.expect::("body")?; - // TODO(set): Style paragraphs with x alignment. - Ok(Value::block(body.into_block().aligned(aligns))) + let mut styles = Styles::new(); + if let Some(align) = aligns.x { + styles.set(ParNode::ALIGN, align); + } + + Ok(Value::block( + body.into_block().styled(styles).aligned(aligns), + )) } /// A node that aligns its child. diff --git a/src/library/deco.rs b/src/library/deco.rs index b1ca030ac..d12f60b03 100644 --- a/src/library/deco.rs +++ b/src/library/deco.rs @@ -22,7 +22,7 @@ fn line_impl(args: &mut Args, kind: LineKind) -> TypResult { let offset = args.named("offset")?; let extent = args.named("extent")?.unwrap_or_default(); let body: Node = args.expect("body")?; - Ok(Value::Node(body.decorate(Decoration::Line( + Ok(Value::Node(body.decorated(Decoration::Line( LineDecoration { kind, stroke, thickness, offset, extent }, )))) } @@ -37,7 +37,7 @@ pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult { } Node::Text(text.into()) }); - Ok(Value::Node(body.decorate(Decoration::Link(url)))) + Ok(Value::Node(body.decorated(Decoration::Link(url)))) } /// A decoration for a frame. diff --git a/src/library/flow.rs b/src/library/flow.rs index dddd38a43..41760e516 100644 --- a/src/library/flow.rs +++ b/src/library/flow.rs @@ -1,7 +1,7 @@ use std::fmt::{self, Debug, Formatter}; use super::prelude::*; -use super::{AlignNode, PlacedNode, Spacing}; +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 { @@ -158,6 +158,13 @@ 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()); + } + if let Some(placed) = node.downcast::() { let frame = node.layout(ctx, &self.regions).remove(0); if placed.out_of_flow() { diff --git a/src/library/page.rs b/src/library/page.rs index a4ad84f64..490eef662 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -49,7 +49,12 @@ pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult { /// Layouts its child onto one or multiple pages. #[derive(Debug, Hash)] -pub struct PageNode(pub PackedNode); +pub struct PageNode { + /// The node producing the content. + pub node: PackedNode, + /// The page's styles. + pub styles: Styles, +} properties! { PageNode, @@ -77,30 +82,31 @@ properties! { impl PageNode { /// Layout the page run into a sequence of frames, one per page. pub fn layout(&self, ctx: &mut LayoutContext) -> Vec> { - // TODO(set): Take styles as parameter. - let styles = Styles::new(); + // TODO(set): Use chaining. + let prev = std::mem::replace(&mut ctx.styles, self.styles.clone()); + ctx.styles.apply(&prev); // When one of the lengths is infinite the page fits its content along // that axis. - let width = styles.get(Self::WIDTH).unwrap_or(Length::inf()); - let height = styles.get(Self::HEIGHT).unwrap_or(Length::inf()); + let width = ctx.styles.get(Self::WIDTH).unwrap_or(Length::inf()); + let height = ctx.styles.get(Self::HEIGHT).unwrap_or(Length::inf()); let mut size = Size::new(width, height); - if styles.get(Self::FLIPPED) { + if ctx.styles.get(Self::FLIPPED) { std::mem::swap(&mut size.x, &mut size.y); } // Determine the margins. - let class = styles.get(Self::CLASS); + let class = ctx.styles.get(Self::CLASS); let default = class.default_margins(); let padding = Sides { - left: styles.get(Self::LEFT).unwrap_or(default.left), - right: styles.get(Self::RIGHT).unwrap_or(default.right), - top: styles.get(Self::TOP).unwrap_or(default.top), - bottom: styles.get(Self::BOTTOM).unwrap_or(default.bottom), + left: ctx.styles.get(Self::LEFT).unwrap_or(default.left), + right: ctx.styles.get(Self::RIGHT).unwrap_or(default.right), + top: ctx.styles.get(Self::TOP).unwrap_or(default.top), + bottom: ctx.styles.get(Self::BOTTOM).unwrap_or(default.bottom), }; // Pad the child. - let padded = PadNode { child: self.0.clone(), padding }.pack(); + let padded = PadNode { child: self.node.clone(), padding }.pack(); // Layout the child. let expand = size.map(Length::is_finite); @@ -109,13 +115,14 @@ impl PageNode { padded.layout(ctx, ®ions).into_iter().map(|c| c.item).collect(); // Add background fill if requested. - if let Some(fill) = styles.get(Self::FILL) { + if let Some(fill) = ctx.styles.get(Self::FILL) { for frame in &mut frames { let shape = Shape::filled(Geometry::Rect(frame.size), fill); Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); } } + ctx.styles = prev; frames } } diff --git a/src/library/par.rs b/src/library/par.rs index 217602255..e7433e3e5 100644 --- a/src/library/par.rs +++ b/src/library/par.rs @@ -73,19 +73,16 @@ impl Layout for ParNode { ctx: &mut LayoutContext, regions: &Regions, ) -> Vec>> { - // TODO(set): Take styles as parameter. - let styles = Styles::new(); - // Collect all text into one string used for BiDi analysis. let text = self.collect_text(); // Find out the BiDi embedding levels. - let default_level = Level::from_dir(styles.get(Self::DIR)); + let default_level = Level::from_dir(ctx.styles.get(Self::DIR)); let bidi = BidiInfo::new(&text, default_level); // Prepare paragraph layout by building a representation on which we can // do line breaking without layouting each and every line from scratch. - let layouter = ParLayouter::new(self, ctx, regions, bidi, &styles); + let layouter = ParLayouter::new(self, ctx, regions, bidi); // Find suitable linebreaks. layouter.layout(ctx, regions.clone()) @@ -119,8 +116,8 @@ impl ParNode { fn strings(&self) -> impl Iterator { self.0.iter().map(|child| match child { ParChild::Spacing(_) => " ", - ParChild::Text(ref piece, ..) => &piece.0, - ParChild::Node(..) => "\u{FFFC}", + ParChild::Text(ref node) => &node.text, + ParChild::Node(_) => "\u{FFFC}", ParChild::Decorate(_) | ParChild::Undecorate => "", }) } @@ -132,7 +129,6 @@ pub enum ParChild { /// Spacing between other nodes. Spacing(Spacing), /// A run of text and how to align it in its line. - // TODO(set): A single text run may also have its own style. Text(TextNode), /// Any child node and how to align it in its line. Node(PackedNode), @@ -146,7 +142,7 @@ impl Debug for ParChild { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::Spacing(v) => write!(f, "Spacing({:?})", v), - Self::Text(text) => write!(f, "Text({:?})", text), + Self::Text(node) => write!(f, "Text({:?})", node.text), Self::Node(node) => node.fmt(f), Self::Decorate(deco) => write!(f, "Decorate({:?})", deco), Self::Undecorate => write!(f, "Undecorate"), @@ -193,7 +189,6 @@ impl<'a> ParLayouter<'a> { ctx: &mut LayoutContext, regions: &Regions, bidi: BidiInfo<'a>, - styles: &'a Styles, ) -> Self { let mut items = vec![]; let mut ranges = vec![]; @@ -212,7 +207,7 @@ impl<'a> ParLayouter<'a> { items.push(ParItem::Fractional(v)); ranges.push(range); } - ParChild::Text(_) => { + ParChild::Text(ref 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) { @@ -220,7 +215,8 @@ impl<'a> ParLayouter<'a> { cursor += group.len(); let subrange = start .. cursor; let text = &bidi.text[subrange.clone()]; - let shaped = shape(ctx, text, styles, level.dir()); + let styles = node.styles.chain(&ctx.styles); + let shaped = shape(&mut ctx.fonts, text, styles, level.dir()); items.push(ParItem::Text(shaped)); ranges.push(subrange); } @@ -248,8 +244,8 @@ impl<'a> ParLayouter<'a> { } Self { - align: styles.get(ParNode::ALIGN), - leading: styles.get(ParNode::LEADING), + align: ctx.styles.get(ParNode::ALIGN), + leading: ctx.styles.get(ParNode::LEADING), bidi, items, ranges, @@ -426,7 +422,7 @@ impl<'a> LineLayout<'a> { // empty string. if !range.is_empty() || rest.is_empty() { // Reshape that part. - let reshaped = shaped.reshape(ctx, range); + let reshaped = shaped.reshape(&mut ctx.fonts, range); last = Some(ParItem::Text(reshaped)); } @@ -447,7 +443,7 @@ impl<'a> LineLayout<'a> { // Reshape if necessary. if range.len() < shaped.text.len() { if !range.is_empty() { - let reshaped = shaped.reshape(ctx, range); + let reshaped = shaped.reshape(&mut ctx.fonts, range); first = Some(ParItem::Text(reshaped)); } diff --git a/src/library/text.rs b/src/library/text.rs index 012180871..e8bb6093d 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -53,7 +53,12 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult { /// A single run of text with the same style. #[derive(Debug, Hash)] -pub struct TextNode(pub EcoString); +pub struct TextNode { + /// The run's text. + pub text: EcoString, + /// The run's styles. + pub styles: Styles, +} properties! { TextNode, @@ -138,12 +143,12 @@ pub enum FontFamily { impl Debug for FontFamily { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::Serif => "serif", - Self::SansSerif => "sans-serif", - Self::Monospace => "monospace", - Self::Named(s) => s, - }) + match self { + Self::Serif => f.pad("serif"), + Self::SansSerif => f.pad("sans-serif"), + Self::Monospace => f.pad("monospace"), + Self::Named(s) => s.fmt(f), + } } } @@ -329,28 +334,28 @@ castable! { /// Shape text into [`ShapedText`]. pub fn shape<'a>( - ctx: &mut LayoutContext, + fonts: &mut FontStore, text: &'a str, - styles: &'a Styles, + styles: Styles, dir: Dir, ) -> ShapedText<'a> { let mut glyphs = vec![]; if !text.is_empty() { shape_segment( - ctx.fonts, + fonts, &mut glyphs, 0, text, - variant(styles), - families(styles), + variant(&styles), + families(&styles), None, dir, - &tags(styles), + &tags(&styles), ); } track(&mut glyphs, styles.get(TextNode::TRACKING)); - let (size, baseline) = measure(ctx, &glyphs, styles); + let (size, baseline) = measure(fonts, &glyphs, &styles); ShapedText { text, @@ -507,7 +512,7 @@ fn track(glyphs: &mut [ShapedGlyph], tracking: Em) { /// Measure the size and baseline of a run of shaped glyphs with the given /// properties. fn measure( - ctx: &mut LayoutContext, + fonts: &mut FontStore, glyphs: &[ShapedGlyph], styles: &Styles, ) -> (Size, Length) { @@ -529,14 +534,14 @@ fn measure( // When there are no glyphs, we just use the vertical metrics of the // first available font. for family in families(styles) { - if let Some(face_id) = ctx.fonts.select(family, variant(styles)) { - expand(ctx.fonts.get(face_id)); + if let Some(face_id) = fonts.select(family, variant(styles)) { + expand(fonts.get(face_id)); break; } } } else { for (face_id, group) in glyphs.group_by_key(|g| g.face_id) { - let face = ctx.fonts.get(face_id); + let face = fonts.get(face_id); expand(face); for glyph in group { @@ -685,7 +690,8 @@ pub struct ShapedText<'a> { /// The text direction. pub dir: Dir, /// The text's style properties. - pub styles: &'a Styles, + // TODO(set): Go back to reference. + pub styles: Styles, /// The font size. pub size: Size, /// The baseline from the top of the frame. @@ -749,21 +755,21 @@ impl<'a> ShapedText<'a> { /// shaping process if possible. pub fn reshape( &'a self, - ctx: &mut LayoutContext, + fonts: &mut FontStore, text_range: Range, ) -> ShapedText<'a> { if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { - let (size, baseline) = measure(ctx, glyphs, self.styles); + let (size, baseline) = measure(fonts, glyphs, &self.styles); Self { text: &self.text[text_range], dir: self.dir, - styles: self.styles, + styles: self.styles.clone(), size, baseline, glyphs: Cow::Borrowed(glyphs), } } else { - shape(ctx, &self.text[text_range], self.styles, self.dir) + shape(fonts, &self.text[text_range], self.styles.clone(), self.dir) } }