shell.rs raw

//! The winit application shell: window lifecycle, backend selection, frame
//! presentation, and AccessKit event interception.

use std::error::Error;
use std::fmt;
use std::num::NonZeroU32;
use std::sync::{Arc, Mutex};

use crate::gpu::GpuSurface;
use guiduck_core::clipboard::{Clipboard, Selection, TextRequest};
use guiduck_core::{
    ImeInput, Key as CoreKey, KeyInput, Modifiers, PointerButton, PointerInput, WidgetTree,
};
use guiduck_render_tinyskia::TinySkiaBackend;
use guiduck_render_vello::VelloBackend;
use guiduck_scene::geom::{Point, Size};
use guiduck_scene::{FragmentId, FragmentStore, RenderBackend, RenderError};
use winit::application::ApplicationHandler;
use winit::event::{ElementState, KeyEvent, MouseButton, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy};
use winit::keyboard::{Key, NamedKey};
use winit::window::{Window, WindowId};

/// Produces the frame's fragment tree. [`WidgetTree`] implements this (layout
/// then paint); anything else that can emit fragments — like the raw scene
/// examples — can implement it directly.
pub trait FrameSource {
    /// Build (or update) the scene for a viewport of the given logical size,
    /// returning the root fragment and the store holding it. The source owns
    /// its store, which is what lets it retain unchanged fragments across
    /// frames instead of rebuilding the scene from scratch.
    fn frame(&mut self, viewport: Size) -> (FragmentId, &FragmentStore);

    /// The current accessibility tree, if this source has one. Sources
    /// without semantic content return `None` and assistive technologies see
    /// a bare window.
    fn accessibility(&mut self) -> Option<accesskit::TreeUpdate> {
        None
    }

    /// Feed pointer input (logical window coordinates). Sources without
    /// interaction ignore it. The clipboard travels with pointer input
    /// because of the primary selection (drag-select stores, middle-click
    /// pastes).
    fn pointer(&mut self, _input: PointerInput, _clipboard: &mut dyn Clipboard) {}

    /// A file was dropped on the window. Sources without a drop target ignore
    /// it; the framework carries the path and the app interprets it.
    fn file_drop(&mut self, _path: std::path::PathBuf) {}

    /// Whether internal state changed such that a new frame should be
    /// rendered. The shell polls this after delivering input and after each
    /// frame; returning false is what keeps an idle app at zero frames.
    fn needs_frame(&mut self) -> bool {
        false
    }

    /// Handle a keyboard event; return true to consume it.
    fn key(&mut self, _key: KeyInput, _clipboard: &mut dyn Clipboard) -> bool {
        false
    }

    /// Handle an input-method event.
    fn ime(&mut self, _ime: ImeInput) {}

    /// Perform an accessibility action requested by an assistive technology.
    fn accessibility_action(&mut self, _request: &accesskit::ActionRequest) {}

    /// Where the input method should place candidate windows, in logical
    /// window coordinates.
    fn ime_cursor_area(&mut self) -> Option<guiduck_scene::geom::Rect> {
        None
    }

    /// The pointer cursor for the current hover target.
    fn cursor(&mut self) -> guiduck_core::CursorShape {
        guiduck_core::CursorShape::Default
    }

    /// The earliest moment this source needs waking (a pending timer), or
    /// `None` to wait indefinitely — which is what keeps an idle app at
    /// zero frames. `now` is supplied by the shell so sources stay
    /// clock-free and deterministic under test.
    fn next_wake(&mut self, _now: std::time::Instant) -> Option<std::time::Instant> {
        None
    }

    /// A deadline from [`next_wake`](Self::next_wake) has passed; run due
    /// timers. A needs-frame check follows.
    fn tick(&mut self, _now: std::time::Instant) {}

    /// Receive a handle that wakes the event loop from other threads.
    /// Called once at startup; sources without background activity ignore
    /// it.
    fn connect_waker(&mut self, _waker: Waker) {}

    /// Process out-of-band work (e.g. hot-reload file events). Called on
    /// every [`Waker`] wake; a needs-frame check follows.
    fn poll(&mut self) {}
}

impl FrameSource for WidgetTree {
    fn connect_waker(&mut self, waker: Waker) {
        // Background work (a file dialog, a clipboard load) finishes on its
        // own thread and needs to say so; this is the handle that lets it.
        crate::background::set_waker(waker.clone());
        // So does a watcher noticing a saved component file.
        self.connect_live_waker(move || waker.wake());
    }

