From 76fc4cca62f5b955200b2c62cc85b69eea491ece Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 25 Mar 2021 21:32:33 +0100 Subject: [PATCH] =?UTF-8?q?Refactor=20alignments=20&=20directions=20?= =?UTF-8?q?=F0=9F=93=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds lang function - Refactors execution context - Adds StackChild and ParChild enums --- src/eval/mod.rs | 1 - src/exec/context.rs | 330 ++++++++++++++++++---------------- src/exec/mod.rs | 2 +- src/exec/state.rs | 25 ++- src/geom/align.rs | 3 - src/geom/dir.rs | 3 - src/geom/gen.rs | 10 +- src/geom/mod.rs | 5 +- src/geom/point.rs | 4 +- src/geom/size.rs | 4 +- src/geom/spec.rs | 13 +- src/layout/background.rs | 10 +- src/layout/fixed.rs | 4 +- src/layout/mod.rs | 121 +++++++------ src/layout/node.rs | 108 ----------- src/layout/pad.rs | 12 +- src/layout/par.rs | 127 +++++++------ src/layout/spacing.rs | 35 ---- src/layout/stack.rs | 76 ++++---- src/layout/text.rs | 36 ---- src/library/align.rs | 144 +++++---------- src/library/image.rs | 18 +- src/library/lang.rs | 45 +++++ src/library/mod.rs | 9 +- src/library/pad.rs | 6 +- src/library/page.rs | 12 -- src/library/par.rs | 14 +- src/library/shapes.rs | 18 +- src/library/spacing.rs | 21 ++- tests/ref/library/lang.png | Bin 0 -> 1897 bytes tests/ref/library/page.png | Bin 8180 -> 7438 bytes tests/ref/library/spacing.png | Bin 3845 -> 3251 bytes tests/typ/library/lang.typ | 16 ++ tests/typ/library/page.typ | 7 - tests/typ/library/spacing.typ | 7 - 35 files changed, 539 insertions(+), 707 deletions(-) delete mode 100644 src/layout/node.rs delete mode 100644 src/layout/spacing.rs delete mode 100644 src/layout/text.rs create mode 100644 src/library/lang.rs create mode 100644 tests/ref/library/lang.png create mode 100644 tests/typ/library/lang.typ diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 802e1347a..88110d88f 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -39,7 +39,6 @@ pub fn eval(env: &mut Env, tree: &Tree, scope: &Scope) -> Pass { pub type NodeMap = HashMap<*const Node, Value>; /// The context for evaluation. -#[derive(Debug)] pub struct EvalContext<'a> { /// The environment from which resources are gathered. pub env: &'a mut Env, diff --git a/src/exec/context.rs b/src/exec/context.rs index b6a67a2ec..333ad3bad 100644 --- a/src/exec/context.rs +++ b/src/exec/context.rs @@ -4,15 +4,14 @@ use super::{Exec, FontFamily, State}; use crate::diag::{Diag, DiagSet, Pass}; use crate::env::Env; use crate::eval::TemplateValue; -use crate::geom::{Dir, Gen, Linear, Sides, Size}; +use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size}; use crate::layout::{ - Node, PadNode, PageRun, ParNode, SpacingNode, StackNode, TextNode, Tree, + AnyNode, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode, TextNode, Tree, }; use crate::parse::{is_newline, Scanner}; -use crate::syntax::{Span, Spanned}; +use crate::syntax::Span; /// The context for execution. -#[derive(Debug)] pub struct ExecContext<'a> { /// The environment from which resources are gathered. pub env: &'a mut Env, @@ -22,13 +21,11 @@ pub struct ExecContext<'a> { pub diags: DiagSet, /// The tree of finished page runs. tree: Tree, - /// Metrics of the active page. - page: Option, - /// The content of the active stack. This may be the top-level stack for the - /// page or a lower one created by [`exec`](Self::exec). - stack: StackNode, - /// The content of the active paragraph. - par: ParNode, + /// When we are building the top-level stack, this contains metrics of the + /// page. While building a group stack through `exec_group`, this is `None`. + page: Option, + /// The currently built stack of paragraphs. + stack: StackBuilder, } impl<'a> ExecContext<'a> { @@ -38,9 +35,8 @@ impl<'a> ExecContext<'a> { env, diags: DiagSet::new(), tree: Tree { runs: vec![] }, - page: Some(PageInfo::new(&state, true)), - stack: StackNode::new(&state), - par: ParNode::new(&state), + page: Some(PageBuilder::new(&state, true)), + stack: StackBuilder::new(&state), state, } } @@ -50,45 +46,23 @@ impl<'a> ExecContext<'a> { self.diags.insert(diag); } - /// Set the directions. - /// - /// Produces an error if the axes aligned. - pub fn set_dirs(&mut self, new: Gen>>) { - let dirs = Gen::new( - new.main.map(|s| s.v).unwrap_or(self.state.dirs.main), - new.cross.map(|s| s.v).unwrap_or(self.state.dirs.cross), - ); - - if dirs.main.axis() != dirs.cross.axis() { - self.state.dirs = dirs; - } else { - for dir in new.main.iter().chain(new.cross.iter()) { - self.diag(error!(dir.span, "aligned axis")); - } - } - } - /// Set the font to monospace. pub fn set_monospace(&mut self) { let families = self.state.font.families_mut(); families.list.insert(0, FontFamily::Monospace); } - /// Push a layout node into the active paragraph. - /// - /// Spacing nodes will be handled according to their - /// [`softness`](SpacingNode::softness). - pub fn push(&mut self, node: impl Into) { - push(&mut self.par.children, node.into()); - } + /// Execute a template and return the result as a stack node. + pub fn exec_group(&mut self, template: &TemplateValue) -> StackNode { + let snapshot = self.state.clone(); + let page = self.page.take(); + let stack = mem::replace(&mut self.stack, StackBuilder::new(&self.state)); - /// Push a word space into the active paragraph. - pub fn push_space(&mut self) { - let em = self.state.font.resolve_size(); - self.push(SpacingNode { - amount: self.state.par.word_spacing.resolve(em), - softness: 1, - }); + template.exec(self); + + self.state = snapshot; + self.page = page; + mem::replace(&mut self.stack, stack).build() } /// Push text into the active paragraph. @@ -97,96 +71,85 @@ impl<'a> ExecContext<'a> { pub fn push_text(&mut self, text: &str) { let mut scanner = Scanner::new(text); let mut line = String::new(); + let push = |this: &mut Self, text| { + let props = this.state.font.resolve_props(); + let node = TextNode { text, props }; + let align = this.state.aligns.cross; + this.stack.par.folder.push(ParChild::Text(node, align)) + }; while let Some(c) = scanner.eat_merging_crlf() { if is_newline(c) { - self.push(TextNode::new(mem::take(&mut line), &self.state)); + push(self, mem::take(&mut line)); self.push_linebreak(); } else { line.push(c); } } - self.push(TextNode::new(line, &self.state)); + push(self, line); + } + + /// Push a word space. + pub fn push_word_space(&mut self) { + let em = self.state.font.resolve_size(); + let amount = self.state.par.word_spacing.resolve(em); + self.push_spacing(GenAxis::Cross, amount, 1); } /// Apply a forced line break. pub fn push_linebreak(&mut self) { let em = self.state.font.resolve_size(); - self.push_into_stack(SpacingNode { - amount: self.state.par.leading.resolve(em), - softness: 2, - }); + let amount = self.state.par.leading.resolve(em); + self.push_spacing(GenAxis::Main, amount, 2); } /// Apply a forced paragraph break. pub fn push_parbreak(&mut self) { let em = self.state.font.resolve_size(); - self.push_into_stack(SpacingNode { - amount: self.state.par.spacing.resolve(em), - softness: 1, - }); + let amount = self.state.par.spacing.resolve(em); + self.push_spacing(GenAxis::Main, amount, 1); } - /// Push a node directly into the stack above the paragraph. This finishes - /// the active paragraph and starts a new one. - pub fn push_into_stack(&mut self, node: impl Into) { - self.finish_par(); - push(&mut self.stack.children, node.into()); - } - - /// Execute a template and return the result as a stack node. - pub fn exec(&mut self, template: &TemplateValue) -> StackNode { - let page = self.page.take(); - let stack = mem::replace(&mut self.stack, StackNode::new(&self.state)); - let par = mem::replace(&mut self.par, ParNode::new(&self.state)); - - template.exec(self); - let result = self.finish_stack(); - - self.page = page; - self.stack = stack; - self.par = par; - - result - } - - /// Finish the active paragraph. - fn finish_par(&mut self) { - let mut par = mem::replace(&mut self.par, ParNode::new(&self.state)); - trim(&mut par.children); - - if !par.children.is_empty() { - self.stack.children.push(par.into()); + /// Push spacing into paragraph or stack depending on `axis`. + /// + /// The `softness` configures how the spacing interacts with surrounding + /// spacing. + pub fn push_spacing(&mut self, axis: GenAxis, amount: Length, softness: u8) { + match axis { + GenAxis::Main => { + let spacing = StackChild::Spacing(amount); + self.stack.finish_par(&self.state); + self.stack.folder.push_soft(spacing, softness); + } + GenAxis::Cross => { + let spacing = ParChild::Spacing(amount); + self.stack.par.folder.push_soft(spacing, softness); + } } } - /// Finish the active stack. - fn finish_stack(&mut self) -> StackNode { - self.finish_par(); + /// Push any node into the active paragraph. + pub fn push_into_par(&mut self, node: impl Into) { + let align = self.state.aligns.cross; + self.stack.par.folder.push(ParChild::Any(node.into(), align)); + } - let mut stack = mem::replace(&mut self.stack, StackNode::new(&self.state)); - trim(&mut stack.children); - - stack + /// Push any node directly into the stack of paragraphs. + /// + /// This finishes the active paragraph and starts a new one. + pub fn push_into_stack(&mut self, node: impl Into) { + let aligns = self.state.aligns; + self.stack.finish_par(&self.state); + self.stack.folder.push(StackChild::Any(node.into(), aligns)); } /// Finish the active page. pub fn finish_page(&mut self, keep: bool, hard: bool, source: Span) { - if let Some(info) = &mut self.page { - let info = mem::replace(info, PageInfo::new(&self.state, hard)); - let stack = self.finish_stack(); - - if !stack.children.is_empty() || (keep && info.hard) { - self.tree.runs.push(PageRun { - size: info.size, - child: PadNode { - padding: info.padding, - child: stack.into(), - } - .into(), - }); - } + if let Some(builder) = &mut self.page { + let page = mem::replace(builder, PageBuilder::new(&self.state, hard)); + let stack = mem::replace(&mut self.stack, StackBuilder::new(&self.state)); + self.tree.runs.extend(page.build(stack.build(), keep)); } else { self.diag(error!(source, "cannot modify page from here")); } @@ -200,44 +163,13 @@ impl<'a> ExecContext<'a> { } } -/// Push a node into a list, taking care of spacing softness. -fn push(nodes: &mut Vec, node: Node) { - if let Node::Spacing(spacing) = node { - if nodes.is_empty() && spacing.softness > 0 { - return; - } - - if let Some(&Node::Spacing(other)) = nodes.last() { - if spacing.softness > 0 && spacing.softness >= other.softness { - return; - } - - if spacing.softness < other.softness { - nodes.pop(); - } - } - } - - nodes.push(node); -} - -/// Remove trailing soft spacing from a node list. -fn trim(nodes: &mut Vec) { - if let Some(&Node::Spacing(spacing)) = nodes.last() { - if spacing.softness > 0 { - nodes.pop(); - } - } -} - -#[derive(Debug)] -struct PageInfo { +struct PageBuilder { size: Size, padding: Sides, hard: bool, } -impl PageInfo { +impl PageBuilder { fn new(state: &State, hard: bool) -> Self { Self { size: state.page.size, @@ -245,37 +177,119 @@ impl PageInfo { hard, } } -} -impl StackNode { - fn new(state: &State) -> Self { - Self { - dirs: state.dirs, - aligns: state.aligns, - children: vec![], - } + fn build(self, child: StackNode, keep: bool) -> Option { + let Self { size, padding, hard } = self; + (!child.children.is_empty() || (keep && hard)).then(|| PageRun { + size, + child: PadNode { padding, child: child.into() }.into(), + }) } } -impl ParNode { +struct StackBuilder { + dirs: Gen, + folder: SoftFolder, + par: ParBuilder, +} + +impl StackBuilder { + fn new(state: &State) -> Self { + Self { + dirs: Gen::new(Dir::TTB, state.lang.dir), + folder: SoftFolder::new(), + par: ParBuilder::new(state), + } + } + + fn finish_par(&mut self, state: &State) { + let par = mem::replace(&mut self.par, ParBuilder::new(state)); + self.folder.extend(par.build()); + } + + fn build(self) -> StackNode { + let Self { dirs, mut folder, par } = self; + folder.extend(par.build()); + StackNode { dirs, children: folder.finish() } + } +} + +struct ParBuilder { + aligns: Gen, + dir: Dir, + line_spacing: Length, + folder: SoftFolder, +} + +impl ParBuilder { fn new(state: &State) -> Self { let em = state.font.resolve_size(); Self { - dirs: state.dirs, aligns: state.aligns, + dir: state.lang.dir, line_spacing: state.par.leading.resolve(em), - children: vec![], + folder: SoftFolder::new(), } } + + fn build(self) -> Option { + let Self { aligns, dir, line_spacing, folder } = self; + let children = folder.finish(); + (!children.is_empty()).then(|| { + let node = ParNode { dir, line_spacing, children }; + StackChild::Any(node.into(), aligns) + }) + } } -impl TextNode { - fn new(text: String, state: &State) -> Self { - Self { - text, - dir: state.dirs.cross, - aligns: state.aligns, - props: state.font.resolve_props(), +/// This is used to remove leading and trailing word/line/paragraph spacing +/// as well as collapse sequences of spacings into just one. +struct SoftFolder { + nodes: Vec, + last: Last, +} + +enum Last { + None, + Hard, + Soft(N, u8), +} + +impl SoftFolder { + fn new() -> Self { + Self { nodes: vec![], last: Last::Hard } + } + + fn push(&mut self, node: N) { + let last = mem::replace(&mut self.last, Last::None); + if let Last::Soft(soft, _) = last { + self.nodes.push(soft); + } + self.nodes.push(node); + } + + fn push_soft(&mut self, node: N, softness: u8) { + if softness == 0 { + self.last = Last::Hard; + self.nodes.push(node); + } else { + match self.last { + Last::Hard => {} + Last::Soft(_, other) if softness >= other => {} + _ => self.last = Last::Soft(node, softness), + } + } + } + + fn finish(self) -> Vec { + self.nodes + } +} + +impl Extend for SoftFolder { + fn extend>(&mut self, iter: T) { + for elem in iter { + self.push(elem); } } } diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 6f3b9c837..69a41beb0 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -65,7 +65,7 @@ impl ExecWithMap for Node { fn exec_with_map(&self, ctx: &mut ExecContext, map: &NodeMap) { match self { Node::Text(text) => ctx.push_text(text), - Node::Space => ctx.push_space(), + Node::Space => ctx.push_word_space(), _ => map[&(self as *const _)].exec(ctx), } } diff --git a/src/exec/state.rs b/src/exec/state.rs index 7957f3127..c579bc4e4 100644 --- a/src/exec/state.rs +++ b/src/exec/state.rs @@ -12,30 +12,43 @@ use crate::paper::{Paper, PaperClass, PAPER_A4}; /// The evaluation state. #[derive(Debug, Clone, PartialEq)] pub struct State { - /// The current directions along which layouts are placed in their parents. - pub dirs: LayoutDirs, - /// The current alignments of layouts in their parents. - pub aligns: LayoutAligns, + /// The current language-related settings. + pub lang: LangState, /// The current page settings. pub page: PageState, /// The current paragraph settings. pub par: ParState, /// The current font settings. pub font: FontState, + /// The current alignments of layouts in their parents. + pub aligns: Gen, } impl Default for State { fn default() -> Self { Self { - dirs: LayoutDirs::new(Dir::TTB, Dir::LTR), - aligns: LayoutAligns::new(Align::Start, Align::Start), + lang: LangState::default(), page: PageState::default(), par: ParState::default(), font: FontState::default(), + aligns: Gen::new(Align::Start, Align::Start), } } } +/// Defines language properties. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct LangState { + /// The direction for text and other inline objects. + pub dir: Dir, +} + +impl Default for LangState { + fn default() -> Self { + Self { dir: Dir::LTR } + } +} + /// Defines page properties. #[derive(Debug, Copy, Clone, PartialEq)] pub struct PageState { diff --git a/src/geom/align.rs b/src/geom/align.rs index e13da3781..422624d84 100644 --- a/src/geom/align.rs +++ b/src/geom/align.rs @@ -1,8 +1,5 @@ use super::*; -/// The alignments of a layout in its parent. -pub type LayoutAligns = Gen; - /// Where to align something along a directed axis. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] pub enum Align { diff --git a/src/geom/dir.rs b/src/geom/dir.rs index 3eddd7d3c..cfcb4c09a 100644 --- a/src/geom/dir.rs +++ b/src/geom/dir.rs @@ -1,8 +1,5 @@ use super::*; -/// The directions along which layouts are placed in their parent. -pub type LayoutDirs = Gen; - /// The four directions into which content can be laid out. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum Dir { diff --git a/src/geom/gen.rs b/src/geom/gen.rs index c80cc21b4..7e0214126 100644 --- a/src/geom/gen.rs +++ b/src/geom/gen.rs @@ -50,8 +50,8 @@ impl Get for Gen { impl Switch for Gen { type Other = Spec; - fn switch(self, dirs: LayoutDirs) -> Self::Other { - match dirs.main.axis() { + fn switch(self, main: SpecAxis) -> Self::Other { + match main { SpecAxis::Horizontal => Spec::new(self.main, self.cross), SpecAxis::Vertical => Spec::new(self.cross, self.main), } @@ -86,10 +86,10 @@ impl GenAxis { impl Switch for GenAxis { type Other = SpecAxis; - fn switch(self, dirs: LayoutDirs) -> Self::Other { + fn switch(self, main: SpecAxis) -> Self::Other { match self { - Self::Main => dirs.main.axis(), - Self::Cross => dirs.cross.axis(), + Self::Main => main, + Self::Cross => main.other(), } } } diff --git a/src/geom/mod.rs b/src/geom/mod.rs index 5099c6b06..0031c6df1 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -53,7 +53,6 @@ pub trait Switch { /// The type of the other version. type Other; - /// The other version of this type based on the current layouting - /// directions. - fn switch(self, dirs: LayoutDirs) -> Self::Other; + /// The other version of this type based on the current main axis. + fn switch(self, main: SpecAxis) -> Self::Other; } diff --git a/src/geom/point.rs b/src/geom/point.rs index cf8bc1a9b..292985654 100644 --- a/src/geom/point.rs +++ b/src/geom/point.rs @@ -45,8 +45,8 @@ impl Get for Point { impl Switch for Point { type Other = Gen; - fn switch(self, dirs: LayoutDirs) -> Self::Other { - match dirs.main.axis() { + fn switch(self, main: SpecAxis) -> Self::Other { + match main { SpecAxis::Horizontal => Gen::new(self.x, self.y), SpecAxis::Vertical => Gen::new(self.y, self.x), } diff --git a/src/geom/size.rs b/src/geom/size.rs index 2feaa950e..1ba2f04b1 100644 --- a/src/geom/size.rs +++ b/src/geom/size.rs @@ -74,8 +74,8 @@ impl Get for Size { impl Switch for Size { type Other = Gen; - fn switch(self, dirs: LayoutDirs) -> Self::Other { - match dirs.main.axis() { + fn switch(self, main: SpecAxis) -> Self::Other { + match main { SpecAxis::Horizontal => Gen::new(self.width, self.height), SpecAxis::Vertical => Gen::new(self.height, self.width), } diff --git a/src/geom/spec.rs b/src/geom/spec.rs index 510bac84a..546eac7b6 100644 --- a/src/geom/spec.rs +++ b/src/geom/spec.rs @@ -66,8 +66,8 @@ impl Get for Spec { impl Switch for Spec { type Other = Gen; - fn switch(self, dirs: LayoutDirs) -> Self::Other { - match dirs.main.axis() { + fn switch(self, main: SpecAxis) -> Self::Other { + match main { SpecAxis::Horizontal => Gen::new(self.horizontal, self.vertical), SpecAxis::Vertical => Gen::new(self.vertical, self.horizontal), } @@ -102,13 +102,8 @@ impl SpecAxis { impl Switch for SpecAxis { type Other = GenAxis; - fn switch(self, dirs: LayoutDirs) -> Self::Other { - if self == dirs.main.axis() { - GenAxis::Main - } else { - debug_assert_eq!(self, dirs.cross.axis()); - GenAxis::Cross - } + fn switch(self, main: SpecAxis) -> Self::Other { + if self == main { GenAxis::Main } else { GenAxis::Cross } } } diff --git a/src/layout/background.rs b/src/layout/background.rs index 17280a86a..d34081820 100644 --- a/src/layout/background.rs +++ b/src/layout/background.rs @@ -8,7 +8,7 @@ pub struct BackgroundNode { /// The background fill. pub fill: Fill, /// The child node to be filled. - pub child: Node, + pub child: AnyNode, } /// The kind of shape to use as a background. @@ -19,10 +19,10 @@ pub enum BackgroundShape { } impl Layout for BackgroundNode { - fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment { - let mut fragment = self.child.layout(ctx, areas); + fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec { + let mut frames = self.child.layout(ctx, areas); - for frame in fragment.frames_mut() { + for frame in &mut frames { let (point, shape) = match self.shape { BackgroundShape::Rect => (Point::ZERO, Shape::Rect(frame.size)), BackgroundShape::Ellipse => { @@ -34,7 +34,7 @@ impl Layout for BackgroundNode { frame.elements.insert(0, (point, element)); } - fragment + frames } } diff --git a/src/layout/fixed.rs b/src/layout/fixed.rs index 22c45ef12..04ea5a3a9 100644 --- a/src/layout/fixed.rs +++ b/src/layout/fixed.rs @@ -12,11 +12,11 @@ pub struct FixedNode { /// The resulting frame will satisfy `width = aspect * height`. pub aspect: Option, /// The child node whose size to fix. - pub child: Node, + pub child: AnyNode, } impl Layout for FixedNode { - fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment { + fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec { let Areas { current, full, .. } = areas; let full = Size::new( diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 360c9d84b..6f28fcb9b 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -3,24 +3,21 @@ mod background; mod fixed; mod frame; -mod node; mod pad; mod par; mod shaping; -mod spacing; mod stack; -mod text; pub use background::*; pub use fixed::*; pub use frame::*; -pub use node::*; pub use pad::*; pub use par::*; pub use shaping::*; -pub use spacing::*; pub use stack::*; -pub use text::*; + +use std::any::Any; +use std::fmt::{self, Debug, Formatter}; use crate::env::Env; use crate::geom::*; @@ -51,25 +48,88 @@ pub struct PageRun { pub size: Size, /// The layout node that produces the actual pages (typically a /// [`StackNode`]). - pub child: Node, + pub child: AnyNode, } impl PageRun { /// Layout the page run. pub fn layout(&self, ctx: &mut LayoutContext) -> Vec { let areas = Areas::repeat(self.size, Spec::uniform(Expand::Fill)); - self.child.layout(ctx, &areas).into_frames() + self.child.layout(ctx, &areas) + } +} + +/// A wrapper around a dynamic layouting node. +pub struct AnyNode(Box); + +impl AnyNode { + /// Create a new instance from any node that satisifies the required bounds. + pub fn new(any: T) -> Self + where + T: Layout + Debug + Clone + PartialEq + 'static, + { + Self(Box::new(any)) + } +} + +impl Layout for AnyNode { + fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec { + self.0.layout(ctx, areas) + } +} + +impl Clone for AnyNode { + fn clone(&self) -> Self { + Self(self.0.dyn_clone()) + } +} + +impl PartialEq for AnyNode { + fn eq(&self, other: &Self) -> bool { + self.0.dyn_eq(other.0.as_ref()) + } +} + +impl Debug for AnyNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +trait Bounds: Layout + Debug + 'static { + fn as_any(&self) -> &dyn Any; + fn dyn_eq(&self, other: &dyn Bounds) -> bool; + fn dyn_clone(&self) -> Box; +} + +impl Bounds for T +where + T: Layout + Debug + PartialEq + Clone + 'static, +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn dyn_eq(&self, other: &dyn Bounds) -> bool { + if let Some(other) = other.as_any().downcast_ref::() { + self == other + } else { + false + } + } + + fn dyn_clone(&self) -> Box { + Box::new(self.clone()) } } /// Layout a node. pub trait Layout { /// Layout the node into the given areas. - fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment; + fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec; } /// The context for layouting. -#[derive(Debug)] pub struct LayoutContext<'a> { /// The environment from which fonts are gathered. pub env: &'a mut Env, @@ -183,44 +243,3 @@ impl Expand { } } } - -/// The result of layouting a node. -#[derive(Debug, Clone, PartialEq)] -pub enum Fragment { - /// Spacing that should be added to the parent. - Spacing(Length), - /// A layout that should be added to and aligned in the parent. - Frame(Frame, LayoutAligns), - /// Multiple layouts. - Frames(Vec, LayoutAligns), -} - -impl Fragment { - /// Return a reference to all frames contained in this variant (zero, one or - /// arbitrarily many). - pub fn frames(&self) -> &[Frame] { - match self { - Self::Spacing(_) => &[], - Self::Frame(frame, _) => std::slice::from_ref(frame), - Self::Frames(frames, _) => frames, - } - } - - /// Return a mutable reference to all frames contained in this variant. - pub fn frames_mut(&mut self) -> &mut [Frame] { - match self { - Self::Spacing(_) => &mut [], - Self::Frame(frame, _) => std::slice::from_mut(frame), - Self::Frames(frames, _) => frames, - } - } - - /// Return all frames contained in this varian. - pub fn into_frames(self) -> Vec { - match self { - Self::Spacing(_) => vec![], - Self::Frame(frame, _) => vec![frame], - Self::Frames(frames, _) => frames, - } - } -} diff --git a/src/layout/node.rs b/src/layout/node.rs deleted file mode 100644 index 443a96ae7..000000000 --- a/src/layout/node.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::any::Any; -use std::fmt::{self, Debug, Formatter}; - -use super::*; - -/// A self-contained layout node. -#[derive(Clone, PartialEq)] -pub enum Node { - /// A text node. - Text(TextNode), - /// A spacing node. - Spacing(SpacingNode), - /// A dynamic node that can implement custom layouting behaviour. - Any(AnyNode), -} - -impl Layout for Node { - fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment { - match self { - Self::Spacing(spacing) => spacing.layout(ctx, areas), - Self::Text(text) => text.layout(ctx, areas), - Self::Any(any) => any.layout(ctx, areas), - } - } -} - -impl Debug for Node { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Spacing(spacing) => spacing.fmt(f), - Self::Text(text) => text.fmt(f), - Self::Any(any) => any.fmt(f), - } - } -} - -/// A wrapper around a dynamic layouting node. -pub struct AnyNode(Box); - -impl AnyNode { - /// Create a new instance from any node that satisifies the required bounds. - pub fn new(any: T) -> Self - where - T: Layout + Debug + Clone + PartialEq + 'static, - { - Self(Box::new(any)) - } -} - -impl Layout for AnyNode { - fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment { - self.0.layout(ctx, areas) - } -} - -impl Clone for AnyNode { - fn clone(&self) -> Self { - Self(self.0.dyn_clone()) - } -} - -impl PartialEq for AnyNode { - fn eq(&self, other: &Self) -> bool { - self.0.dyn_eq(other.0.as_ref()) - } -} - -impl Debug for AnyNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl From for Node -where - T: Into, -{ - fn from(t: T) -> Self { - Self::Any(t.into()) - } -} - -trait Bounds: Layout + Debug + 'static { - fn as_any(&self) -> &dyn Any; - fn dyn_eq(&self, other: &dyn Bounds) -> bool; - fn dyn_clone(&self) -> Box; -} - -impl Bounds for T -where - T: Layout + Debug + PartialEq + Clone + 'static, -{ - fn as_any(&self) -> &dyn Any { - self - } - - fn dyn_eq(&self, other: &dyn Bounds) -> bool { - if let Some(other) = other.as_any().downcast_ref::() { - self == other - } else { - false - } - } - - fn dyn_clone(&self) -> Box { - Box::new(self.clone()) - } -} diff --git a/src/layout/pad.rs b/src/layout/pad.rs index fb0389965..2c8712af6 100644 --- a/src/layout/pad.rs +++ b/src/layout/pad.rs @@ -6,19 +6,17 @@ pub struct PadNode { /// The amount of padding. pub padding: Sides, /// The child node whose sides to pad. - pub child: Node, + pub child: AnyNode, } impl Layout for PadNode { - fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment { + fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec { let areas = shrink(areas, self.padding); - - let mut fragment = self.child.layout(ctx, &areas); - for frame in fragment.frames_mut() { + let mut frames = self.child.layout(ctx, &areas); + for frame in &mut frames { pad(frame, self.padding); } - - fragment + frames } } diff --git a/src/layout/par.rs b/src/layout/par.rs index 0364a03a0..02e27cbdc 100644 --- a/src/layout/par.rs +++ b/src/layout/par.rs @@ -1,38 +1,63 @@ +use std::fmt::{self, Debug, Formatter}; + use super::*; +use crate::exec::FontProps; /// A node that arranges its children into a paragraph. #[derive(Debug, Clone, PartialEq)] pub struct ParNode { - /// The `main` and `cross` directions of this paragraph. - /// - /// The children are placed in lines along the `cross` direction. The lines - /// are stacked along the `main` direction. - pub dirs: LayoutDirs, - /// How to align this paragraph in its parent. - pub aligns: LayoutAligns, - /// The spacing to insert after each line. + /// The inline direction of this paragraph. + pub dir: Dir, + /// The spacing to insert between each line. pub line_spacing: Length, /// The nodes to be arranged in a paragraph. - pub children: Vec, + pub children: Vec, +} + +/// A child of a paragraph node. +#[derive(Debug, Clone, PartialEq)] +pub enum ParChild { + /// Spacing between other nodes. + Spacing(Length), + /// A run of text and how to align it in its line. + Text(TextNode, Align), + /// Any child node and how to align it in its line. + Any(AnyNode, Align), +} + +/// A consecutive, styled run of text. +#[derive(Clone, PartialEq)] +pub struct TextNode { + /// The text. + pub text: String, + /// Properties used for font selection and layout. + pub props: FontProps, +} + +impl Debug for TextNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Text({})", self.text) + } } impl Layout for ParNode { - fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment { - let mut layouter = ParLayouter::new(self.dirs, self.line_spacing, areas.clone()); + fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec { + let mut layouter = ParLayouter::new(self.dir, self.line_spacing, areas.clone()); for child in &self.children { - match child.layout(ctx, &layouter.areas) { - Fragment::Spacing(spacing) => layouter.push_spacing(spacing), - Fragment::Frame(frame, aligns) => { - layouter.push_frame(frame, aligns.cross) + match *child { + ParChild::Spacing(amount) => layouter.push_spacing(amount), + ParChild::Text(ref node, align) => { + let frame = shape(&node.text, &mut ctx.env.fonts, &node.props); + layouter.push_frame(frame, align); } - Fragment::Frames(frames, aligns) => { - for frame in frames { - layouter.push_frame(frame, aligns.cross); + ParChild::Any(ref node, align) => { + for frame in node.layout(ctx, &layouter.areas) { + layouter.push_frame(frame, align); } } } } - Fragment::Frames(layouter.finish(), self.aligns) + layouter.finish() } } @@ -43,30 +68,30 @@ impl From for AnyNode { } struct ParLayouter { + dirs: Gen, main: SpecAxis, cross: SpecAxis, - dirs: LayoutDirs, line_spacing: Length, areas: Areas, finished: Vec, - lines: Vec<(Length, Frame, Align)>, - lines_size: Gen, + stack: Vec<(Length, Frame, Align)>, + stack_size: Gen, line: Vec<(Length, Frame, Align)>, line_size: Gen, line_ruler: Align, } impl ParLayouter { - fn new(dirs: LayoutDirs, line_spacing: Length, areas: Areas) -> Self { + fn new(dir: Dir, line_spacing: Length, areas: Areas) -> Self { Self { - main: dirs.main.axis(), - cross: dirs.cross.axis(), - dirs, + dirs: Gen::new(Dir::TTB, dir), + main: SpecAxis::Vertical, + cross: SpecAxis::Horizontal, line_spacing, areas, finished: vec![], - lines: vec![], - lines_size: Gen::ZERO, + stack: vec![], + stack_size: Gen::ZERO, line: vec![], line_size: Gen::ZERO, line_ruler: Align::Start, @@ -122,12 +147,10 @@ impl ParLayouter { } } - let size = frame.size.switch(self.dirs); - // A line can contain frames with different alignments. They exact // positions are calculated later depending on the alignments. + let size = frame.size.switch(self.main); self.line.push((self.line_size.cross, frame, align)); - self.line_size.cross += size.cross; self.line_size.main = self.line_size.main.max(size.main); self.line_ruler = align; @@ -135,15 +158,15 @@ impl ParLayouter { fn finish_line(&mut self) { let full_size = { - let expand = self.areas.expand.switch(self.dirs); - let full = self.areas.full.switch(self.dirs); + let expand = self.areas.expand.get(self.cross); + let full = self.areas.full.get(self.cross); Gen::new( self.line_size.main, - expand.cross.resolve(self.line_size.cross, full.cross), + expand.resolve(self.line_size.cross, full), ) }; - let mut output = Frame::new(full_size.switch(self.dirs).to_size()); + let mut output = Frame::new(full_size.switch(self.main).to_size()); for (before, frame, align) in std::mem::take(&mut self.line) { let child_cross_size = frame.size.get(self.cross); @@ -158,49 +181,47 @@ impl ParLayouter { full_size.cross - before_with_self .. after }); - let pos = Gen::new(Length::ZERO, cross).switch(self.dirs).to_point(); + let pos = Gen::new(Length::ZERO, cross).switch(self.main).to_point(); output.push_frame(pos, frame); } // Add line spacing, but only between lines. - if !self.lines.is_empty() { - self.lines_size.main += self.line_spacing; + if !self.stack.is_empty() { + self.stack_size.main += self.line_spacing; *self.areas.current.get_mut(self.main) -= self.line_spacing; } - // Update metrics of the whole paragraph. - self.lines.push((self.lines_size.main, output, self.line_ruler)); - self.lines_size.main += full_size.main; - self.lines_size.cross = self.lines_size.cross.max(full_size.cross); + // Update metrics of paragraph and reset for line. + self.stack.push((self.stack_size.main, output, self.line_ruler)); + self.stack_size.main += full_size.main; + self.stack_size.cross = self.stack_size.cross.max(full_size.cross); *self.areas.current.get_mut(self.main) -= full_size.main; - - // Reset metrics for the single line. self.line_size = Gen::ZERO; self.line_ruler = Align::Start; } fn finish_area(&mut self) { - let size = self.lines_size; - let mut output = Frame::new(size.switch(self.dirs).to_size()); + let full_size = self.stack_size; + let mut output = Frame::new(full_size.switch(self.main).to_size()); - for (before, line, cross_align) in std::mem::take(&mut self.lines) { - let child_size = line.size.switch(self.dirs); + for (before, line, cross_align) in std::mem::take(&mut self.stack) { + let child_size = line.size.switch(self.main); // Position along the main axis. let main = if self.dirs.main.is_positive() { before } else { - size.main - (before + child_size.main) + full_size.main - (before + child_size.main) }; // Align along the cross axis. let cross = cross_align.resolve(if self.dirs.cross.is_positive() { - Length::ZERO .. size.cross - child_size.cross + Length::ZERO .. full_size.cross - child_size.cross } else { - size.cross - child_size.cross .. Length::ZERO + full_size.cross - child_size.cross .. Length::ZERO }); - let pos = Gen::new(main, cross).switch(self.dirs).to_point(); + let pos = Gen::new(main, cross).switch(self.main).to_point(); output.push_frame(pos, line); } @@ -208,7 +229,7 @@ impl ParLayouter { self.areas.next(); // Reset metrics for the whole paragraph. - self.lines_size = Gen::ZERO; + self.stack_size = Gen::ZERO; } fn finish(mut self) -> Vec { diff --git a/src/layout/spacing.rs b/src/layout/spacing.rs deleted file mode 100644 index 361b03ee3..000000000 --- a/src/layout/spacing.rs +++ /dev/null @@ -1,35 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; - -use super::*; - -/// A node that adds spacing to its parent. -#[derive(Copy, Clone, PartialEq)] -pub struct SpacingNode { - /// The amount of spacing to insert. - pub amount: Length, - /// Defines how spacing interacts with surrounding spacing. - /// - /// Hard spacing (`softness = 0`) assures that a fixed amount of spacing - /// will always be inserted. Soft spacing (`softness >= 1`) will be consumed - /// by other spacing with lower softness and can be used to insert - /// overridable spacing, e.g. between words or paragraphs. - pub softness: u8, -} - -impl Layout for SpacingNode { - fn layout(&self, _: &mut LayoutContext, _: &Areas) -> Fragment { - Fragment::Spacing(self.amount) - } -} - -impl Debug for SpacingNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Spacing({}, {})", self.amount, self.softness) - } -} - -impl From for Node { - fn from(spacing: SpacingNode) -> Self { - Self::Spacing(spacing) - } -} diff --git a/src/layout/stack.rs b/src/layout/stack.rs index 6a87290ea..79fde72d1 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -7,28 +7,34 @@ pub struct StackNode { /// /// The children are stacked along the `main` direction. The `cross` /// direction is required for aligning the children. - pub dirs: LayoutDirs, - /// How to align this stack in its parent. - pub aligns: LayoutAligns, + pub dirs: Gen, /// The nodes to be stacked. - pub children: Vec, + pub children: Vec, +} + +/// A child of a stack node. +#[derive(Debug, Clone, PartialEq)] +pub enum StackChild { + /// Spacing between other nodes. + Spacing(Length), + /// Any child node and how to align it in the stack. + Any(AnyNode, Gen), } impl Layout for StackNode { - fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment { + fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec { let mut layouter = StackLayouter::new(self.dirs, areas.clone()); for child in &self.children { - match child.layout(ctx, &layouter.areas) { - Fragment::Spacing(spacing) => layouter.push_spacing(spacing), - Fragment::Frame(frame, aligns) => layouter.push_frame(frame, aligns), - Fragment::Frames(frames, aligns) => { - for frame in frames { + match *child { + StackChild::Spacing(amount) => layouter.push_spacing(amount), + StackChild::Any(ref node, aligns) => { + for frame in node.layout(ctx, &layouter.areas) { layouter.push_frame(frame, aligns); } } } } - Fragment::Frames(layouter.finish(), self.aligns) + layouter.finish() } } @@ -39,24 +45,24 @@ impl From for AnyNode { } struct StackLayouter { + dirs: Gen, main: SpecAxis, - dirs: LayoutDirs, areas: Areas, finished: Vec, - frames: Vec<(Length, Frame, LayoutAligns)>, - used: Gen, + frames: Vec<(Length, Frame, Gen)>, + size: Gen, ruler: Align, } impl StackLayouter { - fn new(dirs: LayoutDirs, areas: Areas) -> Self { + fn new(dirs: Gen, areas: Areas) -> Self { Self { - main: dirs.main.axis(), dirs, + main: dirs.main.axis(), areas, finished: vec![], frames: vec![], - used: Gen::ZERO, + size: Gen::ZERO, ruler: Align::Start, } } @@ -65,10 +71,10 @@ impl StackLayouter { let main_rest = self.areas.current.get_mut(self.main); let capped = amount.min(*main_rest); *main_rest -= capped; - self.used.main += capped; + self.size.main += capped; } - fn push_frame(&mut self, frame: Frame, aligns: LayoutAligns) { + fn push_frame(&mut self, frame: Frame, aligns: Gen) { if self.ruler > aligns.main { self.finish_area(); } @@ -82,21 +88,18 @@ impl StackLayouter { } } - let size = frame.size.switch(self.dirs); - self.frames.push((self.used.main, frame, aligns)); - - *self.areas.current.get_mut(self.main) -= size.main; - self.used.main += size.main; - self.used.cross = self.used.cross.max(size.cross); + let size = frame.size.switch(self.main); + self.frames.push((self.size.main, frame, aligns)); self.ruler = aligns.main; + self.size.main += size.main; + self.size.cross = self.size.cross.max(size.cross); + *self.areas.current.get_mut(self.main) -= size.main; } fn finish_area(&mut self) { let full_size = { - let expand = self.areas.expand; - let full = self.areas.full; - let current = self.areas.current; - let used = self.used.switch(self.dirs).to_size(); + let Areas { current, full, expand, .. } = self.areas; + let used = self.size.switch(self.main).to_size(); let mut size = Size::new( expand.horizontal.resolve(used.width, full.width), @@ -113,21 +116,21 @@ impl StackLayouter { size = Size::new(width, width / aspect); } - size.switch(self.dirs) + size.switch(self.main) }; - let mut output = Frame::new(full_size.switch(self.dirs).to_size()); + let mut output = Frame::new(full_size.switch(self.main).to_size()); for (before, frame, aligns) in std::mem::take(&mut self.frames) { - let child_size = frame.size.switch(self.dirs); + let child_size = frame.size.switch(self.main); // Align along the main axis. let main = aligns.main.resolve(if self.dirs.main.is_positive() { - let after_with_self = self.used.main - before; + let after_with_self = self.size.main - before; before .. full_size.main - after_with_self } else { let before_with_self = before + child_size.main; - let after = self.used.main - (before + child_size.main); + let after = self.size.main - (before + child_size.main); full_size.main - before_with_self .. after }); @@ -138,15 +141,14 @@ impl StackLayouter { full_size.cross - child_size.cross .. Length::ZERO }); - let pos = Gen::new(main, cross).switch(self.dirs).to_point(); + let pos = Gen::new(main, cross).switch(self.main).to_point(); output.push_frame(pos, frame); } self.finished.push(output); - self.areas.next(); - self.used = Gen::ZERO; self.ruler = Align::Start; + self.size = Gen::ZERO; } fn finish(mut self) -> Vec { diff --git a/src/layout/text.rs b/src/layout/text.rs deleted file mode 100644 index 398669076..000000000 --- a/src/layout/text.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; - -use super::*; -use crate::exec::FontProps; - -/// A consecutive, styled run of text. -#[derive(Clone, PartialEq)] -pub struct TextNode { - /// The text direction. - pub dir: Dir, - /// How to align this text node in its parent. - pub aligns: LayoutAligns, - /// The text. - pub text: String, - /// Properties used for font selection and layout. - pub props: FontProps, -} - -impl Layout for TextNode { - fn layout(&self, ctx: &mut LayoutContext, _: &Areas) -> Fragment { - let frame = shape(&self.text, &mut ctx.env.fonts, &self.props); - Fragment::Frame(frame, self.aligns) - } -} - -impl Debug for TextNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Text({})", self.text) - } -} - -impl From for Node { - fn from(text: TextNode) -> Self { - Self::Text(text) - } -} diff --git a/src/library/align.rs b/src/library/align.rs index 765ed9882..d5811bf4a 100644 --- a/src/library/align.rs +++ b/src/library/align.rs @@ -6,11 +6,6 @@ use super::*; /// - Alignments: variadic, of type `alignment`. /// - Body: optional, of type `template`. /// -/// Which axis an alignment should apply to (main or cross) is inferred from -/// either the argument itself (for anything other than `center`) or from the -/// second argument if present, defaulting to the cross axis for a single -/// `center` alignment. -/// /// # Named parameters /// - Horizontal alignment: `horizontal`, of type `alignment`. /// - Vertical alignment: `vertical`, of type `alignment`. @@ -21,32 +16,44 @@ use super::*; /// /// # Relevant types and constants /// - Type `alignment` +/// - `start` +/// - `center` +/// - `end` /// - `left` /// - `right` /// - `top` /// - `bottom` -/// - `center` pub fn align(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let first = args.find(ctx); - let second = args.find(ctx); - let hor = args.get(ctx, "horizontal"); - let ver = args.get(ctx, "vertical"); + let first = args.find::(ctx); + let second = args.find::(ctx); + let mut horizontal = args.get::(ctx, "horizontal"); + let mut vertical = args.get::(ctx, "vertical"); let body = args.find::(ctx); + for value in first.into_iter().chain(second) { + match value.axis() { + Some(SpecAxis::Horizontal) | None if horizontal.is_none() => { + horizontal = Some(value); + } + Some(SpecAxis::Vertical) | None if vertical.is_none() => { + vertical = Some(value); + } + _ => {} + } + } + Value::template("align", move |ctx| { let snapshot = ctx.state.clone(); - let values = first - .into_iter() - .chain(second.into_iter()) - .map(|arg: Spanned| (arg.v.axis(), arg)) - .chain(hor.into_iter().map(|arg| (Some(SpecAxis::Horizontal), arg))) - .chain(ver.into_iter().map(|arg| (Some(SpecAxis::Vertical), arg))); + if let Some(horizontal) = horizontal { + ctx.state.aligns.cross = horizontal.to_align(ctx.state.lang.dir); + } - apply(ctx, values); - - if ctx.state.aligns.main != snapshot.aligns.main { - ctx.push_linebreak(); + if let Some(vertical) = vertical { + ctx.state.aligns.main = vertical.to_align(Dir::TTB); + if ctx.state.aligns.main != snapshot.aligns.main { + ctx.push_linebreak(); + } } if let Some(body) = &body { @@ -56,109 +63,48 @@ pub fn align(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { }) } -/// Deduplicate and apply the alignments. -fn apply( - ctx: &mut ExecContext, - values: impl Iterator, Spanned)>, -) { - let mut had = Gen::uniform(false); - let mut had_center = false; - - for (axis, Spanned { v: arg, span }) in values { - // Check whether we know which axis this alignment belongs to. - if let Some(axis) = axis { - // We know the axis. - let gen_axis = axis.switch(ctx.state.dirs); - let gen_align = arg.switch(ctx.state.dirs); - - if arg.axis().map_or(false, |a| a != axis) { - ctx.diag(error!(span, "invalid alignment for {} axis", axis)); - } else if had.get(gen_axis) { - ctx.diag(error!(span, "duplicate alignment for {} axis", axis)); - } else { - *ctx.state.aligns.get_mut(gen_axis) = gen_align; - *had.get_mut(gen_axis) = true; - } - } else { - // We don't know the axis: This has to be a `center` alignment for a - // positional argument. - debug_assert_eq!(arg, AlignValue::Center); - - if had.main && had.cross { - ctx.diag(error!(span, "duplicate alignment")); - } else if had_center { - // Both this and the previous one are unspecified `center` - // alignments. Both axes should be centered. - ctx.state.aligns.main = Align::Center; - ctx.state.aligns.cross = Align::Center; - had = Gen::uniform(true); - } else { - had_center = true; - } - } - - // If we we know the other alignment, we can handle the unspecified - // `center` alignment. - if had_center && (had.main || had.cross) { - if had.main { - ctx.state.aligns.cross = Align::Center; - } else { - ctx.state.aligns.main = Align::Center; - } - had = Gen::uniform(true); - had_center = false; - } - } - - // If `had_center` wasn't flushed by now, it's the only argument and - // then we default to applying it to the cross axis. - if had_center { - ctx.state.aligns.cross = Align::Center; - } -} - -/// An alignment value. +/// An alignment specifier passed to `align`. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] pub(super) enum AlignValue { - Left, + Start, Center, + End, + Left, Right, Top, Bottom, } impl AlignValue { - /// The specific axis this alignment refers to. fn axis(self) -> Option { match self { + Self::Start => None, + Self::Center => None, + Self::End => None, Self::Left => Some(SpecAxis::Horizontal), Self::Right => Some(SpecAxis::Horizontal), Self::Top => Some(SpecAxis::Vertical), Self::Bottom => Some(SpecAxis::Vertical), - Self::Center => None, } } -} -impl Switch for AlignValue { - type Other = Align; - - fn switch(self, dirs: LayoutDirs) -> Self::Other { - let get = |dir: Dir, at_positive_start| { - if dir.is_positive() == at_positive_start { + fn to_align(self, dir: Dir) -> Align { + let side = |is_at_positive_start| { + if dir.is_positive() == is_at_positive_start { Align::Start } else { Align::End } }; - let dirs = dirs.switch(dirs); match self { - Self::Left => get(dirs.horizontal, true), - Self::Right => get(dirs.horizontal, false), - Self::Top => get(dirs.vertical, true), - Self::Bottom => get(dirs.vertical, false), + Self::Start => Align::Start, Self::Center => Align::Center, + Self::End => Align::End, + Self::Left => side(true), + Self::Right => side(false), + Self::Top => side(true), + Self::Bottom => side(false), } } } @@ -166,8 +112,10 @@ impl Switch for AlignValue { impl Display for AlignValue { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.pad(match self { - Self::Left => "left", + Self::Start => "start", Self::Center => "center", + Self::End => "end", + Self::Left => "left", Self::Right => "right", Self::Top => "top", Self::Bottom => "bottom", diff --git a/src/library/image.rs b/src/library/image.rs index 9f39073be..020f7d50d 100644 --- a/src/library/image.rs +++ b/src/library/image.rs @@ -2,9 +2,7 @@ use ::image::GenericImageView; use super::*; use crate::env::{ImageResource, ResourceId}; -use crate::layout::{ - AnyNode, Areas, Element, Fragment, Frame, Image, Layout, LayoutContext, -}; +use crate::layout::{AnyNode, Areas, Element, Frame, Image, Layout, LayoutContext}; /// `image`: An image. /// @@ -25,13 +23,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let loaded = ctx.env.resources.load(&path.v, ImageResource::parse); if let Some((res, img)) = loaded { let dimensions = img.buf.dimensions(); - ctx.push(ImageNode { - res, - dimensions, - width, - height, - aligns: ctx.state.aligns, - }); + ctx.push_into_par(ImageNode { res, dimensions, width, height }); } else { ctx.diag(error!(path.span, "failed to load image")); } @@ -42,8 +34,6 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { /// An image node. #[derive(Debug, Clone, PartialEq)] struct ImageNode { - /// How to align this image node in its parent. - aligns: LayoutAligns, /// The resource id of the image file. res: ResourceId, /// The pixel dimensions of the image. @@ -55,7 +45,7 @@ struct ImageNode { } impl Layout for ImageNode { - fn layout(&self, _: &mut LayoutContext, areas: &Areas) -> Fragment { + fn layout(&self, _: &mut LayoutContext, areas: &Areas) -> Vec { let Areas { current, full, .. } = areas; let pixel_width = self.dimensions.0 as f64; @@ -86,7 +76,7 @@ impl Layout for ImageNode { let mut frame = Frame::new(size); frame.push(Point::ZERO, Element::Image(Image { res: self.res, size })); - Fragment::Frame(frame, self.aligns) + vec![frame] } } diff --git a/src/library/lang.rs b/src/library/lang.rs new file mode 100644 index 000000000..79015c7d2 --- /dev/null +++ b/src/library/lang.rs @@ -0,0 +1,45 @@ +use super::*; + +/// `lang`: Configure the language. +/// +/// # Positional parameters +/// - Language: of type `string`. Has to be a valid ISO 639-1 code. +/// +/// # Named parameters +/// - Text direction: `dir`, of type `direction`, must be horizontal. +/// +/// # Return value +/// A template that configures language properties. +/// +/// # Relevant types and constants +/// - Type `direction` +/// - `ltr` +/// - `rtl` +pub fn lang(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let iso = args.find::(ctx).map(|s| s.to_ascii_lowercase()); + let dir = args.get::>(ctx, "dir"); + + Value::template("lang", move |ctx| { + if let Some(iso) = &iso { + ctx.state.lang.dir = lang_dir(iso); + } + + if let Some(dir) = dir { + if dir.v.axis() == SpecAxis::Horizontal { + ctx.state.lang.dir = dir.v; + } else { + ctx.diag(error!(dir.span, "must be horizontal")); + } + } + + ctx.push_parbreak(); + }) +} + +/// The default direction for the language identified by `iso`. +fn lang_dir(iso: &str) -> Dir { + match iso { + "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL, + "en" | "fr" | "de" | _ => Dir::LTR, + } +} diff --git a/src/library/mod.rs b/src/library/mod.rs index 1f412cd0f..9c2a661a6 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -7,6 +7,7 @@ mod align; mod base; mod font; mod image; +mod lang; mod markup; mod pad; mod page; @@ -18,6 +19,7 @@ pub use self::image::*; pub use align::*; pub use base::*; pub use font::*; +pub use lang::*; pub use markup::*; pub use pad::*; pub use page::*; @@ -31,7 +33,7 @@ use fontdock::{FontStyle, FontWeight}; use crate::eval::{AnyValue, FuncValue, Scope}; use crate::eval::{EvalContext, FuncArgs, TemplateValue, Value}; -use crate::exec::{Exec, ExecContext, FontFamily}; +use crate::exec::{Exec, FontFamily}; use crate::font::VerticalFontMetric; use crate::geom::*; use crate::syntax::{Node, Spanned}; @@ -67,6 +69,7 @@ pub fn _new() -> Scope { func!("font", font); func!("h", h); func!("image", image); + func!("lang", lang); func!("pad", pad); func!("page", page); func!("pagebreak", pagebreak); @@ -79,8 +82,10 @@ pub fn _new() -> Scope { func!("v", v); // Constants. - constant!("left", AlignValue::Left); + constant!("start", AlignValue::Start); constant!("center", AlignValue::Center); + constant!("end", AlignValue::End); + constant!("left", AlignValue::Left); constant!("right", AlignValue::Right); constant!("top", AlignValue::Top); constant!("bottom", AlignValue::Bottom); diff --git a/src/library/pad.rs b/src/library/pad.rs index 5a685d2ae..d6b690070 100644 --- a/src/library/pad.rs +++ b/src/library/pad.rs @@ -31,9 +31,7 @@ pub fn pad(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { ); Value::template("pad", move |ctx| { - let snapshot = ctx.state.clone(); - let child = ctx.exec(&body).into(); - ctx.push(PadNode { padding, child }); - ctx.state = snapshot; + let child = ctx.exec_group(&body).into(); + ctx.push_into_par(PadNode { padding, child }); }) } diff --git a/src/library/page.rs b/src/library/page.rs index 89722ba3b..fb3542edc 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -17,19 +17,10 @@ use crate::paper::{Paper, PaperClass}; /// - Top margin: `top`, of type `linear` relative to height. /// - Bottom margin: `bottom`, of type `linear` relative to height. /// - Flip width and height: `flip`, of type `bool`. -/// - Main layouting direction: `main-dir`, of type `direction`. -/// - Cross layouting direction: `cross-dir`, of type `direction`. /// /// # Return value /// A template that configures page properties. The effect is scoped to the body /// if present. -/// -/// # Relevant types and constants -/// - Type `direction` -/// - `ltr` (left to right) -/// - `rtl` (right to left) -/// - `ttb` (top to bottom) -/// - `btt` (bottom to top) pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let paper = args.find::>(ctx).and_then(|name| { Paper::from_name(&name.v).or_else(|| { @@ -46,8 +37,6 @@ pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let right = args.get(ctx, "right"); let bottom = args.get(ctx, "bottom"); let flip = args.get(ctx, "flip"); - let main = args.get(ctx, "main-dir"); - let cross = args.get(ctx, "cross-dir"); let body = args.find::(ctx); let span = args.span; @@ -94,7 +83,6 @@ pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { std::mem::swap(&mut page.size.width, &mut page.size.height); } - ctx.set_dirs(Gen::new(main, cross)); ctx.finish_page(false, true, span); if let Some(body) = &body { diff --git a/src/library/par.rs b/src/library/par.rs index 0467af44b..cf2549bf7 100644 --- a/src/library/par.rs +++ b/src/library/par.rs @@ -2,26 +2,19 @@ use super::*; /// `par`: Configure paragraphs. /// -/// # Positional parameters -/// - Body: optional, of type `template`. -/// /// # Named parameters /// - Paragraph spacing: `spacing`, of type `linear` relative to current font size. /// - Line leading: `leading`, of type `linear` relative to current font size. /// - Word spacing: `word-spacing`, of type `linear` relative to current font size. /// /// # Return value -/// A template that configures paragraph properties. The effect is scoped to the -/// body if present. +/// A template that configures paragraph properties. pub fn par(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let spacing = args.get(ctx, "spacing"); let leading = args.get(ctx, "leading"); let word_spacing = args.get(ctx, "word-spacing"); - let body = args.find::(ctx); Value::template("par", move |ctx| { - let snapshot = ctx.state.clone(); - if let Some(spacing) = spacing { ctx.state.par.spacing = spacing; } @@ -35,10 +28,5 @@ pub fn par(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { } ctx.push_parbreak(); - - if let Some(body) = &body { - body.exec(ctx); - ctx.state = snapshot; - } }) } diff --git a/src/library/shapes.rs b/src/library/shapes.rs index 9f705ef72..6f9e66770 100644 --- a/src/library/shapes.rs +++ b/src/library/shapes.rs @@ -59,21 +59,18 @@ fn rect_impl( body: TemplateValue, ) -> Value { Value::template(name, move |ctx| { - let snapshot = ctx.state.clone(); - let child = ctx.exec(&body).into(); + let child = ctx.exec_group(&body).into(); let node = FixedNode { width, height, aspect, child }; if let Some(color) = fill { - ctx.push(BackgroundNode { + ctx.push_into_par(BackgroundNode { shape: BackgroundShape::Rect, fill: Fill::Color(color), child: node.into(), }); } else { - ctx.push(node); + ctx.push_into_par(node); } - - ctx.state = snapshot; }) } @@ -136,8 +133,7 @@ fn ellipse_impl( // perfectly into the ellipse. const PAD: f64 = 0.5 - SQRT_2 / 4.0; - let snapshot = ctx.state.clone(); - let child = ctx.exec(&body).into(); + let child = ctx.exec_group(&body).into(); let node = FixedNode { width, height, @@ -150,15 +146,13 @@ fn ellipse_impl( }; if let Some(color) = fill { - ctx.push(BackgroundNode { + ctx.push_into_par(BackgroundNode { shape: BackgroundShape::Ellipse, fill: Fill::Color(color), child: node.into(), }); } else { - ctx.push(node); + ctx.push_into_par(node); } - - ctx.state = snapshot; }) } diff --git a/src/library/spacing.rs b/src/library/spacing.rs index d4648566d..6a67a6535 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -1,5 +1,4 @@ use super::*; -use crate::layout::SpacingNode; /// `h`: Horizontal spacing. /// @@ -9,7 +8,7 @@ use crate::layout::SpacingNode; /// # Return value /// A template that inserts horizontal spacing. pub fn h(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - spacing_impl(ctx, args, SpecAxis::Horizontal) + spacing_impl("h", ctx, args, GenAxis::Cross) } /// `v`: Vertical spacing. @@ -20,20 +19,20 @@ pub fn h(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { /// # Return value /// A template that inserts vertical spacing. pub fn v(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - spacing_impl(ctx, args, SpecAxis::Vertical) + spacing_impl("v", ctx, args, GenAxis::Main) } -fn spacing_impl(ctx: &mut EvalContext, args: &mut FuncArgs, axis: SpecAxis) -> Value { +fn spacing_impl( + name: &str, + ctx: &mut EvalContext, + args: &mut FuncArgs, + axis: GenAxis, +) -> Value { let spacing: Option = args.require(ctx, "spacing"); - Value::template("spacing", move |ctx| { + Value::template(name, move |ctx| { if let Some(linear) = spacing { let amount = linear.resolve(ctx.state.font.resolve_size()); - let spacing = SpacingNode { amount, softness: 0 }; - if axis == ctx.state.dirs.main.axis() { - ctx.push_into_stack(spacing); - } else { - ctx.push(spacing); - } + ctx.push_spacing(axis, amount, 0); } }) } diff --git a/tests/ref/library/lang.png b/tests/ref/library/lang.png new file mode 100644 index 0000000000000000000000000000000000000000..98a63b6e7e4115bf5a2670461df8847c58bb2633 GIT binary patch literal 1897 zcmZwIX*k=776$OYB#3mliEZr7Bt=Q|l49&j5ZkS-Rjrnx#fW7FBWfv)*hTHsmNAB+ z?UX37$9AiAX-lI7p`^8^gxad1Zf8Euy+nL8&GNj4@+Sc|rUSILn`qg>-YvERf+2DHw(bxC%0Fq z>n^JO)EIAwE|(e?vgmv2rbO!2Q5=~2=zeKa<{d>!!OHRmHFOS4QIYCzz@*`{0Fe{R zVEXav9Jtv`SR*uCZuHdC%eBPJ9&8V7NBQYjFY#yZR zTp!&K9@7tvh9~==s;^@DwnuzApRd?jz*e(rt93((Le6ap9i&Xq_YMG!T3UiK?fRLFLSI+@p2pxK(qXP8usd; z7E19!d^0@R<}*oj(-cxRygQ6QOs+qEiB~;ee<98KUZ%(b8|UH(6yxWu6o zwW!`b-_s(EL2}iKg2Rd97mb7|kN=w)0~lzP5$~x@8{WC|h8-q&5q9zj0SH>GHc*b+ z)7bi+P)-FP*V(J@8>HM%UhUa!tU{KJ{;N3ME8Yuo*k>$JTRWoJs`E< z2C?@9rl4({S0y4{Lzqmx)*Av7yw-tRCJNyjSvUy|TdMV*fLF3m%RpX;~9I zSTnrv)anPJYRJ|*i(nym*UtLFeIn;_LK?X^yAYHw(Z3)y-0Im;Yh2UzLNc4|!`t0t zLw7t?boGR;kjTTIh;Zx_F3;qA_^W^K8`IEa$DSO-)Ezx?<=` zX&fgGTDwkaROR+c^xbXVpnfc{N8V8%@L|KA99@F+h2~uSkMlXPD4P=71Wgc}sy6U= zyfVvT74u+aw1L(&d;WjA=W8oLg-nylJ#|M4|7o%|dr2V5 ziElk3pWgono$^4i-Uzum@xM(OMUfG=!XQD1ZP0!d<{1<9*?1Z5ROmn|oJ9|?`!PvQ z67(BUBiAk4Z4;5KRi_4Y*6uJBD_B zG-39;*{}H;Qqx9=-Ir1-a#E2+zubI{?VGJm5i0h4olx#!-DOSI4}YoVbq23~PWuXF z7alC;Gx$mO`t6o=@+3}*T4?7|%QeFUD(naon*D|4z*tGB`&5Kjh(q;f_g8&AiqBMO z=(~xAkEumt=0E4M(pVt~PM$fkJ63Ig{~bM{!9ypf*@@(Q&y|Z+iG8grEN^tI1+|2{ z8`EOgTUA~oMcXxXEAXBsl`SVcC?b{j3UjfhO&=!xEPM6&ovmVP`LT*}ZzJkI7;QUA z3tUNK(bA9rLyd5Fb+o(~3$7E0?jT509nI(c770J`Vn%p+B9{Y|RB47-z*`xk3}`u5)Ni|CMS VYT~%Yy@NjuwYPDyZovCc{tKOUhoS%g literal 0 HcmV?d00001 diff --git a/tests/ref/library/page.png b/tests/ref/library/page.png index 8e5a83ffbe1f75afdc5f121aa3ccb990c04aaa3a..9d2a6b959a6c867a995347a0b8d3e4e5df23d961 100644 GIT binary patch literal 7438 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 diff --git a/tests/ref/library/spacing.png b/tests/ref/library/spacing.png index c266b9fab7b0adc692ccd702a380d781dc71cd64..fa403a6dfae692f943c7a648e2bb796d4d2531fe 100644 GIT binary patch literal 3251 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 IGRhVIZ^4rfH~;_u diff --git a/tests/typ/library/lang.typ b/tests/typ/library/lang.typ new file mode 100644 index 000000000..87d2c154c --- /dev/null +++ b/tests/typ/library/lang.typ @@ -0,0 +1,16 @@ +// Test the `lang` function. + +--- +Left to right. + +#lang("ar") +Right to left. + +#lang(dir: ltr) +Back again. + +--- +// Ref: false + +// Error: 12-15 must be horizontal +#lang(dir: ttb) diff --git a/tests/typ/library/page.typ b/tests/typ/library/page.typ index 5123b8762..7f9a0d2c3 100644 --- a/tests/typ/library/page.typ +++ b/tests/typ/library/page.typ @@ -27,9 +27,6 @@ // Error: 7-18 unknown variable #page(nonexistant) -// Error: 17-20 aligned axis -#page(main-dir: ltr) - // Flipped predefined paper. #page("a11", flip: true)[Flipped A11] @@ -38,10 +35,6 @@ #page(flip: true) Wide -// Test changing the layouting directions of pages. -#page(height: 50pt, main-dir: btt, cross-dir: rtl) -Right to left! - --- // Test a combination of pages with bodies and normal content. diff --git a/tests/typ/library/spacing.typ b/tests/typ/library/spacing.typ index 6d50f0dcf..bd38631e0 100644 --- a/tests/typ/library/spacing.typ +++ b/tests/typ/library/spacing.typ @@ -16,10 +16,3 @@ Relative #h(100%) spacing // Missing spacing. // Error: 12 missing argument: spacing Totally #h() ignored - -// Swapped axes. -#page(main-dir: rtl, cross-dir: ttb, height: 80pt)[ - 1 #h(1cm) 2 - - 3 #v(1cm) 4 #v(-1cm) 5 -]