Skip to main content

fenestra_shell/
window.rs

1//! The windowed runners: winit event loop + wgpu surface + vello renderer.
2//!
3//! [`run_scene`] paints via a raw scene callback (no input). [`run_static`]
4//! runs an element view with scrolling and animation frames; the full `App`
5//! runner with messages arrives in M4 and builds on the same plumbing.
6
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9
10use fenestra_core::{
11    App, Element, Fonts, FrameState, InputEvent, Key, KeyInput, Theme, build_frame, dispatch,
12    refresh_hover,
13};
14use kurbo::Point;
15use vello::peniko::Color;
16use vello::util::{RenderContext, RenderSurface};
17use vello::wgpu::{self, CurrentSurfaceTexture};
18use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
19use winit::application::ApplicationHandler;
20use winit::dpi::LogicalSize;
21use winit::event::{MouseScrollDelta, StartCause, WindowEvent};
22use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
23use winit::window::{Window, WindowId};
24
25use crate::ShellError;
26
27/// One wheel "line" in logical pixels.
28const LINE_SCROLL_PX: f64 = 40.0;
29
30/// A raw paint callback: `(scene, logical_w, logical_h, background)`.
31type PaintFn = Box<dyn FnMut(&mut Scene, f64, f64, Color)>;
32/// A message-free element view function.
33type ViewFn = Box<dyn Fn(&Theme) -> Element<()>>;
34
35/// Options for the application window.
36#[derive(Debug, Clone)]
37pub struct WindowOptions {
38    /// Window title.
39    pub title: String,
40    /// Initial inner size in logical pixels.
41    pub inner_size: (f64, f64),
42}
43
44impl WindowOptions {
45    /// A window with the given title and the default 1024x768 logical size.
46    pub fn titled(title: impl Into<String>) -> Self {
47        Self {
48            title: title.into(),
49            inner_size: (1024.0, 768.0),
50        }
51    }
52
53    /// Sets the initial inner size in logical pixels.
54    pub fn with_size(mut self, width: f64, height: f64) -> Self {
55        self.inner_size = (width, height);
56        self
57    }
58}
59
60enum RenderState {
61    Active {
62        surface: Box<RenderSurface<'static>>,
63        valid_surface: bool,
64        window: Arc<Window>,
65    },
66    Suspended(Option<Arc<Window>>),
67}
68
69/// Shared surface plumbing for every windowed runner.
70struct WindowShell {
71    context: RenderContext,
72    renderers: Vec<Option<Renderer>>,
73    state: RenderState,
74    scene: Scene,
75    options: WindowOptions,
76    background: Color,
77}
78
79impl WindowShell {
80    fn new(options: WindowOptions, background: Color) -> Self {
81        Self {
82            context: RenderContext::new(),
83            renderers: Vec::new(),
84            state: RenderState::Suspended(None),
85            scene: Scene::new(),
86            options,
87            background,
88        }
89    }
90
91    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
92        self.resumed_with(event_loop, |_, _| {});
93    }
94
95    /// Like [`Self::resumed`], but runs `before_visible` between window
96    /// creation and the first `set_visible(true)` — the AccessKit adapter
97    /// must attach while the window is still hidden.
98    fn resumed_with(
99        &mut self,
100        event_loop: &ActiveEventLoop,
101        before_visible: impl FnOnce(&ActiveEventLoop, &Arc<Window>),
102    ) {
103        let RenderState::Suspended(cached_window) = &mut self.state else {
104            return;
105        };
106        let window = cached_window.take().unwrap_or_else(|| {
107            let attrs = Window::default_attributes()
108                .with_title(self.options.title.clone())
109                .with_inner_size(LogicalSize::new(
110                    self.options.inner_size.0,
111                    self.options.inner_size.1,
112                ))
113                .with_visible(false);
114            Arc::new(
115                event_loop
116                    .create_window(attrs)
117                    .expect("failed to create window"),
118            )
119        });
120        before_visible(event_loop, &window);
121        let was_hidden = window.is_visible() == Some(false);
122        self.activate(window.clone());
123        if was_hidden {
124            window.set_visible(true);
125        }
126    }
127
128    /// Builds (or rebuilds, after a lost surface) the swapchain for `window`
129    /// and enters the active state.
130    fn activate(&mut self, window: Arc<Window>) {
131        let size = window.inner_size();
132        let surface = pollster::block_on(self.context.create_surface(
133            window.clone(),
134            size.width.max(1),
135            size.height.max(1),
136            wgpu::PresentMode::AutoVsync,
137        ))
138        .expect("failed to create wgpu surface");
139
140        self.renderers
141            .resize_with(self.context.devices.len(), || None);
142        self.renderers[surface.dev_id].get_or_insert_with(|| {
143            Renderer::new(
144                &self.context.devices[surface.dev_id].device,
145                RendererOptions {
146                    use_cpu: false,
147                    antialiasing_support: AaSupport::area_only(),
148                    ..Default::default()
149                },
150            )
151            .expect("failed to create vello renderer")
152        });
153
154        self.state = RenderState::Active {
155            surface: Box::new(surface),
156            valid_surface: size.width != 0 && size.height != 0,
157            window,
158        };
159    }
160
161    fn suspended(&mut self) {
162        if let RenderState::Active { window, .. } = &self.state {
163            self.state = RenderState::Suspended(Some(window.clone()));
164        }
165    }
166
167    fn window(&self) -> Option<&Arc<Window>> {
168        match &self.state {
169            RenderState::Active { window, .. } => Some(window),
170            RenderState::Suspended(_) => None,
171        }
172    }
173
174    fn resized(&mut self, width: u32, height: u32) {
175        let RenderState::Active {
176            surface,
177            valid_surface,
178            window,
179        } = &mut self.state
180        else {
181            return;
182        };
183        if width != 0 && height != 0 {
184            self.context.resize_surface(surface, width, height);
185            *valid_surface = true;
186        } else {
187            *valid_surface = false;
188        }
189        window.request_redraw();
190    }
191
192    /// Logical size and scale factor of the active window.
193    fn logical_size(&self) -> Option<(f64, f64, f64)> {
194        match &self.state {
195            RenderState::Active {
196                surface, window, ..
197            } => {
198                let scale = window.scale_factor();
199                Some((
200                    f64::from(surface.config.width) / scale,
201                    f64::from(surface.config.height) / scale,
202                    scale,
203                ))
204            }
205            RenderState::Suspended(_) => None,
206        }
207    }
208
209    /// Scales the logical fragment to physical pixels and presents it.
210    fn present(&mut self, fragment: &Scene) {
211        let RenderState::Active {
212            surface,
213            valid_surface,
214            window,
215        } = &mut self.state
216        else {
217            return;
218        };
219        if !*valid_surface {
220            return;
221        }
222        let width = surface.config.width;
223        let height = surface.config.height;
224        let scale = window.scale_factor();
225
226        self.scene.reset();
227        self.scene
228            .append(fragment, Some(vello::kurbo::Affine::scale(scale)));
229
230        let handle = &self.context.devices[surface.dev_id];
231        self.renderers[surface.dev_id]
232            .as_mut()
233            .expect("renderer exists for surface device")
234            .render_to_texture(
235                &handle.device,
236                &handle.queue,
237                &self.scene,
238                &surface.target_view,
239                &RenderParams {
240                    base_color: self.background,
241                    width,
242                    height,
243                    antialiasing_method: AaConfig::Area,
244                },
245            )
246            .expect("vello render failed");
247
248        let surface_texture = match surface.surface.get_current_texture() {
249            CurrentSurfaceTexture::Success(texture) => texture,
250            CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Suboptimal(_) => {
251                self.context.configure_surface(surface);
252                window.request_redraw();
253                return;
254            }
255            CurrentSurfaceTexture::Occluded => {
256                // Hidden window: skip the frame; WindowEvent::Occluded(false)
257                // requests the next redraw when it becomes visible again.
258                return;
259            }
260            CurrentSurfaceTexture::Timeout => {
261                window.request_redraw();
262                return;
263            }
264            CurrentSurfaceTexture::Lost => {
265                // Recoverable (GPU reset, driver update, display change):
266                // rebuild the swapchain on the same window and repaint.
267                let window = window.clone();
268                window.request_redraw();
269                self.activate(window);
270                return;
271            }
272            CurrentSurfaceTexture::Validation => {
273                panic!("validation error acquiring wgpu surface texture")
274            }
275        };
276
277        let mut encoder = handle
278            .device
279            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
280                label: Some("fenestra surface blit"),
281            });
282        surface.blitter.copy(
283            &handle.device,
284            &mut encoder,
285            &surface.target_view,
286            &surface_texture
287                .texture
288                .create_view(&wgpu::TextureViewDescriptor::default()),
289        );
290        handle.queue.submit([encoder.finish()]);
291        surface_texture.present();
292        handle.device.poll(wgpu::PollType::Poll).unwrap();
293    }
294}
295
296// ------------------------------------------------------------- run_scene
297
298/// Opens a window and repaints via `paint(scene, logical_w, logical_h, bg)`
299/// on every redraw. Blocks until the window closes. Low-level escape hatch;
300/// element views should prefer [`run_static`] (or the M4 `App` runner).
301pub fn run_scene(
302    options: WindowOptions,
303    background: Color,
304    paint: impl FnMut(&mut Scene, f64, f64, Color) + 'static,
305) -> Result<(), ShellError> {
306    let event_loop = EventLoop::new().map_err(ShellError::EventLoop)?;
307    let mut app = SceneApp {
308        shell: WindowShell::new(options, background),
309        fragment: Scene::new(),
310        paint: Box::new(paint),
311    };
312    event_loop.run_app(&mut app).map_err(ShellError::EventLoop)
313}
314
315struct SceneApp {
316    shell: WindowShell,
317    fragment: Scene,
318    paint: PaintFn,
319}
320
321impl ApplicationHandler for SceneApp {
322    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
323        self.shell.resumed(event_loop);
324    }
325
326    fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
327        self.shell.suspended();
328    }
329
330    fn window_event(
331        &mut self,
332        event_loop: &ActiveEventLoop,
333        window_id: WindowId,
334        event: WindowEvent,
335    ) {
336        if self.shell.window().is_none_or(|w| w.id() != window_id) {
337            return;
338        }
339        match event {
340            WindowEvent::CloseRequested => event_loop.exit(),
341            WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
342            WindowEvent::ScaleFactorChanged { .. } => {
343                if let Some(w) = self.shell.window() {
344                    w.request_redraw();
345                }
346            }
347            WindowEvent::Occluded(occluded) => {
348                if !occluded && let Some(w) = self.shell.window() {
349                    w.request_redraw();
350                }
351            }
352            WindowEvent::RedrawRequested => {
353                let Some((lw, lh, _scale)) = self.shell.logical_size() else {
354                    return;
355                };
356                self.fragment.reset();
357                let bg = self.shell.background;
358                (self.paint)(&mut self.fragment, lw, lh, bg);
359                let fragment = std::mem::replace(&mut self.fragment, Scene::new());
360                self.shell.present(&fragment);
361                self.fragment = fragment;
362            }
363            _ => {}
364        }
365    }
366}
367
368// ------------------------------------------------------------- run_static
369
370/// Opens a window showing a message-free element view. The view is rebuilt
371/// on every redraw; scroll state persists in a [`FrameState`]. Blocks until
372/// the window closes.
373pub fn run_static(
374    options: WindowOptions,
375    theme: Theme,
376    view: impl Fn(&Theme) -> Element<()> + 'static,
377) -> Result<(), ShellError> {
378    let event_loop = EventLoop::new().map_err(ShellError::EventLoop)?;
379    let background = theme.bg;
380    let mut app = StaticApp {
381        shell: WindowShell::new(options, background),
382        theme,
383        fonts: Fonts::with_system(),
384        state: FrameState::new(),
385        view: Box::new(view),
386        cursor: Point::ORIGIN,
387        started: Instant::now(),
388        last_frame: None,
389    };
390    event_loop.run_app(&mut app).map_err(ShellError::EventLoop)
391}
392
393struct StaticApp {
394    shell: WindowShell,
395    theme: Theme,
396    fonts: Fonts,
397    state: FrameState,
398    view: ViewFn,
399    /// Cursor position in logical coordinates.
400    cursor: Point,
401    started: Instant,
402    /// The frame from the last redraw, used to route input between frames.
403    last_frame: Option<fenestra_core::Frame>,
404}
405
406impl StaticApp {
407    fn redraw(&mut self, event_loop: &ActiveEventLoop) {
408        let Some((lw, lh, scale)) = self.shell.logical_size() else {
409            return;
410        };
411        self.state.tick(self.started.elapsed().as_secs_f64());
412        let el = (self.view)(&self.theme);
413        #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
414        let frame = build_frame(
415            &el,
416            &self.theme,
417            &mut self.fonts,
418            &mut self.state,
419            (lw as f32, lh as f32),
420            scale,
421        );
422        let scene = frame.paint(&mut self.fonts, &mut self.state);
423        self.shell.present(&scene);
424        if frame.animating {
425            event_loop.set_control_flow(ControlFlow::WaitUntil(
426                Instant::now() + Duration::from_millis(16),
427            ));
428        } else {
429            event_loop.set_control_flow(ControlFlow::Wait);
430        }
431        self.last_frame = Some(frame);
432    }
433}
434
435impl ApplicationHandler for StaticApp {
436    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
437        self.shell.resumed(event_loop);
438    }
439
440    fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
441        self.shell.suspended();
442    }
443
444    fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
445        if matches!(cause, StartCause::ResumeTimeReached { .. })
446            && let Some(w) = self.shell.window()
447        {
448            w.request_redraw();
449        }
450    }
451
452    fn window_event(
453        &mut self,
454        event_loop: &ActiveEventLoop,
455        window_id: WindowId,
456        event: WindowEvent,
457    ) {
458        if self.shell.window().is_none_or(|w| w.id() != window_id) {
459            return;
460        }
461        match event {
462            WindowEvent::CloseRequested => event_loop.exit(),
463            WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
464            WindowEvent::ScaleFactorChanged { .. } => {
465                if let Some(w) = self.shell.window() {
466                    w.request_redraw();
467                }
468            }
469            WindowEvent::CursorMoved { position, .. } => {
470                let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
471                self.cursor = Point::new(position.x / scale, position.y / scale);
472            }
473            WindowEvent::MouseWheel { delta, .. } => {
474                let dy = match delta {
475                    MouseScrollDelta::LineDelta(_, y) => f64::from(y) * LINE_SCROLL_PX,
476                    MouseScrollDelta::PixelDelta(pos) => {
477                        let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
478                        pos.y / scale
479                    }
480                };
481                if let Some(frame) = &self.last_frame
482                    && let Some(id) = frame.scrollable_at(self.cursor)
483                {
484                    #[expect(
485                        clippy::cast_possible_truncation,
486                        reason = "scroll deltas fit in f32"
487                    )]
488                    self.state.scroll_by(id, -dy as f32);
489                    if let Some(w) = self.shell.window() {
490                        w.request_redraw();
491                    }
492                }
493            }
494            WindowEvent::RedrawRequested => self.redraw(event_loop),
495            _ => {}
496        }
497    }
498}
499
500// ------------------------------------------------------------- run_app
501
502/// User events crossing into the app runner's loop: type-erased app
503/// messages from a [`fenestra_core::Proxy`] (any thread), and AccessKit's
504/// activation/action events.
505enum RunnerEvent {
506    App(Box<dyn std::any::Any + Send>),
507    Access(accesskit_winit::Event),
508}
509
510impl From<accesskit_winit::Event> for RunnerEvent {
511    fn from(event: accesskit_winit::Event) -> Self {
512        Self::Access(event)
513    }
514}
515
516/// Runs an [`App`]: the full Elm-shaped loop with hit testing, hover/active/
517/// focus, keyboard navigation, message dispatch, and event-driven repaint
518/// (animation frames only while something animates). Calls [`App::init`]
519/// with a [`fenestra_core::Proxy`] before the first frame; proxied messages
520/// wake the loop and repaint. Blocks until the window closes.
521pub fn run_app<A: App + 'static>(mut app: A, options: WindowOptions) -> Result<(), ShellError>
522where
523    A::Msg: Send,
524{
525    let event_loop = EventLoop::<RunnerEvent>::with_user_event()
526        .build()
527        .map_err(ShellError::EventLoop)?;
528    let access_proxy = event_loop.create_proxy();
529    let proxy = event_loop.create_proxy();
530    app.init(fenestra_core::Proxy::new(move |msg: A::Msg| {
531        // Dropped silently once the loop is gone (window closed).
532        let _ = proxy.send_event(RunnerEvent::App(Box::new(msg)));
533    }));
534    let background = app.theme().bg;
535    let mut state = FrameState::new();
536    state.set_clipboard(Box::new(crate::OsClipboard::default()));
537    let mut runner = AppRunner {
538        shell: WindowShell::new(options, background),
539        app,
540        fonts: Fonts::with_system(),
541        state,
542        cursor: Point::ORIGIN,
543        started: Instant::now(),
544        last: None,
545        modifiers: winit::keyboard::ModifiersState::empty(),
546        adapter: None,
547        proxy: access_proxy,
548    };
549    event_loop
550        .run_app(&mut runner)
551        .map_err(ShellError::EventLoop)
552}
553
554struct AppRunner<A: App> {
555    shell: WindowShell,
556    app: A,
557    fonts: Fonts,
558    state: FrameState,
559    cursor: Point,
560    started: Instant,
561    /// View and frame from the last redraw, for input routing.
562    last: Option<(Element<A::Msg>, fenestra_core::Frame)>,
563    modifiers: winit::keyboard::ModifiersState,
564    /// The AccessKit adapter, created before the window first shows.
565    adapter: Option<accesskit_winit::Adapter>,
566    /// Loop proxy handed to the adapter for activation/action events.
567    proxy: EventLoopProxy<RunnerEvent>,
568}
569
570impl<A: App> AppRunner<A> {
571    fn redraw(&mut self, event_loop: &ActiveEventLoop) {
572        let Some((lw, lh, scale)) = self.shell.logical_size() else {
573            return;
574        };
575        let theme = self.app.theme();
576        self.shell.background = theme.bg;
577        self.state.tick(self.started.elapsed().as_secs_f64());
578        let view = self.app.view();
579        #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
580        let frame = build_frame(
581            &view,
582            &theme,
583            &mut self.fonts,
584            &mut self.state,
585            (lw as f32, lh as f32),
586            scale,
587        );
588        let scene = frame.paint(&mut self.fonts, &mut self.state);
589        self.shell.present(&scene);
590        // Content may have moved under a stationary pointer (scroll,
591        // layout change): refresh hover and repaint once more if it did.
592        if refresh_hover(&view, &frame, &mut self.state)
593            && let Some(w) = self.shell.window()
594        {
595            w.request_redraw();
596        }
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 = Some((view, frame));
605        self.push_access_tree();
606    }
607
608    /// Pushes the current frame's accessibility projection to the platform
609    /// (no-op until assistive technology activates the tree).
610    fn push_access_tree(&mut self) {
611        let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
612        let focus = self.state.focused();
613        if let Some(adapter) = &mut self.adapter
614            && let Some((_, frame)) = &self.last
615        {
616            adapter.update_if_active(|| crate::access::tree_update(frame, focus, scale));
617        }
618    }
619
620    fn input(&mut self, event: InputEvent) {
621        let Some((view, frame)) = &self.last else {
622            return;
623        };
624        let result = dispatch(view, frame, &mut self.state, &mut self.fonts, event);
625        if let Some(cursor) = result.cursor
626            && let Some(w) = self.shell.window()
627        {
628            w.set_cursor(winit::window::Cursor::Icon(map_cursor(cursor)));
629        }
630        let had_msgs = !result.msgs.is_empty();
631        for msg in result.msgs {
632            self.app.update(msg);
633        }
634        if (result.redraw || had_msgs)
635            && let Some(w) = self.shell.window()
636        {
637            w.request_redraw();
638        }
639    }
640}
641
642fn map_cursor(cursor: fenestra_core::Cursor) -> winit::window::CursorIcon {
643    match cursor {
644        fenestra_core::Cursor::Default => winit::window::CursorIcon::Default,
645        fenestra_core::Cursor::Pointer => winit::window::CursorIcon::Pointer,
646        fenestra_core::Cursor::Text => winit::window::CursorIcon::Text,
647        fenestra_core::Cursor::NotAllowed => winit::window::CursorIcon::NotAllowed,
648    }
649}
650
651/// Translates a winit key event into a fenestra [`InputEvent`].
652fn map_key(
653    event: &winit::event::KeyEvent,
654    mods: winit::keyboard::ModifiersState,
655) -> Option<InputEvent> {
656    use winit::keyboard::{Key as WKey, NamedKey};
657    let key = match &event.logical_key {
658        WKey::Named(NamedKey::Tab) => {
659            return Some(if mods.shift_key() {
660                InputEvent::ShiftTab
661            } else {
662                InputEvent::Tab
663            });
664        }
665        WKey::Named(named) => match named {
666            NamedKey::Enter => Key::Enter,
667            NamedKey::Space => Key::Space,
668            NamedKey::Escape => Key::Escape,
669            NamedKey::ArrowLeft => Key::ArrowLeft,
670            NamedKey::ArrowRight => Key::ArrowRight,
671            NamedKey::ArrowUp => Key::ArrowUp,
672            NamedKey::ArrowDown => Key::ArrowDown,
673            NamedKey::Home => Key::Home,
674            NamedKey::End => Key::End,
675            NamedKey::Backspace => Key::Backspace,
676            NamedKey::Delete => Key::Delete,
677            _ => return None,
678        },
679        WKey::Character(s) => Key::Char(s.chars().next()?),
680        _ => return None,
681    };
682    Some(InputEvent::Key(KeyInput {
683        key,
684        shift: mods.shift_key(),
685        ctrl: mods.control_key(),
686        alt: mods.alt_key(),
687        meta: mods.super_key(),
688    }))
689}
690
691impl<A: App> ApplicationHandler<RunnerEvent> for AppRunner<A> {
692    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
693        let adapter = &mut self.adapter;
694        let proxy = self.proxy.clone();
695        self.shell.resumed_with(event_loop, |el, window| {
696            // The adapter must attach while the window is still hidden.
697            if adapter.is_none() {
698                *adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
699                    el, window, proxy,
700                ));
701            }
702        });
703        if let Some(w) = self.shell.window() {
704            w.set_ime_allowed(true);
705        }
706    }
707
708    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: RunnerEvent) {
709        match event {
710            RunnerEvent::App(msg) => {
711                if let Ok(msg) = msg.downcast::<A::Msg>() {
712                    self.app.update(*msg);
713                    if let Some(w) = self.shell.window() {
714                        w.request_redraw();
715                    }
716                }
717            }
718            RunnerEvent::Access(ev) => match ev.window_event {
719                accesskit_winit::WindowEvent::InitialTreeRequested => {
720                    if self.last.is_some() {
721                        self.push_access_tree();
722                    } else if let Some(w) = self.shell.window() {
723                        w.request_redraw();
724                    }
725                }
726                accesskit_winit::WindowEvent::ActionRequested(req) => {
727                    let id = fenestra_core::WidgetId(req.target_node.0);
728                    match req.action {
729                        accesskit::Action::Click => {
730                            if let Some((view, _)) = &self.last
731                                && let Some(msg) = fenestra_core::click_msg_of(view, id)
732                            {
733                                self.app.update(msg);
734                                if let Some(w) = self.shell.window() {
735                                    w.request_redraw();
736                                }
737                            }
738                        }
739                        accesskit::Action::Focus => {
740                            self.state.set_focus(Some(id));
741                            if let Some(w) = self.shell.window() {
742                                w.request_redraw();
743                            }
744                        }
745                        _ => {}
746                    }
747                }
748                accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
749            },
750        }
751    }
752
753    fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
754        self.shell.suspended();
755    }
756
757    fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
758        if matches!(cause, StartCause::ResumeTimeReached { .. })
759            && let Some(w) = self.shell.window()
760        {
761            w.request_redraw();
762        }
763    }
764
765    fn window_event(
766        &mut self,
767        event_loop: &ActiveEventLoop,
768        window_id: WindowId,
769        event: WindowEvent,
770    ) {
771        if self.shell.window().is_none_or(|w| w.id() != window_id) {
772            return;
773        }
774        if let Some(adapter) = &mut self.adapter
775            && let Some(window) = self.shell.window()
776        {
777            adapter.process_event(window, &event);
778        }
779        match event {
780            WindowEvent::CloseRequested => event_loop.exit(),
781            WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
782            WindowEvent::ScaleFactorChanged { .. } => {
783                if let Some(w) = self.shell.window() {
784                    w.request_redraw();
785                }
786            }
787            WindowEvent::ModifiersChanged(mods) => self.modifiers = mods.state(),
788            WindowEvent::Occluded(occluded) => {
789                if !occluded && let Some(w) = self.shell.window() {
790                    w.request_redraw();
791                }
792            }
793            WindowEvent::CursorLeft { .. } => self.input(InputEvent::PointerLeave),
794            WindowEvent::CursorMoved { position, .. } => {
795                let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
796                self.cursor = Point::new(position.x / scale, position.y / scale);
797                #[expect(clippy::cast_possible_truncation, reason = "positions fit in f32")]
798                self.input(InputEvent::PointerMove {
799                    x: self.cursor.x as f32,
800                    y: self.cursor.y as f32,
801                });
802            }
803            WindowEvent::MouseInput {
804                state,
805                button: winit::event::MouseButton::Left,
806                ..
807            } => {
808                self.input(match state {
809                    winit::event::ElementState::Pressed => InputEvent::PointerDown,
810                    winit::event::ElementState::Released => InputEvent::PointerUp,
811                });
812            }
813            WindowEvent::MouseWheel { delta, .. } => {
814                let dy = match delta {
815                    MouseScrollDelta::LineDelta(_, y) => f64::from(y) * LINE_SCROLL_PX,
816                    MouseScrollDelta::PixelDelta(pos) => {
817                        let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
818                        pos.y / scale
819                    }
820                };
821                #[expect(clippy::cast_possible_truncation, reason = "deltas fit in f32")]
822                self.input(InputEvent::Wheel { dy: dy as f32 });
823            }
824            WindowEvent::KeyboardInput { event, .. }
825                if event.state == winit::event::ElementState::Pressed =>
826            {
827                {
828                    let mods = self.modifiers;
829                    // Printable input arrives as Text (it may be multi-char);
830                    // named keys and shortcuts go through Key.
831                    let printable = !mods.control_key()
832                        && !mods.super_key()
833                        && event
834                            .text
835                            .as_ref()
836                            .is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control()));
837                    if printable {
838                        if let Some(t) = &event.text {
839                            self.input(InputEvent::Text(t.to_string()));
840                        }
841                    } else if let Some(input) = map_key(&event, mods) {
842                        self.input(input);
843                    }
844                }
845            }
846            WindowEvent::Ime(ime) => match ime {
847                winit::event::Ime::Preedit(text, cursor) => {
848                    self.input(InputEvent::ImePreedit { text, cursor });
849                }
850                winit::event::Ime::Commit(text) => {
851                    self.input(InputEvent::Text(text));
852                }
853                winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
854            },
855            WindowEvent::RedrawRequested => self.redraw(event_loop),
856            _ => {}
857        }
858    }
859}