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;
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
34/// One wheel "line" in logical pixels.
35pub(crate) const LINE_SCROLL_PX: f64 = 40.0;
36
37/// A raw paint callback: `(scene, logical_w, logical_h, background)`.
38#[cfg(not(target_arch = "wasm32"))]
39type PaintFn = Box<dyn FnMut(&mut Scene, f64, f64, Color)>;
40/// A message-free element view function.
41#[cfg(not(target_arch = "wasm32"))]
42type ViewFn = Box<dyn Fn(&Theme) -> Element<()>>;
43
44/// Options for the application window.
45#[derive(Debug, Clone)]
46pub struct WindowOptions {
47    /// Window title.
48    pub title: String,
49    /// Initial inner size in logical pixels.
50    pub inner_size: (f64, f64),
51    /// Minimum inner size in logical pixels.
52    pub min_size: Option<(f64, f64)>,
53    /// Whether the window can be resized (true by default).
54    pub resizable: bool,
55    /// Open maximized.
56    pub maximized: bool,
57    /// Open borderless-fullscreen on the current monitor.
58    pub fullscreen: bool,
59    /// Window icon as straight-alpha RGBA8 `(width, height, pixels)`.
60    pub icon: Option<(u32, u32, Vec<u8>)>,
61    /// Custom faces registered on the runner's fonts before the first
62    /// frame: design languages work in windows, not just headlessly.
63    pub fonts: Vec<(fenestra_core::FamilyRole, Vec<u8>)>,
64}
65
66impl WindowOptions {
67    /// A window with the given title and the default 1024x768 logical size.
68    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    /// Sets the initial inner size in logical pixels.
82    pub fn with_size(mut self, width: f64, height: f64) -> Self {
83        self.inner_size = (width, height);
84        self
85    }
86
87    /// Sets the minimum inner size in logical pixels.
88    pub fn with_min_size(mut self, width: f64, height: f64) -> Self {
89        self.min_size = Some((width, height));
90        self
91    }
92
93    /// Allows or forbids resizing (allowed by default).
94    pub fn with_resizable(mut self, resizable: bool) -> Self {
95        self.resizable = resizable;
96        self
97    }
98
99    /// Opens the window maximized.
100    pub fn maximized(mut self) -> Self {
101        self.maximized = true;
102        self
103    }
104
105    /// Opens borderless-fullscreen on the current monitor.
106    pub fn fullscreen(mut self) -> Self {
107        self.fullscreen = true;
108        self
109    }
110
111    /// Sets the window icon from straight-alpha RGBA8 pixels (ignored on
112    /// platforms without window icons, including the web).
113    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    /// Registers a custom face under a family role for this window's
119    /// fonts (TTF/OTF bytes; see `Fonts::register`).
120    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    /// Window created; the async surface setup is in flight (web only).
134    #[cfg(target_arch = "wasm32")]
135    Pending(Arc<Window>),
136}
137
138/// Shared surface plumbing for every windowed runner.
139struct WindowShell {
140    context: RenderContext,
141    renderers: Vec<Option<Renderer>>,
142    state: RenderState,
143    scene: Scene,
144    options: WindowOptions,
145    background: Color,
146    /// Completed async surface setup, parked until the next [`Self::pump`]
147    /// (web only; the web is single-threaded so `Rc<RefCell>` suffices).
148    #[cfg(target_arch = "wasm32")]
149    ready: WasmReady,
150}
151
152/// The handoff slot for the web's async surface creation.
153#[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    /// Like [`Self::resumed`], but runs `before_visible` between window
176    /// creation and the first `set_visible(true)` — the AccessKit adapter
177    /// must attach while the window is still hidden.
178    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                    // Malformed icon data: open without one, never panic.
210                    Err(_) => attrs,
211                },
212                None => attrs,
213            };
214            #[cfg(target_arch = "wasm32")]
215            let attrs = {
216                use winit::platform::web::WindowAttributesExtWebSys;
217                // winit creates the canvas; have it inserted into the page.
218                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    /// Builds (or rebuilds, after a lost surface) the swapchain for `window`
235    /// and enters the active state.
236    #[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    /// Web: surface/device setup is async — kick it off and park in
269    /// `Pending`; [`Self::pump`] finishes the activation when it lands.
270    #[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    /// Completes a pending web activation once the async setup finished.
293    /// No-op on native and while nothing is pending.
294    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    /// Logical size and scale factor of the active window.
356    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    /// Scales the logical fragment to physical pixels and presents it.
373    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                // Hidden window: skip the frame; WindowEvent::Occluded(false)
420                // requests the next redraw when it becomes visible again.
421                return;
422            }
423            CurrentSurfaceTexture::Timeout => {
424                window.request_redraw();
425                return;
426            }
427            CurrentSurfaceTexture::Lost => {
428                // Recoverable (GPU reset, driver update, display change):
429                // rebuild the swapchain on the same window and repaint.
430                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// ------------------------------------------------------------- run_scene
460
461/// Opens a window and repaints via `paint(scene, logical_w, logical_h, bg)`
462/// on every redraw. Blocks until the window closes. Low-level escape hatch;
463/// element views should prefer [`run_static`] (or the M4 `App` runner).
464#[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// ------------------------------------------------------------- run_static
535
536/// Opens a window showing a message-free element view. The view is rebuilt
537/// on every redraw; scroll state persists in a [`FrameState`]. Blocks until
538/// the window closes.
539#[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 position in logical coordinates.
572    cursor: Point,
573    started: Instant,
574    /// The frame from the last redraw, used to route input between frames.
575    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
674// ------------------------------------------------------------- run_app
675
676/// User events crossing into the app runner's loop: type-erased app
677/// messages from a [`fenestra_core::Proxy`] (any thread), and AccessKit's
678/// activation/action events.
679enum 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
692/// Runs an [`App`]: the full Elm-shaped loop with hit testing, hover/active/
693/// focus, keyboard navigation, message dispatch, and event-driven repaint
694/// (animation frames only while something animates). Calls [`App::init`]
695/// with a [`fenestra_core::Proxy`] before the first frame; proxied messages
696/// wake the loop and repaint. Blocks until the window closes.
697pub 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        // Dropped silently once the loop is gone (window closed).
709        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        // Non-blocking on the web: the loop keeps running after main returns.
749        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    /// View and frame from the last redraw, for input routing.
762    last: Option<(Element<A::Msg>, fenestra_core::Frame)>,
763    modifiers: winit::keyboard::ModifiersState,
764    /// The AccessKit adapter, created before the window first shows.
765    #[cfg(not(target_arch = "wasm32"))]
766    adapter: Option<accesskit_winit::Adapter>,
767    /// Loop proxy handed to the adapter for activation/action events.
768    #[cfg(not(target_arch = "wasm32"))]
769    proxy: EventLoopProxy<RunnerEvent>,
770    /// Secondary windows declared by [`App::windows`], keyed by their
771    /// stable key and reconciled after every update (native only).
772    #[cfg(not(target_arch = "wasm32"))]
773    secondary: std::collections::HashMap<String, SecondaryWindow<A>>,
774}
775
776/// One reconciled secondary window: its own surface, retained state, and
777/// accessibility adapter; app state and fonts are shared.
778#[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        // Content may have moved under a stationary pointer (scroll,
811        // layout change): refresh hover and repaint once more if it did.
812        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            // The browser paces frames; just ask for the next one.
823            #[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        // Anchor the IME candidate window to the focused caret.
841        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    /// Pushes the current frame's accessibility projection to the platform
854    /// (no-op until assistive technology activates the tree).
855    #[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    /// Routes one input event into the main window, reconciling windows
889    /// afterwards if it produced messages.
890    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    /// Routes one input event into a secondary window, reconciling
897    /// windows afterwards if it produced messages.
898    #[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    /// Reconciles secondary windows against [`App::windows`] and asks
906    /// every window for a repaint — called whenever messages were applied.
907    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    /// Opens, closes, and retitles secondary windows to match the app's
924    /// declared list.
925    #[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    /// Redraws one secondary window: the same pipeline as the main one,
978    /// against its own retained state and `view_for(key)`.
979    #[cfg(not(target_arch = "wasm32"))]
980    fn secondary_redraw(&mut self, key: &str, event_loop: &ActiveEventLoop) {
981        let theme = self.app.theme_for(key);
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    /// The full event handler for one secondary window — the same arms as
1032    /// the main window, against the bundle's own surface and state.
1033    #[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) => {
1064                self.modifiers = mods.state();
1065                let m = self.modifiers;
1066                self.secondary_input_main(
1067                    key,
1068                    event_loop,
1069                    InputEvent::Modifiers {
1070                        shift: m.shift_key(),
1071                        ctrl: m.control_key(),
1072                        alt: m.alt_key(),
1073                        meta: m.super_key(),
1074                    },
1075                );
1076            }
1077            WindowEvent::DroppedFile(path) => {
1078                self.secondary_input_main(key, event_loop, InputEvent::FileDrop(path));
1079            }
1080            WindowEvent::CursorLeft { .. } => {
1081                self.secondary_input_main(key, event_loop, InputEvent::PointerLeave);
1082            }
1083            WindowEvent::CursorMoved { position, .. } => {
1084                let Some(bundle) = self.secondary.get_mut(key) else {
1085                    return;
1086                };
1087                let scale = bundle.shell.window().map_or(1.0, |w| w.scale_factor());
1088                bundle.cursor = Point::new(position.x / scale, position.y / scale);
1089                #[expect(clippy::cast_possible_truncation, reason = "positions fit in f32")]
1090                let (x, y) = (bundle.cursor.x as f32, bundle.cursor.y as f32);
1091                self.secondary_input_main(key, event_loop, InputEvent::PointerMove { x, y });
1092            }
1093            WindowEvent::MouseInput {
1094                state,
1095                button: winit::event::MouseButton::Left,
1096                ..
1097            } => {
1098                self.secondary_input_main(
1099                    key,
1100                    event_loop,
1101                    match state {
1102                        winit::event::ElementState::Pressed => InputEvent::PointerDown,
1103                        winit::event::ElementState::Released => InputEvent::PointerUp,
1104                    },
1105                );
1106            }
1107            WindowEvent::MouseInput {
1108                state,
1109                button: winit::event::MouseButton::Right,
1110                ..
1111            } => {
1112                self.secondary_input_main(
1113                    key,
1114                    event_loop,
1115                    match state {
1116                        winit::event::ElementState::Pressed => InputEvent::RightDown,
1117                        winit::event::ElementState::Released => InputEvent::RightUp,
1118                    },
1119                );
1120            }
1121            WindowEvent::MouseWheel { delta, .. } => {
1122                let dy = match delta {
1123                    MouseScrollDelta::LineDelta(_, y) => f64::from(y) * LINE_SCROLL_PX,
1124                    MouseScrollDelta::PixelDelta(pos) => {
1125                        let scale = self
1126                            .secondary
1127                            .get(key)
1128                            .and_then(|b| b.shell.window())
1129                            .map_or(1.0, |w| w.scale_factor());
1130                        pos.y / scale
1131                    }
1132                };
1133                #[expect(clippy::cast_possible_truncation, reason = "deltas fit in f32")]
1134                self.secondary_input_main(key, event_loop, InputEvent::Wheel { dy: dy as f32 });
1135            }
1136            WindowEvent::KeyboardInput { event, .. }
1137                if event.state == winit::event::ElementState::Pressed =>
1138            {
1139                let mods = self.modifiers;
1140                let printable = !mods.control_key()
1141                    && !mods.super_key()
1142                    && event
1143                        .text
1144                        .as_ref()
1145                        .is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control()));
1146                if printable {
1147                    if let Some(t) = &event.text {
1148                        self.secondary_input_main(key, event_loop, InputEvent::Text(t.to_string()));
1149                    }
1150                } else if let Some(input) = map_key(&event, mods) {
1151                    self.secondary_input_main(key, event_loop, input);
1152                }
1153            }
1154            WindowEvent::Ime(ime) => match ime {
1155                winit::event::Ime::Preedit(text, cursor) => {
1156                    self.secondary_input_main(
1157                        key,
1158                        event_loop,
1159                        InputEvent::ImePreedit { text, cursor },
1160                    );
1161                }
1162                winit::event::Ime::Commit(text) => {
1163                    self.secondary_input_main(key, event_loop, InputEvent::Text(text));
1164                }
1165                winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
1166            },
1167            WindowEvent::RedrawRequested => self.secondary_redraw(key, event_loop),
1168            _ => {}
1169        }
1170    }
1171
1172    /// Dispatches one input event against a secondary window. Returns
1173    /// whether messages were applied (the caller then reconciles).
1174    #[cfg(not(target_arch = "wasm32"))]
1175    fn secondary_input(&mut self, key: &str, event: InputEvent) -> bool {
1176        let Some(bundle) = self.secondary.get_mut(key) else {
1177            return false;
1178        };
1179        let Some((view, frame)) = &bundle.last else {
1180            return false;
1181        };
1182        let result = dispatch(view, frame, &mut bundle.state, &mut self.fonts, event);
1183        if let Some(cursor) = result.cursor
1184            && let Some(w) = bundle.shell.window()
1185        {
1186            w.set_cursor(winit::window::Cursor::Icon(map_cursor(cursor)));
1187        }
1188        let had_msgs = !result.msgs.is_empty();
1189        if (result.redraw || had_msgs)
1190            && let Some(w) = bundle.shell.window()
1191        {
1192            w.request_redraw();
1193        }
1194        let msgs = result.msgs;
1195        for msg in msgs {
1196            self.app.update(msg);
1197        }
1198        had_msgs
1199    }
1200}
1201
1202pub(crate) fn map_cursor(cursor: fenestra_core::Cursor) -> winit::window::CursorIcon {
1203    match cursor {
1204        fenestra_core::Cursor::Default => winit::window::CursorIcon::Default,
1205        fenestra_core::Cursor::Pointer => winit::window::CursorIcon::Pointer,
1206        fenestra_core::Cursor::Text => winit::window::CursorIcon::Text,
1207        fenestra_core::Cursor::NotAllowed => winit::window::CursorIcon::NotAllowed,
1208    }
1209}
1210
1211/// Translates a winit key event into a fenestra [`InputEvent`].
1212pub(crate) fn map_key(
1213    event: &winit::event::KeyEvent,
1214    mods: winit::keyboard::ModifiersState,
1215) -> Option<InputEvent> {
1216    use winit::keyboard::{Key as WKey, NamedKey};
1217    let key = match &event.logical_key {
1218        WKey::Named(NamedKey::Tab) => {
1219            return Some(if mods.shift_key() {
1220                InputEvent::ShiftTab
1221            } else {
1222                InputEvent::Tab
1223            });
1224        }
1225        WKey::Named(named) => match named {
1226            NamedKey::Enter => Key::Enter,
1227            NamedKey::Space => Key::Space,
1228            NamedKey::Escape => Key::Escape,
1229            NamedKey::ArrowLeft => Key::ArrowLeft,
1230            NamedKey::ArrowRight => Key::ArrowRight,
1231            NamedKey::ArrowUp => Key::ArrowUp,
1232            NamedKey::ArrowDown => Key::ArrowDown,
1233            NamedKey::Home => Key::Home,
1234            NamedKey::End => Key::End,
1235            NamedKey::Backspace => Key::Backspace,
1236            NamedKey::Delete => Key::Delete,
1237            NamedKey::PageUp => Key::PageUp,
1238            NamedKey::PageDown => Key::PageDown,
1239            _ => return None,
1240        },
1241        WKey::Character(s) => Key::Char(s.chars().next()?),
1242        _ => return None,
1243    };
1244    Some(InputEvent::Key(KeyInput {
1245        key,
1246        shift: mods.shift_key(),
1247        ctrl: mods.control_key(),
1248        alt: mods.alt_key(),
1249        meta: mods.super_key(),
1250    }))
1251}
1252
1253impl<A: App> ApplicationHandler<RunnerEvent> for AppRunner<A> {
1254    #[cfg(not(target_arch = "wasm32"))]
1255    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1256        let adapter = &mut self.adapter;
1257        let proxy = self.proxy.clone();
1258        self.shell.resumed_with(event_loop, |el, window| {
1259            // The adapter must attach while the window is still hidden.
1260            if adapter.is_none() {
1261                *adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
1262                    el, window, proxy,
1263                ));
1264            }
1265        });
1266        if let Some(w) = self.shell.window() {
1267            w.set_ime_allowed(true);
1268        }
1269        for bundle in self.secondary.values_mut() {
1270            bundle.shell.resumed(event_loop);
1271        }
1272        self.reconcile_windows(event_loop);
1273    }
1274
1275    #[cfg(target_arch = "wasm32")]
1276    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1277        self.shell.resumed(event_loop);
1278        if let Some(w) = self.shell.window() {
1279            w.set_ime_allowed(true);
1280        }
1281    }
1282
1283    fn user_event(&mut self, event_loop: &ActiveEventLoop, event: RunnerEvent) {
1284        match event {
1285            RunnerEvent::App(msg) => {
1286                if let Ok(msg) = msg.downcast::<A::Msg>() {
1287                    self.app.update(*msg);
1288                    self.after_update(event_loop);
1289                }
1290            }
1291            #[cfg(not(target_arch = "wasm32"))]
1292            RunnerEvent::Access(ev) => {
1293                // Route by window id: `None` is the main window, `Some(key)`
1294                // a secondary one; unknown ids are stale events.
1295                let is_main = self.shell.window().is_some_and(|w| w.id() == ev.window_id);
1296                let skey = (!is_main)
1297                    .then(|| {
1298                        self.secondary
1299                            .iter()
1300                            .find(|(_, b)| b.shell.window().is_some_and(|w| w.id() == ev.window_id))
1301                            .map(|(k, _)| k.clone())
1302                    })
1303                    .flatten();
1304                if !is_main && skey.is_none() {
1305                    return;
1306                }
1307                match ev.window_event {
1308                    accesskit_winit::WindowEvent::InitialTreeRequested => match &skey {
1309                        None => {
1310                            if self.last.is_some() {
1311                                self.push_access_tree();
1312                            } else if let Some(w) = self.shell.window() {
1313                                w.request_redraw();
1314                            }
1315                        }
1316                        Some(key) => {
1317                            if let Some(bundle) = self.secondary.get_mut(key) {
1318                                let scale = bundle.shell.window().map_or(1.0, |w| w.scale_factor());
1319                                let focus = bundle.state.focused();
1320                                if let Some((_, frame)) = &bundle.last {
1321                                    if let Some(adapter) = &mut bundle.adapter {
1322                                        adapter.update_if_active(|| {
1323                                            crate::access::tree_update(frame, focus, scale)
1324                                        });
1325                                    }
1326                                } else if let Some(w) = bundle.shell.window() {
1327                                    w.request_redraw();
1328                                }
1329                            }
1330                        }
1331                    },
1332                    accesskit_winit::WindowEvent::ActionRequested(req) => {
1333                        let id = fenestra_core::WidgetId(req.target_node.0);
1334                        match req.action {
1335                            accesskit::Action::Click => {
1336                                let msg = match &skey {
1337                                    None => self.last.as_ref().and_then(|(view, frame)| {
1338                                        fenestra_core::click_msg_of(view, frame, &self.state, id)
1339                                    }),
1340                                    Some(key) => self.secondary.get(key).and_then(|bundle| {
1341                                        bundle.last.as_ref().and_then(|(view, frame)| {
1342                                            fenestra_core::click_msg_of(
1343                                                view,
1344                                                frame,
1345                                                &bundle.state,
1346                                                id,
1347                                            )
1348                                        })
1349                                    }),
1350                                };
1351                                if let Some(msg) = msg {
1352                                    self.app.update(msg);
1353                                    self.after_update(event_loop);
1354                                }
1355                            }
1356                            accesskit::Action::Focus => match &skey {
1357                                None => {
1358                                    self.state.set_focus(Some(id));
1359                                    if let Some(w) = self.shell.window() {
1360                                        w.request_redraw();
1361                                    }
1362                                }
1363                                Some(key) => {
1364                                    if let Some(bundle) = self.secondary.get_mut(key) {
1365                                        bundle.state.set_focus(Some(id));
1366                                        if let Some(w) = bundle.shell.window() {
1367                                            w.request_redraw();
1368                                        }
1369                                    }
1370                                }
1371                            },
1372                            _ => {}
1373                        }
1374                    }
1375                    accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
1376                }
1377            }
1378        }
1379    }
1380
1381    fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
1382        self.shell.suspended();
1383        #[cfg(not(target_arch = "wasm32"))]
1384        for bundle in self.secondary.values_mut() {
1385            bundle.shell.suspended();
1386        }
1387    }
1388
1389    fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
1390        if !matches!(cause, StartCause::ResumeTimeReached { .. }) {
1391            return;
1392        }
1393        if let Some(w) = self.shell.window() {
1394            w.request_redraw();
1395        }
1396        #[cfg(not(target_arch = "wasm32"))]
1397        for bundle in self.secondary.values() {
1398            if let Some(w) = bundle.shell.window() {
1399                w.request_redraw();
1400            }
1401        }
1402    }
1403
1404    fn window_event(
1405        &mut self,
1406        event_loop: &ActiveEventLoop,
1407        window_id: WindowId,
1408        event: WindowEvent,
1409    ) {
1410        if self.shell.window().is_none_or(|w| w.id() != window_id) {
1411            #[cfg(not(target_arch = "wasm32"))]
1412            if let Some(key) = self
1413                .secondary
1414                .iter()
1415                .find(|(_, b)| b.shell.window().is_some_and(|w| w.id() == window_id))
1416                .map(|(k, _)| k.clone())
1417            {
1418                self.secondary_window_event(&key, event_loop, event);
1419            }
1420            return;
1421        }
1422        #[cfg(not(target_arch = "wasm32"))]
1423        if let Some(adapter) = &mut self.adapter
1424            && let Some(window) = self.shell.window()
1425        {
1426            adapter.process_event(window, &event);
1427        }
1428        match event {
1429            WindowEvent::CloseRequested => event_loop.exit(),
1430            WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
1431            WindowEvent::ScaleFactorChanged { .. } => {
1432                if let Some(w) = self.shell.window() {
1433                    w.request_redraw();
1434                }
1435            }
1436            WindowEvent::ModifiersChanged(mods) => {
1437                self.modifiers = mods.state();
1438                let m = self.modifiers;
1439                self.input_main(
1440                    event_loop,
1441                    InputEvent::Modifiers {
1442                        shift: m.shift_key(),
1443                        ctrl: m.control_key(),
1444                        alt: m.alt_key(),
1445                        meta: m.super_key(),
1446                    },
1447                );
1448            }
1449            WindowEvent::Occluded(occluded) => {
1450                if !occluded && let Some(w) = self.shell.window() {
1451                    w.request_redraw();
1452                }
1453            }
1454            WindowEvent::DroppedFile(path) => {
1455                self.input_main(event_loop, InputEvent::FileDrop(path))
1456            }
1457            WindowEvent::CursorLeft { .. } => self.input_main(event_loop, InputEvent::PointerLeave),
1458            WindowEvent::CursorMoved { position, .. } => {
1459                let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
1460                self.cursor = Point::new(position.x / scale, position.y / scale);
1461                #[expect(clippy::cast_possible_truncation, reason = "positions fit in f32")]
1462                self.input_main(
1463                    event_loop,
1464                    InputEvent::PointerMove {
1465                        x: self.cursor.x as f32,
1466                        y: self.cursor.y as f32,
1467                    },
1468                );
1469            }
1470            WindowEvent::MouseInput {
1471                state,
1472                button: winit::event::MouseButton::Left,
1473                ..
1474            } => {
1475                self.input_main(
1476                    event_loop,
1477                    match state {
1478                        winit::event::ElementState::Pressed => InputEvent::PointerDown,
1479                        winit::event::ElementState::Released => InputEvent::PointerUp,
1480                    },
1481                );
1482            }
1483            WindowEvent::MouseInput {
1484                state,
1485                button: winit::event::MouseButton::Right,
1486                ..
1487            } => {
1488                self.input_main(
1489                    event_loop,
1490                    match state {
1491                        winit::event::ElementState::Pressed => InputEvent::RightDown,
1492                        winit::event::ElementState::Released => InputEvent::RightUp,
1493                    },
1494                );
1495            }
1496            WindowEvent::MouseWheel { delta, .. } => {
1497                let dy = match delta {
1498                    MouseScrollDelta::LineDelta(_, y) => f64::from(y) * LINE_SCROLL_PX,
1499                    MouseScrollDelta::PixelDelta(pos) => {
1500                        let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
1501                        pos.y / scale
1502                    }
1503                };
1504                #[expect(clippy::cast_possible_truncation, reason = "deltas fit in f32")]
1505                self.input_main(event_loop, InputEvent::Wheel { dy: dy as f32 });
1506            }
1507            WindowEvent::KeyboardInput { event, .. }
1508                if event.state == winit::event::ElementState::Pressed =>
1509            {
1510                {
1511                    let mods = self.modifiers;
1512                    // Printable input arrives as Text (it may be multi-char);
1513                    // named keys and shortcuts go through Key.
1514                    let printable = !mods.control_key()
1515                        && !mods.super_key()
1516                        && event
1517                            .text
1518                            .as_ref()
1519                            .is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control()));
1520                    if printable {
1521                        if let Some(t) = &event.text {
1522                            self.input_main(event_loop, InputEvent::Text(t.to_string()));
1523                        }
1524                    } else if let Some(input) = map_key(&event, mods) {
1525                        self.input_main(event_loop, input);
1526                    }
1527                }
1528            }
1529            WindowEvent::Ime(ime) => match ime {
1530                winit::event::Ime::Preedit(text, cursor) => {
1531                    self.input_main(event_loop, InputEvent::ImePreedit { text, cursor });
1532                }
1533                winit::event::Ime::Commit(text) => {
1534                    self.input_main(event_loop, InputEvent::Text(text));
1535                }
1536                winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
1537            },
1538            WindowEvent::RedrawRequested => self.redraw(event_loop),
1539            _ => {}
1540        }
1541    }
1542}