fenestra_shell/
element_render.rs1use std::sync::{Mutex, OnceLock};
4
5use fenestra_core::{Color, Element, Fonts, FrameState, Theme, build_frame};
6use image::RgbaImage;
7
8use crate::{Headless, ShellError};
9
10static SHARED: OnceLock<Mutex<Headless>> = OnceLock::new();
11static FONTS: OnceLock<Mutex<Fonts>> = OnceLock::new();
12
13pub fn with_fonts<R>(f: impl FnOnce(&mut Fonts) -> R) -> R {
16 let fonts = FONTS.get_or_init(|| Mutex::new(Fonts::embedded()));
17 let mut guard = fonts
18 .lock()
19 .unwrap_or_else(std::sync::PoisonError::into_inner);
20 f(&mut guard)
21}
22
23pub fn with_headless<R>(f: impl FnOnce(&mut Headless) -> R) -> Result<R, ShellError> {
26 if SHARED.get().is_none() {
29 let headless = Headless::new()?;
30 let _ = SHARED.set(Mutex::new(headless));
31 }
32 let mutex = SHARED.get().ok_or(ShellError::NoDevice)?;
33 let mut guard = mutex
34 .lock()
35 .unwrap_or_else(std::sync::PoisonError::into_inner);
36 Ok(f(&mut guard))
37}
38
39pub fn render_element<Msg>(el: Element<Msg>, theme: &Theme, size: (u32, u32)) -> RgbaImage {
50 let mut state = FrameState::new();
51 state.reduced_motion = true;
52 render_element_with_state(el, theme, size, &mut state)
53}
54
55pub fn render_element_with<Msg>(
63 el: Element<Msg>,
64 theme: &Theme,
65 size: (u32, u32),
66 fonts: &mut Fonts,
67) -> RgbaImage {
68 let size =
69 with_headless(|h| h.clamp_size(size.0, size.1)).expect("headless renderer unavailable");
70 let mut state = FrameState::new();
71 state.reduced_motion = true;
72 #[expect(clippy::cast_precision_loss, reason = "window sizes fit in f32")]
73 let frame = build_frame(
74 &el,
75 theme,
76 fonts,
77 &mut state,
78 (size.0 as f32, size.1 as f32),
79 1.0,
80 );
81 with_headless(|headless| {
84 headless.render_plan(&frame, fonts, &mut state, size.0, size.1, theme.bg)
85 })
86 .expect("headless renderer unavailable")
87 .expect("headless render failed")
88}
89
90pub fn render_element_over<Msg>(
111 el: Element<Msg>,
112 theme: &Theme,
113 size: (u32, u32),
114 scale: f64,
115 bg: Color,
116 fonts: &mut Fonts,
117 state: &mut FrameState,
118) -> Result<RgbaImage, ShellError> {
119 let scale = if scale.is_finite() && scale > 0.0 {
120 scale
121 } else {
122 1.0
123 };
124 #[expect(
125 clippy::cast_possible_truncation,
126 clippy::cast_sign_loss,
127 reason = "physical sizes are clamped to the device range right after"
128 )]
129 let physical = (
130 (f64::from(size.0) * scale).round() as u32,
131 (f64::from(size.1) * scale).round() as u32,
132 );
133 let (pw, ph) = with_headless(|h| h.clamp_size(physical.0, physical.1))?;
134 #[expect(clippy::cast_precision_loss, reason = "window sizes fit in f32")]
135 let logical = (size.0 as f32, size.1 as f32);
136 let frame = build_frame(&el, theme, fonts, state, logical, scale);
137 if (scale - 1.0).abs() < f64::EPSILON {
138 return with_headless(|headless| headless.render_plan(&frame, fonts, state, pw, ph, bg))?;
139 }
140 let logical_scene = frame.paint(fonts, state);
141 let mut scene = vello::Scene::new();
142 scene.append(&logical_scene, Some(vello::kurbo::Affine::scale(scale)));
143 with_headless(|headless| headless.render(&scene, pw, ph, bg))?
144}
145
146pub fn render_element_with_state<Msg>(
153 el: Element<Msg>,
154 theme: &Theme,
155 size: (u32, u32),
156 state: &mut FrameState,
157) -> RgbaImage {
158 let size =
160 with_headless(|h| h.clamp_size(size.0, size.1)).expect("headless renderer unavailable");
161 with_fonts(|fonts| {
164 #[expect(clippy::cast_precision_loss, reason = "window sizes fit in f32")]
165 let frame = build_frame(
166 &el,
167 theme,
168 fonts,
169 state,
170 (size.0 as f32, size.1 as f32),
171 1.0,
172 );
173 with_headless(|headless| {
174 headless.render_plan(&frame, fonts, state, size.0, size.1, theme.bg)
175 })
176 .expect("headless renderer unavailable")
177 })
178 .expect("headless render failed")
179}