Skip to main content

telar_platform_desktop/
platform.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use platform_core::{
5    Event, EventHandler, FullscreenMode, MultiSurfacePlatform, Platform, PlatformError,
6    PointerButton, PointerSource, ScrollDelta, SurfaceId, Window, WindowConfig, WindowPosition,
7};
8use winit::application::ApplicationHandler;
9use winit::event::{ElementState, MouseScrollDelta, StartCause, Touch, TouchPhase, WindowEvent};
10use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
11use winit::keyboard::Key as WinitKey;
12use winit::window::{Fullscreen, WindowAttributes, WindowId, WindowLevel};
13
14use platform_winit::WinitWindow;
15
16// Winit user-event payloads injected from background threads (via EventLoopProxy) to wake the loop.
17enum UserEvent {
18    // The OS color-scheme flipped; carries the new dark (`true`) / light preference (Linux portal watch).
19    ColorScheme(bool),
20    // A background thread asked to wake the UI (via the app redraw waker); redraw every live surface so each
21    // one's `on_frame` runs and drains its channels — wherever the waking app's content currently lives.
22    Wake,
23}
24
25pub struct WinitPlatform {
26    event_loop: EventLoop<UserEvent>,
27}
28
29impl WinitPlatform {
30    pub fn try_new() -> Result<Self, PlatformError> {
31        Ok(Self {
32            event_loop: EventLoop::<UserEvent>::with_user_event()
33                .build()
34                .map_err(|e| PlatformError(e.to_string()))?,
35        })
36    }
37}
38
39struct WinitRunner<H: EventHandler<WinitWindow>> {
40    handler: H,
41    window: Option<WinitWindow>,
42    config: WindowConfig,
43    cursor_position: (f64, f64),
44    scale_factor: f64,
45    modifiers: platform_core::ModifiersState,
46    // True only on WaitUntil timer expiry; gates keepalive request_redraw() so it doesn't fire on every event queue drain.
47    timer_has_fired: bool,
48}
49
50impl<H: EventHandler<WinitWindow>> ApplicationHandler<UserEvent> for WinitRunner<H> {
51    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
52        match event {
53            UserEvent::ColorScheme(dark) => {
54                if let Some(window) = &self.window {
55                    self.handler
56                        .on_event(Event::ColorSchemeChanged { dark }, window);
57                }
58            }
59            UserEvent::Wake => {
60                if let Some(window) = &self.window {
61                    window.request_redraw();
62                }
63            }
64        }
65    }
66
67    fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
68        self.timer_has_fired = matches!(cause, StartCause::ResumeTimeReached { .. });
69        self.handler.new_events();
70    }
71
72    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
73        if let Some(d) = self.handler.about_to_wait() {
74            // Only request_redraw() on timer expiry, not every drain; reactive changes call it themselves via flush_notify.
75            if self.timer_has_fired {
76                if let Some(window) = &self.window {
77                    window.request_redraw();
78                }
79            }
80            event_loop.set_control_flow(ControlFlow::WaitUntil(std::time::Instant::now() + d));
81        } else {
82            event_loop.set_control_flow(ControlFlow::Wait);
83        }
84    }
85
86    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
87        let Some(window) = create_window_from_config(event_loop, &self.config) else {
88            return;
89        };
90        // Deliver the OS light/dark preference before the tree mounts, so its first layout uses the
91        // right theme. winit reports it on Windows/macOS; on Linux fall back to the freedesktop portal.
92        if let Some(dark) = initial_prefers_dark(&window) {
93            self.handler
94                .on_event(Event::ColorSchemeChanged { dark }, &window);
95        }
96        if !self.handler.on_resume(&window) {
97            event_loop.exit();
98            return;
99        }
100        window.request_redraw();
101        self.window = Some(window);
102    }
103
104    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
105        // Clone (a cheap Arc bump) so the immutable window borrow doesn't conflict with the mutable
106        // self.handler/cursor/scale/modifiers borrows the shared dispatcher takes.
107        let Some(window) = self.window.clone() else {
108            return;
109        };
110        let outcome = dispatch_window_event(
111            &mut self.handler,
112            &window,
113            &mut self.cursor_position,
114            &mut self.scale_factor,
115            &mut self.modifiers,
116            event,
117        );
118        // A custom title-bar close button sets the handler's exit request during dispatch; honor it alongside
119        // the OS close (window manager X / Alt-F4).
120        if matches!(outcome, WindowEventOutcome::CloseRequested) || self.handler.take_exit_request()
121        {
122            event_loop.exit();
123        }
124    }
125}
126
127enum WindowEventOutcome {
128    Continue,
129    CloseRequested,
130}
131
132// Builds a winit window from a `WindowConfig`. Shared by the single- and multi-surface runners.
133fn create_window_from_config(
134    event_loop: &ActiveEventLoop,
135    config: &WindowConfig,
136) -> Option<WinitWindow> {
137    let mut attributes = WindowAttributes::default()
138        .with_title(config.title.as_str())
139        .with_inner_size(winit::dpi::LogicalSize::new(config.width, config.height))
140        .with_resizable(config.is_resizable)
141        .with_decorations(config.has_decorations)
142        .with_transparent(config.is_transparent);
143
144    if let Some((w, h)) = config.min_size {
145        attributes = attributes.with_min_inner_size(winit::dpi::LogicalSize::new(w, h));
146    }
147    if let Some((w, h)) = config.max_size {
148        attributes = attributes.with_max_inner_size(winit::dpi::LogicalSize::new(w, h));
149    }
150    match config.fullscreen {
151        FullscreenMode::Disabled => {}
152        // Exclusive requires a concrete video mode; fall back to borderless for now.
153        FullscreenMode::Borderless | FullscreenMode::Exclusive => {
154            attributes = attributes.with_fullscreen(Some(Fullscreen::Borderless(None)));
155        }
156    }
157    if let WindowPosition::At(x, y) = config.position {
158        attributes = attributes.with_position(winit::dpi::PhysicalPosition::new(x, y));
159    }
160    if config.is_always_on_top {
161        attributes = attributes.with_window_level(WindowLevel::AlwaysOnTop);
162    }
163
164    match event_loop.create_window(attributes) {
165        Ok(w) => Some(WinitWindow(std::sync::Arc::new(w))),
166        Err(e) => {
167            tracing::error!(error = %e, "failed to create window");
168            None
169        }
170    }
171}
172
173// What a mapped winit `WindowEvent` means at the platform level, decoupled from *how* it's applied. The
174// single-window runner applies it to a handler directly; the multi-window runner forwards it to that
175// surface's worker thread. Keeping the mapping here (and the application at the call site) lets both share
176// the exact same winit→platform translation.
177enum SurfaceIntent {
178    // Deliver this platform event to the handler.
179    Event(Event),
180    // Deliver this platform event, then request a redraw (winit `Resized`).
181    Resized(Event),
182    // Render now (winit `RedrawRequested`).
183    Redraw,
184    // Deliver `WindowCloseRequested`, then close this surface.
185    Close(Event),
186    // State-only (e.g. `ModifiersChanged`) or an unmapped event — nothing to deliver.
187    Ignore,
188}
189
190// Pure winit `WindowEvent` → [`SurfaceIntent`] translation, updating this surface's cursor/scale/modifiers.
191// No handler and no window side effects, so it can run on the winit thread while the handler lives elsewhere.
192fn map_window_event(
193    event: WindowEvent,
194    cursor_position: &mut (f64, f64),
195    scale_factor: &mut f64,
196    modifiers: &mut platform_core::ModifiersState,
197) -> SurfaceIntent {
198    match event {
199        WindowEvent::CloseRequested => SurfaceIntent::Close(Event::WindowCloseRequested),
200        WindowEvent::Resized(size) => SurfaceIntent::Resized(Event::WindowResized {
201            width: (size.width as f64 / *scale_factor).round() as u32,
202            height: (size.height as f64 / *scale_factor).round() as u32,
203        }),
204        WindowEvent::RedrawRequested => SurfaceIntent::Redraw,
205        WindowEvent::CursorMoved { position, .. } => {
206            let lx = position.x / *scale_factor;
207            let ly = position.y / *scale_factor;
208            *cursor_position = (lx, ly);
209            SurfaceIntent::Event(Event::PointerMoved {
210                x: lx,
211                y: ly,
212                source: PointerSource::Mouse,
213            })
214        }
215        WindowEvent::MouseInput { state, button, .. } => {
216            let Some(btn) = platform_winit::map_mouse_button(button) else {
217                return SurfaceIntent::Ignore;
218            };
219            let (x, y) = *cursor_position;
220            SurfaceIntent::Event(match state {
221                ElementState::Pressed => Event::PointerPressed {
222                    x,
223                    y,
224                    button: btn,
225                    source: PointerSource::Mouse,
226                },
227                ElementState::Released => Event::PointerReleased {
228                    x,
229                    y,
230                    button: btn,
231                    source: PointerSource::Mouse,
232                },
233            })
234        }
235        WindowEvent::Touch(Touch {
236            phase,
237            location,
238            id,
239            ..
240        }) => {
241            let x = location.x / *scale_factor;
242            let y = location.y / *scale_factor;
243            let source = PointerSource::Touch { id };
244            SurfaceIntent::Event(match phase {
245                TouchPhase::Started => Event::PointerPressed {
246                    x,
247                    y,
248                    button: PointerButton::Primary,
249                    source,
250                },
251                TouchPhase::Moved => Event::PointerMoved { x, y, source },
252                TouchPhase::Ended | TouchPhase::Cancelled => Event::PointerReleased {
253                    x,
254                    y,
255                    button: PointerButton::Primary,
256                    source,
257                },
258            })
259        }
260        WindowEvent::Focused(is_focused) => {
261            SurfaceIntent::Event(Event::FocusChanged { is_focused })
262        }
263        WindowEvent::CursorEntered { .. } => SurfaceIntent::Event(Event::CursorEntered),
264        WindowEvent::CursorLeft { .. } => SurfaceIntent::Event(Event::CursorLeft),
265        WindowEvent::ScaleFactorChanged {
266            scale_factor: new_scale,
267            ..
268        } => {
269            *scale_factor = new_scale;
270            SurfaceIntent::Event(Event::ScaleFactorChanged {
271                scale_factor: new_scale,
272            })
273        }
274        WindowEvent::MouseWheel { delta, .. } => {
275            let scroll_delta = match delta {
276                MouseScrollDelta::LineDelta(x, y) => ScrollDelta::Lines { x, y },
277                MouseScrollDelta::PixelDelta(pos) => ScrollDelta::Pixels {
278                    x: (pos.x / *scale_factor) as f32,
279                    y: (pos.y / *scale_factor) as f32,
280                },
281            };
282            SurfaceIntent::Event(Event::Scrolled {
283                delta: scroll_delta,
284            })
285        }
286        WindowEvent::ModifiersChanged(mods) => {
287            *modifiers = platform_winit::map_modifiers(&mods);
288            SurfaceIntent::Ignore
289        }
290        WindowEvent::KeyboardInput { event, .. } => {
291            let key = match &event.logical_key {
292                WinitKey::Character(c) => match c.as_str().chars().next() {
293                    Some(ch) => platform_core::Key::Char(ch),
294                    None => return SurfaceIntent::Ignore,
295                },
296                WinitKey::Named(named) => match platform_winit::map_named_key(*named) {
297                    Some(nk) => platform_core::Key::Named(nk),
298                    None => return SurfaceIntent::Ignore,
299                },
300                _ => return SurfaceIntent::Ignore,
301            };
302            let mods = *modifiers;
303            SurfaceIntent::Event(match event.state {
304                ElementState::Pressed => Event::KeyPressed {
305                    key,
306                    modifiers: mods,
307                },
308                ElementState::Released => Event::KeyReleased {
309                    key,
310                    modifiers: mods,
311                },
312            })
313        }
314        WindowEvent::ThemeChanged(theme) => SurfaceIntent::Event(Event::ColorSchemeChanged {
315            dark: theme == winit::window::Theme::Dark,
316        }),
317        _ => SurfaceIntent::Ignore,
318    }
319}
320
321// Applies one winit `WindowEvent` to a single surface's [`EventHandler`] on the same thread. Returns whether
322// the surface requested close.
323fn dispatch_window_event<H: EventHandler<WinitWindow>>(
324    handler: &mut H,
325    window: &WinitWindow,
326    cursor_position: &mut (f64, f64),
327    scale_factor: &mut f64,
328    modifiers: &mut platform_core::ModifiersState,
329    event: WindowEvent,
330) -> WindowEventOutcome {
331    match map_window_event(event, cursor_position, scale_factor, modifiers) {
332        SurfaceIntent::Event(e) => handler.on_event(e, window),
333        SurfaceIntent::Resized(e) => {
334            handler.on_event(e, window);
335            window.request_redraw();
336        }
337        SurfaceIntent::Redraw => handler.on_redraw(window),
338        SurfaceIntent::Close(e) => {
339            handler.on_event(e, window);
340            return WindowEventOutcome::CloseRequested;
341        }
342        SurfaceIntent::Ignore => {}
343    }
344    WindowEventOutcome::Continue
345}
346
347impl Platform for WinitPlatform {
348    type Window = WinitWindow;
349
350    fn run<H: EventHandler<Self::Window>>(
351        self,
352        config: WindowConfig,
353        handler: H,
354    ) -> Result<(), PlatformError> {
355        let mut runner = WinitRunner {
356            handler,
357            window: None,
358            config,
359            cursor_position: (0.0, 0.0),
360            scale_factor: 1.0,
361            modifiers: platform_core::ModifiersState::default(),
362            timer_has_fired: false,
363        };
364        // The app-facing redraw waker (handed to background threads) wakes the loop through this proxy, not by
365        // holding a window — so caching it can't pin a window open.
366        let wake_proxy = self.event_loop.create_proxy();
367        platform_core::set_loop_waker(std::sync::Arc::new(move || {
368            let _ = wake_proxy.send_event(UserEvent::Wake);
369        }));
370        // Live OS color-scheme changes: winit has no Linux integration, so a portal watch thread pushes them
371        // back through the loop via a proxy. Elsewhere winit delivers WindowEvent::ThemeChanged natively.
372        #[cfg(target_os = "linux")]
373        {
374            let proxy = self.event_loop.create_proxy();
375            crate::color_scheme::spawn_watch(move |dark| {
376                let _ = proxy.send_event(UserEvent::ColorScheme(dark));
377            });
378        }
379        self.event_loop
380            .run_app(&mut runner)
381            .map_err(|e| PlatformError(e.to_string()))
382    }
383}
384
385// The initial OS light/dark preference at window creation: winit's native answer (Windows/macOS), falling
386// back to the freedesktop portal on Linux where winit always reports `None`.
387fn initial_prefers_dark(window: &WinitWindow) -> Option<bool> {
388    let winit = window.prefers_dark();
389    #[cfg(target_os = "linux")]
390    {
391        winit.or_else(crate::color_scheme::portal_prefers_dark)
392    }
393    #[cfg(not(target_os = "linux"))]
394    {
395        winit
396    }
397}
398
399// ---- Multi-surface (multi-window) backend --------------------------------------------------------------
400//
401// M3: every surface shares this one UI thread and one reactive runtime. winit already creates windows and
402// pumps their events on the main thread; each surface's `EventHandler` — built here by the factory, carrying
403// its own `Surface` world — runs directly on the main thread too. The handler activates its surface around
404// every lifecycle call, so the surfaces stay isolated without a thread apiece, and a signal shared between
405// them re-runs each surface's effects under its own context. The hardware backend still presents on its own
406// per-surface render thread (as in single-window).
407//
408// Each dispatch is bracketed by the handler's own `new_events`/`about_to_wait` (begin/end of the reactive
409// batch), so batch_depth always returns to 0 within one callback — no cross-callback bookkeeping, and a
410// surface created in `resumed` (after the iteration's `new_events`) can never leave the batch unbalanced.
411
412// A dynamically-opened surface (`open_surface`) awaiting creation by the running runner. Enqueued from app
413// code deep inside an event handler — where `&ActiveEventLoop` (needed to create a winit window) is not
414// available — and drained by the runner on its next `about_to_wait`.
415struct DynamicRequest {
416    config: WindowConfig,
417    handler: Box<dyn EventHandler<WinitWindow>>,
418    close: Arc<std::sync::atomic::AtomicBool>,
419}
420
421thread_local! {
422    static DYNAMIC_QUEUE: std::cell::RefCell<Vec<DynamicRequest>> =
423        const { std::cell::RefCell::new(Vec::new()) };
424}
425
426/// Requests a new top-level window rendering `handler`, created on the next event-loop iteration by the
427/// running multi-surface runner (which shares this thread and the one reactive runtime). Returns a flag the
428/// caller flips to close the surface. rsx's winit `SurfaceHost` uses this to implement `open_surface` without
429/// a per-surface thread. If no multi-surface runner is running, the request simply sits undrained.
430pub fn request_dynamic_surface(
431    config: WindowConfig,
432    handler: Box<dyn EventHandler<WinitWindow>>,
433) -> Arc<std::sync::atomic::AtomicBool> {
434    let close = Arc::new(std::sync::atomic::AtomicBool::new(false));
435    DYNAMIC_QUEUE.with(|q| {
436        q.borrow_mut().push(DynamicRequest {
437            config,
438            handler,
439            close: Arc::clone(&close),
440        })
441    });
442    close
443}
444
445fn drain_dynamic_requests() -> Vec<DynamicRequest> {
446    DYNAMIC_QUEUE.with(|q| std::mem::take(&mut *q.borrow_mut()))
447}
448
449// Per-surface main-thread state: the handler plus that surface's input state (needed to translate winit
450// events), its last frame-pacing deadline, and — for a dynamically-opened surface — the flag its
451// `SurfaceControl` flips to request close.
452struct SurfaceRunner {
453    handler: Box<dyn EventHandler<WinitWindow>>,
454    window: WinitWindow,
455    cursor_position: (f64, f64),
456    scale_factor: f64,
457    modifiers: platform_core::ModifiersState,
458    pace: Option<std::time::Duration>,
459    // `None` for a statically-declared surface; `Some` for one opened via `open_surface`.
460    close_flag: Option<Arc<std::sync::atomic::AtomicBool>>,
461    // A dynamically-opened window defers on_resume until its first event, when the compositor has given it
462    // its real size (a tiling WM may override the requested size); rendering before that would size the
463    // surface and the layout differently. `false` until resumed.
464    resumed: bool,
465}
466
467// Brings a surface up: build under a panic guard (T-4.2), so a build that fails/panics returns `false` and
468// the caller drops it without disturbing the others. Reads the window's *current* size, so calling it once
469// the compositor has configured the window keeps the layout and the render surface the same size.
470fn resume_surface(surface: &mut SurfaceRunner) -> bool {
471    let window = surface.window.clone();
472    surface.handler.new_events();
473    let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
474        if let Some(dark) = initial_prefers_dark(&window) {
475            surface
476                .handler
477                .on_event(Event::ColorSchemeChanged { dark }, &window);
478        }
479        surface.handler.on_resume(&window)
480    }));
481    surface.pace = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
482        surface.handler.about_to_wait()
483    }))
484    .unwrap_or(None);
485    matches!(built, Ok(true))
486}
487
488// The runner is non-generic over the handler type: both the statically-declared surfaces (boxed from the
489// factory) and the dynamically-opened ones share one `Box<dyn EventHandler<WinitWindow>>` map.
490type BoxedFactory = Box<dyn Fn(SurfaceId) -> Box<dyn EventHandler<WinitWindow>>>;
491
492struct WinitMultiRunner {
493    factory: BoxedFactory,
494    pending: Vec<(SurfaceId, WindowConfig)>,
495    surfaces: HashMap<WindowId, SurfaceRunner>,
496    created: bool,
497    // True only on WaitUntil timer expiry; gates keepalive request_redraw so it fires only on timer ticks.
498    timer_has_fired: bool,
499}
500
501impl WinitMultiRunner {
502    // Creates a window for `handler` and inserts it into the live surface map. `resume_now` brings it up
503    // immediately (the initial surfaces, created in `resumed`, whose window winit has already configured);
504    // a dynamically-opened surface passes `false` and is resumed on its first event instead (see
505    // `window_event`), once the compositor has given it its real size.
506    fn spawn_surface(
507        &mut self,
508        event_loop: &ActiveEventLoop,
509        config: WindowConfig,
510        handler: Box<dyn EventHandler<WinitWindow>>,
511        close_flag: Option<Arc<std::sync::atomic::AtomicBool>>,
512        resume_now: bool,
513    ) {
514        // Creating the window creates its `wl_surface`; hold the GPU lifecycle lock so it can't race another
515        // window's render thread present/acquire on the shared Wayland/driver connection. Scoped tightly so
516        // `resume_surface` below (which builds the renderer under its own lifecycle lock) isn't nested under it.
517        let Some(window) = ({
518            let _gpu = renderer_core::gpu_sync::lifecycle_guard();
519            create_window_from_config(event_loop, &config)
520        }) else {
521            return;
522        };
523        let window_id = window.0.id();
524        let mut surface = SurfaceRunner {
525            handler,
526            window,
527            cursor_position: (0.0, 0.0),
528            scale_factor: 1.0,
529            modifiers: platform_core::ModifiersState::default(),
530            pace: None,
531            close_flag,
532            resumed: false,
533        };
534        if resume_now {
535            if !resume_surface(&mut surface) {
536                tracing::error!("surface on_resume failed or panicked; skipping it");
537                return;
538            }
539            surface.window.request_redraw();
540            surface.resumed = true;
541        }
542        self.surfaces.insert(window_id, surface);
543    }
544}
545
546impl ApplicationHandler<UserEvent> for WinitMultiRunner {
547    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
548        match event {
549            UserEvent::ColorScheme(dark) => {
550                // Bracket each surface's write on its own (this callback is not inside a shared batch bracket).
551                for surface in self.surfaces.values_mut() {
552                    surface.handler.new_events();
553                    surface
554                        .handler
555                        .on_event(Event::ColorSchemeChanged { dark }, &surface.window);
556                    surface.pace = surface.handler.about_to_wait();
557                }
558            }
559            UserEvent::Wake => {
560                // Redraw every surface so each one's `on_frame` runs — the waking app's content may now live in
561                // any of them (a tabbed host can move it between windows), so we don't assume which.
562                for surface in self.surfaces.values() {
563                    surface.window.request_redraw();
564                }
565            }
566        }
567    }
568
569    fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
570        // Only gate keepalive redraws on a real timer expiry (not every event-queue drain).
571        self.timer_has_fired = matches!(cause, StartCause::ResumeTimeReached { .. });
572    }
573
574    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
575        // Bring up any surfaces opened via `open_surface` since the last iteration, and tear down any whose
576        // SurfaceControl flag was flipped — both are cheap and immediate on this one thread (no polling).
577        for req in drain_dynamic_requests() {
578            self.spawn_surface(event_loop, req.config, req.handler, Some(req.close), false);
579        }
580        let to_close: Vec<WindowId> = self
581            .surfaces
582            .iter()
583            .filter(|(_, s)| {
584                s.close_flag
585                    .as_ref()
586                    .is_some_and(|f| f.load(std::sync::atomic::Ordering::Relaxed))
587            })
588            .map(|(&id, _)| id)
589            .collect();
590        for id in to_close {
591            if let Some(mut removed) = self.surfaces.remove(&id) {
592                removed.handler.on_suspend();
593                // Renderer + window teardown, serialized against sibling render threads (see the close path in
594                // window_event and renderer_core::gpu_sync).
595                let _gpu = renderer_core::gpu_sync::lifecycle_guard();
596                drop(removed);
597            }
598        }
599        if self.created && self.surfaces.is_empty() {
600            event_loop.exit();
601            return;
602        }
603
604        // Aggregate the soonest frame-pacing deadline across surfaces; wake each animating surface for its own
605        // frame on a timer tick (reactive changes call request_redraw themselves via flush_notify).
606        let mut next_wake: Option<std::time::Duration> = None;
607        for surface in self.surfaces.values() {
608            if let Some(d) = surface.pace {
609                if self.timer_has_fired {
610                    surface.window.request_redraw();
611                }
612                next_wake = Some(next_wake.map_or(d, |cur| cur.min(d)));
613            }
614        }
615        match next_wake {
616            Some(d) => {
617                event_loop.set_control_flow(ControlFlow::WaitUntil(std::time::Instant::now() + d))
618            }
619            None => event_loop.set_control_flow(ControlFlow::Wait),
620        }
621    }
622
623    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
624        // Create every static surface once. winit can emit `resumed` more than once on some platforms; the
625        // guard keeps us from spawning duplicate windows.
626        if self.created {
627            return;
628        }
629        self.created = true;
630        for (id, config) in std::mem::take(&mut self.pending) {
631            // The factory gives each handler its own `Surface` world, activated around each lifecycle call.
632            let handler = (self.factory)(id);
633            self.spawn_surface(event_loop, config, handler, None, true);
634        }
635        if self.surfaces.is_empty() {
636            event_loop.exit();
637        }
638    }
639
640    fn window_event(&mut self, _event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
641        let Some(surface) = self.surfaces.get_mut(&id) else {
642            return;
643        };
644        if !surface.resumed {
645            // Bring a dynamically-opened window up only on its first non-empty `Resized` — the compositor's
646            // configure, when the window has its real size (a tiling WM overrides the requested one). Ignore
647            // earlier events (e.g. a pre-configure `RedrawRequested`), which would resume at the requested
648            // size and overflow the smaller surface.
649            let configured =
650                matches!(&event, WindowEvent::Resized(s) if s.width > 0 && s.height > 0);
651            if !configured {
652                return;
653            }
654            if resume_surface(surface) {
655                surface.resumed = true;
656                // Fall through to dispatch this Resized: it carries the authoritative size, which relays the
657                // layout out to match the surface even if `window.inner_size()` (read in on_resume) lagged it.
658            } else {
659                // Exiting the whole loop is `about_to_wait`'s job (it runs after pending `open_surface`
660                // requests are spawned), so a just-removed last surface can't kill a window being born.
661                if let Some(removed) = self.surfaces.remove(&id) {
662                    let _gpu = renderer_core::gpu_sync::lifecycle_guard();
663                    drop(removed);
664                }
665                return;
666            }
667        }
668        // Clone (a cheap Arc bump) so the window borrow doesn't conflict with the mutable handler/input borrows.
669        let window = surface.window.clone();
670        surface.handler.new_events();
671        // Dispatch under a panic guard (T-4.2): a widget handler / render / effect panic unmounts just this
672        // surface. about_to_wait (end_batch) is guarded separately so it always runs, keeping the reactive
673        // batch balanced (T-1.3 leaves the shared runtime consistent after the unwind). Under panic=unwind
674        // only; a panic=abort release build aborts instead.
675        let dispatched = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
676            dispatch_window_event(
677                &mut surface.handler,
678                &window,
679                &mut surface.cursor_position,
680                &mut surface.scale_factor,
681                &mut surface.modifiers,
682                event,
683            )
684        }));
685        let paced = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
686            surface.handler.about_to_wait()
687        }));
688        surface.pace = paced.as_ref().copied().unwrap_or(None);
689        let panicked = dispatched.is_err() || paced.is_err();
690        let close = matches!(dispatched, Ok(WindowEventOutcome::CloseRequested));
691        // A panicked handler is in an unknown state; don't poll it, just unmount.
692        let exit_requested = !panicked && surface.handler.take_exit_request();
693        if panicked {
694            tracing::error!(?id, "surface panicked; unmounting it");
695        }
696        // OS close (WM X / Alt-F4), a custom title-bar close button, or a panic: tear down just this surface.
697        if panicked || close || exit_requested {
698            if let Some(mut removed) = self.surfaces.remove(&id) {
699                // on_suspend joins THIS surface's render thread (which needs the render guard to finish its
700                // last frame), so it must run before we take the lifecycle lock — otherwise we'd deadlock.
701                if !panicked {
702                    removed.handler.on_suspend();
703                }
704                // Destroying this window's swapchain/surface and its winit window (wl_surface) must not race a
705                // sibling window's render thread; the lock waits for every in-flight frame and blocks new ones
706                // for the duration of the drop (see renderer_core::gpu_sync). The GPU device/instance is shared
707                // process-wide, so this drops only a swapchain — never a device — which is what makes it safe.
708                let _gpu = renderer_core::gpu_sync::lifecycle_guard();
709                drop(removed);
710            }
711            tracing::debug!(
712                ?id,
713                close,
714                exit_requested,
715                panicked,
716                remaining = self.surfaces.len(),
717                "surface closed"
718            );
719            // The whole-loop exit is decided in `about_to_wait`, after the same iteration's queued
720            // `open_surface`/`open_window` requests are spawned — so detaching the last tab (which closes the
721            // host and opens a new window at once) doesn't exit before the new window exists.
722        }
723    }
724}
725
726impl MultiSurfacePlatform for WinitPlatform {
727    type Window = WinitWindow;
728
729    fn run_surfaces<H, F>(
730        self,
731        surfaces: Vec<(SurfaceId, WindowConfig)>,
732        factory: F,
733    ) -> Result<(), PlatformError>
734    where
735        H: EventHandler<WinitWindow> + 'static,
736        F: Fn(SurfaceId) -> H + 'static,
737    {
738        // Box the factory output so static and dynamic surfaces share one handler type in the runner's map.
739        let factory: BoxedFactory =
740            Box::new(move |id| Box::new(factory(id)) as Box<dyn EventHandler<WinitWindow>>);
741        let mut runner = WinitMultiRunner {
742            factory,
743            pending: surfaces,
744            surfaces: HashMap::new(),
745            created: false,
746            timer_has_fired: false,
747        };
748        // The app-facing redraw waker wakes the loop through this proxy (which redraws every surface), not by
749        // holding a window — so an app can cache it and, if its content is later moved to another window, the
750        // original still closes and background wakeups still reach it wherever it now lives.
751        let wake_proxy = self.event_loop.create_proxy();
752        platform_core::set_loop_waker(std::sync::Arc::new(move || {
753            let _ = wake_proxy.send_event(UserEvent::Wake);
754        }));
755        // Live OS color-scheme changes, delivered to every surface (see the single-window `run`).
756        #[cfg(target_os = "linux")]
757        {
758            let proxy = self.event_loop.create_proxy();
759            crate::color_scheme::spawn_watch(move |dark| {
760                let _ = proxy.send_event(UserEvent::ColorScheme(dark));
761            });
762        }
763        self.event_loop
764            .run_app(&mut runner)
765            .map_err(|e| PlatformError(e.to_string()))
766    }
767}