summaryrefslogtreecommitdiff
path: root/src/exec/context.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/exec/context.rs')
-rw-r--r--src/exec/context.rs316
1 files changed, 316 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,
+}