    fn poll(&mut self) {
        crate::background::deliver(self);
        self.poll_live_reload();
    }

    fn frame(&mut self, viewport: Size) -> (FragmentId, &FragmentStore) {
        let root = self
            .render_frame(viewport)
            .expect("a widget tree used as a frame source must have a root");
        (root, self.fragments())
    }

    fn accessibility(&mut self) -> Option<accesskit::TreeUpdate> {
        Some(self.accessibility_tree())
    }

    fn pointer(&mut self, input: PointerInput, clipboard: &mut dyn Clipboard) {
        self.dispatch_pointer(input, clipboard);
    }

    fn file_drop(&mut self, path: std::path::PathBuf) {
        self.drop_file(path.to_string_lossy().into_owned());
    }

    fn key(&mut self, key: KeyInput, clipboard: &mut dyn Clipboard) -> bool {
        self.dispatch_key(&key, clipboard)
    }

    fn ime(&mut self, ime: ImeInput) {
        self.dispatch_ime(&ime);
    }

    fn accessibility_action(&mut self, request: &accesskit::ActionRequest) {
        WidgetTree::accessibility_action(self, request);
    }

    fn ime_cursor_area(&mut self) -> Option<guiduck_scene::geom::Rect> {
        WidgetTree::ime_cursor_area(self)
    }

    fn needs_frame(&mut self) -> bool {
        WidgetTree::needs_frame(self)
    }

    fn cursor(&mut self) -> guiduck_core::CursorShape {
        self.cursor_shape()
    }

    fn next_wake(&mut self, now: std::time::Instant) -> Option<std::time::Instant> {
        WidgetTree::next_wake(self, now)
    }

    fn tick(&mut self, now: std::time::Instant) {
        WidgetTree::tick(self, now);
    }
}

/// Which render backend the shell drives.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum BackendKind {
    Vello,
    Software,
}

impl BackendKind {
    /// Read the backend choice from `GUIDUCK_BACKEND` (`vello` or
    /// `software`), defaulting to vello.
    pub fn from_env() -> Result<Self, ShellError> {
        match std::env::var("GUIDUCK_BACKEND") {
            Ok(value) => match value.as_str() {
                "vello" => Ok(Self::Vello),
                "software" => Ok(Self::Software),
                other => Err(ShellError::Config(format!(
                    "unknown GUIDUCK_BACKEND {other:?}; expected \"vello\" or \"software\""
                ))),
            },
            Err(std::env::VarError::NotPresent) => Ok(Self::Vello),
            Err(e) => Err(ShellError::Config(format!("GUIDUCK_BACKEND: {e}"))),
        }
    }
}

/// Failure while running the shell.
#[derive(Debug)]
pub enum ShellError {
    Config(String),
    Render(RenderError),
    Platform(Box<dyn Error>),
}

impl fmt::Display for ShellError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ShellError::Config(msg) => write!(f, "configuration error: {msg}"),
            ShellError::Render(e) => write!(f, "render error: {e}"),
            ShellError::Platform(e) => write!(f, "platform error: {e}"),
        }
    }
}

impl Error for ShellError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            ShellError::Render(e) => Some(e),
            ShellError::Platform(e) => Some(e.as_ref()),
            ShellError::Config(_) => None,
        }
    }
}

impl From<RenderError> for ShellError {
    fn from(e: RenderError) -> Self {
        ShellError::Render(e)
    }
}

/// Events delivered through the winit user-event channel.
#[derive(Debug)]
pub enum ShellEvent {
    Access(accesskit_winit::Event),
    /// An external thread asked the loop to wake and poll the source.
    Wake,
}

impl From<accesskit_winit::Event> for ShellEvent {
    fn from(event: accesskit_winit::Event) -> Self {
        Self::Access(event)
    }
}

/// Wakes the UI event loop from any thread; the shell then calls
/// [`FrameSource::poll`]. Hot reload uses this to apply file changes
/// immediately instead of on the next input event.
#[derive(Clone)]
pub struct Waker {
    proxy: EventLoopProxy<ShellEvent>,
}

impl Waker {
    pub fn wake(&self) {
        // A closed event loop just means shutdown; nothing to do.
        let _ = self.proxy.send_event(ShellEvent::Wake);
    }
}

