1use std::sync::Arc;
8#[cfg(not(target_arch = "wasm32"))]
9use std::time::{Duration, Instant};
10#[cfg(target_arch = "wasm32")]
11use web_time::Instant;
12
13#[cfg(not(target_arch = "wasm32"))]
14use fenestra_core::Theme;
15use fenestra_core::{
16 App, Element, Fonts, FrameState, InputEvent, Key, KeyInput, build_frame, dispatch,
17 refresh_hover,
18};
19use kurbo::Point;
20use vello::peniko::Color;
21use vello::util::{RenderContext, RenderSurface};
22use vello::wgpu::{self, CurrentSurfaceTexture};
23use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
24use winit::application::ApplicationHandler;
25use winit::dpi::LogicalSize;
26use winit::event::{MouseScrollDelta, StartCause, WindowEvent};
27#[cfg(not(target_arch = "wasm32"))]
28use winit::event_loop::EventLoopProxy;
29use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
30use winit::window::{Window, WindowId};
31
32use crate::ShellError;
33
34const LINE_SCROLL_PX: f64 = 40.0;
36
37#[cfg(not(target_arch = "wasm32"))]
39type PaintFn = Box<dyn FnMut(&mut Scene, f64, f64, Color)>;
40#[cfg(not(target_arch = "wasm32"))]
42type ViewFn = Box<dyn Fn(&Theme) -> Element<()>>;
43
44#[derive(Debug, Clone)]
46pub struct WindowOptions {
47 pub title: String,
49 pub inner_size: (f64, f64),
51 pub min_size: Option<(f64, f64)>,
53 pub resizable: bool,
55 pub maximized: bool,
57 pub fullscreen: bool,
59 pub icon: Option<(u32, u32, Vec<u8>)>,
61 pub fonts: Vec<(fenestra_core::FamilyRole, Vec<u8>)>,
64}
65
66impl WindowOptions {
67 pub fn titled(title: impl Into<String>) -> Self {
69 Self {
70 title: title.into(),
71 inner_size: (1024.0, 768.0),
72 min_size: None,
73 resizable: true,
74 maximized: false,
75 fullscreen: false,
76 icon: None,
77 fonts: Vec::new(),
78 }
79 }
80
81 pub fn with_size(mut self, width: f64, height: f64) -> Self {
83 self.inner_size = (width, height);
84 self
85 }
86
87 pub fn with_min_size(mut self, width: f64, height: f64) -> Self {
89 self.min_size = Some((width, height));
90 self
91 }
92
93 pub fn with_resizable(mut self, resizable: bool) -> Self {
95 self.resizable = resizable;
96 self
97 }
98
99 pub fn maximized(mut self) -> Self {
101 self.maximized = true;
102 self
103 }
104
105 pub fn fullscreen(mut self) -> Self {
107 self.fullscreen = true;
108 self
109 }
110
111 pub fn with_icon(mut self, width: u32, height: u32, rgba: Vec<u8>) -> Self {
114 self.icon = Some((width, height, rgba));
115 self
116 }
117
118 pub fn with_font(mut self, role: fenestra_core::FamilyRole, data: Vec<u8>) -> Self {
121 self.fonts.push((role, data));
122 self
123 }
124}
125
126enum RenderState {
127 Active {
128 surface: Box<RenderSurface<'static>>,
129 valid_surface: bool,
130 window: Arc<Window>,
131 },
132 Suspended(Option<Arc<Window>>),
133 #[cfg(target_arch = "wasm32")]
135 Pending(Arc<Window>),
136}
137
138struct WindowShell {
140 context: RenderContext,
141 renderers: Vec<Option<Renderer>>,
142 state: RenderState,
143 scene: Scene,
144 options: WindowOptions,
145 background: Color,
146 #[cfg(target_arch = "wasm32")]
149 ready: WasmReady,
150}
151
152#[cfg(target_arch = "wasm32")]
154type WasmReady =
155 std::rc::Rc<std::cell::RefCell<Option<(RenderContext, Box<RenderSurface<'static>>)>>>;
156
157impl WindowShell {
158 fn new(options: WindowOptions, background: Color) -> Self {
159 Self {
160 context: RenderContext::new(),
161 renderers: Vec::new(),
162 state: RenderState::Suspended(None),
163 scene: Scene::new(),
164 options,
165 background,
166 #[cfg(target_arch = "wasm32")]
167 ready: WasmReady::default(),
168 }
169 }
170
171 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
172 self.resumed_with(event_loop, |_, _| {});
173 }
174
175 fn resumed_with(
179 &mut self,
180 event_loop: &ActiveEventLoop,
181 before_visible: impl FnOnce(&ActiveEventLoop, &Arc<Window>),
182 ) {
183 let RenderState::Suspended(cached_window) = &mut self.state else {
184 return;
185 };
186 let window = cached_window.take().unwrap_or_else(|| {
187 let attrs = Window::default_attributes()
188 .with_title(self.options.title.clone())
189 .with_inner_size(LogicalSize::new(
190 self.options.inner_size.0,
191 self.options.inner_size.1,
192 ))
193 .with_resizable(self.options.resizable)
194 .with_maximized(self.options.maximized)
195 .with_visible(false);
196 let attrs = match self.options.min_size {
197 Some((w, h)) => attrs.with_min_inner_size(LogicalSize::new(w, h)),
198 None => attrs,
199 };
200 let attrs = if self.options.fullscreen {
201 attrs.with_fullscreen(Some(winit::window::Fullscreen::Borderless(None)))
202 } else {
203 attrs
204 };
205 #[cfg(not(target_arch = "wasm32"))]
206 let attrs = match self.options.icon.clone() {
207 Some((w, h, rgba)) => match winit::window::Icon::from_rgba(rgba, w, h) {
208 Ok(icon) => attrs.with_window_icon(Some(icon)),
209 Err(_) => attrs,
211 },
212 None => attrs,
213 };
214 #[cfg(target_arch = "wasm32")]
215 let attrs = {
216 use winit::platform::web::WindowAttributesExtWebSys;
217 attrs.with_append(true)
219 };
220 Arc::new(
221 event_loop
222 .create_window(attrs)
223 .expect("failed to create window"),
224 )
225 });
226 before_visible(event_loop, &window);
227 let was_hidden = window.is_visible() == Some(false);
228 self.activate(window.clone());
229 if was_hidden {
230 window.set_visible(true);
231 }
232 }
233
234 #[cfg(not(target_arch = "wasm32"))]
237 fn activate(&mut self, window: Arc<Window>) {
238 let size = window.inner_size();
239 let surface = pollster::block_on(self.context.create_surface(
240 window.clone(),
241 size.width.max(1),
242 size.height.max(1),
243 wgpu::PresentMode::AutoVsync,
244 ))
245 .expect("failed to create wgpu surface");
246
247 self.renderers
248 .resize_with(self.context.devices.len(), || None);
249 self.renderers[surface.dev_id].get_or_insert_with(|| {
250 Renderer::new(
251 &self.context.devices[surface.dev_id].device,
252 RendererOptions {
253 use_cpu: false,
254 antialiasing_support: AaSupport::area_only(),
255 ..Default::default()
256 },
257 )
258 .expect("failed to create vello renderer")
259 });
260
261 self.state = RenderState::Active {
262 surface: Box::new(surface),
263 valid_surface: size.width != 0 && size.height != 0,
264 window,
265 };
266 }
267
268 #[cfg(target_arch = "wasm32")]
271 fn activate(&mut self, window: Arc<Window>) {
272 let size = window.inner_size();
273 let ready = std::rc::Rc::clone(&self.ready);
274 let win = window.clone();
275 wasm_bindgen_futures::spawn_local(async move {
276 let mut context = RenderContext::new();
277 let surface = context
278 .create_surface(
279 win.clone(),
280 size.width.max(1),
281 size.height.max(1),
282 wgpu::PresentMode::AutoVsync,
283 )
284 .await
285 .expect("failed to create wgpu surface");
286 *ready.borrow_mut() = Some((context, Box::new(surface)));
287 win.request_redraw();
288 });
289 self.state = RenderState::Pending(window);
290 }
291
292 fn pump(&mut self) {
295 #[cfg(target_arch = "wasm32")]
296 if let RenderState::Pending(window) = &self.state
297 && let Some((context, surface)) = self.ready.borrow_mut().take()
298 {
299 let window = window.clone();
300 self.context = context;
301 self.renderers.clear();
302 self.renderers
303 .resize_with(self.context.devices.len(), || None);
304 self.renderers[surface.dev_id].get_or_insert_with(|| {
305 Renderer::new(
306 &self.context.devices[surface.dev_id].device,
307 RendererOptions {
308 use_cpu: false,
309 antialiasing_support: AaSupport::area_only(),
310 ..Default::default()
311 },
312 )
313 .expect("failed to create vello renderer")
314 });
315 let size = window.inner_size();
316 self.state = RenderState::Active {
317 surface,
318 valid_surface: size.width != 0 && size.height != 0,
319 window,
320 };
321 }
322 }
323
324 fn suspended(&mut self) {
325 if let RenderState::Active { window, .. } = &self.state {
326 self.state = RenderState::Suspended(Some(window.clone()));
327 }
328 }
329
330 fn window(&self) -> Option<&Arc<Window>> {
331 match &self.state {
332 RenderState::Active { window, .. } => Some(window),
333 _ => None,
334 }
335 }
336
337 fn resized(&mut self, width: u32, height: u32) {
338 let RenderState::Active {
339 surface,
340 valid_surface,
341 window,
342 } = &mut self.state
343 else {
344 return;
345 };
346 if width != 0 && height != 0 {
347 self.context.resize_surface(surface, width, height);
348 *valid_surface = true;
349 } else {
350 *valid_surface = false;
351 }
352 window.request_redraw();
353 }
354
355 fn logical_size(&self) -> Option<(f64, f64, f64)> {
357 match &self.state {
358 RenderState::Active {
359 surface, window, ..
360 } => {
361 let scale = window.scale_factor();
362 Some((
363 f64::from(surface.config.width) / scale,
364 f64::from(surface.config.height) / scale,
365 scale,
366 ))
367 }
368 _ => None,
369 }
370 }
371
372 fn present(&mut self, fragment: &Scene) {
374 let RenderState::Active {
375 surface,
376 valid_surface,
377 window,
378 } = &mut self.state
379 else {
380 return;
381 };
382 if !*valid_surface {
383 return;
384 }
385 let width = surface.config.width;
386 let height = surface.config.height;
387 let scale = window.scale_factor();
388
389 self.scene.reset();
390 self.scene
391 .append(fragment, Some(vello::kurbo::Affine::scale(scale)));
392
393 let handle = &self.context.devices[surface.dev_id];
394 self.renderers[surface.dev_id]
395 .as_mut()
396 .expect("renderer exists for surface device")
397 .render_to_texture(
398 &handle.device,
399 &handle.queue,
400 &self.scene,
401 &surface.target_view,
402 &RenderParams {
403 base_color: self.background,
404 width,
405 height,
406 antialiasing_method: AaConfig::Area,
407 },
408 )
409 .expect("vello render failed");
410
411 let surface_texture = match surface.surface.get_current_texture() {
412 CurrentSurfaceTexture::Success(texture) => texture,
413 CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Suboptimal(_) => {
414 self.context.configure_surface(surface);
415 window.request_redraw();
416 return;
417 }
418 CurrentSurfaceTexture::Occluded => {
419 return;
422 }
423 CurrentSurfaceTexture::Timeout => {
424 window.request_redraw();
425 return;
426 }
427 CurrentSurfaceTexture::Lost => {
428 let window = window.clone();
431 window.request_redraw();
432 self.activate(window);
433 return;
434 }
435 CurrentSurfaceTexture::Validation => {
436 panic!("validation error acquiring wgpu surface texture")
437 }
438 };
439
440 let mut encoder = handle
441 .device
442 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
443 label: Some("fenestra surface blit"),
444 });
445 surface.blitter.copy(
446 &handle.device,
447 &mut encoder,
448 &surface.target_view,
449 &surface_texture
450 .texture
451 .create_view(&wgpu::TextureViewDescriptor::default()),
452 );
453 handle.queue.submit([encoder.finish()]);
454 surface_texture.present();
455 handle.device.poll(wgpu::PollType::Poll).unwrap();
456 }
457}
458
459#[cfg(not(target_arch = "wasm32"))]
465pub fn run_scene(
466 options: WindowOptions,
467 background: Color,
468 paint: impl FnMut(&mut Scene, f64, f64, Color) + 'static,
469) -> Result<(), ShellError> {
470 let event_loop = EventLoop::new().map_err(ShellError::EventLoop)?;
471 let mut app = SceneApp {
472 shell: WindowShell::new(options, background),
473 fragment: Scene::new(),
474 paint: Box::new(paint),
475 };
476 event_loop.run_app(&mut app).map_err(ShellError::EventLoop)
477}
478
479#[cfg(not(target_arch = "wasm32"))]
480struct SceneApp {
481 shell: WindowShell,
482 fragment: Scene,
483 paint: PaintFn,
484}
485
486#[cfg(not(target_arch = "wasm32"))]
487impl ApplicationHandler for SceneApp {
488 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
489 self.shell.resumed(event_loop);
490 }
491
492 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
493 self.shell.suspended();
494 }
495
496 fn window_event(
497 &mut self,
498 event_loop: &ActiveEventLoop,
499 window_id: WindowId,
500 event: WindowEvent,
501 ) {
502 if self.shell.window().is_none_or(|w| w.id() != window_id) {
503 return;
504 }
505 match event {
506 WindowEvent::CloseRequested => event_loop.exit(),
507 WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
508 WindowEvent::ScaleFactorChanged { .. } => {
509 if let Some(w) = self.shell.window() {
510 w.request_redraw();
511 }
512 }
513 WindowEvent::Occluded(occluded) => {
514 if !occluded && let Some(w) = self.shell.window() {
515 w.request_redraw();
516 }
517 }
518 WindowEvent::RedrawRequested => {
519 let Some((lw, lh, _scale)) = self.shell.logical_size() else {
520 return;
521 };
522 self.fragment.reset();
523 let bg = self.shell.background;
524 (self.paint)(&mut self.fragment, lw, lh, bg);
525 let fragment = std::mem::replace(&mut self.fragment, Scene::new());
526 self.shell.present(&fragment);
527 self.fragment = fragment;
528 }
529 _ => {}
530 }
531 }
532}
533
534#[cfg(not(target_arch = "wasm32"))]
540pub fn run_static(
541 options: WindowOptions,
542 theme: Theme,
543 view: impl Fn(&Theme) -> Element<()> + 'static,
544) -> Result<(), ShellError> {
545 let event_loop = EventLoop::new().map_err(ShellError::EventLoop)?;
546 let background = theme.bg;
547 let mut fonts = Fonts::with_system();
548 for (role, data) in &options.fonts {
549 fonts.register(*role, data.clone());
550 }
551 let mut app = StaticApp {
552 shell: WindowShell::new(options, background),
553 theme,
554 fonts,
555 state: FrameState::new(),
556 view: Box::new(view),
557 cursor: Point::ORIGIN,
558 started: Instant::now(),
559 last_frame: None,
560 };
561 event_loop.run_app(&mut app).map_err(ShellError::EventLoop)
562}
563
564#[cfg(not(target_arch = "wasm32"))]
565struct StaticApp {
566 shell: WindowShell,
567 theme: Theme,
568 fonts: Fonts,
569 state: FrameState,
570 view: ViewFn,
571 cursor: Point,
573 started: Instant,
574 last_frame: Option<fenestra_core::Frame>,
576}
577
578#[cfg(not(target_arch = "wasm32"))]
579impl StaticApp {
580 fn redraw(&mut self, event_loop: &ActiveEventLoop) {
581 let Some((lw, lh, scale)) = self.shell.logical_size() else {
582 return;
583 };
584 self.state.tick(self.started.elapsed().as_secs_f64());
585 let el = (self.view)(&self.theme);
586 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
587 let frame = build_frame(
588 &el,
589 &self.theme,
590 &mut self.fonts,
591 &mut self.state,
592 (lw as f32, lh as f32),
593 scale,
594 );
595 let scene = frame.paint(&mut self.fonts, &mut self.state);
596 self.shell.present(&scene);
597 if frame.animating {
598 event_loop.set_control_flow(ControlFlow::WaitUntil(
599 Instant::now() + Duration::from_millis(16),
600 ));
601 } else {
602 event_loop.set_control_flow(ControlFlow::Wait);
603 }
604 self.last_frame = Some(frame);
605 }
606}
607
608#[cfg(not(target_arch = "wasm32"))]
609impl ApplicationHandler for StaticApp {
610 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
611 self.shell.resumed(event_loop);
612 }
613
614 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
615 self.shell.suspended();
616 }
617
618 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
619 if matches!(cause, StartCause::ResumeTimeReached { .. })
620 && let Some(w) = self.shell.window()
621 {
622 w.request_redraw();
623 }
624 }
625
626 fn window_event(
627 &mut self,
628 event_loop: &ActiveEventLoop,
629 window_id: WindowId,
630 event: WindowEvent,
631 ) {
632 if self.shell.window().is_none_or(|w| w.id() != window_id) {
633 return;
634 }
635 match event {
636 WindowEvent::CloseRequested => event_loop.exit(),
637 WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
638 WindowEvent::ScaleFactorChanged { .. } => {
639 if let Some(w) = self.shell.window() {
640 w.request_redraw();
641 }
642 }
643 WindowEvent::CursorMoved { position, .. } => {
644 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
645 self.cursor = Point::new(position.x / scale, position.y / scale);
646 }
647 WindowEvent::MouseWheel { delta, .. } => {
648 let dy = match delta {
649 MouseScrollDelta::LineDelta(_, y) => f64::from(y) * LINE_SCROLL_PX,
650 MouseScrollDelta::PixelDelta(pos) => {
651 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
652 pos.y / scale
653 }
654 };
655 if let Some(frame) = &self.last_frame
656 && let Some(id) = frame.scrollable_at(self.cursor)
657 {
658 #[expect(
659 clippy::cast_possible_truncation,
660 reason = "scroll deltas fit in f32"
661 )]
662 self.state.scroll_by(id, -dy as f32);
663 if let Some(w) = self.shell.window() {
664 w.request_redraw();
665 }
666 }
667 }
668 WindowEvent::RedrawRequested => self.redraw(event_loop),
669 _ => {}
670 }
671 }
672}
673
674enum RunnerEvent {
680 App(Box<dyn std::any::Any + Send>),
681 #[cfg(not(target_arch = "wasm32"))]
682 Access(accesskit_winit::Event),
683}
684
685#[cfg(not(target_arch = "wasm32"))]
686impl From<accesskit_winit::Event> for RunnerEvent {
687 fn from(event: accesskit_winit::Event) -> Self {
688 Self::Access(event)
689 }
690}
691
692pub fn run_app<A: App + 'static>(mut app: A, options: WindowOptions) -> Result<(), ShellError>
698where
699 A::Msg: Send,
700{
701 let event_loop = EventLoop::<RunnerEvent>::with_user_event()
702 .build()
703 .map_err(ShellError::EventLoop)?;
704 #[cfg(not(target_arch = "wasm32"))]
705 let access_proxy = event_loop.create_proxy();
706 let proxy = event_loop.create_proxy();
707 app.init(fenestra_core::Proxy::new(move |msg: A::Msg| {
708 let _ = proxy.send_event(RunnerEvent::App(Box::new(msg)));
710 }));
711 let background = app.theme().bg;
712 let mut fonts = Fonts::with_system();
713 for (role, data) in &options.fonts {
714 fonts.register(*role, data.clone());
715 }
716 #[cfg(target_arch = "wasm32")]
717 let state = FrameState::new();
718 #[cfg(not(target_arch = "wasm32"))]
719 let mut state = FrameState::new();
720 #[cfg(not(target_arch = "wasm32"))]
721 state.set_clipboard(Box::new(crate::OsClipboard::default()));
722 let runner = AppRunner {
723 shell: WindowShell::new(options, background),
724 app,
725 fonts,
726 state,
727 cursor: Point::ORIGIN,
728 started: Instant::now(),
729 last: None,
730 modifiers: winit::keyboard::ModifiersState::empty(),
731 #[cfg(not(target_arch = "wasm32"))]
732 adapter: None,
733 #[cfg(not(target_arch = "wasm32"))]
734 proxy: access_proxy,
735 #[cfg(not(target_arch = "wasm32"))]
736 secondary: std::collections::HashMap::new(),
737 };
738 #[cfg(not(target_arch = "wasm32"))]
739 {
740 let mut runner = runner;
741 event_loop
742 .run_app(&mut runner)
743 .map_err(ShellError::EventLoop)
744 }
745 #[cfg(target_arch = "wasm32")]
746 {
747 use winit::platform::web::EventLoopExtWebSys;
748 event_loop.spawn_app(runner);
750 Ok(())
751 }
752}
753
754struct AppRunner<A: App> {
755 shell: WindowShell,
756 app: A,
757 fonts: Fonts,
758 state: FrameState,
759 cursor: Point,
760 started: Instant,
761 last: Option<(Element<A::Msg>, fenestra_core::Frame)>,
763 modifiers: winit::keyboard::ModifiersState,
764 #[cfg(not(target_arch = "wasm32"))]
766 adapter: Option<accesskit_winit::Adapter>,
767 #[cfg(not(target_arch = "wasm32"))]
769 proxy: EventLoopProxy<RunnerEvent>,
770 #[cfg(not(target_arch = "wasm32"))]
773 secondary: std::collections::HashMap<String, SecondaryWindow<A>>,
774}
775
776#[cfg(not(target_arch = "wasm32"))]
779struct SecondaryWindow<A: App> {
780 shell: WindowShell,
781 state: FrameState,
782 cursor: Point,
783 last: Option<(Element<A::Msg>, fenestra_core::Frame)>,
784 on_close: A::Msg,
785 title: String,
786 adapter: Option<accesskit_winit::Adapter>,
787}
788
789impl<A: App> AppRunner<A> {
790 fn redraw(&mut self, event_loop: &ActiveEventLoop) {
791 self.shell.pump();
792 let Some((lw, lh, scale)) = self.shell.logical_size() else {
793 return;
794 };
795 let theme = self.app.theme();
796 self.shell.background = theme.bg;
797 self.state.tick(self.started.elapsed().as_secs_f64());
798 let view = self.app.view_for(fenestra_core::MAIN_WINDOW);
799 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
800 let frame = build_frame(
801 &view,
802 &theme,
803 &mut self.fonts,
804 &mut self.state,
805 (lw as f32, lh as f32),
806 scale,
807 );
808 let scene = frame.paint(&mut self.fonts, &mut self.state);
809 self.shell.present(&scene);
810 if refresh_hover(&view, &frame, &mut self.state)
813 && let Some(w) = self.shell.window()
814 {
815 w.request_redraw();
816 }
817 if frame.animating {
818 #[cfg(not(target_arch = "wasm32"))]
819 event_loop.set_control_flow(ControlFlow::WaitUntil(
820 Instant::now() + Duration::from_millis(16),
821 ));
822 #[cfg(target_arch = "wasm32")]
824 if let Some(w) = self.shell.window() {
825 w.request_redraw();
826 }
827 } else {
828 #[cfg(not(target_arch = "wasm32"))]
829 let secondary_animating = self
830 .secondary
831 .values()
832 .any(|b| b.last.as_ref().is_some_and(|(_, f)| f.animating));
833 #[cfg(target_arch = "wasm32")]
834 let secondary_animating = false;
835 if !secondary_animating {
836 event_loop.set_control_flow(ControlFlow::Wait);
837 }
838 }
839 self.last = Some((view, frame));
840 if let Some(caret) = self.state.ime_caret()
842 && let Some(w) = self.shell.window()
843 {
844 w.set_ime_cursor_area(
845 winit::dpi::LogicalPosition::new(caret.x0, caret.y0),
846 winit::dpi::LogicalSize::new(1.0, caret.height()),
847 );
848 }
849 #[cfg(not(target_arch = "wasm32"))]
850 self.push_access_tree();
851 }
852
853 #[cfg(not(target_arch = "wasm32"))]
856 fn push_access_tree(&mut self) {
857 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
858 let focus = self.state.focused();
859 if let Some(adapter) = &mut self.adapter
860 && let Some((_, frame)) = &self.last
861 {
862 adapter.update_if_active(|| crate::access::tree_update(frame, focus, scale));
863 }
864 }
865
866 fn input(&mut self, event: InputEvent) -> bool {
867 let Some((view, frame)) = &self.last else {
868 return false;
869 };
870 let result = dispatch(view, frame, &mut self.state, &mut self.fonts, event);
871 if let Some(cursor) = result.cursor
872 && let Some(w) = self.shell.window()
873 {
874 w.set_cursor(winit::window::Cursor::Icon(map_cursor(cursor)));
875 }
876 let had_msgs = !result.msgs.is_empty();
877 for msg in result.msgs {
878 self.app.update(msg);
879 }
880 if (result.redraw || had_msgs)
881 && let Some(w) = self.shell.window()
882 {
883 w.request_redraw();
884 }
885 had_msgs
886 }
887
888 fn input_main(&mut self, event_loop: &ActiveEventLoop, event: InputEvent) {
891 if self.input(event) {
892 self.after_update(event_loop);
893 }
894 }
895
896 #[cfg(not(target_arch = "wasm32"))]
899 fn secondary_input_main(&mut self, key: &str, event_loop: &ActiveEventLoop, event: InputEvent) {
900 if self.secondary_input(key, event) {
901 self.after_update(event_loop);
902 }
903 }
904
905 fn after_update(&mut self, event_loop: &ActiveEventLoop) {
908 #[cfg(not(target_arch = "wasm32"))]
909 self.reconcile_windows(event_loop);
910 #[cfg(target_arch = "wasm32")]
911 let _ = event_loop;
912 if let Some(w) = self.shell.window() {
913 w.request_redraw();
914 }
915 #[cfg(not(target_arch = "wasm32"))]
916 for bundle in self.secondary.values() {
917 if let Some(w) = bundle.shell.window() {
918 w.request_redraw();
919 }
920 }
921 }
922
923 #[cfg(not(target_arch = "wasm32"))]
926 fn reconcile_windows(&mut self, event_loop: &ActiveEventLoop) {
927 let desired = self.app.windows();
928 self.secondary
929 .retain(|key, _| desired.iter().any(|d| &d.key == key));
930 for desc in desired {
931 match self.secondary.get_mut(&desc.key) {
932 Some(bundle) => {
933 bundle.on_close = desc.on_close;
934 if bundle.title != desc.title {
935 bundle.title.clone_from(&desc.title);
936 if let Some(w) = bundle.shell.window() {
937 w.set_title(&desc.title);
938 }
939 }
940 }
941 None => {
942 let mut shell = WindowShell::new(
943 WindowOptions::titled(desc.title.clone())
944 .with_size(desc.size.0, desc.size.1),
945 self.shell.background,
946 );
947 let proxy = self.proxy.clone();
948 let mut adapter = None;
949 shell.resumed_with(event_loop, |el, window| {
950 adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
951 el, window, proxy,
952 ));
953 });
954 if let Some(w) = shell.window() {
955 w.set_ime_allowed(true);
956 w.request_redraw();
957 }
958 let mut state = FrameState::new();
959 state.set_clipboard(Box::new(crate::OsClipboard::default()));
960 self.secondary.insert(
961 desc.key.clone(),
962 SecondaryWindow {
963 shell,
964 state,
965 cursor: Point::ORIGIN,
966 last: None,
967 on_close: desc.on_close,
968 title: desc.title,
969 adapter,
970 },
971 );
972 }
973 }
974 }
975 }
976
977 #[cfg(not(target_arch = "wasm32"))]
980 fn secondary_redraw(&mut self, key: &str, event_loop: &ActiveEventLoop) {
981 let theme = self.app.theme();
982 let now = self.started.elapsed().as_secs_f64();
983 let Some(bundle) = self.secondary.get_mut(key) else {
984 return;
985 };
986 bundle.shell.pump();
987 let Some((lw, lh, scale)) = bundle.shell.logical_size() else {
988 return;
989 };
990 bundle.shell.background = theme.bg;
991 bundle.state.tick(now);
992 let view = self.app.view_for(key);
993 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
994 let frame = build_frame(
995 &view,
996 &theme,
997 &mut self.fonts,
998 &mut bundle.state,
999 (lw as f32, lh as f32),
1000 scale,
1001 );
1002 let scene = frame.paint(&mut self.fonts, &mut bundle.state);
1003 bundle.shell.present(&scene);
1004 if refresh_hover(&view, &frame, &mut bundle.state)
1005 && let Some(w) = bundle.shell.window()
1006 {
1007 w.request_redraw();
1008 }
1009 if frame.animating {
1010 event_loop.set_control_flow(ControlFlow::WaitUntil(
1011 Instant::now() + Duration::from_millis(16),
1012 ));
1013 }
1014 if let Some(caret) = bundle.state.ime_caret()
1015 && let Some(w) = bundle.shell.window()
1016 {
1017 w.set_ime_cursor_area(
1018 winit::dpi::LogicalPosition::new(caret.x0, caret.y0),
1019 winit::dpi::LogicalSize::new(1.0, caret.height()),
1020 );
1021 }
1022 bundle.last = Some((view, frame));
1023 let focus = bundle.state.focused();
1024 if let Some(adapter) = &mut bundle.adapter
1025 && let Some((_, frame)) = &bundle.last
1026 {
1027 adapter.update_if_active(|| crate::access::tree_update(frame, focus, scale));
1028 }
1029 }
1030
1031 #[cfg(not(target_arch = "wasm32"))]
1034 fn secondary_window_event(
1035 &mut self,
1036 key: &str,
1037 event_loop: &ActiveEventLoop,
1038 event: WindowEvent,
1039 ) {
1040 if let Some(bundle) = self.secondary.get_mut(key)
1041 && let Some(window) = bundle.shell.window()
1042 && let Some(adapter) = &mut bundle.adapter
1043 {
1044 adapter.process_event(window, &event);
1045 }
1046 match event {
1047 WindowEvent::CloseRequested => {
1048 if let Some(msg) = self.secondary.get(key).map(|b| b.on_close.clone()) {
1049 self.app.update(msg);
1050 self.after_update(event_loop);
1051 }
1052 }
1053 WindowEvent::Resized(size) => {
1054 if let Some(bundle) = self.secondary.get_mut(key) {
1055 bundle.shell.resized(size.width, size.height);
1056 }
1057 }
1058 WindowEvent::ScaleFactorChanged { .. } | WindowEvent::Occluded(false) => {
1059 if let Some(w) = self.secondary.get(key).and_then(|b| b.shell.window()) {
1060 w.request_redraw();
1061 }
1062 }
1063 WindowEvent::ModifiersChanged(mods) => self.modifiers = mods.state(),
1064 WindowEvent::DroppedFile(path) => {
1065 self.secondary_input_main(key, event_loop, InputEvent::FileDrop(path));
1066 }
1067 WindowEvent::CursorLeft { .. } => {
1068 self.secondary_input_main(key, event_loop, InputEvent::PointerLeave);
1069 }
1070 WindowEvent::CursorMoved { position, .. } => {
1071 let Some(bundle) = self.secondary.get_mut(key) else {
1072 return;
1073 };
1074 let scale = bundle.shell.window().map_or(1.0, |w| w.scale_factor());
1075 bundle.cursor = Point::new(position.x / scale, position.y / scale);
1076 #[expect(clippy::cast_possible_truncation, reason = "positions fit in f32")]
1077 let (x, y) = (bundle.cursor.x as f32, bundle.cursor.y as f32);
1078 self.secondary_input_main(key, event_loop, InputEvent::PointerMove { x, y });
1079 }
1080 WindowEvent::MouseInput {
1081 state,
1082 button: winit::event::MouseButton::Left,
1083 ..
1084 } => {
1085 self.secondary_input_main(
1086 key,
1087 event_loop,
1088 match state {
1089 winit::event::ElementState::Pressed => InputEvent::PointerDown,
1090 winit::event::ElementState::Released => InputEvent::PointerUp,
1091 },
1092 );
1093 }
1094 WindowEvent::MouseInput {
1095 state,
1096 button: winit::event::MouseButton::Right,
1097 ..
1098 } => {
1099 self.secondary_input_main(
1100 key,
1101 event_loop,
1102 match state {
1103 winit::event::ElementState::Pressed => InputEvent::RightDown,
1104 winit::event::ElementState::Released => InputEvent::RightUp,
1105 },
1106 );
1107 }
1108 WindowEvent::MouseWheel { delta, .. } => {
1109 let dy = match delta {
1110 MouseScrollDelta::LineDelta(_, y) => f64::from(y) * LINE_SCROLL_PX,
1111 MouseScrollDelta::PixelDelta(pos) => {
1112 let scale = self
1113 .secondary
1114 .get(key)
1115 .and_then(|b| b.shell.window())
1116 .map_or(1.0, |w| w.scale_factor());
1117 pos.y / scale
1118 }
1119 };
1120 #[expect(clippy::cast_possible_truncation, reason = "deltas fit in f32")]
1121 self.secondary_input_main(key, event_loop, InputEvent::Wheel { dy: dy as f32 });
1122 }
1123 WindowEvent::KeyboardInput { event, .. }
1124 if event.state == winit::event::ElementState::Pressed =>
1125 {
1126 let mods = self.modifiers;
1127 let printable = !mods.control_key()
1128 && !mods.super_key()
1129 && event
1130 .text
1131 .as_ref()
1132 .is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control()));
1133 if printable {
1134 if let Some(t) = &event.text {
1135 self.secondary_input_main(key, event_loop, InputEvent::Text(t.to_string()));
1136 }
1137 } else if let Some(input) = map_key(&event, mods) {
1138 self.secondary_input_main(key, event_loop, input);
1139 }
1140 }
1141 WindowEvent::Ime(ime) => match ime {
1142 winit::event::Ime::Preedit(text, cursor) => {
1143 self.secondary_input_main(
1144 key,
1145 event_loop,
1146 InputEvent::ImePreedit { text, cursor },
1147 );
1148 }
1149 winit::event::Ime::Commit(text) => {
1150 self.secondary_input_main(key, event_loop, InputEvent::Text(text));
1151 }
1152 winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
1153 },
1154 WindowEvent::RedrawRequested => self.secondary_redraw(key, event_loop),
1155 _ => {}
1156 }
1157 }
1158
1159 #[cfg(not(target_arch = "wasm32"))]
1162 fn secondary_input(&mut self, key: &str, event: InputEvent) -> bool {
1163 let Some(bundle) = self.secondary.get_mut(key) else {
1164 return false;
1165 };
1166 let Some((view, frame)) = &bundle.last else {
1167 return false;
1168 };
1169 let result = dispatch(view, frame, &mut bundle.state, &mut self.fonts, event);
1170 if let Some(cursor) = result.cursor
1171 && let Some(w) = bundle.shell.window()
1172 {
1173 w.set_cursor(winit::window::Cursor::Icon(map_cursor(cursor)));
1174 }
1175 let had_msgs = !result.msgs.is_empty();
1176 if (result.redraw || had_msgs)
1177 && let Some(w) = bundle.shell.window()
1178 {
1179 w.request_redraw();
1180 }
1181 let msgs = result.msgs;
1182 for msg in msgs {
1183 self.app.update(msg);
1184 }
1185 had_msgs
1186 }
1187}
1188
1189fn map_cursor(cursor: fenestra_core::Cursor) -> winit::window::CursorIcon {
1190 match cursor {
1191 fenestra_core::Cursor::Default => winit::window::CursorIcon::Default,
1192 fenestra_core::Cursor::Pointer => winit::window::CursorIcon::Pointer,
1193 fenestra_core::Cursor::Text => winit::window::CursorIcon::Text,
1194 fenestra_core::Cursor::NotAllowed => winit::window::CursorIcon::NotAllowed,
1195 }
1196}
1197
1198fn map_key(
1200 event: &winit::event::KeyEvent,
1201 mods: winit::keyboard::ModifiersState,
1202) -> Option<InputEvent> {
1203 use winit::keyboard::{Key as WKey, NamedKey};
1204 let key = match &event.logical_key {
1205 WKey::Named(NamedKey::Tab) => {
1206 return Some(if mods.shift_key() {
1207 InputEvent::ShiftTab
1208 } else {
1209 InputEvent::Tab
1210 });
1211 }
1212 WKey::Named(named) => match named {
1213 NamedKey::Enter => Key::Enter,
1214 NamedKey::Space => Key::Space,
1215 NamedKey::Escape => Key::Escape,
1216 NamedKey::ArrowLeft => Key::ArrowLeft,
1217 NamedKey::ArrowRight => Key::ArrowRight,
1218 NamedKey::ArrowUp => Key::ArrowUp,
1219 NamedKey::ArrowDown => Key::ArrowDown,
1220 NamedKey::Home => Key::Home,
1221 NamedKey::End => Key::End,
1222 NamedKey::Backspace => Key::Backspace,
1223 NamedKey::Delete => Key::Delete,
1224 NamedKey::PageUp => Key::PageUp,
1225 NamedKey::PageDown => Key::PageDown,
1226 _ => return None,
1227 },
1228 WKey::Character(s) => Key::Char(s.chars().next()?),
1229 _ => return None,
1230 };
1231 Some(InputEvent::Key(KeyInput {
1232 key,
1233 shift: mods.shift_key(),
1234 ctrl: mods.control_key(),
1235 alt: mods.alt_key(),
1236 meta: mods.super_key(),
1237 }))
1238}
1239
1240impl<A: App> ApplicationHandler<RunnerEvent> for AppRunner<A> {
1241 #[cfg(not(target_arch = "wasm32"))]
1242 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1243 let adapter = &mut self.adapter;
1244 let proxy = self.proxy.clone();
1245 self.shell.resumed_with(event_loop, |el, window| {
1246 if adapter.is_none() {
1248 *adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
1249 el, window, proxy,
1250 ));
1251 }
1252 });
1253 if let Some(w) = self.shell.window() {
1254 w.set_ime_allowed(true);
1255 }
1256 for bundle in self.secondary.values_mut() {
1257 bundle.shell.resumed(event_loop);
1258 }
1259 self.reconcile_windows(event_loop);
1260 }
1261
1262 #[cfg(target_arch = "wasm32")]
1263 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1264 self.shell.resumed(event_loop);
1265 if let Some(w) = self.shell.window() {
1266 w.set_ime_allowed(true);
1267 }
1268 }
1269
1270 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: RunnerEvent) {
1271 match event {
1272 RunnerEvent::App(msg) => {
1273 if let Ok(msg) = msg.downcast::<A::Msg>() {
1274 self.app.update(*msg);
1275 self.after_update(event_loop);
1276 }
1277 }
1278 #[cfg(not(target_arch = "wasm32"))]
1279 RunnerEvent::Access(ev) => {
1280 let is_main = self.shell.window().is_some_and(|w| w.id() == ev.window_id);
1283 let skey = (!is_main)
1284 .then(|| {
1285 self.secondary
1286 .iter()
1287 .find(|(_, b)| b.shell.window().is_some_and(|w| w.id() == ev.window_id))
1288 .map(|(k, _)| k.clone())
1289 })
1290 .flatten();
1291 if !is_main && skey.is_none() {
1292 return;
1293 }
1294 match ev.window_event {
1295 accesskit_winit::WindowEvent::InitialTreeRequested => match &skey {
1296 None => {
1297 if self.last.is_some() {
1298 self.push_access_tree();
1299 } else if let Some(w) = self.shell.window() {
1300 w.request_redraw();
1301 }
1302 }
1303 Some(key) => {
1304 if let Some(bundle) = self.secondary.get_mut(key) {
1305 let scale = bundle.shell.window().map_or(1.0, |w| w.scale_factor());
1306 let focus = bundle.state.focused();
1307 if let Some((_, frame)) = &bundle.last {
1308 if let Some(adapter) = &mut bundle.adapter {
1309 adapter.update_if_active(|| {
1310 crate::access::tree_update(frame, focus, scale)
1311 });
1312 }
1313 } else if let Some(w) = bundle.shell.window() {
1314 w.request_redraw();
1315 }
1316 }
1317 }
1318 },
1319 accesskit_winit::WindowEvent::ActionRequested(req) => {
1320 let id = fenestra_core::WidgetId(req.target_node.0);
1321 match req.action {
1322 accesskit::Action::Click => {
1323 let msg = match &skey {
1324 None => self.last.as_ref().and_then(|(view, frame)| {
1325 fenestra_core::click_msg_of(view, frame, &self.state, id)
1326 }),
1327 Some(key) => self.secondary.get(key).and_then(|bundle| {
1328 bundle.last.as_ref().and_then(|(view, frame)| {
1329 fenestra_core::click_msg_of(
1330 view,
1331 frame,
1332 &bundle.state,
1333 id,
1334 )
1335 })
1336 }),
1337 };
1338 if let Some(msg) = msg {
1339 self.app.update(msg);
1340 self.after_update(event_loop);
1341 }
1342 }
1343 accesskit::Action::Focus => match &skey {
1344 None => {
1345 self.state.set_focus(Some(id));
1346 if let Some(w) = self.shell.window() {
1347 w.request_redraw();
1348 }
1349 }
1350 Some(key) => {
1351 if let Some(bundle) = self.secondary.get_mut(key) {
1352 bundle.state.set_focus(Some(id));
1353 if let Some(w) = bundle.shell.window() {
1354 w.request_redraw();
1355 }
1356 }
1357 }
1358 },
1359 _ => {}
1360 }
1361 }
1362 accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
1363 }
1364 }
1365 }
1366 }
1367
1368 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
1369 self.shell.suspended();
1370 #[cfg(not(target_arch = "wasm32"))]
1371 for bundle in self.secondary.values_mut() {
1372 bundle.shell.suspended();
1373 }
1374 }
1375
1376 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
1377 if !matches!(cause, StartCause::ResumeTimeReached { .. }) {
1378 return;
1379 }
1380 if let Some(w) = self.shell.window() {
1381 w.request_redraw();
1382 }
1383 #[cfg(not(target_arch = "wasm32"))]
1384 for bundle in self.secondary.values() {
1385 if let Some(w) = bundle.shell.window() {
1386 w.request_redraw();
1387 }
1388 }
1389 }
1390
1391 fn window_event(
1392 &mut self,
1393 event_loop: &ActiveEventLoop,
1394 window_id: WindowId,
1395 event: WindowEvent,
1396 ) {
1397 if self.shell.window().is_none_or(|w| w.id() != window_id) {
1398 #[cfg(not(target_arch = "wasm32"))]
1399 if let Some(key) = self
1400 .secondary
1401 .iter()
1402 .find(|(_, b)| b.shell.window().is_some_and(|w| w.id() == window_id))
1403 .map(|(k, _)| k.clone())
1404 {
1405 self.secondary_window_event(&key, event_loop, event);
1406 }
1407 return;
1408 }
1409 #[cfg(not(target_arch = "wasm32"))]
1410 if let Some(adapter) = &mut self.adapter
1411 && let Some(window) = self.shell.window()
1412 {
1413 adapter.process_event(window, &event);
1414 }
1415 match event {
1416 WindowEvent::CloseRequested => event_loop.exit(),
1417 WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
1418 WindowEvent::ScaleFactorChanged { .. } => {
1419 if let Some(w) = self.shell.window() {
1420 w.request_redraw();
1421 }
1422 }
1423 WindowEvent::ModifiersChanged(mods) => self.modifiers = mods.state(),
1424 WindowEvent::Occluded(occluded) => {
1425 if !occluded && let Some(w) = self.shell.window() {
1426 w.request_redraw();
1427 }
1428 }
1429 WindowEvent::DroppedFile(path) => {
1430 self.input_main(event_loop, InputEvent::FileDrop(path))
1431 }
1432 WindowEvent::CursorLeft { .. } => self.input_main(event_loop, InputEvent::PointerLeave),
1433 WindowEvent::CursorMoved { position, .. } => {
1434 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
1435 self.cursor = Point::new(position.x / scale, position.y / scale);
1436 #[expect(clippy::cast_possible_truncation, reason = "positions fit in f32")]
1437 self.input_main(
1438 event_loop,
1439 InputEvent::PointerMove {
1440 x: self.cursor.x as f32,
1441 y: self.cursor.y as f32,
1442 },
1443 );
1444 }
1445 WindowEvent::MouseInput {
1446 state,
1447 button: winit::event::MouseButton::Left,
1448 ..
1449 } => {
1450 self.input_main(
1451 event_loop,
1452 match state {
1453 winit::event::ElementState::Pressed => InputEvent::PointerDown,
1454 winit::event::ElementState::Released => InputEvent::PointerUp,
1455 },
1456 );
1457 }
1458 WindowEvent::MouseInput {
1459 state,
1460 button: winit::event::MouseButton::Right,
1461 ..
1462 } => {
1463 self.input_main(
1464 event_loop,
1465 match state {
1466 winit::event::ElementState::Pressed => InputEvent::RightDown,
1467 winit::event::ElementState::Released => InputEvent::RightUp,
1468 },
1469 );
1470 }
1471 WindowEvent::MouseWheel { delta, .. } => {
1472 let dy = match delta {
1473 MouseScrollDelta::LineDelta(_, y) => f64::from(y) * LINE_SCROLL_PX,
1474 MouseScrollDelta::PixelDelta(pos) => {
1475 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
1476 pos.y / scale
1477 }
1478 };
1479 #[expect(clippy::cast_possible_truncation, reason = "deltas fit in f32")]
1480 self.input_main(event_loop, InputEvent::Wheel { dy: dy as f32 });
1481 }
1482 WindowEvent::KeyboardInput { event, .. }
1483 if event.state == winit::event::ElementState::Pressed =>
1484 {
1485 {
1486 let mods = self.modifiers;
1487 let printable = !mods.control_key()
1490 && !mods.super_key()
1491 && event
1492 .text
1493 .as_ref()
1494 .is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control()));
1495 if printable {
1496 if let Some(t) = &event.text {
1497 self.input_main(event_loop, InputEvent::Text(t.to_string()));
1498 }
1499 } else if let Some(input) = map_key(&event, mods) {
1500 self.input_main(event_loop, input);
1501 }
1502 }
1503 }
1504 WindowEvent::Ime(ime) => match ime {
1505 winit::event::Ime::Preedit(text, cursor) => {
1506 self.input_main(event_loop, InputEvent::ImePreedit { text, cursor });
1507 }
1508 winit::event::Ime::Commit(text) => {
1509 self.input_main(event_loop, InputEvent::Text(text));
1510 }
1511 winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
1512 },
1513 WindowEvent::RedrawRequested => self.redraw(event_loop),
1514 _ => {}
1515 }
1516 }
1517}