fenestra_shell/
synthetic.rs1use std::sync::{Arc, Mutex, PoisonError};
5
6use fenestra_core::{App, FrameState, InputEvent, KeyInput, Proxy, Theme, build_frame, dispatch};
7use image::RgbaImage;
8
9use crate::element_render::with_fonts;
10use crate::with_headless;
11
12#[derive(Debug, Clone, PartialEq)]
14pub enum SyntheticEvent {
15 MouseMove {
17 x: f32,
19 y: f32,
21 },
22 MouseDown,
24 MouseUp,
26 RightDown,
28 RightUp,
30 FileDrop(std::path::PathBuf),
32 Key(KeyInput),
34 Text(String),
36 Wheel {
38 dy: f32,
40 },
41 Tab,
43 ShiftTab,
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 { dy } => Self::Wheel { dy: *dy },
59 SyntheticEvent::Tab => Self::Tab,
60 SyntheticEvent::ShiftTab => Self::ShiftTab,
61 }
62 }
63}
64
65pub fn render_app<A: App>(
79 app: &mut A,
80 events: &[SyntheticEvent],
81 size: (u32, u32),
82 theme: &Theme,
83) -> RgbaImage
84where
85 A::Msg: Send,
86{
87 let size =
89 with_headless(|h| h.clamp_size(size.0, size.1)).expect("headless renderer unavailable");
90 let pending: Arc<Mutex<Vec<A::Msg>>> = Arc::new(Mutex::new(Vec::new()));
91 let sink = Arc::clone(&pending);
92 app.init(Proxy::new(move |msg| {
93 sink.lock()
94 .unwrap_or_else(PoisonError::into_inner)
95 .push(msg);
96 }));
97 fn drain<A: App>(app: &mut A, pending: &Mutex<Vec<A::Msg>>) {
98 let msgs = std::mem::take(&mut *pending.lock().unwrap_or_else(PoisonError::into_inner));
99 for msg in msgs {
100 app.update(msg);
101 }
102 }
103 let mut state = FrameState::new();
104 state.reduced_motion = true;
105 #[expect(clippy::cast_precision_loss, reason = "window sizes fit in f32")]
106 let logical = (size.0 as f32, size.1 as f32);
107
108 let scene = with_fonts(|fonts| {
109 for ev in events {
110 drain(app, &pending);
111 let view = app.view();
112 let frame = build_frame(&view, theme, fonts, &mut state, logical, 1.0);
113 let result = dispatch(&view, &frame, &mut state, fonts, ev.into());
114 for msg in result.msgs {
115 app.update(msg);
116 }
117 }
118 drain(app, &pending);
120 let view = app.view();
121 let frame = build_frame(&view, theme, fonts, &mut state, logical, 1.0);
122 frame.paint(fonts, &mut state)
123 });
124 with_headless(|headless| headless.render(&scene, size.0, size.1, theme.bg))
125 .expect("headless renderer unavailable")
126 .expect("headless render failed")
127}