summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/eval/mod.rs32
-rw-r--r--src/eval/node.rs213
-rw-r--r--src/eval/scope.rs1
-rw-r--r--src/eval/styles.rs212
-rw-r--r--src/eval/value.rs7
-rw-r--r--src/layout/mod.rs63
-rw-r--r--src/library/align.rs11
-rw-r--r--src/library/deco.rs4
-rw-r--r--src/library/flow.rs9
-rw-r--r--src/library/page.rs33
-rw-r--r--src/library/par.rs28
-rw-r--r--src/library/text.rs52
12 files changed, 519 insertions, 146 deletions
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index c1f0b024..d5b33280 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<Self::Output> {
- 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<Self::Output> {
- // 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<Self::Output> {
- // 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<Self::Output> {
- // 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 4d259e2d..d3bf9806 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<Self>),
+ 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("<node>")
+ 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<PageNode>,
+ /// The common style properties of all items on the current page.
+ page_styles: Styles,
+ /// The accumulated flow children.
flow: Vec<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,
}
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);
}
}
}
}
+ /// Remove a trailing space.
+ fn trim(&mut self) {
+ if self.last == Last::Space {
+ self.par.pop();
+ self.last = Last::Other;
+ }
+ }
+
+ /// 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() {
- self.flow.push(FlowChild::Node(ParNode(children).pack()));
+ // 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 pagebreak(&mut self) {
+ /// Advance to the next page.
+ fn pagebreak(&mut self, keep: bool) {
+ self.parbreak();
let children = mem::take(&mut self.flow);
- self.document.push(PageNode(FlowNode(children).pack()));
+ 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);
}
- fn push_inline(&mut self, child: ParChild) {
+ /// 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;
}
- fn push_block(&mut self, child: FlowChild) {
+ /// 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 2290affd..ffe2d63e 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 30822edb..9d204843 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<TypeId, Rc<dyn Any>>,
+ 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<P: Property>(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<P: Property>(&mut self, _: P, value: P::Value) {
- self.map.insert(TypeId::of::<P>(), Rc::new(value));
+ pub fn set<P: Property>(&mut self, key: P, value: P::Value)
+ where
+ P::Value: Debug + Hash + PartialEq + 'static,
+ {
+ let id = StyleId::of::<P>();
+ 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<P: Property>(&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<P: Property>(&self, _: P) -> Option<&P::Value> {
+ /// Get a reference to a style directly in this map (no default value).
+ pub fn get_direct<P: Property>(&self, _: P) -> Option<&P::Value> {
self.map
- .get(&TypeId::of::<P>())
- .and_then(|boxed| boxed.downcast_ref())
+ .iter()
+ .find(|pair| pair.0 == StyleId::of::<P>())
+ .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<F>(&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()
+ }
+}
+
+/// An entry for a single style property.
+#[derive(Clone)]
+pub(crate) struct Entry {
+ #[cfg(debug_assertions)]
+ name: &'static str,
+ value: 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 downcast<T: 'static>(&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<H: Hasher>(&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<T> 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>() {
+ 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)
}
}
-/// Stylistic property keys.
+/// 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<P: Property>() -> Self {
+ Self(TypeId::of::<P>())
+ }
}
+/// 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 a6230956..c2a284eb 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 9c57152a..5da0ad9a 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<dyn Bounds>,
+ /// 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<Option<Linear>>) -> Self {
if sizing.any(Option::is_some) {
@@ -156,12 +170,12 @@ impl Layout for PackedNode {
regions: &Regions,
) -> Vec<Constrained<Rc<Frame>>> {
#[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<Constrained<Rc<Frame>>> {
+ 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<H: Hasher>(&self, _state: &mut H) {
+ fn hash<H: Hasher>(&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<T> 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 76db7fc4..96a1c6c5 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<Value> {
let aligns = args.expect::<Spec<_>>("alignment")?;
let body = args.expect::<Node>("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 b1ca030a..d12f60b0 100644
--- a/src/library/deco.rs
+++ b/src/library/deco.rs
@@ -22,7 +22,7 @@ fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
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<Value> {
}
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 dddd38a4..41760e51 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<Value> {
@@ -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::<PlacedNode>() {
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 a4ad84f6..490eef66 100644
--- a/src/library/page.rs
+++ b/src/library/page.rs
@@ -49,7 +49,12 @@ pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
/// 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<Rc<Frame>> {
- // 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, &regions).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 21760225..e7433e3e 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<Constrained<Rc<Frame>>> {
- // 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<Item = &str> {
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 01218087..e8bb6093 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<Value> {
/// 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<usize>,
) -> 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)
}
}