summaryrefslogtreecommitdiff
path: root/src/eval/mod.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2020-10-07 17:07:44 +0200
committerLaurenz <laurmaedje@gmail.com>2020-10-07 17:07:44 +0200
commit537545e7f8351d7677c396456e46568f5a5e2a7a (patch)
treef4c7614293246db06c7fa7496458da01b15c3b84 /src/eval/mod.rs
parentca1256c924f3672feb76dbc2bc2e309eb4fc4cf5 (diff)
Evaluation and node-based layouting 🚀
Diffstat (limited to 'src/eval/mod.rs')
-rw-r--r--src/eval/mod.rs376
1 files changed, 342 insertions, 34 deletions
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<Document> {
+ 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<Pages>,
+ /// 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<dyn Any>, Vec<LayoutNode>)>,
+ /// The nodes in the current innermost group
+ /// (whose metadata is in `groups.last()`).
+ inner: Vec<LayoutNode>,
+}
+
+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<Document> {
+ 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<Diag>) {
+ self.f.diags.push(diag);
+ }
+
+ /// Add a decoration to the feedback.
+ pub fn deco(&mut self, deco: Spanned<Deco>) {
+ 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<LayoutNode>) {
+ 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<T: 'static>(&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<T: 'static>(&mut self) -> (T, Vec<LayoutNode>) {
+ let (any, outer) = self.groups.pop().expect("no pushed group");
+ let group = *any.downcast::<T>().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.