/// Logical pixels per wheel-notch line, for `MouseScrollDelta::LineDelta`.
const LINE_SCROLL_PX: f64 = 40.0;

/// The platform clipboard: smithay-clipboard on Wayland, otherwise inert.
///
/// Two smithay instances, not one. `load()` blocks until the selection's
/// owner has served the content over a pipe — as long as it likes, forever
/// if it is wedged — so loads run on [`background`](crate::background)
/// threads and deliver through `WidgetTree::dispatch_paste`. The instance
/// they share sits behind a `Mutex` because concurrent `load()` calls would
/// cross replies on its single answer channel (`Clipboard` is not `Sync`).
/// Stores are fire-and-forget and must never queue behind a wedged load,
/// which is exactly what sharing that mutex with the UI thread would do —
/// hence their own instance, touched only here.
enum ShellClipboard {
    None,
    Wayland {
        store: smithay_clipboard::Clipboard,
        load: Arc<Mutex<smithay_clipboard::Clipboard>>,
    },
}

impl Clipboard for ShellClipboard {
    fn request_text(&mut self, selection: Selection) -> TextRequest {
        match self {
            ShellClipboard::None => TextRequest::Ready(None),
            ShellClipboard::Wayland { load, .. } => {
                let load = Arc::clone(load);
                crate::background::run(
                    move || {
                        let clipboard = load.lock().expect("not poisoned");
                        match selection {
                            Selection::Clipboard => clipboard.load().ok(),
                            // On a compositor without zwp_primary_selection
                            // this returns Err, which is exactly the None
                            // we want.
                            Selection::Primary => clipboard.load_primary().ok(),
                        }
                    },
                    |tree, text| {
                        // An empty or failed read has nothing to paste.
                        if let Some(text) = text.filter(|t| !t.is_empty()) {
                            tree.dispatch_paste(&text);
                        }
                    },
                );
                TextRequest::Pending
            }
        }
    }

    fn set_text(&mut self, selection: Selection, text: &str) {
        if let ShellClipboard::Wayland { store, .. } = self {
            match selection {
                Selection::Clipboard => store.store(text),
                Selection::Primary => store.store_primary(text),
            }
        }
    }
}

/// Run the shell until the window closes. The backend is chosen via
/// `GUIDUCK_BACKEND` (default vello).
pub fn run(title: &str, source: impl FrameSource) -> Result<(), ShellError> {
    let backend_kind = BackendKind::from_env()?;
    let event_loop = EventLoop::<ShellEvent>::with_user_event()
        .build()
        .map_err(|e| ShellError::Platform(Box::new(e)))?;
    let proxy = event_loop.create_proxy();
    let mut shell = Shell {
        title: title.to_owned(),
        source,
        backend_kind,
        proxy,
        window: None,
        adapter: None,
        backend: None,
        error: None,
        cursor: None,
        cursor_shape: guiduck_core::CursorShape::Default,
        modifiers: Modifiers::default(),
        clipboard: ShellClipboard::None,
        trace_frames: std::env::var_os("GUIDUCK_TRACE_FRAMES").is_some_and(|v| v != "0"),
        trace_input: std::env::var_os("GUIDUCK_TRACE_INPUT").is_some_and(|v| v != "0"),
        frame_counter: 0,
        redraw_deadline: None,
        redraw_gate_suspect: false,
        last_direct_redraw: None,
    };
    event_loop
        .run_app(&mut shell)
        .map_err(|e| ShellError::Platform(Box::new(e)))?;
    match shell.error {
        Some(e) => Err(e),
        None => Ok(()),
    }
}

