1use std::sync::{Arc, Mutex, PoisonError};
26use std::time::Instant;
27
28use fenestra_core::{
29 App, Element, Fonts, Frame, FrameState, InputEvent, Proxy, Theme, build_frame, dispatch,
30};
31use kurbo::Point;
32use vello::wgpu::{self, util::TextureBlitter};
33use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
34
35use crate::window::{LINE_SCROLL_PX, map_cursor, map_key};
36
37#[derive(Debug, Clone, Copy, Default)]
39pub struct EventResponse {
40 pub consumed: bool,
43 pub repaint: bool,
45}
46
47pub struct Embedded<A: App> {
50 app: A,
51 theme: Theme,
52 fonts: Fonts,
53 state: FrameState,
54 renderer: Renderer,
55 blitter: TextureBlitter,
56 target: Option<(wgpu::Texture, wgpu::TextureView, u32, u32)>,
58 last: Option<(Element<A::Msg>, Frame)>,
59 pending: Arc<Mutex<Vec<A::Msg>>>,
60 cursor: Point,
61 cursor_icon: Option<fenestra_core::Cursor>,
64 modifiers: winit::keyboard::ModifiersState,
65 started: Instant,
66 clear: fenestra_core::Color,
67}
68
69impl<A: App> Embedded<A>
70where
71 A::Msg: Send,
72{
73 pub fn new(
80 mut app: A,
81 theme: Theme,
82 device: &wgpu::Device,
83 target_format: wgpu::TextureFormat,
84 ) -> Self {
85 let renderer = Renderer::new(
86 device,
87 RendererOptions {
88 use_cpu: false,
89 antialiasing_support: AaSupport::area_only(),
90 num_init_threads: std::num::NonZeroUsize::new(1),
91 pipeline_cache: None,
92 },
93 )
94 .expect("vello renderer on caller device");
95 let blitter = wgpu::util::TextureBlitterBuilder::new(device, target_format)
96 .blend_state(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING)
97 .build();
98 let pending: Arc<Mutex<Vec<A::Msg>>> = Arc::new(Mutex::new(Vec::new()));
99 let sink = Arc::clone(&pending);
100 app.init(Proxy::new(move |msg| {
101 sink.lock()
102 .unwrap_or_else(PoisonError::into_inner)
103 .push(msg);
104 }));
105 let mut state = FrameState::new();
106 state.set_clipboard(Box::new(crate::OsClipboard::default()));
107 let clear = theme.bg;
108 Self {
109 app,
110 theme,
111 fonts: Fonts::with_system(),
112 state,
113 renderer,
114 blitter,
115 target: None,
116 last: None,
117 pending,
118 cursor: Point::ORIGIN,
119 cursor_icon: None,
120 modifiers: winit::keyboard::ModifiersState::default(),
121 started: Instant::now(),
122 clear,
123 }
124 }
125
126 pub fn set_clear(&mut self, color: fenestra_core::Color) {
129 self.clear = color;
130 }
131
132 pub fn set_theme(&mut self, theme: Theme) {
134 self.theme = theme;
135 }
136
137 pub fn app(&self) -> &A {
139 &self.app
140 }
141
142 pub fn app_mut(&mut self) -> &mut A {
144 &mut self.app
145 }
146
147 pub fn pump(&mut self) -> bool {
150 let msgs =
151 std::mem::take(&mut *self.pending.lock().unwrap_or_else(PoisonError::into_inner));
152 let any = !msgs.is_empty();
153 for msg in msgs {
154 self.app.update(msg);
155 }
156 any
157 }
158
159 fn hits(&self, point: Point) -> bool {
160 self.last
161 .as_ref()
162 .is_some_and(|(_, frame)| frame.hit_chain(point).len() > 1)
163 }
164
165 pub fn input(&mut self, event: InputEvent) -> EventResponse {
169 let consumed = match &event {
172 InputEvent::PointerMove { x, y } => self.hits(Point::new(f64::from(*x), f64::from(*y))),
173 InputEvent::PointerDown
174 | InputEvent::PointerUp
175 | InputEvent::RightDown
176 | InputEvent::RightUp
177 | InputEvent::Wheel { .. } => self.hits(self.cursor),
178 InputEvent::Key(_) | InputEvent::Text(_) | InputEvent::ImePreedit { .. } => {
179 self.state.focused().is_some()
180 }
181 _ => false,
182 };
183 if let InputEvent::PointerMove { x, y } = event {
184 self.cursor = Point::new(f64::from(x), f64::from(y));
185 }
186 let Some((view, frame)) = &self.last else {
187 return EventResponse {
188 consumed: false,
189 repaint: true,
190 };
191 };
192 let result = dispatch(view, frame, &mut self.state, &mut self.fonts, event);
193 self.cursor_icon = result.cursor;
194 let had_msgs = !result.msgs.is_empty();
195 for msg in result.msgs {
196 self.app.update(msg);
197 }
198 EventResponse {
199 consumed,
200 repaint: result.redraw || had_msgs,
201 }
202 }
203
204 pub fn handle_window_event(
208 &mut self,
209 window: &winit::window::Window,
210 event: &winit::event::WindowEvent,
211 ) -> EventResponse {
212 use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
213 let scale = window.scale_factor();
214 match event {
215 WindowEvent::CursorMoved { position, .. } =>
216 {
217 #[expect(clippy::cast_possible_truncation, reason = "positions fit in f32")]
218 self.input(InputEvent::PointerMove {
219 x: (position.x / scale) as f32,
220 y: (position.y / scale) as f32,
221 })
222 }
223 WindowEvent::CursorLeft { .. } => self.input(InputEvent::PointerLeave),
224 WindowEvent::MouseInput { state, button, .. } => {
225 let event = match (button, state) {
226 (MouseButton::Left, ElementState::Pressed) => InputEvent::PointerDown,
227 (MouseButton::Left, ElementState::Released) => InputEvent::PointerUp,
228 (MouseButton::Right, ElementState::Pressed) => InputEvent::RightDown,
229 (MouseButton::Right, ElementState::Released) => InputEvent::RightUp,
230 _ => return EventResponse::default(),
231 };
232 let response = self.input(event);
233 if let Some(cursor) = self.cursor_icon.take() {
234 window.set_cursor(winit::window::Cursor::Icon(map_cursor(cursor)));
235 }
236 response
237 }
238 WindowEvent::MouseWheel { delta, .. } => {
239 let dy = match delta {
240 MouseScrollDelta::LineDelta(_, y) => f64::from(*y) * LINE_SCROLL_PX,
241 MouseScrollDelta::PixelDelta(pos) => pos.y / scale,
242 };
243 #[expect(clippy::cast_possible_truncation, reason = "deltas fit in f32")]
244 self.input(InputEvent::Wheel { dy: dy as f32 })
245 }
246 WindowEvent::ModifiersChanged(mods) => {
247 self.modifiers = mods.state();
248 let m = self.modifiers;
249 self.input(InputEvent::Modifiers {
250 shift: m.shift_key(),
251 ctrl: m.control_key(),
252 alt: m.alt_key(),
253 meta: m.super_key(),
254 })
255 }
256 WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => {
257 let mods = self.modifiers;
258 let printable = !mods.control_key()
259 && !mods.super_key()
260 && event
261 .text
262 .as_ref()
263 .is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control()));
264 if printable {
265 match &event.text {
266 Some(t) => self.input(InputEvent::Text(t.to_string())),
267 None => EventResponse::default(),
268 }
269 } else if let Some(input) = map_key(event, mods) {
270 self.input(input)
271 } else {
272 EventResponse::default()
273 }
274 }
275 WindowEvent::Ime(ime) => match ime {
276 winit::event::Ime::Preedit(text, cursor) => self.input(InputEvent::ImePreedit {
277 text: text.clone(),
278 cursor: *cursor,
279 }),
280 winit::event::Ime::Commit(text) => self.input(InputEvent::Text(text.clone())),
281 _ => EventResponse::default(),
282 },
283 _ => EventResponse::default(),
284 }
285 }
286
287 pub fn animating(&self) -> bool {
289 self.last.as_ref().is_some_and(|(_, f)| f.animating)
290 }
291
292 pub fn render(
300 &mut self,
301 device: &wgpu::Device,
302 queue: &wgpu::Queue,
303 target: &wgpu::TextureView,
304 physical: (u32, u32),
305 scale: f64,
306 ) {
307 self.pump();
308 let (pw, ph) = (physical.0.max(1), physical.1.max(1));
309 self.state.tick(self.started.elapsed().as_secs_f64());
310 let view = self.app.view();
311 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
312 let logical = (
313 (f64::from(pw) / scale) as f32,
314 (f64::from(ph) / scale) as f32,
315 );
316 let frame = build_frame(
317 &view,
318 &self.theme,
319 &mut self.fonts,
320 &mut self.state,
321 logical,
322 scale,
323 );
324 let scene: Scene = frame.paint(&mut self.fonts, &mut self.state);
325
326 if self
327 .target
328 .as_ref()
329 .is_none_or(|(_, _, w, h)| (*w, *h) != (pw, ph))
330 {
331 let texture = device.create_texture(&wgpu::TextureDescriptor {
332 label: Some("fenestra embedded target"),
333 size: wgpu::Extent3d {
334 width: pw,
335 height: ph,
336 depth_or_array_layers: 1,
337 },
338 mip_level_count: 1,
339 sample_count: 1,
340 dimension: wgpu::TextureDimension::D2,
341 format: wgpu::TextureFormat::Rgba8Unorm,
342 usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
343 view_formats: &[],
344 });
345 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
346 self.target = Some((texture, view, pw, ph));
347 }
348 let (_, internal_view, ..) = self.target.as_ref().expect("just ensured");
349
350 self.renderer
351 .render_to_texture(
352 device,
353 queue,
354 &scene,
355 internal_view,
356 &RenderParams {
357 base_color: self.clear,
358 width: pw,
359 height: ph,
360 antialiasing_method: AaConfig::Area,
361 },
362 )
363 .expect("vello render");
364
365 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
366 label: Some("fenestra embedded blit"),
367 });
368 self.blitter
369 .copy(device, &mut encoder, internal_view, target);
370 queue.submit([encoder.finish()]);
371 self.last = Some((view, frame));
372 }
373
374 pub fn frame(&self) -> Option<&Frame> {
377 self.last.as_ref().map(|(_, frame)| frame)
378 }
379
380 pub fn texture_view(&self) -> Option<&wgpu::TextureView> {
384 self.target.as_ref().map(|(_, view, ..)| view)
385 }
386}