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}
52
53impl WindowOptions {
54 pub fn titled(title: impl Into<String>) -> Self {
56 Self {
57 title: title.into(),
58 inner_size: (1024.0, 768.0),
59 }
60 }
61
62 pub fn with_size(mut self, width: f64, height: f64) -> Self {
64 self.inner_size = (width, height);
65 self
66 }
67}
68
69enum RenderState {
70 Active {
71 surface: Box<RenderSurface<'static>>,
72 valid_surface: bool,
73 window: Arc<Window>,
74 },
75 Suspended(Option<Arc<Window>>),
76 #[cfg(target_arch = "wasm32")]
78 Pending(Arc<Window>),
79}
80
81struct WindowShell {
83 context: RenderContext,
84 renderers: Vec<Option<Renderer>>,
85 state: RenderState,
86 scene: Scene,
87 options: WindowOptions,
88 background: Color,
89 #[cfg(target_arch = "wasm32")]
92 ready: WasmReady,
93}
94
95#[cfg(target_arch = "wasm32")]
97type WasmReady =
98 std::rc::Rc<std::cell::RefCell<Option<(RenderContext, Box<RenderSurface<'static>>)>>>;
99
100impl WindowShell {
101 fn new(options: WindowOptions, background: Color) -> Self {
102 Self {
103 context: RenderContext::new(),
104 renderers: Vec::new(),
105 state: RenderState::Suspended(None),
106 scene: Scene::new(),
107 options,
108 background,
109 #[cfg(target_arch = "wasm32")]
110 ready: WasmReady::default(),
111 }
112 }
113
114 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
115 self.resumed_with(event_loop, |_, _| {});
116 }
117
118 fn resumed_with(
122 &mut self,
123 event_loop: &ActiveEventLoop,
124 before_visible: impl FnOnce(&ActiveEventLoop, &Arc<Window>),
125 ) {
126 let RenderState::Suspended(cached_window) = &mut self.state else {
127 return;
128 };
129 let window = cached_window.take().unwrap_or_else(|| {
130 let attrs = Window::default_attributes()
131 .with_title(self.options.title.clone())
132 .with_inner_size(LogicalSize::new(
133 self.options.inner_size.0,
134 self.options.inner_size.1,
135 ))
136 .with_visible(false);
137 #[cfg(target_arch = "wasm32")]
138 let attrs = {
139 use winit::platform::web::WindowAttributesExtWebSys;
140 attrs.with_append(true)
142 };
143 Arc::new(
144 event_loop
145 .create_window(attrs)
146 .expect("failed to create window"),
147 )
148 });
149 before_visible(event_loop, &window);
150 let was_hidden = window.is_visible() == Some(false);
151 self.activate(window.clone());
152 if was_hidden {
153 window.set_visible(true);
154 }
155 }
156
157 #[cfg(not(target_arch = "wasm32"))]
160 fn activate(&mut self, window: Arc<Window>) {
161 let size = window.inner_size();
162 let surface = pollster::block_on(self.context.create_surface(
163 window.clone(),
164 size.width.max(1),
165 size.height.max(1),
166 wgpu::PresentMode::AutoVsync,
167 ))
168 .expect("failed to create wgpu surface");
169
170 self.renderers
171 .resize_with(self.context.devices.len(), || None);
172 self.renderers[surface.dev_id].get_or_insert_with(|| {
173 Renderer::new(
174 &self.context.devices[surface.dev_id].device,
175 RendererOptions {
176 use_cpu: false,
177 antialiasing_support: AaSupport::area_only(),
178 ..Default::default()
179 },
180 )
181 .expect("failed to create vello renderer")
182 });
183
184 self.state = RenderState::Active {
185 surface: Box::new(surface),
186 valid_surface: size.width != 0 && size.height != 0,
187 window,
188 };
189 }
190
191 #[cfg(target_arch = "wasm32")]
194 fn activate(&mut self, window: Arc<Window>) {
195 let size = window.inner_size();
196 let ready = std::rc::Rc::clone(&self.ready);
197 let win = window.clone();
198 wasm_bindgen_futures::spawn_local(async move {
199 let mut context = RenderContext::new();
200 let surface = context
201 .create_surface(
202 win.clone(),
203 size.width.max(1),
204 size.height.max(1),
205 wgpu::PresentMode::AutoVsync,
206 )
207 .await
208 .expect("failed to create wgpu surface");
209 *ready.borrow_mut() = Some((context, Box::new(surface)));
210 win.request_redraw();
211 });
212 self.state = RenderState::Pending(window);
213 }
214
215 fn pump(&mut self) {
218 #[cfg(target_arch = "wasm32")]
219 if let RenderState::Pending(window) = &self.state
220 && let Some((context, surface)) = self.ready.borrow_mut().take()
221 {
222 let window = window.clone();
223 self.context = context;
224 self.renderers.clear();
225 self.renderers
226 .resize_with(self.context.devices.len(), || None);
227 self.renderers[surface.dev_id].get_or_insert_with(|| {
228 Renderer::new(
229 &self.context.devices[surface.dev_id].device,
230 RendererOptions {
231 use_cpu: false,
232 antialiasing_support: AaSupport::area_only(),
233 ..Default::default()
234 },
235 )
236 .expect("failed to create vello renderer")
237 });
238 let size = window.inner_size();
239 self.state = RenderState::Active {
240 surface,
241 valid_surface: size.width != 0 && size.height != 0,
242 window,
243 };
244 }
245 }
246
247 fn suspended(&mut self) {
248 if let RenderState::Active { window, .. } = &self.state {
249 self.state = RenderState::Suspended(Some(window.clone()));
250 }
251 }
252
253 fn window(&self) -> Option<&Arc<Window>> {
254 match &self.state {
255 RenderState::Active { window, .. } => Some(window),
256 _ => None,
257 }
258 }
259
260 fn resized(&mut self, width: u32, height: u32) {
261 let RenderState::Active {
262 surface,
263 valid_surface,
264 window,
265 } = &mut self.state
266 else {
267 return;
268 };
269 if width != 0 && height != 0 {
270 self.context.resize_surface(surface, width, height);
271 *valid_surface = true;
272 } else {
273 *valid_surface = false;
274 }
275 window.request_redraw();
276 }
277
278 fn logical_size(&self) -> Option<(f64, f64, f64)> {
280 match &self.state {
281 RenderState::Active {
282 surface, window, ..
283 } => {
284 let scale = window.scale_factor();
285 Some((
286 f64::from(surface.config.width) / scale,
287 f64::from(surface.config.height) / scale,
288 scale,
289 ))
290 }
291 _ => None,
292 }
293 }
294
295 fn present(&mut self, fragment: &Scene) {
297 let RenderState::Active {
298 surface,
299 valid_surface,
300 window,
301 } = &mut self.state
302 else {
303 return;
304 };
305 if !*valid_surface {
306 return;
307 }
308 let width = surface.config.width;
309 let height = surface.config.height;
310 let scale = window.scale_factor();
311
312 self.scene.reset();
313 self.scene
314 .append(fragment, Some(vello::kurbo::Affine::scale(scale)));
315
316 let handle = &self.context.devices[surface.dev_id];
317 self.renderers[surface.dev_id]
318 .as_mut()
319 .expect("renderer exists for surface device")
320 .render_to_texture(
321 &handle.device,
322 &handle.queue,
323 &self.scene,
324 &surface.target_view,
325 &RenderParams {
326 base_color: self.background,
327 width,
328 height,
329 antialiasing_method: AaConfig::Area,
330 },
331 )
332 .expect("vello render failed");
333
334 let surface_texture = match surface.surface.get_current_texture() {
335 CurrentSurfaceTexture::Success(texture) => texture,
336 CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Suboptimal(_) => {
337 self.context.configure_surface(surface);
338 window.request_redraw();
339 return;
340 }
341 CurrentSurfaceTexture::Occluded => {
342 return;
345 }
346 CurrentSurfaceTexture::Timeout => {
347 window.request_redraw();
348 return;
349 }
350 CurrentSurfaceTexture::Lost => {
351 let window = window.clone();
354 window.request_redraw();
355 self.activate(window);
356 return;
357 }
358 CurrentSurfaceTexture::Validation => {
359 panic!("validation error acquiring wgpu surface texture")
360 }
361 };
362
363 let mut encoder = handle
364 .device
365 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
366 label: Some("fenestra surface blit"),
367 });
368 surface.blitter.copy(
369 &handle.device,
370 &mut encoder,
371 &surface.target_view,
372 &surface_texture
373 .texture
374 .create_view(&wgpu::TextureViewDescriptor::default()),
375 );
376 handle.queue.submit([encoder.finish()]);
377 surface_texture.present();
378 handle.device.poll(wgpu::PollType::Poll).unwrap();
379 }
380}
381
382#[cfg(not(target_arch = "wasm32"))]
388pub fn run_scene(
389 options: WindowOptions,
390 background: Color,
391 paint: impl FnMut(&mut Scene, f64, f64, Color) + 'static,
392) -> Result<(), ShellError> {
393 let event_loop = EventLoop::new().map_err(ShellError::EventLoop)?;
394 let mut app = SceneApp {
395 shell: WindowShell::new(options, background),
396 fragment: Scene::new(),
397 paint: Box::new(paint),
398 };
399 event_loop.run_app(&mut app).map_err(ShellError::EventLoop)
400}
401
402#[cfg(not(target_arch = "wasm32"))]
403struct SceneApp {
404 shell: WindowShell,
405 fragment: Scene,
406 paint: PaintFn,
407}
408
409#[cfg(not(target_arch = "wasm32"))]
410impl ApplicationHandler for SceneApp {
411 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
412 self.shell.resumed(event_loop);
413 }
414
415 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
416 self.shell.suspended();
417 }
418
419 fn window_event(
420 &mut self,
421 event_loop: &ActiveEventLoop,
422 window_id: WindowId,
423 event: WindowEvent,
424 ) {
425 if self.shell.window().is_none_or(|w| w.id() != window_id) {
426 return;
427 }
428 match event {
429 WindowEvent::CloseRequested => event_loop.exit(),
430 WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
431 WindowEvent::ScaleFactorChanged { .. } => {
432 if let Some(w) = self.shell.window() {
433 w.request_redraw();
434 }
435 }
436 WindowEvent::Occluded(occluded) => {
437 if !occluded && let Some(w) = self.shell.window() {
438 w.request_redraw();
439 }
440 }
441 WindowEvent::RedrawRequested => {
442 let Some((lw, lh, _scale)) = self.shell.logical_size() else {
443 return;
444 };
445 self.fragment.reset();
446 let bg = self.shell.background;
447 (self.paint)(&mut self.fragment, lw, lh, bg);
448 let fragment = std::mem::replace(&mut self.fragment, Scene::new());
449 self.shell.present(&fragment);
450 self.fragment = fragment;
451 }
452 _ => {}
453 }
454 }
455}
456
457#[cfg(not(target_arch = "wasm32"))]
463pub fn run_static(
464 options: WindowOptions,
465 theme: Theme,
466 view: impl Fn(&Theme) -> Element<()> + 'static,
467) -> Result<(), ShellError> {
468 let event_loop = EventLoop::new().map_err(ShellError::EventLoop)?;
469 let background = theme.bg;
470 let mut app = StaticApp {
471 shell: WindowShell::new(options, background),
472 theme,
473 fonts: Fonts::with_system(),
474 state: FrameState::new(),
475 view: Box::new(view),
476 cursor: Point::ORIGIN,
477 started: Instant::now(),
478 last_frame: None,
479 };
480 event_loop.run_app(&mut app).map_err(ShellError::EventLoop)
481}
482
483#[cfg(not(target_arch = "wasm32"))]
484struct StaticApp {
485 shell: WindowShell,
486 theme: Theme,
487 fonts: Fonts,
488 state: FrameState,
489 view: ViewFn,
490 cursor: Point,
492 started: Instant,
493 last_frame: Option<fenestra_core::Frame>,
495}
496
497#[cfg(not(target_arch = "wasm32"))]
498impl StaticApp {
499 fn redraw(&mut self, event_loop: &ActiveEventLoop) {
500 let Some((lw, lh, scale)) = self.shell.logical_size() else {
501 return;
502 };
503 self.state.tick(self.started.elapsed().as_secs_f64());
504 let el = (self.view)(&self.theme);
505 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
506 let frame = build_frame(
507 &el,
508 &self.theme,
509 &mut self.fonts,
510 &mut self.state,
511 (lw as f32, lh as f32),
512 scale,
513 );
514 let scene = frame.paint(&mut self.fonts, &mut self.state);
515 self.shell.present(&scene);
516 if frame.animating {
517 event_loop.set_control_flow(ControlFlow::WaitUntil(
518 Instant::now() + Duration::from_millis(16),
519 ));
520 } else {
521 event_loop.set_control_flow(ControlFlow::Wait);
522 }
523 self.last_frame = Some(frame);
524 }
525}
526
527#[cfg(not(target_arch = "wasm32"))]
528impl ApplicationHandler for StaticApp {
529 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
530 self.shell.resumed(event_loop);
531 }
532
533 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
534 self.shell.suspended();
535 }
536
537 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
538 if matches!(cause, StartCause::ResumeTimeReached { .. })
539 && let Some(w) = self.shell.window()
540 {
541 w.request_redraw();
542 }
543 }
544
545 fn window_event(
546 &mut self,
547 event_loop: &ActiveEventLoop,
548 window_id: WindowId,
549 event: WindowEvent,
550 ) {
551 if self.shell.window().is_none_or(|w| w.id() != window_id) {
552 return;
553 }
554 match event {
555 WindowEvent::CloseRequested => event_loop.exit(),
556 WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
557 WindowEvent::ScaleFactorChanged { .. } => {
558 if let Some(w) = self.shell.window() {
559 w.request_redraw();
560 }
561 }
562 WindowEvent::CursorMoved { position, .. } => {
563 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
564 self.cursor = Point::new(position.x / scale, position.y / scale);
565 }
566 WindowEvent::MouseWheel { delta, .. } => {
567 let dy = match delta {
568 MouseScrollDelta::LineDelta(_, y) => f64::from(y) * LINE_SCROLL_PX,
569 MouseScrollDelta::PixelDelta(pos) => {
570 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
571 pos.y / scale
572 }
573 };
574 if let Some(frame) = &self.last_frame
575 && let Some(id) = frame.scrollable_at(self.cursor)
576 {
577 #[expect(
578 clippy::cast_possible_truncation,
579 reason = "scroll deltas fit in f32"
580 )]
581 self.state.scroll_by(id, -dy as f32);
582 if let Some(w) = self.shell.window() {
583 w.request_redraw();
584 }
585 }
586 }
587 WindowEvent::RedrawRequested => self.redraw(event_loop),
588 _ => {}
589 }
590 }
591}
592
593enum RunnerEvent {
599 App(Box<dyn std::any::Any + Send>),
600 #[cfg(not(target_arch = "wasm32"))]
601 Access(accesskit_winit::Event),
602}
603
604#[cfg(not(target_arch = "wasm32"))]
605impl From<accesskit_winit::Event> for RunnerEvent {
606 fn from(event: accesskit_winit::Event) -> Self {
607 Self::Access(event)
608 }
609}
610
611pub fn run_app<A: App + 'static>(mut app: A, options: WindowOptions) -> Result<(), ShellError>
617where
618 A::Msg: Send,
619{
620 let event_loop = EventLoop::<RunnerEvent>::with_user_event()
621 .build()
622 .map_err(ShellError::EventLoop)?;
623 #[cfg(not(target_arch = "wasm32"))]
624 let access_proxy = event_loop.create_proxy();
625 let proxy = event_loop.create_proxy();
626 app.init(fenestra_core::Proxy::new(move |msg: A::Msg| {
627 let _ = proxy.send_event(RunnerEvent::App(Box::new(msg)));
629 }));
630 let background = app.theme().bg;
631 #[cfg(target_arch = "wasm32")]
632 let state = FrameState::new();
633 #[cfg(not(target_arch = "wasm32"))]
634 let mut state = FrameState::new();
635 #[cfg(not(target_arch = "wasm32"))]
636 state.set_clipboard(Box::new(crate::OsClipboard::default()));
637 let runner = AppRunner {
638 shell: WindowShell::new(options, background),
639 app,
640 fonts: Fonts::with_system(),
641 state,
642 cursor: Point::ORIGIN,
643 started: Instant::now(),
644 last: None,
645 modifiers: winit::keyboard::ModifiersState::empty(),
646 #[cfg(not(target_arch = "wasm32"))]
647 adapter: None,
648 #[cfg(not(target_arch = "wasm32"))]
649 proxy: access_proxy,
650 };
651 #[cfg(not(target_arch = "wasm32"))]
652 {
653 let mut runner = runner;
654 event_loop
655 .run_app(&mut runner)
656 .map_err(ShellError::EventLoop)
657 }
658 #[cfg(target_arch = "wasm32")]
659 {
660 use winit::platform::web::EventLoopExtWebSys;
661 event_loop.spawn_app(runner);
663 Ok(())
664 }
665}
666
667struct AppRunner<A: App> {
668 shell: WindowShell,
669 app: A,
670 fonts: Fonts,
671 state: FrameState,
672 cursor: Point,
673 started: Instant,
674 last: Option<(Element<A::Msg>, fenestra_core::Frame)>,
676 modifiers: winit::keyboard::ModifiersState,
677 #[cfg(not(target_arch = "wasm32"))]
679 adapter: Option<accesskit_winit::Adapter>,
680 #[cfg(not(target_arch = "wasm32"))]
682 proxy: EventLoopProxy<RunnerEvent>,
683}
684
685impl<A: App> AppRunner<A> {
686 fn redraw(&mut self, event_loop: &ActiveEventLoop) {
687 self.shell.pump();
688 let Some((lw, lh, scale)) = self.shell.logical_size() else {
689 return;
690 };
691 let theme = self.app.theme();
692 self.shell.background = theme.bg;
693 self.state.tick(self.started.elapsed().as_secs_f64());
694 let view = self.app.view();
695 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
696 let frame = build_frame(
697 &view,
698 &theme,
699 &mut self.fonts,
700 &mut self.state,
701 (lw as f32, lh as f32),
702 scale,
703 );
704 let scene = frame.paint(&mut self.fonts, &mut self.state);
705 self.shell.present(&scene);
706 if refresh_hover(&view, &frame, &mut self.state)
709 && let Some(w) = self.shell.window()
710 {
711 w.request_redraw();
712 }
713 if frame.animating {
714 #[cfg(not(target_arch = "wasm32"))]
715 event_loop.set_control_flow(ControlFlow::WaitUntil(
716 Instant::now() + Duration::from_millis(16),
717 ));
718 #[cfg(target_arch = "wasm32")]
720 if let Some(w) = self.shell.window() {
721 w.request_redraw();
722 }
723 } else {
724 event_loop.set_control_flow(ControlFlow::Wait);
725 }
726 self.last = Some((view, frame));
727 #[cfg(not(target_arch = "wasm32"))]
728 self.push_access_tree();
729 }
730
731 #[cfg(not(target_arch = "wasm32"))]
734 fn push_access_tree(&mut self) {
735 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
736 let focus = self.state.focused();
737 if let Some(adapter) = &mut self.adapter
738 && let Some((_, frame)) = &self.last
739 {
740 adapter.update_if_active(|| crate::access::tree_update(frame, focus, scale));
741 }
742 }
743
744 fn input(&mut self, event: InputEvent) {
745 let Some((view, frame)) = &self.last else {
746 return;
747 };
748 let result = dispatch(view, frame, &mut self.state, &mut self.fonts, event);
749 if let Some(cursor) = result.cursor
750 && let Some(w) = self.shell.window()
751 {
752 w.set_cursor(winit::window::Cursor::Icon(map_cursor(cursor)));
753 }
754 let had_msgs = !result.msgs.is_empty();
755 for msg in result.msgs {
756 self.app.update(msg);
757 }
758 if (result.redraw || had_msgs)
759 && let Some(w) = self.shell.window()
760 {
761 w.request_redraw();
762 }
763 }
764}
765
766fn map_cursor(cursor: fenestra_core::Cursor) -> winit::window::CursorIcon {
767 match cursor {
768 fenestra_core::Cursor::Default => winit::window::CursorIcon::Default,
769 fenestra_core::Cursor::Pointer => winit::window::CursorIcon::Pointer,
770 fenestra_core::Cursor::Text => winit::window::CursorIcon::Text,
771 fenestra_core::Cursor::NotAllowed => winit::window::CursorIcon::NotAllowed,
772 }
773}
774
775fn map_key(
777 event: &winit::event::KeyEvent,
778 mods: winit::keyboard::ModifiersState,
779) -> Option<InputEvent> {
780 use winit::keyboard::{Key as WKey, NamedKey};
781 let key = match &event.logical_key {
782 WKey::Named(NamedKey::Tab) => {
783 return Some(if mods.shift_key() {
784 InputEvent::ShiftTab
785 } else {
786 InputEvent::Tab
787 });
788 }
789 WKey::Named(named) => match named {
790 NamedKey::Enter => Key::Enter,
791 NamedKey::Space => Key::Space,
792 NamedKey::Escape => Key::Escape,
793 NamedKey::ArrowLeft => Key::ArrowLeft,
794 NamedKey::ArrowRight => Key::ArrowRight,
795 NamedKey::ArrowUp => Key::ArrowUp,
796 NamedKey::ArrowDown => Key::ArrowDown,
797 NamedKey::Home => Key::Home,
798 NamedKey::End => Key::End,
799 NamedKey::Backspace => Key::Backspace,
800 NamedKey::Delete => Key::Delete,
801 _ => return None,
802 },
803 WKey::Character(s) => Key::Char(s.chars().next()?),
804 _ => return None,
805 };
806 Some(InputEvent::Key(KeyInput {
807 key,
808 shift: mods.shift_key(),
809 ctrl: mods.control_key(),
810 alt: mods.alt_key(),
811 meta: mods.super_key(),
812 }))
813}
814
815impl<A: App> ApplicationHandler<RunnerEvent> for AppRunner<A> {
816 #[cfg(not(target_arch = "wasm32"))]
817 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
818 let adapter = &mut self.adapter;
819 let proxy = self.proxy.clone();
820 self.shell.resumed_with(event_loop, |el, window| {
821 if adapter.is_none() {
823 *adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
824 el, window, proxy,
825 ));
826 }
827 });
828 if let Some(w) = self.shell.window() {
829 w.set_ime_allowed(true);
830 }
831 }
832
833 #[cfg(target_arch = "wasm32")]
834 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
835 self.shell.resumed(event_loop);
836 if let Some(w) = self.shell.window() {
837 w.set_ime_allowed(true);
838 }
839 }
840
841 fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: RunnerEvent) {
842 match event {
843 RunnerEvent::App(msg) => {
844 if let Ok(msg) = msg.downcast::<A::Msg>() {
845 self.app.update(*msg);
846 if let Some(w) = self.shell.window() {
847 w.request_redraw();
848 }
849 }
850 }
851 #[cfg(not(target_arch = "wasm32"))]
852 RunnerEvent::Access(ev) => match ev.window_event {
853 accesskit_winit::WindowEvent::InitialTreeRequested => {
854 if self.last.is_some() {
855 self.push_access_tree();
856 } else if let Some(w) = self.shell.window() {
857 w.request_redraw();
858 }
859 }
860 accesskit_winit::WindowEvent::ActionRequested(req) => {
861 let id = fenestra_core::WidgetId(req.target_node.0);
862 match req.action {
863 accesskit::Action::Click => {
864 if let Some((view, frame)) = &self.last
865 && let Some(msg) =
866 fenestra_core::click_msg_of(view, frame, &self.state, id)
867 {
868 self.app.update(msg);
869 if let Some(w) = self.shell.window() {
870 w.request_redraw();
871 }
872 }
873 }
874 accesskit::Action::Focus => {
875 self.state.set_focus(Some(id));
876 if let Some(w) = self.shell.window() {
877 w.request_redraw();
878 }
879 }
880 _ => {}
881 }
882 }
883 accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
884 },
885 }
886 }
887
888 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
889 self.shell.suspended();
890 }
891
892 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
893 if matches!(cause, StartCause::ResumeTimeReached { .. })
894 && let Some(w) = self.shell.window()
895 {
896 w.request_redraw();
897 }
898 }
899
900 fn window_event(
901 &mut self,
902 event_loop: &ActiveEventLoop,
903 window_id: WindowId,
904 event: WindowEvent,
905 ) {
906 if self.shell.window().is_none_or(|w| w.id() != window_id) {
907 return;
908 }
909 #[cfg(not(target_arch = "wasm32"))]
910 if let Some(adapter) = &mut self.adapter
911 && let Some(window) = self.shell.window()
912 {
913 adapter.process_event(window, &event);
914 }
915 match event {
916 WindowEvent::CloseRequested => event_loop.exit(),
917 WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
918 WindowEvent::ScaleFactorChanged { .. } => {
919 if let Some(w) = self.shell.window() {
920 w.request_redraw();
921 }
922 }
923 WindowEvent::ModifiersChanged(mods) => self.modifiers = mods.state(),
924 WindowEvent::Occluded(occluded) => {
925 if !occluded && let Some(w) = self.shell.window() {
926 w.request_redraw();
927 }
928 }
929 WindowEvent::CursorLeft { .. } => self.input(InputEvent::PointerLeave),
930 WindowEvent::CursorMoved { position, .. } => {
931 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
932 self.cursor = Point::new(position.x / scale, position.y / scale);
933 #[expect(clippy::cast_possible_truncation, reason = "positions fit in f32")]
934 self.input(InputEvent::PointerMove {
935 x: self.cursor.x as f32,
936 y: self.cursor.y as f32,
937 });
938 }
939 WindowEvent::MouseInput {
940 state,
941 button: winit::event::MouseButton::Left,
942 ..
943 } => {
944 self.input(match state {
945 winit::event::ElementState::Pressed => InputEvent::PointerDown,
946 winit::event::ElementState::Released => InputEvent::PointerUp,
947 });
948 }
949 WindowEvent::MouseWheel { delta, .. } => {
950 let dy = match delta {
951 MouseScrollDelta::LineDelta(_, y) => f64::from(y) * LINE_SCROLL_PX,
952 MouseScrollDelta::PixelDelta(pos) => {
953 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
954 pos.y / scale
955 }
956 };
957 #[expect(clippy::cast_possible_truncation, reason = "deltas fit in f32")]
958 self.input(InputEvent::Wheel { dy: dy as f32 });
959 }
960 WindowEvent::KeyboardInput { event, .. }
961 if event.state == winit::event::ElementState::Pressed =>
962 {
963 {
964 let mods = self.modifiers;
965 let printable = !mods.control_key()
968 && !mods.super_key()
969 && event
970 .text
971 .as_ref()
972 .is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control()));
973 if printable {
974 if let Some(t) = &event.text {
975 self.input(InputEvent::Text(t.to_string()));
976 }
977 } else if let Some(input) = map_key(&event, mods) {
978 self.input(input);
979 }
980 }
981 }
982 WindowEvent::Ime(ime) => match ime {
983 winit::event::Ime::Preedit(text, cursor) => {
984 self.input(InputEvent::ImePreedit { text, cursor });
985 }
986 winit::event::Ime::Commit(text) => {
987 self.input(InputEvent::Text(text));
988 }
989 winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
990 },
991 WindowEvent::RedrawRequested => self.redraw(event_loop),
992 _ => {}
993 }
994 }
995}