struct Shell<F: FrameSource> {
    title: String,
    source: F,
    backend_kind: BackendKind,
    proxy: EventLoopProxy<ShellEvent>,
    window: Option<Arc<Window>>,
    adapter: Option<accesskit_winit::Adapter>,
    backend: Option<BackendState>,
    error: Option<ShellError>,
    /// Last pointer position, in logical window coordinates.
    cursor: Option<Point>,
    /// The cursor shape last handed to the window, to skip redundant sets.
    cursor_shape: guiduck_core::CursorShape,
    modifiers: Modifiers,
    clipboard: ShellClipboard,
    /// `GUIDUCK_TRACE_FRAMES`: log one stderr line per rendered frame, so
    /// tests can assert that idle applications render nothing.
    trace_frames: bool,
    /// `GUIDUCK_TRACE_INPUT`: log translated key and IME events, for
    /// debugging input-method integration.
    trace_input: bool,
    frame_counter: u64,
    /// When the redraw requested from winit should have arrived by; None
    /// while none is outstanding. The renderer's dead-man switch: on
    /// Wayland, winit delivers `RedrawRequested` only after the
    /// compositor's frame callback, and the compositor sends a frame
    /// callback only after the next commit — one lost callback deadlocks
    /// that cycle forever, freezing the display while input keeps
    /// flowing (seen in the wild under Hyprland special workspaces).
    redraw_deadline: Option<std::time::Instant>,
    /// True from the moment a deadline expires until `RedrawRequested`
    /// delivery is observed again. While set, the shell renders directly,
    /// paced to roughly a display refresh; the presents also recommit the
    /// surface, which is what restarts the compositor's callbacks.
    redraw_gate_suspect: bool,
    /// The last direct (watchdog) render, for pacing.
    last_direct_redraw: Option<std::time::Instant>,
}

/// How long a requested redraw may remain undelivered before the shell
/// stops trusting the event loop and renders directly.
const REDRAW_WATCHDOG: std::time::Duration = std::time::Duration::from_millis(1000);

/// Pacing for direct renders while delivery is broken: roughly a 60 Hz
/// frame budget, since no compositor callback is throttling us.
const DIRECT_REDRAW_INTERVAL: std::time::Duration = std::time::Duration::from_millis(16);

enum BackendState {
    Vello {
        surface: GpuSurface,
        backend: VelloBackend,
    },
    Software {
        surface: softbuffer::Surface<Arc<Window>, Arc<Window>>,
        backend: TinySkiaBackend,
    },
}

impl<F: FrameSource> Shell<F> {
    fn fail(&mut self, event_loop: &ActiveEventLoop, error: ShellError) {
        self.error = Some(error);
        event_loop.exit();
    }

    /// Deliver pointer input to the source, follow the hover target's
    /// cursor shape, and request a frame if anything went dirty.
    fn pointer(&mut self, input: PointerInput) {
        self.source.pointer(input, &mut self.clipboard);
        let shape = self.source.cursor();
        if shape != self.cursor_shape {
            self.cursor_shape = shape;
            if let Some(window) = &self.window {
                window.set_cursor(winit::window::Cursor::Icon(match shape {
                    guiduck_core::CursorShape::Default => winit::window::CursorIcon::Default,
                    guiduck_core::CursorShape::Pointer => winit::window::CursorIcon::Pointer,
                    guiduck_core::CursorShape::Text => winit::window::CursorIcon::Text,
                }));
            }
        }
        self.request_frame_if_needed();
    }

    /// After keys or IME events: reposition the IME candidate window at the
    /// focused editor's cursor, and schedule a frame if anything changed.
    fn after_text_input(&mut self) {
        if let (Some(window), Some(area)) = (self.window.clone(), self.source.ime_cursor_area()) {
            window.set_ime_cursor_area(
                winit::dpi::LogicalPosition::new(area.x0, area.y1),
                winit::dpi::LogicalSize::new(area.width(), area.height()),
            );
        }
        self.request_frame_if_needed();
    }

    fn request_frame_if_needed(&mut self) {
        if self.source.needs_frame()
            && let Some(window) = &self.window
        {
            window.request_redraw();
            // Arm the dead-man switch (an already-armed one keeps its
            // deadline). Delivery clears it; expiry means the event loop
            // has stopped delivering redraws and the shell takes over.
            if self.redraw_deadline.is_none() {
                self.redraw_deadline = Some(std::time::Instant::now() + REDRAW_WATCHDOG);
            }
        }
    }

