From c3acb491e38d333acff6897479f7dd9c86fba307 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 12 Mar 2021 14:16:59 +0100 Subject: [PATCH] =?UTF-8?q?Refactor=20execution=20context=20=F0=9F=8F=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The execution context is a lot more structured: Instead of a magic stack of arbitrary objects there are static objects for pages, stacks and paragraphs - Page softness/keeping mechanic is now a lot simpler than before --- src/exec/context.rs | 386 +++++++++++++++++-------------------- src/exec/mod.rs | 25 ++- src/library/align.rs | 7 +- src/library/pad.rs | 2 +- src/library/page.rs | 11 +- src/library/shapes.rs | 2 +- src/library/spacing.rs | 4 +- tests/typ/library/page.typ | 1 + 8 files changed, 201 insertions(+), 237 deletions(-) diff --git a/src/exec/context.rs b/src/exec/context.rs index 3b0fd897b..e10c28ff4 100644 --- a/src/exec/context.rs +++ b/src/exec/context.rs @@ -1,11 +1,11 @@ -use std::any::Any; +use std::mem; use std::rc::Rc; use fontdock::FontStyle; use super::*; use crate::diag::{Diag, DiagSet}; -use crate::geom::{Dir, Gen, LayoutAligns, LayoutDirs, Length, Linear, Sides, Size}; +use crate::geom::{Dir, Gen, Linear, Sides, Size}; use crate::layout::{ Node, NodePad, NodePages, NodePar, NodeSpacing, NodeStack, NodeText, Tree, }; @@ -20,17 +20,15 @@ pub struct ExecContext<'a> { pub state: State, /// Execution diagnostics. pub diags: DiagSet, - /// The finished page runs. - runs: Vec, - /// The stack of logical groups (paragraphs and such). - /// - /// Each entry contains metadata about the group and nodes that are at the - /// same level as the group, which will return to `inner` once the group is - /// finished. - groups: Vec<(Box, Vec)>, - /// The nodes in the current innermost group - /// (whose metadata is in `groups.last()`). - inner: Vec, + /// The tree of finished page runs. + tree: Tree, + /// Metrics of the active page. + page: PageData, + /// 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: NodeStack, + /// The content of the active paragraph. + par: NodePar, } impl<'a> ExecContext<'a> { @@ -38,185 +36,23 @@ impl<'a> ExecContext<'a> { pub fn new(env: &'a mut Env, state: State) -> Self { Self { env, - state, diags: DiagSet::new(), - runs: vec![], - groups: vec![], - inner: vec![], + tree: Tree { runs: vec![] }, + page: PageData::new(&state, Softness::Hard), + stack: NodeStack::new(&state), + par: NodePar::new(&state), + state, } } - /// Finish execution and return the created layout tree. - pub fn finish(self) -> Pass { - assert!(self.groups.is_empty(), "unfinished group"); - Pass::new(Tree { runs: self.runs }, self.diags) - } - /// Add a diagnostic. pub fn diag(&mut self, diag: Diag) { self.diags.insert(diag); } - /// Push a layout node to the active group. + /// Set the directions. /// - /// Spacing nodes will be handled according to their [`Softness`]. - pub fn push(&mut self, node: impl Into) { - let node = node.into(); - - if let Node::Spacing(this) = node { - if this.softness == Softness::Soft && self.inner.is_empty() { - return; - } - - if let Some(&Node::Spacing(other)) = self.inner.last() { - if this.softness > other.softness { - self.inner.pop(); - } else if this.softness == Softness::Soft { - return; - } - } - } - - self.inner.push(node); - } - - /// Push a normal word space. - pub fn push_space(&mut self) { - let em = self.state.font.font_size(); - self.push(NodeSpacing { - amount: self.state.par.word_spacing.resolve(em), - softness: Softness::Soft, - }); - } - - /// Push text into the context. - /// - /// The text is split into lines at newlines. - pub fn push_text(&mut self, text: &str) { - let mut newline = false; - for line in text.split_terminator(is_newline) { - if newline { - self.apply_linebreak(); - } - - let node = self.make_text_node(line.into()); - self.push(node); - newline = true; - } - } - - /// Execute a template and return the result as a stack node. - pub fn exec(&mut self, template: &ValueTemplate) -> Node { - let dirs = self.state.dirs; - let aligns = self.state.aligns; - - self.start_group(ContentGroup); - self.start_par_group(); - template.exec(self); - self.end_par_group(); - let children = self.end_group::().1; - - NodeStack { dirs, aligns, children }.into() - } - - /// Start a page group based on the active page state. - /// - /// The `softness` is a hint on whether empty pages should be kept in the - /// output. - /// - /// This also starts an inner paragraph. - pub fn start_page_group(&mut self, softness: Softness) { - self.start_group(PageGroup { - size: self.state.page.size, - padding: self.state.page.margins(), - dirs: self.state.dirs, - aligns: self.state.aligns, - softness, - }); - self.start_par_group(); - } - - /// End a page group, returning its [`Softness`]. - /// - /// Whether the page is kept when it's empty is decided by `keep_empty` - /// based on its softness. If kept, the page is pushed to the finished page - /// runs. - /// - /// This also ends an inner paragraph. - pub fn end_page_group(&mut self, keep_empty: F) -> Softness - where - F: FnOnce(Softness) -> bool, - { - self.end_par_group(); - let (group, children) = self.end_group::(); - if !children.is_empty() || keep_empty(group.softness) { - self.runs.push(NodePages { - size: group.size, - child: NodePad { - padding: group.padding, - child: NodeStack { - dirs: group.dirs, - aligns: group.aligns, - children, - } - .into(), - } - .into(), - }) - } - group.softness - } - - /// Start a paragraph group based on the active text state. - pub fn start_par_group(&mut self) { - let em = self.state.font.font_size(); - self.start_group(ParGroup { - dirs: self.state.dirs, - aligns: self.state.aligns, - line_spacing: self.state.par.line_spacing.resolve(em), - }); - } - - /// End a paragraph group and push it to its parent group if it's not empty. - pub fn end_par_group(&mut self) { - let (group, children) = self.end_group::(); - if !children.is_empty() { - self.push(NodePar { - dirs: group.dirs, - aligns: group.aligns, - line_spacing: group.line_spacing, - children, - }); - } - } - - /// Start a layouting group. - /// - /// All further calls to [`push`](Self::push) will collect nodes for this - /// group. The given metadata will be returned alongside the collected nodes - /// in a matching call to [`end_group`](Self::end_group). - fn start_group(&mut self, meta: T) { - self.groups.push((Box::new(meta), std::mem::take(&mut self.inner))); - } - - /// End a layouting group started with [`start_group`](Self::start_group). - /// - /// This returns the stored metadata and the collected nodes. - #[track_caller] - fn end_group(&mut self) -> (T, Vec) { - if let Some(&Node::Spacing(spacing)) = self.inner.last() { - if spacing.softness == Softness::Soft { - self.inner.pop(); - } - } - - let (any, outer) = self.groups.pop().expect("no pushed group"); - let group = *any.downcast::().expect("bad group type"); - (group, std::mem::replace(&mut self.inner, outer)) - } - - /// Set the directions if they would apply to different axes, producing an - /// appropriate error otherwise. + /// 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), @@ -233,27 +69,77 @@ impl<'a> ExecContext<'a> { } /// Set the font to monospace. - pub fn apply_monospace(&mut self) { + pub fn set_monospace(&mut self) { let families = self.state.font.families_mut(); families.list.insert(0, "monospace".to_string()); families.flatten(); } + /// Push a layout node into the active paragraph. + /// + /// Spacing nodes will be handled according to their [`Softness`]. + pub fn push(&mut self, node: impl Into) { + push(&mut self.par.children, node.into()); + } + + /// Push a word space into the active paragraph. + pub fn push_space(&mut self) { + let em = self.state.font.font_size(); + self.push(NodeSpacing { + amount: self.state.par.word_spacing.resolve(em), + softness: Softness::Soft, + }); + } + + /// Push text into the active paragraph. + /// + /// The text is split into lines at newlines. + pub fn push_text(&mut self, text: &str) { + let mut newline = false; + for line in text.split_terminator(is_newline) { + if newline { + self.push_linebreak(); + } + + let node = self.make_text_node(line.into()); + self.push(node); + newline = true; + } + } + /// Apply a forced line break. - pub fn apply_linebreak(&mut self) { - self.end_par_group(); - self.start_par_group(); + pub fn push_linebreak(&mut self) { + self.finish_par(); } /// Apply a forced paragraph break. - pub fn apply_parbreak(&mut self) { - self.end_par_group(); + pub fn push_parbreak(&mut self) { let em = self.state.font.font_size(); - self.push(NodeSpacing { + self.push_into_stack(NodeSpacing { amount: self.state.par.par_spacing.resolve(em), softness: Softness::Soft, }); - self.start_par_group(); + } + + /// 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: &ValueTemplate) -> NodeStack { + let prev_par = mem::replace(&mut self.par, NodePar::new(&self.state)); + let prev_stack = mem::replace(&mut self.stack, NodeStack::new(&self.state)); + + template.exec(self); + let stack = self.finish_stack(); + + self.par = prev_par; + self.stack = prev_stack; + + stack } /// Construct a text node from the given string based on the active text @@ -282,35 +168,113 @@ impl<'a> ExecContext<'a> { variant, } } + + /// Finish the active paragraph. + fn finish_par(&mut self) { + let mut par = mem::replace(&mut self.par, NodePar::new(&self.state)); + trim(&mut par.children); + + if !par.children.is_empty() { + self.stack.children.push(par.into()); + } + } + + /// Finish the active stack. + fn finish_stack(&mut self) -> NodeStack { + self.finish_par(); + + let mut stack = mem::replace(&mut self.stack, NodeStack::new(&self.state)); + trim(&mut stack.children); + + stack + } + + /// Finish the active page. + pub fn finish_page(&mut self, keep: bool, new_softnes: Softness) { + let stack = self.finish_stack(); + let data = mem::replace(&mut self.page, PageData::new(&self.state, new_softnes)); + if !stack.children.is_empty() || (keep && data.softness == Softness::Hard) { + self.tree.runs.push(NodePages { + size: data.size, + child: NodePad { + padding: data.padding, + child: stack.into(), + } + .into(), + }); + } + } + + /// Finish execution and return the created layout tree. + pub fn finish(mut self) -> Pass { + self.finish_page(true, Softness::Soft); + Pass::new(self.tree, self.diags) + } } -/// Defines how an item interacts with surrounding items. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub enum Softness { - /// A soft item can be skipped in some circumstances. - Soft, - /// A hard item is always retained. - Hard, +/// 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 spacing.softness == Softness::Soft && nodes.is_empty() { + return; + } + + if let Some(&Node::Spacing(other)) = nodes.last() { + if spacing.softness > other.softness { + nodes.pop(); + } else if spacing.softness == Softness::Soft { + return; + } + } + } + + 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 == Softness::Soft { + nodes.pop(); + } + } } -/// A group for a page run. #[derive(Debug)] -struct PageGroup { - dirs: LayoutDirs, - aligns: LayoutAligns, +struct PageData { size: Size, padding: Sides, softness: Softness, } -/// A group for generic content. -#[derive(Debug)] -struct ContentGroup; - -/// A group for a paragraph. -#[derive(Debug)] -struct ParGroup { - dirs: LayoutDirs, - aligns: LayoutAligns, - line_spacing: Length, +impl PageData { + fn new(state: &State, softness: Softness) -> Self { + Self { + size: state.page.size, + padding: state.page.margins(), + softness, + } + } +} + +impl NodeStack { + fn new(state: &State) -> Self { + Self { + dirs: state.dirs, + aligns: state.aligns, + children: vec![], + } + } +} + +impl NodePar { + fn new(state: &State) -> Self { + let em = state.font.font_size(); + Self { + dirs: state.dirs, + aligns: state.aligns, + line_spacing: state.par.line_spacing.resolve(em), + children: vec![], + } + } } diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 58b5cdc05..45abca021 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -31,12 +31,19 @@ pub fn exec( state: State, ) -> Pass { let mut ctx = ExecContext::new(env, state); - ctx.start_page_group(Softness::Hard); tree.exec_with_map(&mut ctx, &map); - ctx.end_page_group(|s| s == Softness::Hard); ctx.finish() } +/// Defines how an item interacts with surrounding items. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum Softness { + /// A soft item can be skipped in some circumstances. + Soft, + /// A hard item is always retained. + Hard, +} + /// Execute a node. /// /// This manipulates active styling and document state and produces layout @@ -68,8 +75,8 @@ impl ExecWithMap for Node { match self { Node::Text(text) => ctx.push_text(text), Node::Space => ctx.push_space(), - Node::Linebreak => ctx.apply_linebreak(), - Node::Parbreak => ctx.apply_parbreak(), + Node::Linebreak => ctx.push_linebreak(), + Node::Parbreak => ctx.push_parbreak(), Node::Strong => ctx.state.font.strong ^= true, Node::Emph => ctx.state.font.emph ^= true, Node::Heading(heading) => heading.exec_with_map(ctx, map), @@ -87,7 +94,7 @@ impl ExecWithMap for NodeHeading { ctx.state.font.strong = true; self.contents.exec_with_map(ctx, map); - ctx.apply_parbreak(); + ctx.push_parbreak(); ctx.state = prev; } @@ -96,7 +103,7 @@ impl ExecWithMap for NodeHeading { impl Exec for NodeRaw { fn exec(&self, ctx: &mut ExecContext) { let prev = Rc::clone(&ctx.state.font.families); - ctx.apply_monospace(); + ctx.set_monospace(); let em = ctx.state.font.font_size(); let line_spacing = ctx.state.par.line_spacing.resolve(em); @@ -116,7 +123,7 @@ impl Exec for NodeRaw { } if self.block { - ctx.apply_parbreak(); + ctx.push_parbreak(); } // This is wrapped in a fixed node to make sure the stack fits to its @@ -133,7 +140,7 @@ impl Exec for NodeRaw { }); if self.block { - ctx.apply_parbreak(); + ctx.push_parbreak(); } ctx.state.font.families = prev; @@ -153,7 +160,7 @@ impl Exec for Value { // For values which can't be shown "naturally", we print // the representation in monospace. let prev = Rc::clone(&ctx.state.font.families); - ctx.apply_monospace(); + ctx.set_monospace(); ctx.push_text(&pretty(other)); ctx.state.font.families = prev; } diff --git a/src/library/align.rs b/src/library/align.rs index 07566f2d3..98feb3ef7 100644 --- a/src/library/align.rs +++ b/src/library/align.rs @@ -89,15 +89,14 @@ pub fn align(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { } } - // 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` 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; } if ctx.state.aligns.main != snapshot.aligns.main { - ctx.end_par_group(); - ctx.start_par_group(); + ctx.push_linebreak(); } if let Some(body) = &body { diff --git a/src/library/pad.rs b/src/library/pad.rs index 0bf93093e..ba28bf8e1 100644 --- a/src/library/pad.rs +++ b/src/library/pad.rs @@ -29,7 +29,7 @@ pub fn pad(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { Value::template("pad", move |ctx| { let snapshot = ctx.state.clone(); - let child = ctx.exec(&body); + let child = ctx.exec(&body).into(); ctx.push(NodePad { padding, child }); ctx.state = snapshot; diff --git a/src/library/page.rs b/src/library/page.rs index 4ab92b31e..963ab9137 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -83,25 +83,20 @@ pub fn page(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { } ctx.set_dirs(Gen::new(main, cross)); + ctx.finish_page(false, Softness::Hard); - let mut softness = ctx.end_page_group(|_| false); if let Some(body) = &body { // TODO: Restrict body to a single page? - ctx.start_page_group(Softness::Hard); body.exec(ctx); - ctx.end_page_group(|s| s == Softness::Hard); - softness = Softness::Soft; ctx.state = snapshot; + ctx.finish_page(true, Softness::Soft); } - - ctx.start_page_group(softness); }) } /// `pagebreak`: Start a new page. pub fn pagebreak(_: &mut EvalContext, _: &mut ValueArgs) -> Value { Value::template("pagebreak", move |ctx| { - ctx.end_page_group(|_| true); - ctx.start_page_group(Softness::Hard); + ctx.finish_page(true, Softness::Hard); }) } diff --git a/src/library/shapes.rs b/src/library/shapes.rs index 9c4663185..254a22852 100644 --- a/src/library/shapes.rs +++ b/src/library/shapes.rs @@ -31,7 +31,7 @@ pub fn box_(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { ctx.set_dirs(Gen::new(main, cross)); - let child = ctx.exec(&body); + let child = ctx.exec(&body).into(); let fixed = NodeFixed { width, height, child }; if let Some(color) = color { ctx.push(NodeBackground { diff --git a/src/library/spacing.rs b/src/library/spacing.rs index 624890e93..3cc718b2f 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -25,9 +25,7 @@ fn spacing(ctx: &mut EvalContext, args: &mut ValueArgs, axis: SpecAxis) -> Value let amount = linear.resolve(ctx.state.font.font_size()); let spacing = NodeSpacing { amount, softness: Softness::Hard }; if axis == ctx.state.dirs.main.axis() { - ctx.end_par_group(); - ctx.push(spacing); - ctx.start_par_group(); + ctx.push_into_stack(spacing); } else { ctx.push(spacing); } diff --git a/tests/typ/library/page.typ b/tests/typ/library/page.typ index ac222e043..5123b8762 100644 --- a/tests/typ/library/page.typ +++ b/tests/typ/library/page.typ @@ -50,6 +50,7 @@ Right to left! #page[First] #page[Second] #pagebreak() +#pagebreak() Fourth #page[] Sixth