Skip to main content

blockworx/script/
headless.rs

1//! A [`Session`] plus the harness to drive it without a windowing session: a
2//! context with the canvas fonts installed and a throwaway painter for the
3//! tools to draw into. egui is GPU-free until tessellation, so this runs in
4//! plain `cargo test`.
5
6use crate::schema::model as schema;
7use crate::theme::Theme;
8
9use super::lowering::{Lowering, SimFrame, UnresolvedTarget};
10use super::session::Session;
11use super::step::Script;
12
13pub struct Headless {
14    ctx: egui::Context,
15    theme: Theme,
16    session: Session,
17}
18
19impl Headless {
20    pub fn new(initial_kdl: Option<&str>, name: &str) -> Self {
21        let ctx = egui::Context::default();
22        // The canvas draws in `CANVAS_FAMILY`, which nothing binds until the
23        // app's fonts are installed; `set_fonts` lands on the *next* pass, so
24        // run an empty one before the first real frame.
25        ctx.set_fonts(crate::font::build_fonts(
26            crate::preferences::FontChoice::default(),
27        ));
28        let _ = ctx.run_ui(egui::RawInput::default(), |_| {});
29        Self {
30            ctx,
31            theme: Theme::default(),
32            session: Session::new(initial_kdl, name),
33        }
34    }
35
36    /// Replay a whole script, resolving doc-anchored targets against the
37    /// session's own document as each step begins. Ends with one idle frame,
38    /// which delivers a queued editor commit (Enter lands one frame after
39    /// the text) and is otherwise a no-op.
40    pub fn run_script(&mut self, script: &Script) -> Result<(), UnresolvedTarget> {
41        let mut lowering = Lowering::new(script);
42        loop {
43            let next = lowering.next(|| self.session.projected())?;
44            match next {
45                Some(frame) => self.step(frame),
46                None => break,
47            }
48        }
49        let idle = self.session.idle_frame();
50        self.step(idle);
51        Ok(())
52    }
53
54    /// One synthetic frame, inside a headless context pass (the session needs
55    /// live fonts to lay text out).
56    fn step(&mut self, frame: SimFrame) {
57        let Self {
58            ctx,
59            theme,
60            session,
61        } = self;
62        let _ = ctx.clone().run_ui(egui::RawInput::default(), |ui| {
63            let painter = ui.ctx().layer_painter(egui::LayerId::new(
64                egui::Order::Background,
65                egui::Id::new("script_headless"),
66            ));
67            session.step(frame, theme, &painter);
68        });
69    }
70
71    pub fn projected(&self) -> schema::Document {
72        self.session.projected()
73    }
74
75    /// Whether the session is mid-rename (used by tests to sanity-check the
76    /// editor seam engaged).
77    #[cfg(test)]
78    pub fn renaming(&self) -> bool {
79        self.session.renaming()
80    }
81}