    /// The dead-man switch (see the `redraw_deadline` field): if a
    /// requested redraw is overdue, stop waiting for the event loop and
    /// render directly, paced to a frame budget, until delivery resumes.
    /// The direct presents recommit the surface, which restarts the
    /// compositor's frame callbacks — so besides keeping the display
    /// truthful, this usually breaks the deadlock that caused it.
    fn check_redraw_watchdog(&mut self) {
        if self
            .redraw_deadline
            .is_some_and(|deadline| std::time::Instant::now() >= deadline)
        {
            self.redraw_deadline = None;
            if !self.redraw_gate_suspect {
                self.redraw_gate_suspect = true;
                eprintln!(
                    "guiduck: RedrawRequested overdue by {}ms+; rendering directly until delivery resumes",
                    REDRAW_WATCHDOG.as_millis()
                );
            }
        }
        if self.redraw_gate_suspect && self.source.needs_frame() {
            let now = std::time::Instant::now();
            if self
                .last_direct_redraw
                .is_none_or(|last| now - last >= DIRECT_REDRAW_INTERVAL)
            {
                self.last_direct_redraw = Some(now);
                // Not fatal, unlike the delivered-redraw path: a hidden or
                // mid-reconfigure surface can refuse a frame, and the
                // watchdog simply tries again on the next check.
                match self.redraw() {
                    Ok(()) => {
                        if self.trace_frames {
                            self.frame_counter += 1;
                            eprintln!("guiduck-frame {} (watchdog)", self.frame_counter);
                        }
                        self.push_accessibility();
                    }
                    Err(e) => eprintln!("guiduck: watchdog render failed: {e}"),
                }
            }
        }
    }

    fn scale(&self) -> f64 {
        self.window.as_ref().map_or(1.0, |w| w.scale_factor())
    }

    /// Send the source's current accessibility tree to any active assistive
    /// technology. No-op (and no tree construction) when none is listening.
    fn push_accessibility(&mut self) {
        let Some(adapter) = self.adapter.as_mut() else {
            return;
        };
        let source = &mut self.source;
        adapter.update_if_active(|| source.accessibility().unwrap_or_else(bare_window_tree));
    }

    fn init_window(&mut self, event_loop: &ActiveEventLoop) -> Result<(), ShellError> {
        let attrs = Window::default_attributes()
            .with_title(self.title.clone())
            // The AccessKit adapter must exist before the window is first
            // shown, so the window starts invisible.
            .with_visible(false);
        let window = Arc::new(
            event_loop
                .create_window(attrs)
                .map_err(|e| ShellError::Platform(Box::new(e)))?,
        );
        let adapter = accesskit_winit::Adapter::with_event_loop_proxy(
            event_loop,
            &window,
            self.proxy.clone(),
        );

        let size = window.inner_size();
        let backend = match self.backend_kind {
            BackendKind::Vello => {
                let surface = pollster::block_on(GpuSurface::new(
                    window.clone(),
                    size.width.max(1),
                    size.height.max(1),
                    wgpu::PresentMode::AutoVsync,
                ))
                .map_err(ShellError::Platform)?;
                let backend = VelloBackend::new(surface.device.clone(), surface.queue.clone())?;
                BackendState::Vello { surface, backend }
            }
            BackendKind::Software => {
                let context = softbuffer::Context::new(window.clone())
                    .map_err(|e| ShellError::Platform(Box::new(e)))?;
                let surface = softbuffer::Surface::new(&context, window.clone())
                    .map_err(|e| ShellError::Platform(Box::new(e)))?;
                BackendState::Software {
                    surface,
                    backend: TinySkiaBackend::new(),
                }
            }
        };

        // Text widgets drive the input method; harmless when nothing
        // focusable exists.
        window.set_ime_allowed(true);

        // The clipboard is tied to the Wayland connection.
        self.clipboard = wayland_clipboard(&window);

        window.set_visible(true);
        self.window = Some(window);
        self.adapter = Some(adapter);
        self.backend = Some(backend);
        Ok(())
    }

    fn resize(&mut self, width: u32, height: u32) {
        if width == 0 || height == 0 {
            return;
        }
        match self.backend.as_mut() {
            Some(BackendState::Vello { surface, .. }) => {
                surface.resize(width, height);
            }
            Some(BackendState::Software { surface, .. }) => {
                let (Some(w), Some(h)) = (NonZeroU32::new(width), NonZeroU32::new(height)) else {
                    return;
                };
                // Errors here recur on present; report them there instead.
                let _ = surface.resize(w, h);
            }
            None => {}
        }
        if let Some(window) = &self.window {
            window.request_redraw();
        }
    }

