Evaluation and node-based layouting 🚀

This commit is contained in:
Laurenz 2020-10-07 17:07:44 +02:00
parent ca1256c924
commit 537545e7f8
25 changed files with 1260 additions and 834 deletions

View File

@ -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)*) => {

View File

@ -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"));

View File

@ -14,42 +14,355 @@ pub use scope::*;
pub use state::*;
pub use value::*;
use std::any::Any;
use std::mem;
use std::rc::Rc;
use async_trait::async_trait;
use fontdock::FontStyle;
use crate::layout::LayoutContext;
use crate::diag::Diag;
use crate::geom::Size;
use crate::layout::nodes::{
Document, LayoutNode, Pad, Pages, Par, Softness, Spacing, Stack, Text,
};
use crate::layout::{Gen2, Spec2, Switch};
use crate::syntax::*;
use crate::{Feedback, Pass};
/// Evaluate an syntactic item into an output value.
/// Evaluate a syntax tree into a document.
///
/// The given `state` the base state that may be updated over the course of
/// evaluation.
pub fn eval(tree: &SynTree, state: State) -> Pass<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;
}
#[async_trait(?Send)]
impl Eval for Expr {
type Output = Value;
impl Eval for SynTree {
type Output = ();
async fn eval(&self, ctx: &mut LayoutContext) -> Self::Output {
match self {
Self::Lit(lit) => lit.eval(ctx).await,
Self::Call(call) => call.eval(ctx).await,
Self::Unary(unary) => unary.eval(ctx).await,
Self::Binary(binary) => binary.eval(ctx).await,
fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
for node in self {
node.v.eval(ctx);
}
}
}
impl Eval for SynNode {
type Output = ();
fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
match self {
SynNode::Space => {
ctx.push(Spacing {
amount: ctx.state.text.word_spacing(),
softness: Softness::Soft,
});
}
SynNode::Text(text) => {
let node = ctx.make_text_node(text.clone());
ctx.push(node);
}
SynNode::Linebreak => {
ctx.end_par_group();
ctx.start_par_group();
}
SynNode::Parbreak => {
ctx.end_par_group();
ctx.push(Spacing {
amount: ctx.state.text.par_spacing(),
softness: Softness::Soft,
});
ctx.start_par_group();
}
SynNode::Emph => {
ctx.state.text.emph ^= true;
}
SynNode::Strong => {
ctx.state.text.strong ^= true;
}
SynNode::Heading(heading) => {
heading.eval(ctx);
}
SynNode::Raw(raw) => {
raw.eval(ctx);
}
SynNode::Expr(expr) => {
let value = expr.eval(ctx);
value.eval(ctx);
}
}
}
}
impl Eval for NodeHeading {
type Output = ();
fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
let prev = ctx.state.clone();
let upscale = 1.5 - 0.1 * self.level.v as f64;
ctx.state.text.font_size.scale *= upscale;
ctx.state.text.strong = true;
self.contents.eval(ctx);
ctx.state = prev;
}
}
impl Eval for NodeRaw {
type Output = ();
fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
let prev = Rc::clone(&ctx.state.text.fallback);
let fallback = Rc::make_mut(&mut ctx.state.text.fallback);
fallback.list.insert(0, "monospace".to_string());
fallback.flatten();
let mut children = vec![];
for line in &self.lines {
children.push(LayoutNode::Text(ctx.make_text_node(line.clone())));
}
ctx.push(Stack {
dirs: ctx.state.dirs,
children,
aligns: ctx.state.aligns,
expand: Spec2::new(false, false),
});
ctx.state.text.fallback = prev;
}
}
impl Eval for Expr {
type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
match self {
Self::Lit(lit) => lit.eval(ctx),
Self::Call(call) => call.eval(ctx),
Self::Unary(unary) => unary.eval(ctx),
Self::Binary(binary) => binary.eval(ctx),
}
}
}
#[async_trait(?Send)]
impl Eval for Lit {
type Output = Value;
async fn eval(&self, ctx: &mut LayoutContext) -> Self::Output {
fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
match *self {
Lit::Ident(ref v) => Value::Ident(v.clone()),
Lit::Bool(v) => Value::Bool(v),
@ -59,20 +372,19 @@ impl Eval for Lit {
Lit::Percent(v) => Value::Relative(v / 100.0),
Lit::Color(v) => Value::Color(v),
Lit::Str(ref v) => Value::Str(v.clone()),
Lit::Dict(ref v) => Value::Dict(v.eval(ctx).await),
Lit::Dict(ref v) => Value::Dict(v.eval(ctx)),
Lit::Content(ref v) => Value::Content(v.clone()),
}
}
}
#[async_trait(?Send)]
impl Eval for LitDict {
type Output = ValueDict;
async fn eval(&self, ctx: &mut LayoutContext) -> Self::Output {
fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
let mut dict = ValueDict::new();
for entry in &self.0 {
let val = entry.expr.v.eval(ctx).await;
let val = entry.expr.v.eval(ctx);
let spanned = val.span_with(entry.expr.span);
if let Some(key) = &entry.key {
dict.insert(&key.v, SpannedEntry::new(key.span, spanned));
@ -85,19 +397,18 @@ impl Eval for LitDict {
}
}
#[async_trait(?Send)]
impl Eval for ExprCall {
type Output = Value;
async fn eval(&self, ctx: &mut LayoutContext) -> Self::Output {
fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
let name = &self.name.v;
let span = self.name.span;
let dict = self.args.v.eval(ctx).await;
let dict = self.args.v.eval(ctx);
if let Some(func) = ctx.state.scope.get(name) {
let args = Args(dict.span_with(self.args.span));
ctx.f.decos.push(Deco::Resolved.span_with(span));
(func.clone())(args, ctx).await
(func.clone())(args, ctx)
} else {
if !name.is_empty() {
ctx.diag(error!(span, "unknown function"));
@ -108,14 +419,13 @@ impl Eval for ExprCall {
}
}
#[async_trait(?Send)]
impl Eval for ExprUnary {
type Output = Value;
async fn eval(&self, ctx: &mut LayoutContext) -> Self::Output {
fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
use Value::*;
let value = self.expr.v.eval(ctx).await;
let value = self.expr.v.eval(ctx);
if value == Error {
return Error;
}
@ -127,13 +437,12 @@ impl Eval for ExprUnary {
}
}
#[async_trait(?Send)]
impl Eval for ExprBinary {
type Output = Value;
async fn eval(&self, ctx: &mut LayoutContext) -> Self::Output {
let lhs = self.lhs.v.eval(ctx).await;
let rhs = self.rhs.v.eval(ctx).await;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
let lhs = self.lhs.v.eval(ctx);
let rhs = self.rhs.v.eval(ctx);
if lhs == Value::Error || rhs == Value::Error {
return Value::Error;
@ -150,7 +459,7 @@ impl Eval for ExprBinary {
}
/// Compute the negation of a value.
fn neg(ctx: &mut LayoutContext, span: Span, value: Value) -> Value {
fn neg(ctx: &mut EvalContext, span: Span, value: Value) -> Value {
use Value::*;
match value {
Int(v) => Int(-v),
@ -166,7 +475,7 @@ fn neg(ctx: &mut LayoutContext, span: Span, value: Value) -> Value {
}
/// Compute the sum of two values.
fn add(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value {
fn add(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value {
use crate::geom::Linear as Lin;
use Value::*;
match (lhs, rhs) {
@ -193,7 +502,6 @@ fn add(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value {
(Str(a), Str(b)) => Str(a + &b),
(Dict(a), Dict(b)) => Dict(concat(a, b)),
(Content(a), Content(b)) => Content(concat(a, b)),
(Commands(a), Commands(b)) => Commands(concat(a, b)),
(a, b) => {
ctx.diag(error!(span, "cannot add {} and {}", a.ty(), b.ty()));
@ -203,7 +511,7 @@ fn add(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value {
}
/// Compute the difference of two values.
fn sub(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value {
fn sub(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value {
use crate::geom::Linear as Lin;
use Value::*;
match (lhs, rhs) {
@ -232,7 +540,7 @@ fn sub(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value {
}
/// Compute the product of two values.
fn mul(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value {
fn mul(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value {
use Value::*;
match (lhs, rhs) {
// Numbers with themselves.
@ -267,7 +575,7 @@ fn mul(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value {
}
/// Compute the quotient of two values.
fn div(ctx: &mut LayoutContext, span: Span, lhs: Value, rhs: Value) -> Value {
fn div(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value {
use Value::*;
match (lhs, rhs) {
// Numbers by themselves.

View File

@ -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),
}
}
}

View File

@ -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 Eval for Value {
type Output = ();
/// Evaluate everything contained in this value.
fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
match self {
// Don't print out none values.
Value::None => {}
// Pass through.
Value::Content(tree) => tree.eval(ctx),
// Forward to each dictionary entry.
Value::Dict(dict) => {
for entry in dict.values() {
entry.value.v.eval(ctx);
}
}
// Format with debug.
val => ctx.push(ctx.make_text_node(format!("{:?}", val))),
}
}
}
impl Default for Value {
fn default() -> Self {
Value::None
}
}
impl Spanned<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 {
// Don't print out none values.
Value::None => vec![],
// Pass-through.
Value::Commands(commands) => commands,
Value::Content(tree) => vec![Command::LayoutSyntaxTree(tree)],
// Forward to each entry, separated with spaces.
Value::Dict(dict) => {
let mut commands = vec![];
let mut end = None;
for entry in dict.into_values() {
if let Some(last_end) = end {
let span = Span::new(last_end, entry.key_span.start);
let tree = vec![SynNode::Space.span_with(span)];
commands.push(Command::LayoutSyntaxTree(tree));
}
end = Some(entry.value.span.end);
commands.extend(entry.value.into_commands());
}
commands
}
// Format with debug.
val => {
let fmt = format!("{:?}", val);
let tree = vec![SynNode::Text(fmt).span_with(self.span)];
vec![Command::LayoutSyntaxTree(tree)]
}
}
}
}
impl Debug for Value {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
@ -138,7 +118,6 @@ impl Debug for Value {
Self::Dict(v) => v.fmt(f),
Self::Content(v) => v.fmt(f),
Self::Func(v) => v.fmt(f),
Self::Commands(v) => v.fmt(f),
Self::Error => f.pad("<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))
}

View File

@ -1,52 +1,82 @@
//! Layouting of syntax trees.
//! Layouting of documents.
pub mod nodes;
pub mod primitive;
mod line;
mod stack;
mod tree;
pub use line::*;
pub use primitive::*;
pub use stack::*;
pub use tree::*;
use crate::diag::Diag;
use async_trait::async_trait;
use crate::eval::{PageState, State, TextState};
use crate::font::SharedFontLoader;
use crate::geom::{Insets, Point, Rect, Size, SizeExt};
use crate::shaping::Shaped;
use crate::syntax::{Deco, Spanned, SynTree};
use crate::{Feedback, Pass};
use crate::syntax::SynTree;
/// Layout a syntax tree and return the produced layout.
pub async fn layout(
tree: &SynTree,
state: State,
loader: SharedFontLoader,
) -> Pass<Vec<BoxLayout>> {
let space = LayoutSpace {
size: state.page.size,
insets: state.page.insets(),
expansion: Spec2::new(true, true),
};
use nodes::Document;
let constraints = LayoutConstraints {
root: true,
base: space.usable(),
spaces: vec![space],
repeat: true,
};
/// Layout a document and return the produced layouts.
pub async fn layout(document: &Document, loader: SharedFontLoader) -> Vec<BoxLayout> {
let mut ctx = LayoutContext { loader };
document.layout(&mut ctx).await
}
let mut ctx = LayoutContext {
loader,
state,
constraints,
f: Feedback::new(),
};
/// The context for layouting.
#[derive(Debug, Clone)]
pub struct LayoutContext {
/// The font loader to query fonts from when typesetting text.
pub loader: SharedFontLoader,
}
let layouts = layout_tree(&tree, &mut ctx).await;
Pass::new(layouts, ctx.f)
/// Layout a node.
#[async_trait(?Send)]
pub trait Layout {
/// Layout the node in the given layout context.
///
/// This signature looks pretty horrible due to async in trait methods, but
/// it's actually just the following:
/// ```rust,ignore
/// async fn layout(
/// &self,
/// ctx: &mut LayoutContext,
/// constraints: LayoutConstraints,
/// ) -> Vec<LayoutItem>;
/// ```
async fn layout(
&self,
ctx: &mut LayoutContext,
constraints: LayoutConstraints,
) -> Vec<LayoutItem>;
}
/// 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 {
/// The spaces to layout into.
pub 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,
}
/// The space into which content is laid out.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct LayoutSpace {
/// The full size of this container (the base for relative sizes).
pub base: Size,
/// The maximum size of the rectangle to layout into.
pub size: Size,
}
/// A finished box with content at fixed positions.
@ -84,135 +114,3 @@ pub enum LayoutElement {
/// Shaped text.
Text(Shaped),
}
/// The context for layouting.
#[derive(Debug, Clone)]
pub struct LayoutContext {
/// The font loader to query fonts from when typesetting text.
pub loader: SharedFontLoader,
/// The active state.
pub state: State,
/// The active constraints.
pub constraints: LayoutConstraints,
/// The accumulated feedback.
pub f: Feedback,
}
impl LayoutContext {
/// Add a diagnostic to the feedback.
pub fn diag(&mut self, diag: Spanned<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);
}
}
/// 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
/// when the last space is used up.
pub repeat: bool,
}
/// The space into which content is laid out.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct LayoutSpace {
/// The maximum size of the rectangle to layout into.
pub size: Size,
/// Padding that should be respected on each side.
pub insets: Insets,
/// Whether to expand the size of the resulting layout to the full size of
/// this space or to shrink it to fit the content.
pub expansion: Spec2<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)
}
/// The actually usable area (size minus padding).
pub fn usable(&self) -> Size {
self.size + self.insets.size()
}
/// The inner layout space with size reduced by the padding, zero padding of
/// its own and no layout expansion.
pub fn inner(&self) -> Self {
Self {
size: self.usable(),
insets: Insets::ZERO,
expansion: Spec2::new(false, false),
}
}
}
/// Commands executable by the layouting engine.
#[derive(Debug, Clone, PartialEq)]
pub enum Command {
/// Layout the given tree in the current context (i.e. not nested). The
/// content of the tree is not laid out into a separate box and then added,
/// but simply laid out flatly in the active layouting process.
///
/// This has the effect that the content fits nicely into the active line
/// layouting, enabling functions to e.g. change the style of some piece of
/// text while keeping it part of the current paragraph.
LayoutSyntaxTree(SynTree),
/// Add a finished layout.
Add(BoxLayout, Gen2<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);
}

View File

@ -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()
}
}

42
src/layout/nodes/fixed.rs Normal file
View File

@ -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)
}
}

167
src/layout/nodes/mod.rs Normal file
View File

@ -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())
}
}

53
src/layout/nodes/pad.rs Normal file
View File

@ -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)
}
}

View File

@ -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,
}
}
}

View File

@ -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,
}

View File

@ -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;
pub fn push_spacing(&mut self, mut spacing: f64) {
// Reduce the spacing such that it definitely fits.
let axis = self.ctx.dirs.main.axis();
spacing = spacing.min(self.space.usable.get(axis));
// Reduce the spacing such that it definitely fits.
let axis = self.ctx.dirs.main.axis();
spacing = spacing.min(self.usable().get(axis));
let size = Gen2::new(spacing, 0.0);
self.update_metrics(size);
self.space.layouts.push((
BoxLayout::new(size.switch(self.ctx.dirs).to_size()),
Gen2::default(),
));
}
// A soft space is cached if it is not consumed by a hard space or
// previous soft space with higher level.
SpacingKind::Soft(level) => {
let consumes = match self.space.last_spacing {
LastSpacing::None => true,
LastSpacing::Soft(_, prev) if level < prev => true,
_ => false,
};
if consumes {
self.space.last_spacing = LastSpacing::Soft(spacing, level);
}
}
}
let size = Gen2::new(spacing, 0.0);
self.update_metrics(size);
self.space.layouts.push((
BoxLayout::new(size.switch(self.ctx.dirs).to_size()),
Gen2::default(),
));
}
fn update_metrics(&mut self, added: Gen2<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,
}
}
}

51
src/layout/nodes/text.rs Normal file
View File

@ -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)
}
}

View File

@ -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;

View File

@ -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,
}
}
}

View File

@ -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

View File

@ -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);
Value::Commands(match body {
Some(tree) => vec![
SetAlignment(aligns),
LayoutSyntaxTree(tree),
SetAlignment(ctx.state.aligns),
],
None => vec![SetAlignment(aligns)],
})
if prev_main != ctx.state.aligns.main {
ctx.end_par_group();
ctx.start_par_group();
}
if let Some(body) = body {
body.eval(ctx);
ctx.state = snapshot;
}
Value::None
}
/// Deduplicate alignments and deduce to which axes they apply.
fn dedup_aligns(
ctx: &mut LayoutContext,
ctx: &mut EvalContext,
iter: impl Iterator<Item = (Option<SpecAxis>, Spanned<SpecAlign>)>,
) -> Gen2<GenAlign> {
let mut aligns = ctx.state.aligns;

View File

@ -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;
}
let snapshot = ctx.state.clone();
if let Some(height) = height {
let abs = height.eval(constraints.base.height);
constraints.base.height = abs;
constraints.spaces[0].size.height = abs;
constraints.spaces[0].expansion.vertical = true;
}
ctx.start_group(());
ctx.start_par_group();
let layouted = layout_tree(&body, ctx).await;
let layout = layouted.into_iter().next().unwrap();
body.eval(ctx);
Value::Commands(vec![Add(layout, aligns)])
ctx.end_par_group();
let ((), children) = ctx.end_group();
ctx.push(Fixed {
width,
height,
child: LayoutNode::dynamic(Stack {
dirs,
children,
aligns,
expand: Spec2::new(width.is_some(), height.is_some()),
}),
});
ctx.state = snapshot;
Value::None
}

View File

@ -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");

View File

@ -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();
}
Value::Commands(match body {
Some(tree) => vec![
SetTextState(text),
LayoutSyntaxTree(tree),
SetTextState(ctx.state.text.clone()),
],
None => vec![SetTextState(text)],
})
if let Some(body) = body {
body.eval(ctx);
ctx.state = snapshot;
}
Value::None
}

View File

@ -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
}
};

View File

@ -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
}

View File

@ -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
}

View File

@ -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::*;