use std::sync::{Mutex, OnceLock};
use fenestra_core::{Color, Element, Fonts, FrameState, Theme, build_frame};
use image::RgbaImage;
use crate::{Headless, ShellError};
static SHARED: OnceLock<Mutex<Headless>> = OnceLock::new();
static FONTS: OnceLock<Mutex<Fonts>> = OnceLock::new();
pub fn with_fonts<R>(f: impl FnOnce(&mut Fonts) -> R) -> R {
let fonts = FONTS.get_or_init(|| Mutex::new(Fonts::embedded()));
let mut guard = fonts
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
f(&mut guard)
}
pub fn with_headless<R>(f: impl FnOnce(&mut Headless) -> R) -> Result<R, ShellError> {
if SHARED.get().is_none() {
let headless = Headless::new()?;
let _ = SHARED.set(Mutex::new(headless));
}
let mutex = SHARED.get().ok_or(ShellError::NoDevice)?;
let mut guard = mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Ok(f(&mut guard))
}
pub fn render_element<Msg>(el: Element<Msg>, theme: &Theme, size: (u32, u32)) -> RgbaImage {
let mut state = FrameState::new();
state.reduced_motion = true;
render_element_with_state(el, theme, size, &mut state)
}
pub fn render_element_with<Msg>(
el: Element<Msg>,
theme: &Theme,
size: (u32, u32),
fonts: &mut Fonts,
) -> RgbaImage {
let size =
with_headless(|h| h.clamp_size(size.0, size.1)).expect("headless renderer unavailable");
let mut state = FrameState::new();
state.reduced_motion = true;
#[expect(clippy::cast_precision_loss, reason = "window sizes fit in f32")]
let frame = build_frame(
&el,
theme,
fonts,
&mut state,
(size.0 as f32, size.1 as f32),
1.0,
);
with_headless(|headless| {
headless.render_plan(&frame, fonts, &mut state, size.0, size.1, theme.bg)
})
.expect("headless renderer unavailable")
.expect("headless render failed")
}
pub fn render_element_over<Msg>(
el: Element<Msg>,
theme: &Theme,
size: (u32, u32),
scale: f64,
bg: Color,
fonts: &mut Fonts,
state: &mut FrameState,
) -> Result<RgbaImage, ShellError> {
let scale = if scale.is_finite() && scale > 0.0 {
scale
} else {
1.0
};
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "physical sizes are clamped to the device range right after"
)]
let physical = (
(f64::from(size.0) * scale).round() as u32,
(f64::from(size.1) * scale).round() as u32,
);
let (pw, ph) = with_headless(|h| h.clamp_size(physical.0, physical.1))?;
#[expect(clippy::cast_precision_loss, reason = "window sizes fit in f32")]
let logical = (size.0 as f32, size.1 as f32);
let frame = build_frame(&el, theme, fonts, state, logical, scale);
if (scale - 1.0).abs() < f64::EPSILON {
return with_headless(|headless| headless.render_plan(&frame, fonts, state, pw, ph, bg))?;
}
let logical_scene = frame.paint(fonts, state);
let mut scene = vello::Scene::new();
scene.append(&logical_scene, Some(vello::kurbo::Affine::scale(scale)));
with_headless(|headless| headless.render(&scene, pw, ph, bg))?
}
pub fn render_element_with_state<Msg>(
el: Element<Msg>,
theme: &Theme,
size: (u32, u32),
state: &mut FrameState,
) -> RgbaImage {
let size =
with_headless(|h| h.clamp_size(size.0, size.1)).expect("headless renderer unavailable");
with_fonts(|fonts| {
#[expect(clippy::cast_precision_loss, reason = "window sizes fit in f32")]
let frame = build_frame(
&el,
theme,
fonts,
state,
(size.0 as f32, size.1 as f32),
1.0,
);
with_headless(|headless| {
headless.render_plan(&frame, fonts, state, size.0, size.1, theme.bg)
})
.expect("headless renderer unavailable")
})
.expect("headless render failed")
}