From 537545e7f8351d7677c396456e46568f5a5e2a7a Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 7 Oct 2020 17:07:44 +0200 Subject: [PATCH] =?UTF-8?q?Evaluation=20and=20node-based=20layouting=20?= =?UTF-8?q?=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/diag.rs | 4 +- src/eval/args.rs | 9 +- src/eval/mod.rs | 380 ++++++++++++++++++++++++--- src/eval/state.rs | 23 +- src/eval/value.rs | 85 +++--- src/layout/mod.rs | 234 +++++------------ src/layout/nodes/document.rs | 52 ++++ src/layout/nodes/fixed.rs | 42 +++ src/layout/nodes/mod.rs | 167 ++++++++++++ src/layout/nodes/pad.rs | 53 ++++ src/layout/{line.rs => nodes/par.rs} | 157 ++++++----- src/layout/nodes/spacing.rs | 51 ++++ src/layout/{ => nodes}/stack.rs | 255 +++++++++--------- src/layout/nodes/text.rs | 51 ++++ src/layout/primitive.rs | 64 +++-- src/layout/tree.rs | 234 ----------------- src/lib.rs | 36 ++- src/library/align.rs | 28 +- src/library/boxed.rs | 44 ++-- src/library/color.rs | 2 +- src/library/font.rs | 41 +-- src/library/mod.rs | 2 +- src/library/page.rs | 43 +-- src/library/spacing.rs | 29 +- src/prelude.rs | 8 +- 25 files changed, 1260 insertions(+), 834 deletions(-) create mode 100644 src/layout/nodes/document.rs create mode 100644 src/layout/nodes/fixed.rs create mode 100644 src/layout/nodes/mod.rs create mode 100644 src/layout/nodes/pad.rs rename src/layout/{line.rs => nodes/par.rs} (70%) create mode 100644 src/layout/nodes/spacing.rs rename src/layout/{ => nodes}/stack.rs (60%) create mode 100644 src/layout/nodes/text.rs delete mode 100644 src/layout/tree.rs diff --git a/src/diag.rs b/src/diag.rs index 801dc6a3b..8ad41b136 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -55,7 +55,7 @@ impl Display for Level { /// let spanned = error!(span, "there is an error here"); /// ``` /// -/// [`Error`]: diagnostic/enum.Level.html#variant.Error +/// [`Error`]: diag/enum.Level.html#variant.Error #[macro_export] macro_rules! error { ($($tts:tt)*) => { @@ -68,7 +68,7 @@ macro_rules! error { /// This works exactly like `error!`. See its documentation for more /// information. /// -/// [`Warning`]: diagnostic/enum.Level.html#variant.Warning +/// [`Warning`]: diag/enum.Level.html#variant.Warning #[macro_export] macro_rules! warning { ($($tts:tt)*) => { diff --git a/src/eval/args.rs b/src/eval/args.rs index d11deac60..04f83b506 100644 --- a/src/eval/args.rs +++ b/src/eval/args.rs @@ -2,8 +2,7 @@ use std::mem; -use super::{Convert, RefKey, ValueDict}; -use crate::layout::LayoutContext; +use super::{Convert, EvalContext, RefKey, ValueDict}; use crate::syntax::{SpanWith, Spanned}; /// A wrapper around a dictionary value that simplifies argument parsing in @@ -16,7 +15,7 @@ impl Args { /// /// Generates an error if the key exists, but the value can't be converted /// into the type `T`. - pub fn get<'a, K, T>(&mut self, ctx: &mut LayoutContext, key: K) -> Option + pub fn get<'a, K, T>(&mut self, ctx: &mut EvalContext, key: K) -> Option where K: Into>, T: Convert, @@ -37,7 +36,7 @@ impl Args { /// [`get`]: #method.get pub fn need<'a, K, T>( &mut self, - ctx: &mut LayoutContext, + ctx: &mut EvalContext, key: K, name: &str, ) -> Option @@ -126,7 +125,7 @@ impl Args { } /// Generated _unexpected argument_ errors for all remaining entries. - pub fn done(&self, ctx: &mut LayoutContext) { + pub fn done(&self, ctx: &mut EvalContext) { for entry in self.0.v.values() { let span = entry.key_span.join(entry.value.span); ctx.diag(error!(span, "unexpected argument")); diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 6f882ab85..101d26d96 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -14,42 +14,355 @@ pub use scope::*; pub use state::*; pub use value::*; +use std::any::Any; +use std::mem; +use std::rc::Rc; + use async_trait::async_trait; +use fontdock::FontStyle; -use crate::layout::LayoutContext; +use crate::diag::Diag; +use crate::geom::Size; +use crate::layout::nodes::{ + Document, LayoutNode, Pad, Pages, Par, Softness, Spacing, Stack, Text, +}; +use crate::layout::{Gen2, Spec2, Switch}; use crate::syntax::*; +use crate::{Feedback, Pass}; -/// Evaluate an syntactic item into an output value. +/// Evaluate a syntax tree into a document. +/// +/// The given `state` the base state that may be updated over the course of +/// evaluation. +pub fn eval(tree: &SynTree, state: State) -> Pass { + let mut ctx = EvalContext::new(state); + + ctx.start_page_group(false); + tree.eval(&mut ctx); + ctx.end_page_group(); + + ctx.finish() +} + +/// The context for evaluation. +#[derive(Debug)] +pub struct EvalContext { + /// The active evaluation state. + pub state: State, + /// The accumualted feedback. + f: Feedback, + /// The finished page runs. + runs: Vec, + /// The stack of logical groups (paragraphs and such). + /// + /// Each entry contains metadata about the group and nodes that are at the + /// same level as the group, which will return to `inner` once the group is + /// finished. + groups: Vec<(Box, Vec)>, + /// The nodes in the current innermost group + /// (whose metadata is in `groups.last()`). + inner: Vec, +} + +impl EvalContext { + /// Create a new evaluation context with a base state. + pub fn new(state: State) -> Self { + Self { + state, + groups: vec![], + inner: vec![], + runs: vec![], + f: Feedback::new(), + } + } + + /// Finish evaluation and return the created document. + pub fn finish(self) -> Pass { + assert!(self.groups.is_empty(), "unpoped group"); + Pass::new(Document { runs: self.runs }, self.f) + } + + /// Add a diagnostic to the feedback. + pub fn diag(&mut self, diag: Spanned) { + self.f.diags.push(diag); + } + + /// Add a decoration to the feedback. + pub fn deco(&mut self, deco: Spanned) { + self.f.decos.push(deco); + } + + /// Push a layout node to the active group. + /// + /// Spacing nodes will be handled according to their [`Softness`]. + /// + /// [`Softness`]: ../layout/nodes/enum.Softness.html + pub fn push(&mut self, node: impl Into) { + let node = node.into(); + + if let LayoutNode::Spacing(this) = node { + if this.softness == Softness::Soft && self.inner.is_empty() { + return; + } + + if let Some(&LayoutNode::Spacing(other)) = self.inner.last() { + if this.softness > other.softness { + self.inner.pop(); + } else if this.softness == Softness::Soft { + return; + } + } + } + + self.inner.push(node); + } + + /// Start a layouting group. + /// + /// All further calls to [`push`] will collect nodes for this group. + /// The given metadata will be returned alongside the collected nodes + /// in a matching call to [`end_group`]. + /// + /// [`push`]: #method.push + /// [`end_group`]: #method.end_group + pub fn start_group(&mut self, meta: T) { + self.groups.push((Box::new(meta), mem::take(&mut self.inner))); + } + + /// End a layouting group started with [`start_group`]. + /// + /// This returns the stored metadata and the collected nodes. + /// + /// [`start_group`]: #method.start_group + pub fn end_group(&mut self) -> (T, Vec) { + let (any, outer) = self.groups.pop().expect("no pushed group"); + let group = *any.downcast::().expect("bad group type"); + (group, mem::replace(&mut self.inner, outer)) + } + + /// Start a page run group based on the active page state. + /// + /// If `hard` is false, empty page runs will be omitted from the output. + /// + /// This also starts an inner paragraph. + pub fn start_page_group(&mut self, hard: bool) { + let size = self.state.page.size; + let margins = self.state.page.margins(); + let dirs = self.state.dirs; + let aligns = self.state.aligns; + self.start_group((size, margins, dirs, aligns, hard)); + self.start_par_group(); + } + + /// End a page run group and push it to its parent group. + /// + /// This also ends an inner paragraph. + pub fn end_page_group(&mut self) { + self.end_par_group(); + let ((size, padding, dirs, aligns, hard), children) = self.end_group(); + let hard: bool = hard; + if hard || !children.is_empty() { + self.runs.push(Pages { + size, + child: LayoutNode::dynamic(Pad { + padding, + child: LayoutNode::dynamic(Stack { + dirs, + children, + aligns, + expand: Spec2::new(true, true), + }), + }), + }) + } + } + + /// Start a paragraph group based on the active text state. + pub fn start_par_group(&mut self) { + let dirs = self.state.dirs; + let line_spacing = self.state.text.line_spacing(); + let aligns = self.state.aligns; + self.start_group((dirs, line_spacing, aligns)); + } + + /// End a paragraph group and push it to its parent group if its not empty. + pub fn end_par_group(&mut self) { + let ((dirs, line_spacing, aligns), children) = self.end_group(); + if !children.is_empty() { + // FIXME: This is a hack and should be superseded by constraints + // having min and max size. + let expand_cross = self.groups.len() <= 1; + self.push(Par { + dirs, + line_spacing, + children, + aligns, + expand: Gen2::new(false, expand_cross).switch(dirs), + }); + } + } + + /// Construct a text node from the given string based on the active text + /// state. + pub fn make_text_node(&self, text: String) -> Text { + let mut variant = self.state.text.variant; + + if self.state.text.strong { + variant.weight = variant.weight.thicken(300); + } + + if self.state.text.emph { + variant.style = match variant.style { + FontStyle::Normal => FontStyle::Italic, + FontStyle::Italic => FontStyle::Normal, + FontStyle::Oblique => FontStyle::Normal, + } + } + + Text { + text, + dir: self.state.dirs.cross, + size: self.state.text.font_size(), + fallback: Rc::clone(&self.state.text.fallback), + variant, + aligns: self.state.aligns, + } + } +} + +/// Evaluate an item. /// /// _Note_: Evaluation is not necessarily pure, it may change the active state. -#[async_trait(?Send)] pub trait Eval { /// The output of evaluating the item. type Output; /// Evaluate the item to the output value. - async fn eval(&self, ctx: &mut LayoutContext) -> Self::Output; + fn eval(&self, ctx: &mut EvalContext) -> Self::Output; } -#[async_trait(?Send)] -impl Eval for Expr { - type Output = Value; +impl Eval for SynTree { + type Output = (); - async fn eval(&self, ctx: &mut LayoutContext) -> Self::Output { - match self { - Self::Lit(lit) => lit.eval(ctx).await, - Self::Call(call) => call.eval(ctx).await, - Self::Unary(unary) => unary.eval(ctx).await, - Self::Binary(binary) => binary.eval(ctx).await, + fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + for node in self { + node.v.eval(ctx); + } + } +} + +impl Eval for SynNode { + type Output = (); + + fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + match self { + SynNode::Space => { + ctx.push(Spacing { + amount: ctx.state.text.word_spacing(), + softness: Softness::Soft, + }); + } + + SynNode::Text(text) => { + let node = ctx.make_text_node(text.clone()); + ctx.push(node); + } + + SynNode::Linebreak => { + ctx.end_par_group(); + ctx.start_par_group(); + } + + SynNode::Parbreak => { + ctx.end_par_group(); + ctx.push(Spacing { + amount: ctx.state.text.par_spacing(), + softness: Softness::Soft, + }); + ctx.start_par_group(); + } + + SynNode::Emph => { + ctx.state.text.emph ^= true; + } + + SynNode::Strong => { + ctx.state.text.strong ^= true; + } + + SynNode::Heading(heading) => { + heading.eval(ctx); + } + + SynNode::Raw(raw) => { + raw.eval(ctx); + } + + SynNode::Expr(expr) => { + let value = expr.eval(ctx); + value.eval(ctx); + } + } + } +} + +impl Eval for NodeHeading { + type Output = (); + + fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + let prev = ctx.state.clone(); + let upscale = 1.5 - 0.1 * self.level.v as f64; + ctx.state.text.font_size.scale *= upscale; + ctx.state.text.strong = true; + + self.contents.eval(ctx); + + ctx.state = prev; + } +} + +impl Eval for NodeRaw { + type Output = (); + + fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + let prev = Rc::clone(&ctx.state.text.fallback); + let fallback = Rc::make_mut(&mut ctx.state.text.fallback); + fallback.list.insert(0, "monospace".to_string()); + fallback.flatten(); + + let mut children = vec![]; + for line in &self.lines { + children.push(LayoutNode::Text(ctx.make_text_node(line.clone()))); + } + + ctx.push(Stack { + dirs: ctx.state.dirs, + children, + aligns: ctx.state.aligns, + expand: Spec2::new(false, false), + }); + + ctx.state.text.fallback = prev; + } +} + +impl Eval for Expr { + type Output = Value; + + fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + match self { + Self::Lit(lit) => lit.eval(ctx), + Self::Call(call) => call.eval(ctx), + Self::Unary(unary) => unary.eval(ctx), + Self::Binary(binary) => binary.eval(ctx), } } } -#[async_trait(?Send)] impl Eval for Lit { type Output = Value; - async fn eval(&self, ctx: &mut LayoutContext) -> Self::Output { + fn eval(&self, ctx: &mut EvalContext) -> Self::Output { match *self { Lit::Ident(ref v) => Value::Ident(v.clone()), Lit::Bool(v) => Value::Bool(v), @@ -59,20 +372,19 @@ impl Eval for Lit { Lit::Percent(v) => Value::Relative(v / 100.0), Lit::Color(v) => Value::Color(v), Lit::Str(ref v) => Value::Str(v.clone()), - Lit::Dict(ref v) => Value::Dict(v.eval(ctx).await), + Lit::Dict(ref v) => Value::Dict(v.eval(ctx)), Lit::Content(ref v) => Value::Content(v.clone()), } } } -#[async_trait(?Send)] impl Eval for LitDict { type Output = ValueDict; - async fn eval(&self, ctx: &mut LayoutContext) -> Self::Output { + fn eval(&self, ctx: &mut EvalContext) -> Self::Output { let mut dict = ValueDict::new(); for entry in &self.0 { - let val = entry.expr.v.eval(ctx).await; + let val = entry.expr.v.eval(ctx); let spanned = val.span_with(entry.expr.span); if let Some(key) = &entry.key { dict.insert(&key.v, SpannedEntry::new(key.span, spanned)); @@ -85,19 +397,18 @@ impl Eval for LitDict { } } -#[async_trait(?Send)] impl Eval for ExprCall { type Output = Value; - async fn eval(&self, ctx: &mut LayoutContext) -> Self::Output { + fn eval(&self, ctx: &mut EvalContext) -> Self::Output { let name = &self.name.v; let span = self.name.span; - let dict = self.args.v.eval(ctx).await; + let dict = self.args.v.eval(ctx); if let Some(func) = ctx.state.scope.get(name) { let args = Args(dict.span_with(self.args.span)); ctx.f.decos.push(Deco::Resolved.span_with(span)); - (func.clone())(args, ctx).await + (func.clone())(args, ctx) } else { if !name.is_empty() { ctx.diag(error!(span, "unknown function")); @@ -108,14 +419,13 @@ impl Eval for ExprCall { } } -#[async_trait(?Send)] impl Eval for ExprUnary { type Output = Value; - async fn eval(&self, ctx: &mut LayoutContext) -> Self::Output { + fn eval(&self, ctx: &mut EvalContext) -> Self::Output { use Value::*; - let value = self.expr.v.eval(ctx).await; + let value = self.expr.v.eval(ctx); if value == Error { return Error; } @@ -127,13 +437,12 @@ impl Eval for ExprUnary { } } -#[async_trait(?Send)] impl Eval for ExprBinary { type Output = Value; - async fn eval(&self, ctx: &mut LayoutContext) -> Self::Output { - let lhs = self.lhs.v.eval(ctx).await; - let rhs = self.rhs.v.eval(ctx).await; + fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + let lhs = self.lhs.v.eval(ctx); + let rhs = self.rhs.v.eval(ctx); if lhs == Value::Error || rhs == Value::Error { return Value::Error; @@ -150,7 +459,7 @@ impl Eval for ExprBinary { } /// Compute the negation of a value. -fn neg(ctx: &mut LayoutContext, span: Span, value: Value) -> Value { +fn neg(ctx: &mut EvalContext, span: Span, value: Value) -> Value { use Value::*; match value { Int(v) => Int(-v), @@ -166,7 +475,7 @@ fn neg(ctx: &mut LayoutContext, span: Span, value: Value) -> Value { } /// Compute the sum of two values. -fn add(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value { +fn add(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value { use crate::geom::Linear as Lin; use Value::*; match (lhs, rhs) { @@ -193,7 +502,6 @@ fn add(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value { (Str(a), Str(b)) => Str(a + &b), (Dict(a), Dict(b)) => Dict(concat(a, b)), (Content(a), Content(b)) => Content(concat(a, b)), - (Commands(a), Commands(b)) => Commands(concat(a, b)), (a, b) => { ctx.diag(error!(span, "cannot add {} and {}", a.ty(), b.ty())); @@ -203,7 +511,7 @@ fn add(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value { } /// Compute the difference of two values. -fn sub(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value { +fn sub(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value { use crate::geom::Linear as Lin; use Value::*; match (lhs, rhs) { @@ -232,7 +540,7 @@ fn sub(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value { } /// Compute the product of two values. -fn mul(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value { +fn mul(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value { use Value::*; match (lhs, rhs) { // Numbers with themselves. @@ -267,7 +575,7 @@ fn mul(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value { } /// Compute the quotient of two values. -fn div(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value { +fn div(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value { use Value::*; match (lhs, rhs) { // Numbers by themselves. diff --git a/src/eval/state.rs b/src/eval/state.rs index 295a106c9..5861ada11 100644 --- a/src/eval/state.rs +++ b/src/eval/state.rs @@ -1,5 +1,7 @@ //! Evaluation state. +use std::rc::Rc; + use fontdock::{fallback, FallbackTree, FontStretch, FontStyle, FontVariant, FontWeight}; use super::Scope; @@ -39,7 +41,7 @@ impl Default for State { #[derive(Debug, Clone, PartialEq)] pub struct TextState { /// A tree of font family names and generic class names. - pub fallback: FallbackTree, + pub fallback: Rc, /// The selected font variant. pub variant: FontVariant, /// Whether the strong toggle is active or inactive. This determines @@ -83,7 +85,7 @@ impl TextState { impl Default for TextState { fn default() -> Self { Self { - fallback: fallback! { + fallback: Rc::new(fallback! { list: ["sans-serif"], classes: { "serif" => ["source serif pro", "noto serif"], @@ -95,7 +97,7 @@ impl Default for TextState { "source sans pro", "noto sans", "segoe ui emoji", "noto emoji", "latin modern math", ], - }, + }), variant: FontVariant { style: FontStyle::Normal, weight: FontWeight::REGULAR, @@ -160,15 +162,14 @@ impl PageState { } } - /// The absolute insets. - pub fn insets(&self) -> Insets { - let Size { width, height } = self.size; + /// The margins. + pub fn margins(&self) -> Sides { let default = self.class.default_margins(); - Insets { - x0: -self.margins.left.unwrap_or(default.left).eval(width), - y0: -self.margins.top.unwrap_or(default.top).eval(height), - x1: -self.margins.right.unwrap_or(default.right).eval(width), - y1: -self.margins.bottom.unwrap_or(default.bottom).eval(height), + Sides { + left: self.margins.left.unwrap_or(default.left), + top: self.margins.top.unwrap_or(default.top), + right: self.margins.right.unwrap_or(default.right), + bottom: self.margins.bottom.unwrap_or(default.bottom), } } } diff --git a/src/eval/value.rs b/src/eval/value.rs index 85cb261c3..37fd7ead0 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -4,10 +4,9 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::Deref; use std::rc::Rc; -use super::{Args, Dict, SpannedEntry}; +use super::{Args, Dict, Eval, EvalContext, SpannedEntry}; use crate::color::RgbaColor; use crate::geom::Linear; -use crate::layout::{Command, LayoutContext}; use crate::syntax::{Ident, Span, SpanWith, Spanned, SynNode, SynTree}; use crate::DynFuture; @@ -45,8 +44,6 @@ pub enum Value { Content(SynTree), /// An executable function. Func(ValueFunc), - /// Layouting commands. - Commands(Vec), /// The result of invalid operations. Error, } @@ -69,59 +66,42 @@ impl Value { Self::Dict(_) => "dict", Self::Content(_) => "content", Self::Func(_) => "function", - Self::Commands(_) => "commands", Self::Error => "error", } } } +impl Eval for Value { + type Output = (); + + /// Evaluate everything contained in this value. + fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + match self { + // Don't print out none values. + Value::None => {} + + // Pass through. + Value::Content(tree) => tree.eval(ctx), + + // Forward to each dictionary entry. + Value::Dict(dict) => { + for entry in dict.values() { + entry.value.v.eval(ctx); + } + } + + // Format with debug. + val => ctx.push(ctx.make_text_node(format!("{:?}", val))), + } + } +} + impl Default for Value { fn default() -> Self { Value::None } } -impl Spanned { - /// Transform this value into something layoutable. - /// - /// If this is already a command-value, it is simply unwrapped, otherwise - /// the value is represented as layoutable content in a reasonable way. - pub fn into_commands(self) -> Vec { - match self.v { - // Don't print out none values. - Value::None => vec![], - - // Pass-through. - Value::Commands(commands) => commands, - Value::Content(tree) => vec![Command::LayoutSyntaxTree(tree)], - - // Forward to each entry, separated with spaces. - Value::Dict(dict) => { - let mut commands = vec![]; - let mut end = None; - for entry in dict.into_values() { - if let Some(last_end) = end { - let span = Span::new(last_end, entry.key_span.start); - let tree = vec![SynNode::Space.span_with(span)]; - commands.push(Command::LayoutSyntaxTree(tree)); - } - - end = Some(entry.value.span.end); - commands.extend(entry.value.into_commands()); - } - commands - } - - // Format with debug. - val => { - let fmt = format!("{:?}", val); - let tree = vec![SynNode::Text(fmt).span_with(self.span)]; - vec![Command::LayoutSyntaxTree(tree)] - } - } - } -} - impl Debug for Value { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { @@ -138,7 +118,6 @@ impl Debug for Value { Self::Dict(v) => v.fmt(f), Self::Content(v) => v.fmt(f), Self::Func(v) => v.fmt(f), - Self::Commands(v) => v.fmt(f), Self::Error => f.pad(""), } } @@ -157,9 +136,9 @@ pub type ValueDict = Dict>; /// The dynamic function object is wrapped in an `Rc` to keep [`Value`] /// clonable. /// -/// _Note_: This is needed because the compiler can't `derive(PartialEq)` -/// for `Value` when directly putting the boxed function in there, -/// see the [Rust Issue]. +/// _Note_: This is needed because the compiler can't `derive(PartialEq)` for +/// [`Value`] when directly putting the boxed function in there, see the +/// [Rust Issue]. /// /// [`Value`]: enum.Value.html /// [Rust Issue]: https://github.com/rust-lang/rust/issues/31740 @@ -167,13 +146,13 @@ pub type ValueDict = Dict>; pub struct ValueFunc(pub Rc); /// The signature of executable functions. -pub type Func = dyn Fn(Args, &mut LayoutContext) -> DynFuture; +type Func = dyn Fn(Args, &mut EvalContext) -> Value; impl ValueFunc { /// Create a new function value from a rust function or closure. - pub fn new(f: F) -> Self + pub fn new(f: F) -> Self where - F: Fn(Args, &mut LayoutContext) -> DynFuture, + F: Fn(Args, &mut EvalContext) -> Value + 'static, { Self(Rc::new(f)) } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 912ca0104..f709da1a1 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -1,52 +1,82 @@ -//! Layouting of syntax trees. +//! Layouting of documents. +pub mod nodes; pub mod primitive; -mod line; -mod stack; -mod tree; - -pub use line::*; pub use primitive::*; -pub use stack::*; -pub use tree::*; -use crate::diag::Diag; +use async_trait::async_trait; + use crate::eval::{PageState, State, TextState}; use crate::font::SharedFontLoader; use crate::geom::{Insets, Point, Rect, Size, SizeExt}; use crate::shaping::Shaped; -use crate::syntax::{Deco, Spanned, SynTree}; -use crate::{Feedback, Pass}; +use crate::syntax::SynTree; -/// Layout a syntax tree and return the produced layout. -pub async fn layout( - tree: &SynTree, - state: State, - loader: SharedFontLoader, -) -> Pass> { - let space = LayoutSpace { - size: state.page.size, - insets: state.page.insets(), - expansion: Spec2::new(true, true), - }; +use nodes::Document; - let constraints = LayoutConstraints { - root: true, - base: space.usable(), - spaces: vec![space], - repeat: true, - }; +/// Layout a document and return the produced layouts. +pub async fn layout(document: &Document, loader: SharedFontLoader) -> Vec { + let mut ctx = LayoutContext { loader }; + document.layout(&mut ctx).await +} - let mut ctx = LayoutContext { - loader, - state, - constraints, - f: Feedback::new(), - }; +/// The context for layouting. +#[derive(Debug, Clone)] +pub struct LayoutContext { + /// The font loader to query fonts from when typesetting text. + pub loader: SharedFontLoader, +} - let layouts = layout_tree(&tree, &mut ctx).await; - Pass::new(layouts, ctx.f) +/// Layout a node. +#[async_trait(?Send)] +pub trait Layout { + /// Layout the node in the given layout context. + /// + /// This signature looks pretty horrible due to async in trait methods, but + /// it's actually just the following: + /// ```rust,ignore + /// async fn layout( + /// &self, + /// ctx: &mut LayoutContext, + /// constraints: LayoutConstraints, + /// ) -> Vec; + /// ``` + async fn layout( + &self, + ctx: &mut LayoutContext, + constraints: LayoutConstraints, + ) -> Vec; +} + +/// An item that is produced by [layouting] a node. +/// +/// [layouting]: trait.Layout.html#method.layout +#[derive(Debug, Clone, PartialEq)] +pub enum LayoutItem { + /// Spacing that should be added to the parent. + Spacing(f64), + /// A box that should be aligned in the parent. + Box(BoxLayout, Gen2), +} + +/// The constraints for layouting a single node. +#[derive(Debug, Clone)] +pub struct LayoutConstraints { + /// The spaces to layout into. + pub spaces: Vec, + /// Whether to spill over into copies of the last space or finish layouting + /// when the last space is used up. + pub repeat: bool, +} + +/// The space into which content is laid out. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct LayoutSpace { + /// The full size of this container (the base for relative sizes). + pub base: Size, + /// The maximum size of the rectangle to layout into. + pub size: Size, } /// A finished box with content at fixed positions. @@ -84,135 +114,3 @@ pub enum LayoutElement { /// Shaped text. Text(Shaped), } - -/// The context for layouting. -#[derive(Debug, Clone)] -pub struct LayoutContext { - /// The font loader to query fonts from when typesetting text. - pub loader: SharedFontLoader, - /// The active state. - pub state: State, - /// The active constraints. - pub constraints: LayoutConstraints, - /// The accumulated feedback. - pub f: Feedback, -} - -impl LayoutContext { - /// Add a diagnostic to the feedback. - pub fn diag(&mut self, diag: Spanned) { - self.f.diags.push(diag); - } - - /// Add a decoration to the feedback. - pub fn deco(&mut self, deco: Spanned) { - self.f.decos.push(deco); - } -} - -/// The constraints for layouting a single node. -#[derive(Debug, Clone)] -pub struct LayoutConstraints { - /// Whether this layouting process is the root page-building process. - pub root: bool, - /// The unpadded size of this container (the base 100% for relative sizes). - pub base: Size, - /// The spaces to layout into. - pub spaces: Vec, - /// Whether to spill over into copies of the last space or finish layouting - /// when the last space is used up. - pub repeat: bool, -} - -/// The space into which content is laid out. -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct LayoutSpace { - /// The maximum size of the rectangle to layout into. - pub size: Size, - /// Padding that should be respected on each side. - pub insets: Insets, - /// Whether to expand the size of the resulting layout to the full size of - /// this space or to shrink it to fit the content. - pub expansion: Spec2, -} - -impl LayoutSpace { - /// The position of the padded start in the space. - pub fn start(&self) -> Point { - Point::new(-self.insets.x0, -self.insets.y0) - } - - /// The actually usable area (size minus padding). - pub fn usable(&self) -> Size { - self.size + self.insets.size() - } - - /// The inner layout space with size reduced by the padding, zero padding of - /// its own and no layout expansion. - pub fn inner(&self) -> Self { - Self { - size: self.usable(), - insets: Insets::ZERO, - expansion: Spec2::new(false, false), - } - } -} - -/// Commands executable by the layouting engine. -#[derive(Debug, Clone, PartialEq)] -pub enum Command { - /// Layout the given tree in the current context (i.e. not nested). The - /// content of the tree is not laid out into a separate box and then added, - /// but simply laid out flatly in the active layouting process. - /// - /// This has the effect that the content fits nicely into the active line - /// layouting, enabling functions to e.g. change the style of some piece of - /// text while keeping it part of the current paragraph. - LayoutSyntaxTree(SynTree), - - /// Add a finished layout. - Add(BoxLayout, Gen2), - /// Add spacing of the given kind along the given axis. The - /// kind defines how the spacing interacts with surrounding spacing. - AddSpacing(f64, SpacingKind, GenAxis), - - /// Start a new line. - BreakLine, - /// Start a new page, which will be part of the finished layout even if it - /// stays empty (since the page break is a _hard_ space break). - BreakPage, - - /// Update the text style. - SetTextState(TextState), - /// Update the page style. - SetPageState(PageState), - /// Update the alignment for future boxes added to this layouting process. - SetAlignment(Gen2), -} - -/// Defines how spacing interacts with surrounding spacing. -/// -/// There are two options for interaction: Hard and soft spacing. Typically, -/// hard spacing is used when a fixed amount of space needs to be inserted no -/// matter what. In contrast, soft spacing can be used to insert a default -/// spacing between e.g. two words or paragraphs that can still be overridden by -/// a hard space. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum SpacingKind { - /// Hard spaces are always laid out and consume surrounding soft space. - Hard, - /// Soft spaces are not laid out if they are touching a hard space and - /// consume neighbouring soft spaces with higher levels. - Soft(u32), -} - -impl SpacingKind { - /// The standard spacing kind used for paragraph spacing. - pub const PARAGRAPH: Self = Self::Soft(1); - - /// The standard spacing kind used for line spacing. - pub const LINE: Self = Self::Soft(2); - - /// The standard spacing kind used for word spacing. - pub const WORD: Self = Self::Soft(1); -} diff --git a/src/layout/nodes/document.rs b/src/layout/nodes/document.rs new file mode 100644 index 000000000..af7a31e6d --- /dev/null +++ b/src/layout/nodes/document.rs @@ -0,0 +1,52 @@ +use super::*; + +/// The top-level layouting node. +#[derive(Debug, Clone, PartialEq)] +pub struct Document { + pub runs: Vec, +} + +impl Document { + /// Create a new document. + pub fn new() -> Self { + Self { runs: vec![] } + } + + /// Layout the document. + pub async fn layout(&self, ctx: &mut LayoutContext) -> Vec { + let mut layouts = vec![]; + for run in &self.runs { + layouts.extend(run.layout(ctx).await); + } + layouts + } +} + +/// A variable-length run of pages that all have the same properties. +#[derive(Debug, Clone, PartialEq)] +pub struct Pages { + /// The size of the pages. + pub size: Size, + /// The layout node that produces the actual pages. + pub child: LayoutNode, +} + +impl Pages { + /// Layout the page run. + pub async fn layout(&self, ctx: &mut LayoutContext) -> Vec { + let constraints = LayoutConstraints { + spaces: vec![LayoutSpace { base: self.size, size: self.size }], + repeat: true, + }; + + self.child + .layout(ctx, constraints) + .await + .into_iter() + .filter_map(|item| match item { + LayoutItem::Spacing(_) => None, + LayoutItem::Box(layout, _) => Some(layout), + }) + .collect() + } +} diff --git a/src/layout/nodes/fixed.rs b/src/layout/nodes/fixed.rs new file mode 100644 index 000000000..0d438879b --- /dev/null +++ b/src/layout/nodes/fixed.rs @@ -0,0 +1,42 @@ +use super::*; +use crate::geom::Linear; + +/// A node that can fix its child's width and height. +#[derive(Debug, Clone, PartialEq)] +pub struct Fixed { + pub width: Option, + pub height: Option, + pub child: LayoutNode, +} + +#[async_trait(?Send)] +impl Layout for Fixed { + async fn layout( + &self, + ctx: &mut LayoutContext, + constraints: LayoutConstraints, + ) -> Vec { + let space = constraints.spaces[0]; + let size = Size::new( + self.width + .map(|w| w.eval(space.base.width)) + .unwrap_or(space.size.width), + self.height + .map(|h| h.eval(space.base.height)) + .unwrap_or(space.size.height), + ); + + self.child + .layout(ctx, LayoutConstraints { + spaces: vec![LayoutSpace { base: size, size }], + repeat: false, + }) + .await + } +} + +impl From for LayoutNode { + fn from(fixed: Fixed) -> Self { + Self::dynamic(fixed) + } +} diff --git a/src/layout/nodes/mod.rs b/src/layout/nodes/mod.rs new file mode 100644 index 000000000..44c182849 --- /dev/null +++ b/src/layout/nodes/mod.rs @@ -0,0 +1,167 @@ +//! Layout nodes. + +mod document; +mod fixed; +mod pad; +mod par; +mod spacing; +mod stack; +mod text; + +pub use document::*; +pub use fixed::*; +pub use pad::*; +pub use par::*; +pub use spacing::*; +pub use stack::*; +pub use text::*; + +use std::any::Any; +use std::fmt::{self, Debug, Formatter}; +use std::ops::Deref; + +use async_trait::async_trait; + +use super::*; + +/// A self-contained, styled layout node. +#[derive(Clone, PartialEq)] +pub enum LayoutNode { + /// A spacing node. + Spacing(Spacing), + /// A text node. + Text(Text), + /// A dynamic that can implement custom layouting behaviour. + Dyn(Dynamic), +} + +impl LayoutNode { + /// Create a new model node form a type implementing `DynNode`. + pub fn dynamic(inner: T) -> Self { + Self::Dyn(Dynamic::new(inner)) + } +} + +impl Debug for LayoutNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Spacing(spacing) => spacing.fmt(f), + Self::Text(text) => text.fmt(f), + Self::Dyn(boxed) => boxed.fmt(f), + } + } +} + +#[async_trait(?Send)] +impl Layout for LayoutNode { + async fn layout( + &self, + ctx: &mut LayoutContext, + constraints: LayoutConstraints, + ) -> Vec { + match self { + Self::Spacing(spacing) => spacing.layout(ctx, constraints).await, + Self::Text(text) => text.layout(ctx, constraints).await, + Self::Dyn(boxed) => boxed.layout(ctx, constraints).await, + } + } +} + +/// A wrapper around a boxed dynamic node. +/// +/// _Note_: This is needed because the compiler can't `derive(PartialEq)` for +/// [`LayoutNode`] when directly putting the boxed node in there, see +/// the [Rust Issue]. +/// +/// [`LayoutNode`]: enum.LayoutNode.html +/// [Rust Issue]: https://github.com/rust-lang/rust/issues/31740 +#[derive(Clone)] +pub struct Dynamic(pub Box); + +impl Dynamic { + /// Wrap a type implementing `DynNode`. + pub fn new(inner: T) -> Self { + Self(Box::new(inner)) + } +} + +impl PartialEq for Dynamic { + fn eq(&self, other: &Self) -> bool { + &self.0 == &other.0 + } +} + +impl Deref for Dynamic { + type Target = dyn DynNode; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + +impl Debug for Dynamic { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl From for LayoutNode { + fn from(dynamic: Dynamic) -> Self { + Self::Dyn(dynamic) + } +} + +/// A dynamic node, which can implement custom layouting behaviour. +/// +/// This trait just combines the requirements for types to qualify as dynamic +/// nodes. The interesting part happens in the inherited trait [`Layout`]. +/// +/// The trait itself also contains three helper methods to make `Box` able to implement `Clone` and `PartialEq`. However, these are +/// automatically provided by a blanket impl as long as the type in question +/// implements[`Layout`], `Debug`, `PartialEq`, `Clone` and is `'static`. +/// +/// [`Layout`]: ../trait.Layout.html +pub trait DynNode: Debug + Layout + 'static { + /// Convert into a `dyn Any` to enable downcasting. + fn as_any(&self) -> &dyn Any; + + /// Check for equality with another trait object. + fn dyn_eq(&self, other: &dyn DynNode) -> bool; + + /// Clone into a trait object. + fn dyn_clone(&self) -> Box; +} + +impl DynNode for T +where + T: Debug + Layout + PartialEq + Clone + 'static, +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn dyn_eq(&self, other: &dyn DynNode) -> bool { + if let Some(other) = other.as_any().downcast_ref::() { + self == other + } else { + false + } + } + + fn dyn_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.dyn_clone() + } +} + +impl PartialEq for Box { + fn eq(&self, other: &Self) -> bool { + self.dyn_eq(other.as_ref()) + } +} diff --git a/src/layout/nodes/pad.rs b/src/layout/nodes/pad.rs new file mode 100644 index 000000000..10a9e2c62 --- /dev/null +++ b/src/layout/nodes/pad.rs @@ -0,0 +1,53 @@ +use super::*; +use crate::geom::Linear; + +/// A node that pads its child at the sides. +#[derive(Debug, Clone, PartialEq)] +pub struct Pad { + pub padding: Sides, + pub child: LayoutNode, +} + +#[async_trait(?Send)] +impl Layout for Pad { + async fn layout( + &self, + ctx: &mut LayoutContext, + constraints: LayoutConstraints, + ) -> Vec { + self.child + .layout(ctx, LayoutConstraints { + spaces: constraints + .spaces + .into_iter() + .map(|space| LayoutSpace { + base: space.base + self.padding.insets(space.base).size(), + size: space.size + self.padding.insets(space.size).size(), + }) + .collect(), + repeat: constraints.repeat, + }) + .await + .into_iter() + .map(|item| match item { + LayoutItem::Box(boxed, align) => { + let padding = self.padding.insets(boxed.size); + let padded = boxed.size - padding.size(); + + let mut outer = BoxLayout::new(padded); + let start = Point::new(-padding.x0, -padding.y0); + outer.push_layout(start, boxed); + + LayoutItem::Box(outer, align) + } + item => item, + }) + .collect() + } +} + +impl From for LayoutNode { + fn from(pad: Pad) -> Self { + Self::dynamic(pad) + } +} diff --git a/src/layout/line.rs b/src/layout/nodes/par.rs similarity index 70% rename from src/layout/line.rs rename to src/layout/nodes/par.rs index ae3bd9699..38b115295 100644 --- a/src/layout/line.rs +++ b/src/layout/nodes/par.rs @@ -1,16 +1,66 @@ -//! Arranging boxes into lines. -//! -//! The boxes are laid out along the cross axis as long as they fit into a line. -//! When necessary, a line break is inserted and the new line is offset along -//! the main axis by the height of the previous line plus extra line spacing. -//! -//! Internally, the line layouter uses a stack layouter to stack the finished -//! lines on top of each. - use super::*; +/// A node that arranges its children into a paragraph. +/// +/// Boxes are laid out along the cross axis as long as they fit into a line. +/// When necessary, a line break is inserted and the new line is offset along +/// the main axis by the height of the previous line plus extra line spacing. +#[derive(Debug, Clone, PartialEq)] +pub struct Par { + pub dirs: Gen2, + pub line_spacing: f64, + pub children: Vec, + pub aligns: Gen2, + pub expand: Spec2, +} + +#[async_trait(?Send)] +impl Layout for Par { + async fn layout( + &self, + ctx: &mut LayoutContext, + constraints: LayoutConstraints, + ) -> Vec { + let mut layouter = LineLayouter::new(LineContext { + dirs: self.dirs, + spaces: constraints.spaces, + repeat: constraints.repeat, + line_spacing: self.line_spacing, + expand: self.expand, + }); + + for child in &self.children { + let items = child + .layout(ctx, LayoutConstraints { + spaces: layouter.remaining(), + repeat: constraints.repeat, + }) + .await; + + for item in items { + match item { + LayoutItem::Spacing(amount) => layouter.push_spacing(amount), + LayoutItem::Box(boxed, aligns) => layouter.push_box(boxed, aligns), + } + } + } + + layouter + .finish() + .into_iter() + .map(|boxed| LayoutItem::Box(boxed, self.aligns)) + .collect() + } +} + +impl From for LayoutNode { + fn from(par: Par) -> Self { + Self::dynamic(par) + } +} + /// Performs the line layouting. -pub struct LineLayouter { +struct LineLayouter { /// The context used for line layouting. ctx: LineContext, /// The underlying layouter that stacks the finished lines. @@ -21,26 +71,30 @@ pub struct LineLayouter { /// The context for line layouting. #[derive(Debug, Clone)] -pub struct LineContext { +struct LineContext { /// The layout directions. - pub dirs: Gen2, + dirs: Gen2, /// The spaces to layout into. - pub spaces: Vec, + spaces: Vec, /// Whether to spill over into copies of the last space or finish layouting /// when the last space is used up. - pub repeat: bool, + repeat: bool, /// The spacing to be inserted between each pair of lines. - pub line_spacing: f64, + line_spacing: f64, + /// Whether to expand the size of the resulting layout to the full size of + /// this space or to shrink it to fit the content. + expand: Spec2, } impl LineLayouter { /// Create a new line layouter. - pub fn new(ctx: LineContext) -> Self { + fn new(ctx: LineContext) -> Self { Self { stack: StackLayouter::new(StackContext { spaces: ctx.spaces.clone(), dirs: ctx.dirs, repeat: ctx.repeat, + expand: ctx.expand, }), ctx, run: LineRun::new(), @@ -48,7 +102,7 @@ impl LineLayouter { } /// Add a layout. - pub fn add(&mut self, layout: BoxLayout, aligns: Gen2) { + fn push_box(&mut self, layout: BoxLayout, aligns: Gen2) { let dirs = self.ctx.dirs; if let Some(prev) = self.run.aligns { if aligns.main != prev.main { @@ -67,6 +121,8 @@ impl LineLayouter { let mut rest_run = LineRun::new(); rest_run.size.main = self.run.size.main; + + // FIXME: Alignment in non-expanding parent. rest_run.usable = Some(match aligns.cross { GenAlign::Start => unreachable!("start > x"), GenAlign::Center => usable - 2.0 * self.run.size.cross, @@ -76,15 +132,11 @@ impl LineLayouter { self.finish_line(); // Move back up in the stack layouter. - self.stack.add_spacing(-rest_run.size.main, SpacingKind::Hard); + self.stack.push_spacing(-rest_run.size.main - self.ctx.line_spacing); self.run = rest_run; } } - if let LastSpacing::Soft(spacing, _) = self.run.last_spacing { - self.add_cross_spacing(spacing, SpacingKind::Hard); - } - let size = layout.size.switch(dirs); let usable = self.usable(); @@ -105,7 +157,12 @@ impl LineLayouter { self.run.size.cross += size.cross; self.run.size.main = self.run.size.main.max(size.main); - self.run.last_spacing = LastSpacing::None; + } + + /// Add spacing to the line. + fn push_spacing(&mut self, mut spacing: f64) { + spacing = spacing.min(self.usable().cross); + self.run.size.cross += spacing; } /// The remaining usable size of the line. @@ -125,66 +182,35 @@ impl LineLayouter { usable } - /// Finish the line and add spacing to the underlying stack. - pub fn add_main_spacing(&mut self, spacing: f64, kind: SpacingKind) { - self.finish_line_if_not_empty(); - self.stack.add_spacing(spacing, kind) - } - - /// Add spacing to the line. - pub fn add_cross_spacing(&mut self, mut spacing: f64, kind: SpacingKind) { - match kind { - SpacingKind::Hard => { - spacing = spacing.min(self.usable().cross); - self.run.size.cross += spacing; - self.run.last_spacing = LastSpacing::Hard; - } - - // A soft space is cached since it might be consumed by a hard - // spacing. - SpacingKind::Soft(level) => { - let consumes = match self.run.last_spacing { - LastSpacing::None => true, - LastSpacing::Soft(_, prev) if level < prev => true, - _ => false, - }; - - if consumes { - self.run.last_spacing = LastSpacing::Soft(spacing, level); - } - } - } - } - /// Update the layouting spaces. /// /// If `replace_empty` is true, the current space is replaced if there are /// no boxes laid out into it yet. Otherwise, the followup spaces are /// replaced. - pub fn set_spaces(&mut self, spaces: Vec, replace_empty: bool) { + fn set_spaces(&mut self, spaces: Vec, replace_empty: bool) { self.stack.set_spaces(spaces, replace_empty && self.line_is_empty()); } /// Update the line spacing. - pub fn set_line_spacing(&mut self, line_spacing: f64) { + fn set_line_spacing(&mut self, line_spacing: f64) { self.ctx.line_spacing = line_spacing; } /// The remaining inner spaces. If something is laid out into these spaces, /// it will fit into this layouter's underlying stack. - pub fn remaining(&self) -> Vec { + fn remaining(&self) -> Vec { let mut spaces = self.stack.remaining(); *spaces[0].size.get_mut(self.ctx.dirs.main.axis()) -= self.run.size.main; spaces } /// Whether the currently set line is empty. - pub fn line_is_empty(&self) -> bool { + fn line_is_empty(&self) -> bool { self.run.size == Gen2::ZERO && self.run.layouts.is_empty() } /// Finish everything up and return the final collection of boxes. - pub fn finish(mut self) -> Vec { + fn finish(mut self) -> Vec { self.finish_line_if_not_empty(); self.stack.finish() } @@ -192,13 +218,13 @@ impl LineLayouter { /// Finish the active space and start a new one. /// /// At the top level, this is a page break. - pub fn finish_space(&mut self, hard: bool) { + fn finish_space(&mut self, hard: bool) { self.finish_line_if_not_empty(); self.stack.finish_space(hard) } /// Finish the active line and start a new one. - pub fn finish_line(&mut self) { + fn finish_line(&mut self) { let dirs = self.ctx.dirs; let mut layout = BoxLayout::new(self.run.size.switch(dirs).to_size()); @@ -216,10 +242,9 @@ impl LineLayouter { layout.push_layout(pos, child); } - self.stack.add(layout, aligns); - + self.stack.push_box(layout, aligns); + self.stack.push_spacing(self.ctx.line_spacing); self.run = LineRun::new(); - self.stack.add_spacing(self.ctx.line_spacing, SpacingKind::LINE); } fn finish_line_if_not_empty(&mut self) { @@ -245,9 +270,6 @@ struct LineRun { /// The amount of cross-space left by another run on the same line or `None` /// if this is the only run so far. usable: Option, - /// The spacing state. This influences how new spacing is handled, e.g. hard - /// spacing may override soft spacing. - last_spacing: LastSpacing, } impl LineRun { @@ -257,7 +279,6 @@ impl LineRun { size: Gen2::ZERO, aligns: None, usable: None, - last_spacing: LastSpacing::Hard, } } } diff --git a/src/layout/nodes/spacing.rs b/src/layout/nodes/spacing.rs new file mode 100644 index 000000000..66af0d177 --- /dev/null +++ b/src/layout/nodes/spacing.rs @@ -0,0 +1,51 @@ +use std::fmt::{self, Debug, Formatter}; + +use super::*; + +/// A node that inserts spacing. +#[derive(Copy, Clone, PartialEq)] +pub struct Spacing { + pub amount: f64, + pub softness: Softness, +} + +#[async_trait(?Send)] +impl Layout for Spacing { + async fn layout( + &self, + ctx: &mut LayoutContext, + constraints: LayoutConstraints, + ) -> Vec { + vec![LayoutItem::Spacing(self.amount)] + } +} + +impl Debug for Spacing { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self.softness { + Softness::Soft => write!(f, "Soft({})", self.amount), + Softness::Hard => write!(f, "Hard({})", self.amount), + } + } +} + +impl From for LayoutNode { + fn from(spacing: Spacing) -> Self { + Self::Spacing(spacing) + } +} + +/// Defines how spacing interacts with surrounding spacing. +/// +/// Hard spacing assures that a fixed amount of spacing will always be inserted. +/// Soft spacing will be consumed by previous soft spacing or neighbouring hard +/// spacing and can be used to insert overridable spacing, e.g. between words or +/// paragraphs. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum Softness { + /// Soft spacing is not laid out if it directly follows other soft spacing + /// or if it touches hard spacing. + Soft, + /// Hard spacing is always laid out and consumes surrounding soft spacing. + Hard, +} diff --git a/src/layout/stack.rs b/src/layout/nodes/stack.rs similarity index 60% rename from src/layout/stack.rs rename to src/layout/nodes/stack.rs index cca2a3150..983175b83 100644 --- a/src/layout/stack.rs +++ b/src/layout/nodes/stack.rs @@ -1,39 +1,93 @@ -//! Arranging boxes into a stack along the main axis. -//! -//! Individual layouts can be aligned at `Start`, `Center` or `End` along both -//! axes. These alignments are with respect to the size of the finished layout -//! and not the total usable size. This means that a later layout can have -//! influence on the position of an earlier one. Consider the following example. -//! ```typst -//! [align: right][A word.] -//! [align: left][A sentence with a couple more words.] -//! ``` -//! The resulting layout looks like this: -//! ```text -//! |--------------------------------------| -//! | A word. | -//! | | -//! | A sentence with a couple more words. | -//! |--------------------------------------| -//! ``` -//! The position of the first aligned box thus depends on the length of the -//! sentence in the second box. - use super::*; +use crate::geom::Linear; + +/// A node that stacks and aligns its children. +/// +/// # Alignment +/// Individual layouts can be aligned at `Start`, `Center` or `End` along both +/// axes. These alignments are with processed with respect to the size of the +/// finished layout and not the total usable size. This means that a later +/// layout can have influence on the position of an earlier one. Consider the +/// following example. +/// ```typst +/// [align: right][A word.] +/// [align: left][A sentence with a couple more words.] +/// ``` +/// The resulting layout looks like this: +/// ```text +/// |--------------------------------------| +/// | A word. | +/// | | +/// | A sentence with a couple more words. | +/// |--------------------------------------| +/// ``` +/// The position of the first aligned box thus depends on the length of the +/// sentence in the second box. +#[derive(Debug, Clone, PartialEq)] +pub struct Stack { + pub dirs: Gen2, + pub children: Vec, + pub aligns: Gen2, + pub expand: Spec2, +} + +#[async_trait(?Send)] +impl Layout for Stack { + async fn layout( + &self, + ctx: &mut LayoutContext, + constraints: LayoutConstraints, + ) -> Vec { + let mut layouter = StackLayouter::new(StackContext { + dirs: self.dirs, + spaces: constraints.spaces, + repeat: constraints.repeat, + expand: self.expand, + }); + + for child in &self.children { + let items = child + .layout(ctx, LayoutConstraints { + spaces: layouter.remaining(), + repeat: constraints.repeat, + }) + .await; + + for item in items { + match item { + LayoutItem::Spacing(amount) => layouter.push_spacing(amount), + LayoutItem::Box(boxed, aligns) => layouter.push_box(boxed, aligns), + } + } + } + + layouter + .finish() + .into_iter() + .map(|boxed| LayoutItem::Box(boxed, self.aligns)) + .collect() + } +} + +impl From for LayoutNode { + fn from(stack: Stack) -> Self { + Self::dynamic(stack) + } +} /// Performs the stack layouting. -pub struct StackLayouter { +pub(super) struct StackLayouter { /// The context used for stack layouting. - ctx: StackContext, + pub ctx: StackContext, /// The finished layouts. - layouts: Vec, + pub layouts: Vec, /// The in-progress space. - pub(super) space: Space, + pub space: Space, } /// The context for stack layouting. #[derive(Debug, Clone)] -pub struct StackContext { +pub(super) struct StackContext { /// The layouting directions. pub dirs: Gen2, /// The spaces to layout into. @@ -41,6 +95,9 @@ pub struct StackContext { /// Whether to spill over into copies of the last space or finish layouting /// when the last space is used up. pub repeat: bool, + /// Whether to expand the size of the resulting layout to the full size of + /// this space or to shrink it to fit the content. + pub expand: Spec2, } impl StackLayouter { @@ -50,12 +107,12 @@ impl StackLayouter { Self { ctx, layouts: vec![], - space: Space::new(0, true, space.usable()), + space: Space::new(0, true, space.size), } } /// Add a layout to the stack. - pub fn add(&mut self, layout: BoxLayout, aligns: Gen2) { + pub fn push_box(&mut self, layout: BoxLayout, aligns: Gen2) { // If the alignment cannot be fitted in this space, finish it. // // TODO: Issue warning for non-fitting alignment in non-repeating @@ -64,14 +121,9 @@ impl StackLayouter { self.finish_space(true); } - // Add a possibly cached soft spacing. - if let LastSpacing::Soft(spacing, _) = self.space.last_spacing { - self.add_spacing(spacing, SpacingKind::Hard); - } - // TODO: Issue warning about overflow if there is overflow in a // non-repeating context. - if !self.usable().fits(layout.size) && self.ctx.repeat { + if !self.space.usable.fits(layout.size) && self.ctx.repeat { self.skip_to_fitting_space(layout.size); } @@ -82,49 +134,27 @@ impl StackLayouter { // again. self.space.layouts.push((layout, aligns)); self.space.allowed_align = aligns.main; - self.space.last_spacing = LastSpacing::None; } /// Add spacing to the stack. - pub fn add_spacing(&mut self, mut spacing: f64, kind: SpacingKind) { - match kind { - // A hard space is simply an empty box. - SpacingKind::Hard => { - self.space.last_spacing = LastSpacing::Hard; + pub fn push_spacing(&mut self, mut spacing: f64) { + // Reduce the spacing such that it definitely fits. + let axis = self.ctx.dirs.main.axis(); + spacing = spacing.min(self.space.usable.get(axis)); - // Reduce the spacing such that it definitely fits. - let axis = self.ctx.dirs.main.axis(); - spacing = spacing.min(self.usable().get(axis)); - - let size = Gen2::new(spacing, 0.0); - self.update_metrics(size); - self.space.layouts.push(( - BoxLayout::new(size.switch(self.ctx.dirs).to_size()), - Gen2::default(), - )); - } - - // A soft space is cached if it is not consumed by a hard space or - // previous soft space with higher level. - SpacingKind::Soft(level) => { - let consumes = match self.space.last_spacing { - LastSpacing::None => true, - LastSpacing::Soft(_, prev) if level < prev => true, - _ => false, - }; - - if consumes { - self.space.last_spacing = LastSpacing::Soft(spacing, level); - } - } - } + let size = Gen2::new(spacing, 0.0); + self.update_metrics(size); + self.space.layouts.push(( + BoxLayout::new(size.switch(self.ctx.dirs).to_size()), + Gen2::default(), + )); } fn update_metrics(&mut self, added: Gen2) { - let mut size = self.space.size.switch(self.ctx.dirs); - size.cross = size.cross.max(added.cross); - size.main += added.main; - self.space.size = size.switch(self.ctx.dirs).to_size(); + let mut used = self.space.used.switch(self.ctx.dirs); + used.cross = used.cross.max(added.cross); + used.main += added.main; + self.space.used = used.switch(self.ctx.dirs).to_size(); *self.space.usable.get_mut(self.ctx.dirs.main.axis()) -= added.main; } @@ -148,7 +178,7 @@ impl StackLayouter { pub fn skip_to_fitting_space(&mut self, size: Size) { let start = self.next_space(); for (index, space) in self.ctx.spaces[start ..].iter().enumerate() { - if space.usable().fits(size) { + if space.size.fits(size) { self.finish_space(true); self.start_space(start + index, true); break; @@ -160,29 +190,22 @@ impl StackLayouter { /// it will fit into this stack. pub fn remaining(&self) -> Vec { let mut spaces = vec![LayoutSpace { - size: self.usable(), - insets: Insets::ZERO, - expansion: Spec2::new(false, false), + base: self.space.size, + size: self.space.usable, }]; - for space in &self.ctx.spaces[self.next_space() ..] { - spaces.push(space.inner()); - } - + spaces.extend(&self.ctx.spaces[self.next_space() ..]); spaces } /// The remaining usable size. pub fn usable(&self) -> Size { self.space.usable - - Gen2::new(self.space.last_spacing.soft_or_zero(), 0.0) - .switch(self.ctx.dirs) - .to_size() } /// Whether the current layout space is empty. pub fn space_is_empty(&self) -> bool { - self.space.size == Size::ZERO && self.space.layouts.is_empty() + self.space.used == Size::ZERO && self.space.layouts.is_empty() } /// Whether the current layout space is the last in the followup list. @@ -208,23 +231,18 @@ impl StackLayouter { // expand if necessary.) let space = self.ctx.spaces[self.space.index]; - let start = space.start(); - let padded_size = { - let mut used_size = self.space.size; - - let usable = space.usable(); - if space.expansion.horizontal { - used_size.width = usable.width; + let layout_size = { + let mut used_size = self.space.used; + if self.ctx.expand.horizontal { + used_size.width = space.size.width; } - if space.expansion.vertical { - used_size.height = usable.height; + if self.ctx.expand.vertical { + used_size.height = space.size.height; } - used_size }; - let unpadded_size = padded_size - space.insets.size(); - let mut layout = BoxLayout::new(unpadded_size); + let mut layout = BoxLayout::new(layout_size); // ------------------------------------------------------------------ // // Step 2: Forward pass. Create a bounding box for each layout in which @@ -233,10 +251,10 @@ impl StackLayouter { let mut bounds = vec![]; let mut bound = Rect { - x0: start.x, - y0: start.y, - x1: start.x + self.space.size.width, - y1: start.y + self.space.size.height, + x0: 0.0, + y0: 0.0, + x1: layout_size.width, + y1: layout_size.height, }; for (layout, _) in &self.space.layouts { @@ -294,7 +312,7 @@ impl StackLayouter { fn start_space(&mut self, index: usize, hard: bool) { let space = self.ctx.spaces[index]; - self.space = Space::new(index, hard, space.usable()); + self.space = Space::new(index, hard, space.size); } fn next_space(&self) -> usize { @@ -304,6 +322,7 @@ impl StackLayouter { /// A layout space composed of subspaces which can have different directions and /// alignments. +#[derive(Debug)] pub(super) struct Space { /// The index of this space in `ctx.spaces`. index: usize, @@ -311,50 +330,26 @@ pub(super) struct Space { hard: bool, /// The so-far accumulated layouts. layouts: Vec<(BoxLayout, Gen2)>, - /// The size of this space. + /// The full size of this space. size: Size, + /// The used size of this space. + used: Size, /// The remaining space. usable: Size, /// Which alignments for new boxes are still allowed. pub(super) allowed_align: GenAlign, - /// The spacing state. This influences how new spacing is handled, e.g. hard - /// spacing may override soft spacing. - last_spacing: LastSpacing, } impl Space { - fn new(index: usize, hard: bool, usable: Size) -> Self { + fn new(index: usize, hard: bool, size: Size) -> Self { Self { index, hard, layouts: vec![], - size: Size::ZERO, - usable, + size, + used: Size::ZERO, + usable: size, allowed_align: GenAlign::Start, - last_spacing: LastSpacing::Hard, - } - } -} - -/// The spacing kind of the most recently inserted item in a layouting process. -/// -/// Since the last inserted item may not be spacing at all, this can be `None`. -#[derive(Debug, Copy, Clone, PartialEq)] -pub(crate) enum LastSpacing { - /// The last item was hard spacing. - Hard, - /// The last item was soft spacing with the given width and level. - Soft(f64, u32), - /// The last item wasn't spacing. - None, -} - -impl LastSpacing { - /// The width of the soft space if this is a soft space or zero otherwise. - fn soft_or_zero(self) -> f64 { - match self { - LastSpacing::Soft(space, _) => space, - _ => 0.0, } } } diff --git a/src/layout/nodes/text.rs b/src/layout/nodes/text.rs new file mode 100644 index 000000000..b0c4a458c --- /dev/null +++ b/src/layout/nodes/text.rs @@ -0,0 +1,51 @@ +use std::fmt::{self, Debug, Formatter}; +use std::rc::Rc; + +use fontdock::{FallbackTree, FontVariant}; + +use super::*; +use crate::shaping; + +/// A text node. +#[derive(Clone, PartialEq)] +pub struct Text { + pub text: String, + pub size: f64, + pub dir: Dir, + pub fallback: Rc, + pub variant: FontVariant, + pub aligns: Gen2, +} + +#[async_trait(?Send)] +impl Layout for Text { + async fn layout( + &self, + ctx: &mut LayoutContext, + _constraints: LayoutConstraints, + ) -> Vec { + let mut loader = ctx.loader.borrow_mut(); + let boxed = shaping::shape( + &self.text, + self.size, + self.dir, + &mut loader, + &self.fallback, + self.variant, + ) + .await; + vec![LayoutItem::Box(boxed, self.aligns)] + } +} + +impl Debug for Text { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Text({})", self.text) + } +} + +impl From for LayoutNode { + fn from(text: Text) -> Self { + Self::Text(text) + } +} diff --git a/src/layout/primitive.rs b/src/layout/primitive.rs index f932b85b5..b641b5c79 100644 --- a/src/layout/primitive.rs +++ b/src/layout/primitive.rs @@ -2,7 +2,7 @@ use std::fmt::{self, Display, Formatter}; -use crate::geom::{Point, Size, Vec2}; +use crate::geom::{Insets, Linear, Point, Size, Vec2}; /// Generic access to a structure's components. pub trait Get { @@ -126,6 +126,11 @@ impl Gen2 { } } +impl Gen2 { + /// The instance that has both components set to zero. + pub const ZERO: Self = Self { main: 0.0, cross: 0.0 }; +} + impl Get for Gen2 { type Component = T; @@ -155,11 +160,6 @@ impl Switch for Gen2 { } } -impl Gen2 { - /// The instance that has both components set to zero. - pub const ZERO: Self = Self { main: 0.0, cross: 0.0 }; -} - /// A generic container with two components for the two specific axes. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] pub struct Spec2 { @@ -176,6 +176,26 @@ impl Spec2 { } } +impl Spec2 { + /// The instance that has both components set to zero. + pub const ZERO: Self = Self { horizontal: 0.0, vertical: 0.0 }; + + /// Convert to a 2D vector. + pub fn to_vec2(self) -> Vec2 { + Vec2::new(self.horizontal, self.vertical) + } + + /// Convert to a point. + pub fn to_point(self) -> Point { + Point::new(self.horizontal, self.vertical) + } + + /// Convert to a size. + pub fn to_size(self) -> Size { + Size::new(self.horizontal, self.vertical) + } +} + impl Get for Spec2 { type Component = T; @@ -205,26 +225,6 @@ impl Switch for Spec2 { } } -impl Spec2 { - /// The instance that has both components set to zero. - pub const ZERO: Self = Self { horizontal: 0.0, vertical: 0.0 }; - - /// Convert to a 2D vector. - pub fn to_vec2(self) -> Vec2 { - Vec2::new(self.horizontal, self.vertical) - } - - /// Convert to a point. - pub fn to_point(self) -> Point { - Point::new(self.horizontal, self.vertical) - } - - /// Convert to a size. - pub fn to_size(self) -> Size { - Size::new(self.horizontal, self.vertical) - } -} - /// The two generic layouting axes. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum GenAxis { @@ -444,6 +444,18 @@ impl Sides { } } +impl Sides { + /// The absolute insets. + pub fn insets(self, Size { width, height }: Size) -> Insets { + Insets { + x0: -self.left.eval(width), + y0: -self.top.eval(height), + x1: -self.right.eval(width), + y1: -self.bottom.eval(height), + } + } +} + impl Get for Sides { type Component = T; diff --git a/src/layout/tree.rs b/src/layout/tree.rs deleted file mode 100644 index 4e53cedca..000000000 --- a/src/layout/tree.rs +++ /dev/null @@ -1,234 +0,0 @@ -//! Layouting of syntax trees. - -use fontdock::FontStyle; - -use super::*; -use crate::eval::Eval; -use crate::shaping; -use crate::syntax::*; -use crate::DynFuture; - -/// Layout a syntax tree in a given context. -pub async fn layout_tree(tree: &SynTree, ctx: &mut LayoutContext) -> Vec { - let mut layouter = TreeLayouter::new(ctx); - layouter.layout_tree(tree).await; - layouter.finish() -} - -/// Layouts trees. -struct TreeLayouter<'a> { - ctx: &'a mut LayoutContext, - constraints: LayoutConstraints, - layouter: LineLayouter, -} - -impl<'a> TreeLayouter<'a> { - fn new(ctx: &'a mut LayoutContext) -> Self { - let layouter = LineLayouter::new(LineContext { - spaces: ctx.constraints.spaces.clone(), - dirs: ctx.state.dirs, - repeat: ctx.constraints.repeat, - line_spacing: ctx.state.text.line_spacing(), - }); - - Self { - layouter, - constraints: ctx.constraints.clone(), - ctx, - } - } - - fn finish(self) -> Vec { - self.layouter.finish() - } - - fn layout_tree<'t>(&'t mut self, tree: &'t SynTree) -> DynFuture<'t, ()> { - Box::pin(async move { - for node in tree { - self.layout_node(node).await; - } - }) - } - - async fn layout_node(&mut self, node: &Spanned) { - let decorate = |this: &mut Self, deco: Deco| { - this.ctx.f.decos.push(deco.span_with(node.span)); - }; - - match &node.v { - SynNode::Space => self.layout_space(), - SynNode::Text(text) => { - if self.ctx.state.text.emph { - decorate(self, Deco::Emph); - } - if self.ctx.state.text.strong { - decorate(self, Deco::Strong); - } - self.layout_text(text).await; - } - - SynNode::Linebreak => self.layouter.finish_line(), - SynNode::Parbreak => self.layout_parbreak(), - SynNode::Emph => { - self.ctx.state.text.emph ^= true; - decorate(self, Deco::Emph); - } - SynNode::Strong => { - self.ctx.state.text.strong ^= true; - decorate(self, Deco::Strong); - } - - SynNode::Heading(heading) => self.layout_heading(heading).await, - SynNode::Raw(raw) => self.layout_raw(raw).await, - - SynNode::Expr(expr) => { - self.layout_expr(expr.span_with(node.span)).await; - } - } - } - - fn layout_space(&mut self) { - self.layouter - .add_cross_spacing(self.ctx.state.text.word_spacing(), SpacingKind::WORD); - } - - fn layout_parbreak(&mut self) { - self.layouter - .add_main_spacing(self.ctx.state.text.par_spacing(), SpacingKind::PARAGRAPH); - } - - async fn layout_text(&mut self, text: &str) { - let mut variant = self.ctx.state.text.variant; - - if self.ctx.state.text.strong { - variant.weight = variant.weight.thicken(300); - } - - if self.ctx.state.text.emph { - variant.style = match variant.style { - FontStyle::Normal => FontStyle::Italic, - FontStyle::Italic => FontStyle::Normal, - FontStyle::Oblique => FontStyle::Normal, - } - } - - let boxed = shaping::shape( - text, - self.ctx.state.text.font_size(), - self.ctx.state.dirs.cross, - &mut self.ctx.loader.borrow_mut(), - &self.ctx.state.text.fallback, - variant, - ) - .await; - - self.layouter.add(boxed, self.ctx.state.aligns); - } - - async fn layout_heading(&mut self, heading: &NodeHeading) { - let style = self.ctx.state.text.clone(); - - let factor = 1.5 - 0.1 * heading.level.v as f64; - self.ctx.state.text.font_size.scale *= factor; - self.ctx.state.text.strong = true; - - self.layout_parbreak(); - self.layout_tree(&heading.contents).await; - self.layout_parbreak(); - - self.ctx.state.text = style; - } - - async fn layout_raw(&mut self, raw: &NodeRaw) { - if !raw.inline { - self.layout_parbreak(); - } - - // TODO: Make this more efficient. - let fallback = self.ctx.state.text.fallback.clone(); - self.ctx.state.text.fallback.list.insert(0, "monospace".to_string()); - self.ctx.state.text.fallback.flatten(); - - let mut first = true; - for line in &raw.lines { - if !first { - self.layouter.finish_line(); - } - first = false; - self.layout_text(line).await; - } - - self.ctx.state.text.fallback = fallback; - - if !raw.inline { - self.layout_parbreak(); - } - } - - async fn layout_expr(&mut self, expr: Spanned<&Expr>) { - self.ctx.constraints = LayoutConstraints { - root: false, - base: self.constraints.base, - spaces: self.layouter.remaining(), - repeat: self.constraints.repeat, - }; - - let val = expr.v.eval(self.ctx).await; - let commands = val.span_with(expr.span).into_commands(); - for command in commands { - self.execute_command(command, expr.span).await; - } - } - - async fn execute_command(&mut self, command: Command, span: Span) { - use Command::*; - match command { - LayoutSyntaxTree(tree) => self.layout_tree(&tree).await, - - Add(layout, aligns) => self.layouter.add(layout, aligns), - AddSpacing(space, kind, axis) => match axis { - GenAxis::Main => self.layouter.add_main_spacing(space, kind), - GenAxis::Cross => self.layouter.add_cross_spacing(space, kind), - }, - - BreakLine => self.layouter.finish_line(), - BreakPage => { - if self.constraints.root { - self.layouter.finish_space(true) - } else { - self.ctx.diag(error!( - span, - "page break can only be issued from root context", - )); - } - } - - SetTextState(style) => { - self.layouter.set_line_spacing(style.line_spacing()); - self.ctx.state.text = style; - } - SetPageState(style) => { - if self.constraints.root { - self.ctx.state.page = style; - - // The line layouter has no idea of page styles and thus we - // need to recompute the layouting space resulting of the - // new page style and update it within the layouter. - let space = LayoutSpace { - size: style.size, - insets: style.insets(), - expansion: Spec2::new(true, true), - }; - self.constraints.base = space.usable(); - self.layouter.set_spaces(vec![space], true); - } else { - self.ctx.diag(error!( - span, - "page style can only be changed from root context", - )); - } - } - SetAlignment(aligns) => self.ctx.state.aligns = aligns, - } - } -} diff --git a/src/lib.rs b/src/lib.rs index ef81a8ae9..6380b9292 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,25 +3,33 @@ //! # Steps //! - **Parsing:** The parsing step first transforms a plain string into an //! [iterator of tokens][tokens]. This token stream is [parsed] into a [syntax -//! tree]. The structures describing the tree can be found in the [ast] +//! tree]. The structures describing the tree can be found in the [AST] //! module. -//! - **Layouting:** The next step is to transform the syntax tree into a -//! portable representation of the typesetted document. The final output -//! consists of a vector of [`BoxLayouts`] (corresponding to pages), ready for -//! exporting. -//! - **Exporting:** The finished layout can then be exported into a supported +//! - **Evaluation:** The next step is to [evaluate] the parsed "script" to a +//! [document], a high-level, fully styled representation. The [nodes] of the +//! document tree are fully self-contained and order-independent and thus much +//! better suited for layouting than the syntax tree. +//! - **Layouting:** The next step is to [layout] the document into a portable +//! version of the typesetted document. The output of this is a vector of +//! [`BoxLayouts`] (corresponding to pages), ready for exporting. +//! - **Exporting:** The finished layout can be exported into a supported //! format. Submodules for these formats are located in the [export] module. //! Currently, the only supported output format is [_PDF_]. //! -//! [tokens]: parsing/struct.Tokens.html -//! [parsed]: parsing/fn.parse.html +//! [tokens]: parse/struct.Tokens.html +//! [parsed]: parse/fn.parse.html //! [syntax tree]: syntax/ast/type.SynTree.html -//! [ast]: syntax/ast/index.html -//! [layout]: layout/index.html +//! [AST]: syntax/ast/index.html +//! [evaluate]: eval/fn.eval.html +//! [document]: layout/nodes/struct.Document.html +//! [nodes]: layout/nodes/index.html +//! [layout]: layout/fn.layout.html //! [`BoxLayouts`]: layout/struct.BoxLayout.html //! [export]: export/index.html //! [_PDF_]: export/pdf/index.html +#![allow(unused)] + #[macro_use] pub mod diag; @@ -55,10 +63,10 @@ pub async fn typeset( state: State, loader: SharedFontLoader, ) -> Pass> { - let parsed = parse::parse(src); - let layouted = layout::layout(&parsed.output, state, loader).await; - let feedback = Feedback::join(parsed.feedback, layouted.feedback); - Pass::new(layouted.output, feedback) + let Pass { output: tree, feedback: f1 } = parse::parse(src); + let Pass { output: document, feedback: f2 } = eval::eval(&tree, state); + let layouts = layout::layout(&document, loader).await; + Pass::new(layouts, Feedback::join(f1, f2)) } /// A dynamic future type which allows recursive invocation of async functions diff --git a/src/library/align.rs b/src/library/align.rs index f3280065c..acd3a85c0 100644 --- a/src/library/align.rs +++ b/src/library/align.rs @@ -14,7 +14,9 @@ use crate::prelude::*; /// - `vertical`: Any of `top`, `bottom` or `center`. /// /// There may not be two alignment specifications for the same axis. -pub async fn align(mut args: Args, ctx: &mut LayoutContext) -> Value { +pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value { + let snapshot = ctx.state.clone(); + let body = args.find::(); let first = args.get::<_, Spanned>(ctx, 0); let second = args.get::<_, Spanned>(ctx, 1); @@ -29,21 +31,25 @@ pub async fn align(mut args: Args, ctx: &mut LayoutContext) -> Value { .chain(hor.into_iter().map(|align| (Some(SpecAxis::Horizontal), align))) .chain(ver.into_iter().map(|align| (Some(SpecAxis::Vertical), align))); - let aligns = dedup_aligns(ctx, iter); + let prev_main = ctx.state.aligns.main; + ctx.state.aligns = dedup_aligns(ctx, iter); - Value::Commands(match body { - Some(tree) => vec![ - SetAlignment(aligns), - LayoutSyntaxTree(tree), - SetAlignment(ctx.state.aligns), - ], - None => vec![SetAlignment(aligns)], - }) + if prev_main != ctx.state.aligns.main { + ctx.end_par_group(); + ctx.start_par_group(); + } + + if let Some(body) = body { + body.eval(ctx); + ctx.state = snapshot; + } + + Value::None } /// Deduplicate alignments and deduce to which axes they apply. fn dedup_aligns( - ctx: &mut LayoutContext, + ctx: &mut EvalContext, iter: impl Iterator, Spanned)>, ) -> Gen2 { let mut aligns = ctx.state.aligns; diff --git a/src/library/boxed.rs b/src/library/boxed.rs index b88f5b7ce..6edb3b174 100644 --- a/src/library/boxed.rs +++ b/src/library/boxed.rs @@ -1,4 +1,5 @@ use crate::geom::Linear; +use crate::layout::nodes::{Fixed, Stack}; use crate::prelude::*; /// `box`: Layouts its contents into a box. @@ -6,34 +7,37 @@ use crate::prelude::*; /// # Keyword arguments /// - `width`: The width of the box (length or relative to parent's width). /// - `height`: The height of the box (length or relative to parent's height). -pub async fn boxed(mut args: Args, ctx: &mut LayoutContext) -> Value { +pub fn boxed(mut args: Args, ctx: &mut EvalContext) -> Value { let body = args.find::().unwrap_or_default(); let width = args.get::<_, Linear>(ctx, "width"); let height = args.get::<_, Linear>(ctx, "height"); args.done(ctx); + let dirs = ctx.state.dirs; let aligns = ctx.state.aligns; - let constraints = &mut ctx.constraints; - constraints.base = constraints.spaces[0].size; - constraints.spaces.truncate(1); - constraints.repeat = false; - if let Some(width) = width { - let abs = width.eval(constraints.base.width); - constraints.base.width = abs; - constraints.spaces[0].size.width = abs; - constraints.spaces[0].expansion.horizontal = true; - } + let snapshot = ctx.state.clone(); - if let Some(height) = height { - let abs = height.eval(constraints.base.height); - constraints.base.height = abs; - constraints.spaces[0].size.height = abs; - constraints.spaces[0].expansion.vertical = true; - } + ctx.start_group(()); + ctx.start_par_group(); - let layouted = layout_tree(&body, ctx).await; - let layout = layouted.into_iter().next().unwrap(); + body.eval(ctx); - Value::Commands(vec![Add(layout, aligns)]) + ctx.end_par_group(); + let ((), children) = ctx.end_group(); + + ctx.push(Fixed { + width, + height, + child: LayoutNode::dynamic(Stack { + dirs, + children, + aligns, + expand: Spec2::new(width.is_some(), height.is_some()), + }), + }); + + ctx.state = snapshot; + + Value::None } diff --git a/src/library/color.rs b/src/library/color.rs index 261352bab..17c33806f 100644 --- a/src/library/color.rs +++ b/src/library/color.rs @@ -2,7 +2,7 @@ use crate::color::RgbaColor; use crate::prelude::*; /// `rgb`: Create an RGB(A) color. -pub async fn rgb(mut args: Args, ctx: &mut LayoutContext) -> Value { +pub fn rgb(mut args: Args, ctx: &mut EvalContext) -> Value { let r = args.need::<_, Spanned>(ctx, 0, "red value"); let g = args.need::<_, Spanned>(ctx, 1, "green value"); let b = args.need::<_, Spanned>(ctx, 2, "blue value"); diff --git a/src/library/font.rs b/src/library/font.rs index 21fb2d13a..be6823c30 100644 --- a/src/library/font.rs +++ b/src/library/font.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use fontdock::{FontStretch, FontStyle, FontWeight}; use crate::eval::StringLike; @@ -49,37 +51,38 @@ use crate::prelude::*; /// ```typst /// [font: "My Serif", serif] /// ``` -pub async fn font(mut args: Args, ctx: &mut LayoutContext) -> Value { - let mut text = ctx.state.text.clone(); - let mut needs_flattening = false; +pub fn font(mut args: Args, ctx: &mut EvalContext) -> Value { + let snapshot = ctx.state.clone(); let body = args.find::(); if let Some(linear) = args.find::() { if linear.rel == 0.0 { - text.font_size.base = linear.abs; - text.font_size.scale = Linear::rel(1.0); + ctx.state.text.font_size.base = linear.abs; + ctx.state.text.font_size.scale = Linear::rel(1.0); } else { - text.font_size.scale = linear; + ctx.state.text.font_size.scale = linear; } } + let mut needs_flattening = false; let list: Vec<_> = args.find_all::().map(|s| s.to_lowercase()).collect(); + if !list.is_empty() { - text.fallback.list = list; + Rc::make_mut(&mut ctx.state.text.fallback).list = list; needs_flattening = true; } if let Some(style) = args.get::<_, FontStyle>(ctx, "style") { - text.variant.style = style; + ctx.state.text.variant.style = style; } if let Some(weight) = args.get::<_, FontWeight>(ctx, "weight") { - text.variant.weight = weight; + ctx.state.text.variant.weight = weight; } if let Some(stretch) = args.get::<_, FontStretch>(ctx, "stretch") { - text.variant.stretch = stretch; + ctx.state.text.variant.stretch = stretch; } for (class, dict) in args.find_all_str::>() { @@ -88,22 +91,20 @@ pub async fn font(mut args: Args, ctx: &mut LayoutContext) -> Value { .map(|s| s.to_lowercase()) .collect(); - text.fallback.update_class_list(class, fallback); + Rc::make_mut(&mut ctx.state.text.fallback).update_class_list(class, fallback); needs_flattening = true; } args.done(ctx); if needs_flattening { - text.fallback.flatten(); + Rc::make_mut(&mut ctx.state.text.fallback).flatten(); } - Value::Commands(match body { - Some(tree) => vec![ - SetTextState(text), - LayoutSyntaxTree(tree), - SetTextState(ctx.state.text.clone()), - ], - None => vec![SetTextState(text)], - }) + if let Some(body) = body { + body.eval(ctx); + ctx.state = snapshot; + } + + Value::None } diff --git a/src/library/mod.rs b/src/library/mod.rs index 191a3920b..af23d050e 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -21,7 +21,7 @@ macro_rules! std { /// Create a scope with all standard library functions. pub fn _std() -> Scope { let mut std = Scope::new(); - $(std.set($name, ValueFunc::new(|args, ctx| Box::pin($func(args, ctx))));)* + $(std.set($name, ValueFunc::new($func));)* std } }; diff --git a/src/library/page.rs b/src/library/page.rs index fd33039cf..448932a5a 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -19,54 +19,61 @@ use crate::prelude::*; /// - `top`: The top margin (length or relative to height). /// - `bottom`: The bottom margin (length or relative to height). /// - `flip`: Flips custom or paper-defined width and height (boolean). -pub async fn page(mut args: Args, ctx: &mut LayoutContext) -> Value { - let mut page = ctx.state.page.clone(); +pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value { + let snapshot = ctx.state.clone(); if let Some(paper) = args.find::() { - page.class = paper.class; - page.size = paper.size(); + ctx.state.page.class = paper.class; + ctx.state.page.size = paper.size(); } if let Some(Absolute(width)) = args.get::<_, Absolute>(ctx, "width") { - page.class = PaperClass::Custom; - page.size.width = width; + ctx.state.page.class = PaperClass::Custom; + ctx.state.page.size.width = width; } if let Some(Absolute(height)) = args.get::<_, Absolute>(ctx, "height") { - page.class = PaperClass::Custom; - page.size.height = height; + ctx.state.page.class = PaperClass::Custom; + ctx.state.page.size.height = height; } if let Some(margins) = args.get::<_, Linear>(ctx, "margins") { - page.margins = Sides::uniform(Some(margins)); + ctx.state.page.margins = Sides::uniform(Some(margins)); } if let Some(left) = args.get::<_, Linear>(ctx, "left") { - page.margins.left = Some(left); + ctx.state.page.margins.left = Some(left); } if let Some(top) = args.get::<_, Linear>(ctx, "top") { - page.margins.top = Some(top); + ctx.state.page.margins.top = Some(top); } if let Some(right) = args.get::<_, Linear>(ctx, "right") { - page.margins.right = Some(right); + ctx.state.page.margins.right = Some(right); } if let Some(bottom) = args.get::<_, Linear>(ctx, "bottom") { - page.margins.bottom = Some(bottom); + ctx.state.page.margins.bottom = Some(bottom); } if args.get::<_, bool>(ctx, "flip").unwrap_or(false) { - mem::swap(&mut page.size.width, &mut page.size.height); + let size = &mut ctx.state.page.size; + mem::swap(&mut size.width, &mut size.height); } args.done(ctx); - Value::Commands(vec![SetPageState(page)]) + + ctx.end_page_group(); + ctx.start_page_group(false); + + Value::None } -/// `pagebreak`: Ends the current page. -pub async fn pagebreak(args: Args, ctx: &mut LayoutContext) -> Value { +/// `pagebreak`: Starts a new page. +pub fn pagebreak(mut args: Args, ctx: &mut EvalContext) -> Value { args.done(ctx); - Value::Commands(vec![BreakPage]) + ctx.end_page_group(); + ctx.start_page_group(true); + Value::None } diff --git a/src/library/spacing.rs b/src/library/spacing.rs index e254d5e41..6d00bd1c4 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -1,12 +1,12 @@ use crate::geom::Linear; -use crate::layout::SpacingKind; +use crate::layout::nodes::{Softness, Spacing}; use crate::prelude::*; /// `h`: Add horizontal spacing. /// /// # Positional arguments /// - The spacing (length or relative to font size). -pub async fn h(args: Args, ctx: &mut LayoutContext) -> Value { +pub fn h(args: Args, ctx: &mut EvalContext) -> Value { spacing(args, ctx, SpecAxis::Horizontal) } @@ -14,19 +14,26 @@ pub async fn h(args: Args, ctx: &mut LayoutContext) -> Value { /// /// # Positional arguments /// - The spacing (length or relative to font size). -pub async fn v(args: Args, ctx: &mut LayoutContext) -> Value { +pub fn v(args: Args, ctx: &mut EvalContext) -> Value { spacing(args, ctx, SpecAxis::Vertical) } -fn spacing(mut args: Args, ctx: &mut LayoutContext, axis: SpecAxis) -> Value { +/// Apply spacing along a specific axis. +fn spacing(mut args: Args, ctx: &mut EvalContext, axis: SpecAxis) -> Value { let spacing = args.need::<_, Linear>(ctx, 0, "spacing"); args.done(ctx); - Value::Commands(if let Some(spacing) = spacing { - let spacing = spacing.eval(ctx.state.text.font_size()); - let axis = axis.switch(ctx.state.dirs); - vec![AddSpacing(spacing, SpacingKind::Hard, axis)] - } else { - vec![] - }) + if let Some(linear) = spacing { + let amount = linear.eval(ctx.state.text.font_size()); + let spacing = Spacing { amount, softness: Softness::Hard }; + if ctx.state.dirs.main.axis() == axis { + ctx.end_par_group(); + ctx.push(spacing); + ctx.start_par_group(); + } else { + ctx.push(spacing); + } + } + + Value::None } diff --git a/src/prelude.rs b/src/prelude.rs index 7cca0a5c0..3d77263b2 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,11 +1,9 @@ //! A prelude for building custom functions. #[doc(no_inline)] -pub use crate::eval::{Args, Dict, Value, ValueDict}; -#[doc(no_inline)] -pub use crate::layout::{layout_tree, primitive::*, Command, LayoutContext}; +pub use crate::eval::{Args, Dict, Eval, EvalContext, Value, ValueDict}; +pub use crate::layout::nodes::LayoutNode; +pub use crate::layout::primitive::*; #[doc(no_inline)] pub use crate::syntax::{Span, Spanned, SynTree}; pub use crate::{Feedback, Pass}; - -pub use Command::*;