Skip to main content

fenestra_shell/
synthetic.rs

1//! Synthetic event injection for headless testing: agents drive an [`App`]
2//! with scripted input and look at the resulting pixels.
3
4use fenestra_core::{App, InputEvent, KeyInput, Theme};
5use image::RgbaImage;
6
7/// A scripted input event for [`render_app`].
8#[derive(Debug, Clone, PartialEq)]
9pub enum SyntheticEvent {
10    /// Move the pointer to logical coordinates.
11    MouseMove {
12        /// Logical x.
13        x: f32,
14        /// Logical y.
15        y: f32,
16    },
17    /// Press the primary button.
18    MouseDown,
19    /// Release the primary button.
20    MouseUp,
21    /// Press the secondary (right) button.
22    RightDown,
23    /// Release the secondary (right) button.
24    RightUp,
25    /// Drop an OS file at the current pointer position.
26    FileDrop(std::path::PathBuf),
27    /// Press a key.
28    Key(KeyInput),
29    /// Commit text (M5).
30    Text(String),
31    /// Scroll (winit convention: positive `dy` moves content down, positive
32    /// `dx` moves content right).
33    Wheel {
34        /// Horizontal delta in logical px.
35        dx: f32,
36        /// Vertical delta in logical px.
37        dy: f32,
38    },
39    /// Focus next.
40    Tab,
41    /// Focus previous.
42    ShiftTab,
43    /// Modifier keys changed (shift, ctrl, alt, meta).
44    Modifiers(bool, bool, bool, bool),
45}
46
47impl From<&SyntheticEvent> for InputEvent {
48    fn from(ev: &SyntheticEvent) -> Self {
49        match ev {
50            SyntheticEvent::MouseMove { x, y } => Self::PointerMove { x: *x, y: *y },
51            SyntheticEvent::MouseDown => Self::PointerDown,
52            SyntheticEvent::MouseUp => Self::PointerUp,
53            SyntheticEvent::RightDown => Self::RightDown,
54            SyntheticEvent::RightUp => Self::RightUp,
55            SyntheticEvent::FileDrop(p) => Self::FileDrop(p.clone()),
56            SyntheticEvent::Key(k) => Self::Key(*k),
57            SyntheticEvent::Text(s) => Self::Text(s.clone()),
58            SyntheticEvent::Wheel { dx, dy } => Self::Wheel { dx: *dx, dy: *dy },
59            SyntheticEvent::Tab => Self::Tab,
60            SyntheticEvent::ShiftTab => Self::ShiftTab,
61            SyntheticEvent::Modifiers(shift, ctrl, alt, meta) => Self::Modifiers {
62                shift: *shift,
63                ctrl: *ctrl,
64                alt: *alt,
65                meta: *meta,
66            },
67        }
68    }
69}
70
71/// Drives an app headlessly: dispatches each event against the current
72/// view, applies the emitted messages, then renders one settle frame.
73/// Deterministic: scale 1.0, reduced motion, embedded fonts only. The
74/// requested size is clamped to the device-supported range (at least 1x1,
75/// at most the maximum texture dimension).
76///
77/// [`App::init`] runs first with a collecting [`Proxy`]; proxied messages
78/// are applied at deterministic points (before each event and before the
79/// settle frame). Messages sent from spawned threads race those drain
80/// points — keep proxy use synchronous in tests.
81///
82/// # Panics
83/// If no compute-capable GPU adapter exists or rendering fails.
84pub fn render_app<A: App>(
85    app: &mut A,
86    events: &[SyntheticEvent],
87    size: (u32, u32),
88    theme: &Theme,
89) -> RgbaImage
90where
91    A::Msg: Send,
92{
93    let mut harness = crate::Harness::new(&mut *app, theme.clone(), size);
94    for ev in events {
95        harness.input(ev.into());
96    }
97    harness.render()
98}