summaryrefslogtreecommitdiff
path: root/src
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
parentca1256c924f3672feb76dbc2bc2e309eb4fc4cf5 (diff)
Evaluation and node-based layouting 🚀
Diffstat (limited to 'src')
-rw-r--r--src/diag.rs4
-rw-r--r--src/eval/args.rs9
-rw-r--r--src/eval/mod.rs376
-rw-r--r--src/eval/state.rs23
-rw-r--r--src/eval/value.rs71
-rw-r--r--src/layout/mod.rs234
-rw-r--r--src/layout/nodes/document.rs52
-rw-r--r--src/layout/nodes/fixed.rs42
-rw-r--r--src/layout/nodes/mod.rs167
-rw-r--r--src/layout/nodes/pad.rs53
-rw-r--r--src/layout/nodes/par.rs (renamed from src/layout/line.rs)157
-rw-r--r--src/layout/nodes/spacing.rs51
-rw-r--r--src/layout/nodes/stack.rs (renamed from src/layout/stack.rs)257
-rw-r--r--src/layout/nodes/text.rs51
-rw-r--r--src/layout/primitive.rs64
-rw-r--r--src/layout/tree.rs234
-rw-r--r--src/lib.rs36
-rw-r--r--src/library/align.rs28
-rw-r--r--src/library/boxed.rs52
-rw-r--r--src/library/color.rs2
-rw-r--r--src/library/font.rs41
-rw-r--r--src/library/mod.rs2
-rw-r--r--src/library/page.rs43
-rw-r--r--src/library/spacing.rs29
-rw-r--r--src/prelude.rs8
25 files changed, 1256 insertions, 830 deletions
diff --git a/src/diag.rs b/src/diag.rs
index 801dc6a3..8ad41b13 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 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<T>
+ pub fn get<'a, K, T>(&mut self, ctx: &mut EvalContext, key: K) -> Option<T>
where
K: Into<RefKey<'a>>,
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<T>
@@ -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<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.
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<FallbackTree>,
/// 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<Linear> {
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<Command>),
/// 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<Value> {
- /// 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<Command> {
- 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("<error>"),
}
}
@@ -157,9 +136,9 @@ pub type ValueDict = Dict<SpannedEntry<Value>>;
/// 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<SpannedEntry<Value>>;
pub struct ValueFunc(pub Rc<Func>);
/// The signature of executable functions.
-pub type Func = dyn Fn(Args, &mut LayoutContext) -> DynFuture<Value>;
+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: 'static>(f: F) -> Self
+ pub fn new<F>(f: F) -> Self
where
- F: Fn(Args, &mut LayoutContext) -> DynFuture<Value>,
+ F: Fn(Args, &mut EvalContext) -> Value + 'static,
{
Self(Rc::new(f))
}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 912ca010..f709da1a 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -1,88 +1,24 @@
-//! 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};
-
-/// Layout a syntax tree and return the produced layout.
-pub async fn layout(
- tree: &SynTree,
- state: State,
- loader: SharedFontLoader,
-) -> Pass<Vec<BoxLayout>> {
- let space = LayoutSpace {
- size: state.page.size,
- insets: state.page.insets(),
- expansion: Spec2::new(true, true),
- };
-
- let constraints = LayoutConstraints {
- root: true,
- base: space.usable(),
- spaces: vec![space],
- repeat: true,
- };
-
- let mut ctx = LayoutContext {
- loader,
- state,
- constraints,
- f: Feedback::new(),
- };
-
- let layouts = layout_tree(&tree, &mut ctx).await;
- Pass::new(layouts, ctx.f)
-}
-
-/// A finished box with content at fixed positions.
-#[derive(Debug, Clone, PartialEq)]
-pub struct BoxLayout {
- /// The size of the box.
- pub size: Size,
- /// The elements composing this layout.
- pub elements: Vec<(Point, LayoutElement)>,
-}
+use crate::syntax::SynTree;
-impl BoxLayout {
- /// Create a new empty collection.
- pub fn new(size: Size) -> Self {
- Self { size, elements: vec![] }
- }
+use nodes::Document;
- /// Add an element at a position.
- pub fn push(&mut self, pos: Point, element: LayoutElement) {
- self.elements.push((pos, element));
- }
-
- /// Add all elements of another collection, placing them relative to the
- /// given position.
- pub fn push_layout(&mut self, pos: Point, more: Self) {
- for (subpos, element) in more.elements {
- self.push(pos + subpos.to_vec2(), element);
- }
- }
-}
-
-/// A layout element, the basic building block layouts are composed of.
-#[derive(Debug, Clone, PartialEq)]
-pub enum LayoutElement {
- /// Shaped text.
- Text(Shaped),
+/// Layout a document and return the produced layouts.
+pub async fn layout(document: &Document, loader: SharedFontLoader) -> Vec<BoxLayout> {
+ let mut ctx = LayoutContext { loader };
+ document.layout(&mut ctx).await
}
/// The context for layouting.
@@ -90,33 +26,43 @@ pub enum LayoutElement {
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<Diag>) {
- self.f.diags.push(diag);
- }
+/// 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<LayoutItem>;
+ /// ```
+ async fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ constraints: LayoutConstraints,
+ ) -> Vec<LayoutItem>;
+}
- /// Add a decoration to the feedback.
- pub fn deco(&mut self, deco: Spanned<Deco>) {
- self.f.decos.push(deco);
- }
+/// 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<GenAlign>),
}
/// 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<LayoutSpace>,
/// Whether to spill over into copies of the last space or finish layouting
@@ -127,92 +73,44 @@ pub struct LayoutConstraints {
/// 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,
- /// 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<bool>,
}
-impl LayoutSpace {
- /// The position of the padded start in the space.
- pub fn start(&self) -> Point {
- Point::new(-self.insets.x0, -self.insets.y0)
+/// A finished box with content at fixed positions.
+#[derive(Debug, Clone, PartialEq)]
+pub struct BoxLayout {
+ /// The size of the box.
+ pub size: Size,
+ /// The elements composing this layout.
+ pub elements: Vec<(Point, LayoutElement)>,
+}
+
+impl BoxLayout {
+ /// Create a new empty collection.
+ pub fn new(size: Size) -> Self {
+ Self { size, elements: vec![] }
}
- /// The actually usable area (size minus padding).
- pub fn usable(&self) -> Size {
- self.size + self.insets.size()
+ /// Add an element at a position.
+ pub fn push(&mut self, pos: Point, element: LayoutElement) {
+ self.elements.push((pos, element));
}
- /// 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),
+ /// Add all elements of another collection, placing them relative to the
+ /// given position.
+ pub fn push_layout(&mut self, pos: Point, more: Self) {
+ for (subpos, element) in more.elements {
+ self.push(pos + subpos.to_vec2(), element);
}
}
}
-/// Commands executable by the layouting engine.
+/// A layout element, the basic building block layouts are composed of.
#[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<GenAlign>),
- /// 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<GenAlign>),
-}
-
-/// 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);
+pub enum LayoutElement {
+ /// Shaped text.
+ Text(Shaped),
}
diff --git a/src/layout/nodes/document.rs b/src/layout/nodes/document.rs
new file mode 100644
index 00000000..af7a31e6
--- /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<Pages>,
+}
+
+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<BoxLayout> {
+ 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<BoxLayout> {
+ 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 00000000..0d438879
--- /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<Linear>,
+ pub height: Option<Linear>,
+ pub child: LayoutNode,
+}
+
+#[async_trait(?Send)]
+impl Layout for Fixed {
+ async fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ constraints: LayoutConstraints,
+ ) -> Vec<LayoutItem> {
+ 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<Fixed> 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 00000000..44c18284
--- /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<T: DynNode>(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<LayoutItem> {
+ 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<dyn DynNode>);
+
+impl Dynamic {
+ /// Wrap a type implementing `DynNode`.
+ pub fn new<T: DynNode>(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<Dynamic> 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<dyn
+/// DynNode>` 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<dyn DynNode>;
+}
+
+impl<T> 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>() {
+ self == other
+ } else {
+ false
+ }
+ }
+
+ fn dyn_clone(&self) -> Box<dyn DynNode> {
+ Box::new(self.clone())
+ }
+}
+
+impl Clone for Box<dyn DynNode> {
+ fn clone(&self) -> Self {
+ self.dyn_clone()
+ }
+}
+
+impl PartialEq for Box<dyn DynNode> {
+ 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 00000000..10a9e2c6
--- /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<Linear>,
+ pub child: LayoutNode,
+}
+
+#[async_trait(?Send)]
+impl Layout for Pad {
+ async fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ constraints: LayoutConstraints,
+ ) -> Vec<LayoutItem> {
+ 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<Pad> for LayoutNode {
+ fn from(pad: Pad) -> Self {
+ Self::dynamic(pad)
+ }
+}
diff --git a/src/layout/line.rs b/src/layout/nodes/par.rs
index ae3bd969..38b11529 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<Dir>,
+ pub line_spacing: f64,
+ pub children: Vec<LayoutNode>,
+ pub aligns: Gen2<GenAlign>,
+ pub expand: Spec2<bool>,
+}
+
+#[async_trait(?Send)]
+impl Layout for Par {
+ async fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ constraints: LayoutConstraints,
+ ) -> Vec<LayoutItem> {
+ 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<Par> 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<Dir>,
+ dirs: Gen2<Dir>,
/// The spaces to layout into.
- pub spaces: Vec<LayoutSpace>,
+ spaces: Vec<LayoutSpace>,
/// 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<bool>,
}
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<GenAlign>) {
+ fn push_box(&mut self, layout: BoxLayout, aligns: Gen2<GenAlign>) {
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<LayoutSpace>, replace_empty: bool) {
+ fn set_spaces(&mut self, spaces: Vec<LayoutSpace>, 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<LayoutSpace> {
+ fn remaining(&self) -> Vec<LayoutSpace> {
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<BoxLayout> {
+ fn finish(mut self) -> Vec<BoxLayout> {
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<f64>,
- /// 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 00000000..66af0d17
--- /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<LayoutItem> {
+ 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<Spacing> 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
index cca2a315..983175b8 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<Dir>,
+ pub children: Vec<LayoutNode>,
+ pub aligns: Gen2<GenAlign>,
+ pub expand: Spec2<bool>,
+}
+
+#[async_trait(?Send)]
+impl Layout for Stack {
+ async fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ constraints: LayoutConstraints,
+ ) -> Vec<LayoutItem> {
+ 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<Stack> 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<BoxLayout>,
+ pub layouts: Vec<BoxLayout>,
/// 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<Dir>,
/// 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<bool>,
}
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<GenAlign>) {
+ pub fn push_box(&mut self, layout: BoxLayout, aligns: Gen2<GenAlign>) {
// 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;
-
- // 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);
- }
- }
- }
+ 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));
+
+ 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<f64>) {
- 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<LayoutSpace> {
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<GenAlign>)>,
- /// 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 00000000..b0c4a458
--- /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<FallbackTree>,
+ pub variant: FontVariant,
+ pub aligns: Gen2<GenAlign>,
+}
+
+#[async_trait(?Send)]
+impl Layout for Text {
+ async fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ _constraints: LayoutConstraints,
+ ) -> Vec<LayoutItem> {
+ 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<Text> for LayoutNode {
+ fn from(text: Text) -> Self {
+ Self::Text(text)
+ }
+}
diff --git a/src/layout/primitive.rs b/src/layout/primitive.rs
index f932b85b..b641b5c7 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<Index> {
@@ -126,6 +126,11 @@ impl<T> Gen2<T> {
}
}
+impl Gen2<f64> {
+ /// The instance that has both components set to zero.
+ pub const ZERO: Self = Self { main: 0.0, cross: 0.0 };
+}
+
impl<T> Get<GenAxis> for Gen2<T> {
type Component = T;
@@ -155,11 +160,6 @@ impl<T> Switch for Gen2<T> {
}
}
-impl Gen2<f64> {
- /// 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<T> {
@@ -176,6 +176,26 @@ impl<T> Spec2<T> {
}
}
+impl Spec2<f64> {
+ /// 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<T> Get<SpecAxis> for Spec2<T> {
type Component = T;
@@ -205,26 +225,6 @@ impl<T> Switch for Spec2<T> {
}
}
-impl Spec2<f64> {
- /// 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<T> Sides<T> {
}
}
+impl Sides<Linear> {
+ /// 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<T> Get<Side> for Sides<T> {
type Component = T;
diff --git a/src/layout/tree.rs b/src/layout/tree.rs
deleted file mode 100644
index 4e53cedc..00000000
--- 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<BoxLayout> {
- 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<BoxLayout> {
- 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<SynNode>) {
- 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 ef81a8ae..6380b929 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<Vec<BoxLayout>> {
- 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 f3280065..acd3a85c 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::<SynTree>();
let first = args.get::<_, Spanned<SpecAlign>>(ctx, 0);
let second = args.get::<_, Spanned<SpecAlign>>(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);
+
+ 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::Commands(match body {
- Some(tree) => vec![
- SetAlignment(aligns),
- LayoutSyntaxTree(tree),
- SetAlignment(ctx.state.aligns),
- ],
- None => vec![SetAlignment(aligns)],
- })
+ Value::None
}
/// Deduplicate alignments and deduce to which axes they apply.
fn dedup_aligns(
- ctx: &mut LayoutContext,
+ ctx: &mut EvalContext,
iter: impl Iterator<Item = (Option<SpecAxis>, Spanned<SpecAlign>)>,
) -> Gen2<GenAlign> {
let mut aligns = ctx.state.aligns;
diff --git a/src/library/boxed.rs b/src/library/boxed.rs
index b88f5b7c..6edb3b17 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::<SynTree>().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;
- }
-
- 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;
- }
-
- let layouted = layout_tree(&body, ctx).await;
- let layout = layouted.into_iter().next().unwrap();
-
- Value::Commands(vec![Add(layout, aligns)])
+
+ let snapshot = ctx.state.clone();
+
+ ctx.start_group(());
+ ctx.start_par_group();
+
+ body.eval(ctx);
+
+ 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 261352ba..17c33806 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<i64>>(ctx, 0, "red value");
let g = args.need::<_, Spanned<i64>>(ctx, 1, "green value");
let b = args.need::<_, Spanned<i64>>(ctx, 2, "blue value");
diff --git a/src/library/font.rs b/src/library/font.rs
index 21fb2d13..be6823c3 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::<SynTree>();
if let Some(linear) = args.find::<Linear>() {
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::<StringLike>().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::<Spanned<ValueDict>>() {
@@ -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();
+ }
+
+ if let Some(body) = body {
+ body.eval(ctx);
+ ctx.state = snapshot;
}
- Value::Commands(match body {
- Some(tree) => vec![
- SetTextState(text),
- LayoutSyntaxTree(tree),
- SetTextState(ctx.state.text.clone()),
- ],
- None => vec![SetTextState(text)],
- })
+ Value::None
}
diff --git a/src/library/mod.rs b/src/library/mod.rs
index 191a3920..af23d050 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 fd33039c..448932a5 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::<Paper>() {
- 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 e254d5e4..6d00bd1c 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 7cca0a5c..3d77263b 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::*;