use std::sync::{Arc, Mutex, PoisonError};
use fenestra_core::{App, FrameState, InputEvent, KeyInput, Proxy, Theme, build_frame, dispatch};
use image::RgbaImage;
use crate::element_render::with_fonts;
use crate::with_headless;
#[derive(Debug, Clone, PartialEq)]
pub enum SyntheticEvent {
MouseMove {
x: f32,
y: f32,
},
MouseDown,
MouseUp,
RightDown,
RightUp,
FileDrop(std::path::PathBuf),
Key(KeyInput),
Text(String),
Wheel {
dy: f32,
},
Tab,
ShiftTab,
}
impl From<&SyntheticEvent> for InputEvent {
fn from(ev: &SyntheticEvent) -> Self {
match ev {
SyntheticEvent::MouseMove { x, y } => Self::PointerMove { x: *x, y: *y },
SyntheticEvent::MouseDown => Self::PointerDown,
SyntheticEvent::MouseUp => Self::PointerUp,
SyntheticEvent::RightDown => Self::RightDown,
SyntheticEvent::RightUp => Self::RightUp,
SyntheticEvent::FileDrop(p) => Self::FileDrop(p.clone()),
SyntheticEvent::Key(k) => Self::Key(*k),
SyntheticEvent::Text(s) => Self::Text(s.clone()),
SyntheticEvent::Wheel { dy } => Self::Wheel { dy: *dy },
SyntheticEvent::Tab => Self::Tab,
SyntheticEvent::ShiftTab => Self::ShiftTab,
}
}
}
pub fn render_app<A: App>(
app: &mut A,
events: &[SyntheticEvent],
size: (u32, u32),
theme: &Theme,
) -> RgbaImage
where
A::Msg: Send,
{
let size =
with_headless(|h| h.clamp_size(size.0, size.1)).expect("headless renderer unavailable");
let pending: Arc<Mutex<Vec<A::Msg>>> = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&pending);
app.init(Proxy::new(move |msg| {
sink.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(msg);
}));
fn drain<A: App>(app: &mut A, pending: &Mutex<Vec<A::Msg>>) {
let msgs = std::mem::take(&mut *pending.lock().unwrap_or_else(PoisonError::into_inner));
for msg in msgs {
app.update(msg);
}
}
let mut state = FrameState::new();
state.reduced_motion = true;
#[expect(clippy::cast_precision_loss, reason = "window sizes fit in f32")]
let logical = (size.0 as f32, size.1 as f32);
let scene = with_fonts(|fonts| {
for ev in events {
drain(app, &pending);
let view = app.view();
let frame = build_frame(&view, theme, fonts, &mut state, logical, 1.0);
let result = dispatch(&view, &frame, &mut state, fonts, ev.into());
for msg in result.msgs {
app.update(msg);
}
}
drain(app, &pending);
let view = app.view();
let frame = build_frame(&view, theme, fonts, &mut state, logical, 1.0);
frame.paint(fonts, &mut state)
});
with_headless(|headless| headless.render(&scene, size.0, size.1, theme.bg))
.expect("headless renderer unavailable")
.expect("headless render failed")
}