diff options
| author | Laurenz <laurmaedje@gmail.com> | 2021-02-09 19:46:57 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2021-02-09 19:46:57 +0100 |
| commit | 06ca740d01b428f12f6bd327257cd05dce737b03 (patch) | |
| tree | 995bf8ff3a606aedecf296c9e805e11e9cd0ae8e /src/exec | |
| parent | e35bbfffcb1f84b2fb0679759152ca0a5eabfad4 (diff) | |
Split evaluation and execution 🔪
Diffstat (limited to 'src/exec')
| -rw-r--r-- | src/exec/context.rs | 316 | ||||
| -rw-r--r-- | src/exec/mod.rs | 161 | ||||
| -rw-r--r-- | src/exec/state.rs | 163 |
3 files changed, 640 insertions, 0 deletions
diff --git a/src/exec/context.rs b/src/exec/context.rs new file mode 100644 index 00000000..a1998f29 --- /dev/null +++ b/src/exec/context.rs @@ -0,0 +1,316 @@ +use std::any::Any; +use std::rc::Rc; + +use fontdock::FontStyle; + +use super::*; +use crate::diag::Diag; +use crate::diag::{Deco, Feedback, Pass}; +use crate::geom::{ChildAlign, Dir, Gen, LayoutDirs, Length, Linear, Sides, Size}; +use crate::layout::{ + Expansion, Node, NodePad, NodePages, NodePar, NodeSpacing, NodeStack, NodeText, Tree, +}; + +/// The context for execution. +#[derive(Debug)] +pub struct ExecContext<'a> { + /// The environment from which resources are gathered. + pub env: &'a mut Env, + /// The active execution state. + pub state: State, + /// The accumulated feedback. + feedback: Feedback, + /// The finished page runs. + runs: Vec<NodePages>, + /// The stack of logical groups (paragraphs and such). + /// + /// Each entry contains metadata about the group and nodes that are at the + /// same level as the group, which will return to `inner` once the group is + /// finished. + groups: Vec<(Box<dyn Any>, Vec<Node>)>, + /// The nodes in the current innermost group + /// (whose metadata is in `groups.last()`). + inner: Vec<Node>, +} + +impl<'a> ExecContext<'a> { + /// Create a new execution context with a base state. + pub fn new(env: &'a mut Env, state: State) -> Self { + Self { + env, + state, + groups: vec![], + inner: vec![], + runs: vec![], + feedback: Feedback::new(), + } + } + + /// Finish execution and return the created layout tree. + pub fn finish(self) -> Pass<Tree> { + assert!(self.groups.is_empty(), "unfinished group"); + Pass::new(Tree { runs: self.runs }, self.feedback) + } + + /// Add a diagnostic to the feedback. + pub fn diag(&mut self, diag: Spanned<Diag>) { + self.feedback.diags.push(diag); + } + + /// Add a decoration to the feedback. + pub fn deco(&mut self, deco: Spanned<Deco>) { + self.feedback.decos.push(deco); + } + + /// Push a layout node to the active group. + /// + /// Spacing nodes will be handled according to their [`Softness`]. + pub fn push(&mut self, node: impl Into<Node>) { + 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); + } + + /// 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, + expand: self.state.page.expand, + padding: self.state.page.margins(), + dirs: self.state.dirs, + align: self.state.align, + 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<F>(&mut self, keep_empty: F) -> Softness + where + F: FnOnce(Softness) -> bool, + { + self.end_par_group(); + let (group, children) = self.end_group::<PageGroup>(); + 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, + align: group.align, + expand: group.expand, + children, + } + .into(), + } + .into(), + }) + } + group.softness + } + + /// Start a content group. + /// + /// This also starts an inner paragraph. + pub fn start_content_group(&mut self) { + self.start_group(ContentGroup); + self.start_par_group(); + } + + /// End a content group and return the resulting nodes. + /// + /// This also ends an inner paragraph. + pub fn end_content_group(&mut self) -> Vec<Node> { + self.end_par_group(); + self.end_group::<ContentGroup>().1 + } + + /// 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, + align: self.state.align, + 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::<ParGroup>(); + if !children.is_empty() { + self.push(NodePar { + dirs: group.dirs, + align: group.align, + // FIXME: This is a hack and should be superseded by something + // better. + cross_expansion: if self.groups.len() <= 1 { + Expansion::Fill + } else { + Expansion::Fit + }, + 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<T: 'static>(&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<T: 'static>(&mut self) -> (T, Vec<Node>) { + 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::<T>().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. + pub fn set_dirs(&mut self, new: Gen<Option<Spanned<Dir>>>) { + 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")); + } + } + } + + /// Push a normal 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 a text node. + pub fn push_text(&mut self, text: impl Into<String>) { + let node = self.make_text_node(text.into()); + self.push(node); + } + + /// Construct a text node from the given string based on the active text + /// state. + pub fn make_text_node(&self, text: String) -> NodeText { + let mut variant = self.state.font.variant; + + if self.state.font.strong { + variant.weight = variant.weight.thicken(300); + } + + if self.state.font.emph { + variant.style = match variant.style { + FontStyle::Normal => FontStyle::Italic, + FontStyle::Italic => FontStyle::Normal, + FontStyle::Oblique => FontStyle::Normal, + } + } + + NodeText { + text, + align: self.state.align, + dir: self.state.dirs.cross, + font_size: self.state.font.font_size(), + families: Rc::clone(&self.state.font.families), + variant, + } + } + + /// Apply a forced line break. + pub fn apply_linebreak(&mut self) { + self.end_par_group(); + self.start_par_group(); + } + + /// Apply a forced paragraph break. + pub fn apply_parbreak(&mut self) { + self.end_par_group(); + let em = self.state.font.font_size(); + self.push(NodeSpacing { + amount: self.state.par.par_spacing.resolve(em), + softness: Softness::Soft, + }); + self.start_par_group(); + } +} + +/// 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, +} + +/// A group for a page run. +#[derive(Debug)] +struct PageGroup { + size: Size, + expand: Spec<Expansion>, + padding: Sides<Linear>, + dirs: LayoutDirs, + align: ChildAlign, + softness: Softness, +} + +/// A group for generic content. +#[derive(Debug)] +struct ContentGroup; + +/// A group for a paragraph. +#[derive(Debug)] +struct ParGroup { + dirs: LayoutDirs, + align: ChildAlign, + line_spacing: Length, +} diff --git a/src/exec/mod.rs b/src/exec/mod.rs new file mode 100644 index 00000000..25edcce3 --- /dev/null +++ b/src/exec/mod.rs @@ -0,0 +1,161 @@ +//! Execution of syntax trees. + +mod context; +mod state; + +pub use context::*; +pub use state::*; + +use std::rc::Rc; + +use crate::diag::Pass; +use crate::env::Env; +use crate::eval::{ExprMap, TemplateAny, TemplateNode, Value, ValueTemplate}; +use crate::geom::Spec; +use crate::layout::{self, Expansion, NodeSpacing, NodeStack}; +use crate::pretty::pretty; +use crate::syntax::*; + +/// Execute a syntax tree to produce a layout tree. +/// +/// The `map` shall be an expression map computed for this tree with +/// [`eval`](crate::eval::eval). Note that `tree` must be the _exact_ same tree +/// as used for evaluation (no cloned version), because the expression map +/// depends on the pointers being stable. +/// +/// The `state` is the base state that may be updated over the course of +/// execution. +pub fn exec( + env: &mut Env, + tree: &Tree, + map: &ExprMap, + state: State, +) -> Pass<layout::Tree> { + let mut ctx = ExecContext::new(env, state); + ctx.start_page_group(Softness::Hard); + tree.exec_with(&mut ctx, &map); + ctx.end_page_group(|s| s == Softness::Hard); + ctx.finish() +} + +/// Execute a node. +/// +/// This manipulates active styling and document state and produces layout +/// nodes. Because syntax nodes and layout nodes do not correspond one-to-one, +/// constructed layout nodes are pushed into the context instead of returned. +/// The context takes care of reshaping the nodes into the correct tree +/// structure. +pub trait Exec { + /// Execute the node. + fn exec(&self, ctx: &mut ExecContext); +} + +/// Execute a node with an expression map that applies to it. +pub trait ExecWith { + /// Execute the node. + fn exec_with(&self, ctx: &mut ExecContext, map: &ExprMap); +} + +impl ExecWith for Tree { + fn exec_with(&self, ctx: &mut ExecContext, map: &ExprMap) { + for node in self { + match node { + Node::Text(text) => ctx.push_text(text), + Node::Space => ctx.push_space(), + Node::Linebreak => ctx.apply_linebreak(), + Node::Parbreak => ctx.apply_parbreak(), + Node::Strong => ctx.state.font.strong ^= true, + Node::Emph => ctx.state.font.emph ^= true, + Node::Heading(heading) => heading.exec_with(ctx, map), + Node::Raw(raw) => raw.exec(ctx), + Node::Expr(expr) => map[&(expr as *const _)].exec(ctx), + } + } + } +} + +impl ExecWith for NodeHeading { + fn exec_with(&self, ctx: &mut ExecContext, map: &ExprMap) { + let prev = ctx.state.clone(); + let upscale = 1.5 - 0.1 * self.level as f64; + ctx.state.font.scale *= upscale; + ctx.state.font.strong = true; + + self.contents.exec_with(ctx, map); + ctx.apply_parbreak(); + + ctx.state = prev; + } +} + +impl Exec for NodeRaw { + fn exec(&self, ctx: &mut ExecContext) { + let prev = Rc::clone(&ctx.state.font.families); + let families = ctx.state.font.families_mut(); + families.list.insert(0, "monospace".to_string()); + families.flatten(); + + let em = ctx.state.font.font_size(); + let line_spacing = ctx.state.par.line_spacing.resolve(em); + + let mut children = vec![]; + for line in &self.lines { + children.push(layout::Node::Text(ctx.make_text_node(line.clone()))); + children.push(layout::Node::Spacing(NodeSpacing { + amount: line_spacing, + softness: Softness::Hard, + })); + } + + if self.block { + ctx.apply_parbreak(); + } + + ctx.push(NodeStack { + dirs: ctx.state.dirs, + align: ctx.state.align, + expand: Spec::uniform(Expansion::Fit), + children, + }); + + if self.block { + ctx.apply_parbreak(); + } + + ctx.state.font.families = prev; + } +} + +impl Exec for Value { + fn exec(&self, ctx: &mut ExecContext) { + match self { + Value::None => {} + Value::Str(s) => ctx.push_text(s), + Value::Template(template) => template.exec(ctx), + other => ctx.push_text(pretty(other)), + } + } +} + +impl Exec for ValueTemplate { + fn exec(&self, ctx: &mut ExecContext) { + for part in self { + part.exec(ctx); + } + } +} + +impl Exec for TemplateNode { + fn exec(&self, ctx: &mut ExecContext) { + match self { + Self::Tree { tree, map } => tree.exec_with(ctx, &map), + Self::Any(any) => any.exec(ctx), + } + } +} + +impl Exec for TemplateAny { + fn exec(&self, ctx: &mut ExecContext) { + self(ctx); + } +} diff --git a/src/exec/state.rs b/src/exec/state.rs new file mode 100644 index 00000000..21fb7fb6 --- /dev/null +++ b/src/exec/state.rs @@ -0,0 +1,163 @@ +use std::rc::Rc; + +use fontdock::{fallback, FallbackTree, FontStretch, FontStyle, FontVariant, FontWeight}; + +use crate::geom::{ + Align, ChildAlign, Dir, LayoutDirs, Length, Linear, Relative, Sides, Size, Spec, +}; +use crate::layout::Expansion; +use crate::paper::{Paper, PaperClass, PAPER_A4}; + +/// The evaluation state. +#[derive(Debug, Clone, PartialEq)] +pub struct State { + /// The current page settings. + pub page: PageSettings, + /// The current paragraph settings. + pub par: ParSettings, + /// The current font settings. + pub font: FontSettings, + /// The current layouting directions. + pub dirs: LayoutDirs, + /// The current alignments of an item in its parent. + pub align: ChildAlign, +} + +impl Default for State { + fn default() -> Self { + Self { + page: PageSettings::default(), + par: ParSettings::default(), + font: FontSettings::default(), + dirs: LayoutDirs::new(Dir::TTB, Dir::LTR), + align: ChildAlign::new(Align::Start, Align::Start), + } + } +} + +/// Defines page properties. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct PageSettings { + /// The class of this page. + pub class: PaperClass, + /// The width and height of the page. + pub size: Size, + /// Whether the expand the pages to the `size` or to fit the content. + pub expand: Spec<Expansion>, + /// The amount of white space in the order [left, top, right, bottom]. If a + /// side is set to `None`, the default for the paper class is used. + pub margins: Sides<Option<Linear>>, +} + +impl PageSettings { + /// The default page style for the given paper. + pub fn new(paper: Paper) -> Self { + Self { + class: paper.class, + size: paper.size(), + expand: Spec::uniform(Expansion::Fill), + margins: Sides::uniform(None), + } + } + + /// The margins. + pub fn margins(&self) -> Sides<Linear> { + let default = self.class.default_margins(); + Sides { + left: self.margins.left.unwrap_or(default.left), + top: self.margins.top.unwrap_or(default.top), + right: self.margins.right.unwrap_or(default.right), + bottom: self.margins.bottom.unwrap_or(default.bottom), + } + } +} + +impl Default for PageSettings { + fn default() -> Self { + Self::new(PAPER_A4) + } +} + +/// Defines paragraph properties. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct ParSettings { + /// The spacing between words (dependent on scaled font size). + pub word_spacing: Linear, + /// The spacing between lines (dependent on scaled font size). + pub line_spacing: Linear, + /// The spacing between paragraphs (dependent on scaled font size). + pub par_spacing: Linear, +} + +impl Default for ParSettings { + fn default() -> Self { + Self { + word_spacing: Relative::new(0.25).into(), + line_spacing: Relative::new(0.2).into(), + par_spacing: Relative::new(0.5).into(), + } + } +} + +/// Defines font properties. +#[derive(Debug, Clone, PartialEq)] +pub struct FontSettings { + /// A tree of font family names and generic class names. + pub families: Rc<FallbackTree>, + /// The selected font variant. + pub variant: FontVariant, + /// The font size. + pub size: Length, + /// The linear to apply on the base font size. + pub scale: Linear, + /// Whether the strong toggle is active or inactive. This determines + /// whether the next `*` adds or removes font weight. + pub strong: bool, + /// Whether the emphasis toggle is active or inactive. This determines + /// whether the next `_` makes italic or non-italic. + pub emph: bool, +} + +impl FontSettings { + /// Access the `families` mutably. + pub fn families_mut(&mut self) -> &mut FallbackTree { + Rc::make_mut(&mut self.families) + } + + /// The absolute font size. + pub fn font_size(&self) -> Length { + self.scale.resolve(self.size) + } +} + +impl Default for FontSettings { + fn default() -> Self { + Self { + /// The default tree of font fallbacks. + families: Rc::new(fallback! { + list: ["sans-serif"], + classes: { + "serif" => ["source serif pro", "noto serif"], + "sans-serif" => ["source sans pro", "noto sans"], + "monospace" => ["source code pro", "noto sans mono"], + }, + base: [ + "source sans pro", + "noto sans", + "segoe ui emoji", + "noto emoji", + "latin modern math", + ], + }), + variant: FontVariant { + style: FontStyle::Normal, + weight: FontWeight::REGULAR, + stretch: FontStretch::Normal, + }, + size: Length::pt(11.0), + scale: Linear::ONE, + strong: false, + emph: false, + } + } +} |