    fn redraw(&mut self) -> Result<(), ShellError> {
        let Some(window) = self.window.clone() else {
            return Ok(());
        };
        let size = window.inner_size();
        if size.width == 0 || size.height == 0 {
            return Ok(());
        }
        let scale = window.scale_factor();
        let viewport = Size::new(size.width as f64 / scale, size.height as f64 / scale);
        // The source owns the scene and retains unchanged fragments across
        // frames; the shell just asks for the current root.
        let (root, store) = self.source.frame(viewport);

        match self.backend.as_mut().expect("backend initialized") {
            BackendState::Vello { surface, backend } => {
                backend.set_target(surface.target_view());
                backend.render(root, store, size.width, size.height, scale)?;

                let frame = match surface.get_current_texture() {
                    wgpu::CurrentSurfaceTexture::Success(frame)
                    | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
                    // Transient conditions: skip this frame and wait for the
                    // next redraw request.
                    wgpu::CurrentSurfaceTexture::Timeout
                    | wgpu::CurrentSurfaceTexture::Occluded => return Ok(()),
                    // The surface needs reconfiguring; do so and retry on the
                    // next frame.
                    wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
                        surface.resize(size.width, size.height);
                        surface.reconfigure();
                        window.request_redraw();
                        return Ok(());
                    }
                    wgpu::CurrentSurfaceTexture::Validation => {
                        return Err(ShellError::Platform("wgpu surface validation error".into()));
                    }
                };
                let frame_view = frame
                    .texture
                    .create_view(&wgpu::TextureViewDescriptor::default());
                surface.blit_to(&frame_view);
                window.pre_present_notify();
                frame.present();
            }
            BackendState::Software { surface, backend } => {
                backend.render(root, store, size.width, size.height, scale)?;
                let (Some(w), Some(h)) =
                    (NonZeroU32::new(size.width), NonZeroU32::new(size.height))
                else {
                    return Ok(());
                };
                surface
                    .resize(w, h)
                    .map_err(|e| ShellError::Platform(Box::new(e)))?;
                let mut buffer = surface
                    .buffer_mut()
                    .map_err(|e| ShellError::Platform(Box::new(e)))?;
                for (dst, px) in buffer.iter_mut().zip(backend.pixmap().pixels()) {
                    let c = px.demultiply();
                    *dst = (u32::from(c.red()) << 16)
                        | (u32::from(c.green()) << 8)
                        | u32::from(c.blue());
                }
                window.pre_present_notify();
                buffer
                    .present()
                    .map_err(|e| ShellError::Platform(Box::new(e)))?;
            }
        }
        Ok(())
    }
}

