mirror of
https://github.com/typst/typst
synced 2025-05-22 04:55:29 +08:00
Evaluation and node-based layouting 🚀
This commit is contained in:
parent
ca1256c924
commit
537545e7f8
@ -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)*) => {
|
||||
|
@ -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"));
|
||||
|
374
src/eval/mod.rs
374
src/eval/mod.rs
@ -14,42 +14,355 @@ pub use scope::*;
|
||||
pub use state::*;
|
||||
pub use value::*;
|
||||
|
||||
use std::any::Any;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use fontdock::FontStyle;
|
||||
|
||||
use crate::layout::LayoutContext;
|
||||
use crate::diag::Diag;
|
||||
use crate::geom::Size;
|
||||
use crate::layout::nodes::{
|
||||
Document, LayoutNode, Pad, Pages, Par, Softness, Spacing, Stack, Text,
|
||||
};
|
||||
use crate::layout::{Gen2, Spec2, Switch};
|
||||
use crate::syntax::*;
|
||||
use crate::{Feedback, Pass};
|
||||
|
||||
/// Evaluate an syntactic item into an output value.
|
||||
/// Evaluate a syntax tree into a document.
|
||||
///
|
||||
/// The given `state` the base state that may be updated over the course of
|
||||
/// evaluation.
|
||||
pub fn eval(tree: &SynTree, state: State) -> Pass<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.
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
52
src/layout/nodes/document.rs
Normal file
52
src/layout/nodes/document.rs
Normal 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
42
src/layout/nodes/fixed.rs
Normal 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
167
src/layout/nodes/mod.rs
Normal 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
53
src/layout/nodes/pad.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
51
src/layout/nodes/spacing.rs
Normal file
51
src/layout/nodes/spacing.rs
Normal 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,
|
||||
}
|
@ -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,19 +134,13 @@ 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.usable().get(axis));
|
||||
spacing = spacing.min(self.space.usable.get(axis));
|
||||
|
||||
let size = Gen2::new(spacing, 0.0);
|
||||
self.update_metrics(size);
|
||||
@ -104,27 +150,11 @@ impl StackLayouter {
|
||||
));
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
51
src/layout/nodes/text.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
36
src/lib.rs
36
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
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)]
|
||||
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 {
|
||||
vec![]
|
||||
})
|
||||
ctx.push(spacing);
|
||||
}
|
||||
}
|
||||
|
||||
Value::None
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
Loading…
x
Reference in New Issue
Block a user