From 537545e7f8351d7677c396456e46568f5a5e2a7a Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 7 Oct 2020 17:07:44 +0200 Subject: =?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/eval/args.rs | 9 +- src/eval/mod.rs | 376 +++++++++++++++++++++++++++++++++++++++++++++++++----- src/eval/state.rs | 23 ++-- src/eval/value.rs | 71 ++++------- 4 files changed, 383 insertions(+), 96 deletions(-) (limited to 'src/eval') diff --git a/src/eval/args.rs b/src/eval/args.rs index d11deac6..04f83b50 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 6f882ab8..101d26d9 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 async_trait::async_trait; +use std::any::Any; +use std::mem; +use std::rc::Rc; -use crate::layout::LayoutContext; +use async_trait::async_trait; +use fontdock::FontStyle; + +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; +} + +impl Eval for SynTree { + type Output = (); + + 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; + } } -#[async_trait(?Send)] impl Eval for Expr { type Output = Value; - async fn eval(&self, ctx: &mut LayoutContext) -> Self::Output { + fn eval(&self, ctx: &mut EvalContext) -> 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, + 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 295a106c..5861ada1 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 85cb261c..37fd7ead 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 Default for Value { - fn default() -> Self { - Value::None - } -} +impl Eval for Value { + type Output = (); -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 { + /// Evaluate everything contained in this value. + fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + match self { // Don't print out none values. - Value::None => vec![], + Value::None => {} - // Pass-through. - Value::Commands(commands) => commands, - Value::Content(tree) => vec![Command::LayoutSyntaxTree(tree)], + // Pass through. + Value::Content(tree) => tree.eval(ctx), - // Forward to each entry, separated with spaces. + // Forward to each dictionary entry. 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()); + for entry in dict.values() { + entry.value.v.eval(ctx); } - commands } // Format with debug. - val => { - let fmt = format!("{:?}", val); - let tree = vec![SynNode::Text(fmt).span_with(self.span)]; - vec![Command::LayoutSyntaxTree(tree)] - } + val => ctx.push(ctx.make_text_node(format!("{:?}", val))), } } } +impl Default for Value { + fn default() -> Self { + Value::None + } +} + 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)) } -- cgit v1.2.3