impl<F: FrameSource> ApplicationHandler<ShellEvent> for Shell<F> {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.window.is_none() {
            match self.init_window(event_loop) {
                Ok(()) => {
                    let waker = Waker {
                        proxy: self.proxy.clone(),
                    };
                    self.source.connect_waker(waker);
                }
                Err(e) => self.fail(event_loop, e),
            }
        }
    }

    /// Release everything that belongs to the display, while the display is
    /// still there.
    ///
    /// `EventLoop::run_app` takes the loop by value, so by the time it returns
    /// the platform connection is already gone — and on Wayland that means the
    /// `wl_display` these objects were made from has been destroyed. Dropping
    /// them with the `Shell` afterwards is a use-after-free: the clipboard's
    /// worker thread tears down its selection proxies through a freed
    /// connection and the process dies in `wl_proxy_destroy`, after the
    /// application's last line of code has run.
    ///
    /// This hook is the last point at which the connection is guaranteed
    /// alive, so it is where they go. Order within it is the usual one —
    /// surfaces before the window they were made from.
    fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
        self.clipboard = ShellClipboard::None;
        self.backend = None;
        self.adapter = None;
        self.window = None;
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        _window_id: WindowId,
        event: WindowEvent,
    ) {
        // AccessKit must observe events before the application reacts to
        // them; this ordering is part of the adapter's contract.
        if let (Some(adapter), Some(window)) = (self.adapter.as_mut(), self.window.as_ref()) {
            adapter.process_event(window, &event);
        }
        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::ModifiersChanged(modifiers) => {
                let state = modifiers.state();
                self.modifiers = Modifiers {
                    ctrl: state.control_key(),
                    shift: state.shift_key(),
                    alt: state.alt_key(),
                    logo: state.super_key(),
                };
            }
            WindowEvent::KeyboardInput { event, .. } => {
                let input = KeyInput {
                    key: translate_key(&event),
                    modifiers: self.modifiers,
                    pressed: event.state == ElementState::Pressed,
                };
                if self.trace_input {
                    eprintln!("guiduck-input: {input:?}");
                }
                self.source.key(input, &mut self.clipboard);
                self.after_text_input();
            }
            WindowEvent::Ime(ime) => {
                let input = match ime {
                    winit::event::Ime::Enabled => ImeInput::Enabled,
                    winit::event::Ime::Preedit(text, cursor) => ImeInput::Preedit { text, cursor },
                    winit::event::Ime::Commit(text) => ImeInput::Commit(text),
                    winit::event::Ime::Disabled => ImeInput::Disabled,
                };
                if self.trace_input {
                    eprintln!("guiduck-input: ime {input:?}");
                }
                self.source.ime(input);
                self.after_text_input();
            }
            WindowEvent::Resized(size) => self.resize(size.width, size.height),
            WindowEvent::ScaleFactorChanged { .. } => {
                if let Some(window) = &self.window {
                    window.request_redraw();
                }
            }
            WindowEvent::CursorMoved { position, .. } => {
                let scale = self.scale();
                let pos = Point::new(position.x / scale, position.y / scale);
                self.cursor = Some(pos);
                self.pointer(PointerInput::Moved(pos));
            }
            WindowEvent::CursorLeft { .. } => {
                self.cursor = None;
                self.pointer(PointerInput::Left);
            }
            WindowEvent::DroppedFile(path) => {
                self.source.file_drop(path);
                self.request_frame_if_needed();
            }
            WindowEvent::MouseWheel { delta, .. } => {
                if let Some(pos) = self.cursor {
                    let delta = match delta {
                        winit::event::MouseScrollDelta::LineDelta(x, y) => {
                            guiduck_scene::geom::Vec2::new(
                                f64::from(x) * LINE_SCROLL_PX,
                                f64::from(y) * LINE_SCROLL_PX,
                            )
                        }
                        winit::event::MouseScrollDelta::PixelDelta(p) => {
                            let scale = self.scale();
                            guiduck_scene::geom::Vec2::new(p.x / scale, p.y / scale)
                        }
                    };
                    self.pointer(PointerInput::Scroll { pos, delta });
                }
            }
            WindowEvent::MouseInput { state, button, .. } => {
                if let Some(pos) = self.cursor {
                    let button = match button {
                        MouseButton::Left => PointerButton::Left,
                        MouseButton::Right => PointerButton::Right,
                        MouseButton::Middle => PointerButton::Middle,
                        MouseButton::Back => PointerButton::Other(4),
                        MouseButton::Forward => PointerButton::Other(5),
                        MouseButton::Other(n) => PointerButton::Other(n),
                    };
                    let input = match state {
                        ElementState::Pressed => PointerInput::Down {
                            pos,
                            button,
                            time: std::time::Instant::now(),
                        },
                        ElementState::Released => PointerInput::Up { pos, button },
                    };
                    self.pointer(input);
                }
            }
            WindowEvent::RedrawRequested => {
                // The event loop delivered; the dead-man switch stands down.
                self.redraw_deadline = None;
                if self.redraw_gate_suspect {
                    self.redraw_gate_suspect = false;
                    eprintln!("guiduck: RedrawRequested delivery resumed");
                }
                match self.redraw() {
                    Ok(()) => {
                        if self.trace_frames {
                            self.frame_counter += 1;
                            eprintln!("guiduck-frame {}", self.frame_counter);
                        }
                        self.push_accessibility();
                        // Effects run during the frame may have re-dirtied state
                        // (continuation frames).
                        self.request_frame_if_needed();
                    }
                    Err(e) => self.fail(event_loop, e),
                }
            }
            _ => {}
        }
    }

    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: ShellEvent) {
        match event {
            ShellEvent::Wake => {
                self.source.poll();
                self.request_frame_if_needed();
            }
            ShellEvent::Access(event) => match event.window_event {
                accesskit_winit::WindowEvent::InitialTreeRequested => {
                    self.push_accessibility();
                }
                accesskit_winit::WindowEvent::ActionRequested(request) => {
                    self.source.accessibility_action(&request);
                    self.request_frame_if_needed();
                }
                accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
            },
        }
    }

    fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
        // A timer deadline came due: run the source's timers, then render
        // if they dirtied anything.
        if matches!(cause, winit::event::StartCause::ResumeTimeReached { .. }) {
            self.source.tick(std::time::Instant::now());
            self.request_frame_if_needed();
        }
    }

    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
        // This runs after every batch of events, which makes it the one
        // place the watchdog needs checking: every wake passes through
        // here, including the deadline wakes scheduled just below.
        self.check_redraw_watchdog();

        // Sleep until the source's next timer deadline, or indefinitely
        // when it has none — the zero-idle-frames guarantee is exactly
        // this Wait. The watchdog adds its own deadlines: the arrival
        // check for an outstanding redraw, and the next paced render
        // while delivery is broken.
        let now = std::time::Instant::now();
        let mut deadline = self.source.next_wake(now);
        let mut cap = |candidate: std::time::Instant| {
            deadline = Some(deadline.map_or(candidate, |d: std::time::Instant| d.min(candidate)));
        };
        if let Some(pending) = self.redraw_deadline {
            cap(pending);
        }
        if self.redraw_gate_suspect && self.source.needs_frame() {
            cap(self
                .last_direct_redraw
                .map_or(now, |last| last + DIRECT_REDRAW_INTERVAL));
        }
        event_loop.set_control_flow(match deadline {
            Some(deadline) => winit::event_loop::ControlFlow::WaitUntil(deadline),
            None => winit::event_loop::ControlFlow::Wait,
        });
    }
}

