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 000000000..98a63b6e7 Binary files /dev/null and b/tests/ref/library/lang.png differ diff --git a/tests/ref/library/page.png b/tests/ref/library/page.png index 8e5a83ffb..9d2a6b959 100644 Binary files a/tests/ref/library/page.png and b/tests/ref/library/page.png differ diff --git a/tests/ref/library/spacing.png b/tests/ref/library/spacing.png index c266b9fab..fa403a6df 100644 Binary files a/tests/ref/library/spacing.png and b/tests/ref/library/spacing.png differ 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 -]