/// The fallback accessibility tree for sources without semantics: a lone
/// window node.
fn bare_window_tree() -> accesskit::TreeUpdate {
    const ROOT: accesskit::NodeId = accesskit::NodeId(0);
    let mut root = accesskit::Node::new(accesskit::Role::Window);
    root.set_label("guiduck");
    accesskit::TreeUpdate {
        nodes: vec![(ROOT, root)],
        tree: Some(accesskit::Tree::new(ROOT)),
        tree_id: accesskit::TreeId::ROOT,
        focus: ROOT,
    }
}

/// Build the clipboard for the window's Wayland connection, if any.
fn wayland_clipboard(window: &Window) -> ShellClipboard {
    use winit::raw_window_handle::{HasDisplayHandle, RawDisplayHandle};
    match window.display_handle().map(|h| h.as_raw()) {
        Ok(RawDisplayHandle::Wayland(handle)) => {
            // Safety: the display pointer is valid for the window's
            // lifetime, and the shell owns both. (A load wedged on a dead
            // selection owner can keep its instance alive on a background
            // thread past teardown; such a thread dies with the process,
            // which is where a closing shell is headed anyway.)
            let store = unsafe { smithay_clipboard::Clipboard::new(handle.display.as_ptr()) };
            let load = unsafe { smithay_clipboard::Clipboard::new(handle.display.as_ptr()) };
            ShellClipboard::Wayland {
                store,
                load: Arc::new(Mutex::new(load)),
            }
        }
        _ => ShellClipboard::None,
    }
}

/// Reduce a winit key event to the tree's vocabulary.
fn translate_key(event: &KeyEvent) -> CoreKey {
    match &event.logical_key {
        Key::Named(named) => match named {
            NamedKey::Backspace => CoreKey::Backspace,
            NamedKey::Delete => CoreKey::Delete,
            NamedKey::ArrowLeft => CoreKey::Left,
            NamedKey::ArrowRight => CoreKey::Right,
            NamedKey::ArrowUp => CoreKey::Up,
            NamedKey::ArrowDown => CoreKey::Down,
            NamedKey::Home => CoreKey::Home,
            NamedKey::PageUp => CoreKey::PageUp,
            NamedKey::PageDown => CoreKey::PageDown,
            NamedKey::End => CoreKey::End,
            NamedKey::Enter => CoreKey::Enter,
            NamedKey::Tab => CoreKey::Tab,
            NamedKey::Escape => CoreKey::Escape,
            NamedKey::Space => CoreKey::Character(" ".to_owned()),
            _ => CoreKey::Other,
        },
        Key::Character(_) => {
            // Prefer the produced text (dead keys, layouts); fall back to
            // the logical character.
            match event.text.as_ref() {
                Some(text) => CoreKey::Character(text.to_string()),
                None => match &event.logical_key {
                    Key::Character(c) => CoreKey::Character(c.to_string()),
                    _ => CoreKey::Other,
                },
            }
        }
        _ => CoreKey::Other,
    }
}