Skip to main content

kui_native/
lib.rs

1//! The windowed kui runner: a winit window and a wgpu renderer around
2//! `kui_core`, with the [`App`] trait an application implements.
3//!
4//! kui is a Rust UI library whose view is a plain data tree rebuilt every
5//! frame. `kui_core` holds the model, the layout and the widgets, `kui_wgpu`
6//! draws them, and this crate puts a window around both; it is the crate
7//! most apps depend on. It re-exports all of kui-core, so `kui_native::`
8//! reaches everything, and adds the [`App`] trait, the [`app`] launcher
9//! builder, multiple windows, the clipboard, file dialogs, audio, AccessKit
10//! and a headless [`testing`] driver. `kui-derive` supplies
11//! `#[derive(Message)]`; kui-lua, kui-ffi and kui-node are bindings over the
12//! same core.
13//!
14//! # Quick start
15//!
16//! ```rust,no_run
17//! use kui_native::{App, TextStyle, Ui};
18//!
19//! struct Hello;
20//!
21//! impl App for Hello {
22//!     // Called once per frame. Everything on screen is declared here,
23//!     // from scratch, every time.
24//!     fn view(&mut self, ui: &mut Ui<'_>) {
25//!         let theme = ui.theme();
26//!         ui.text("Hello, kui", TextStyle::new(24.0).color(theme.fg));
27//!     }
28//! }
29//!
30//! fn main() -> Result<(), Box<dyn std::error::Error>> {
31//!     kui_native::app("Hello").size(360.0, 200.0).run(Hello)
32//! }
33//! ```
34//!
35//! # State and events
36//!
37//! A button carries a message. After the frame, each event it produced
38//! reaches [`App::on_event`], and [`UiEvent::message`] reads the message
39//! back as the app's own enum.
40//!
41//! ```rust,no_run
42//! use kui_native::widgets;
43//! use kui_native::{App, Message, NodeSpec, TextStyle, Ui, UiEvent};
44//!
45//! // `derive(Message)` turns each variant into plain data on the way out
46//! // and back into `Msg` on the way in.
47//! #[derive(Message, Clone, Debug, PartialEq)]
48//! enum Msg {
49//!     Inc,
50//!     Dec,
51//! }
52//!
53//! #[derive(Default)]
54//! struct Counter {
55//!     count: i64,
56//! }
57//!
58//! impl App for Counter {
59//!     fn view(&mut self, ui: &mut Ui<'_>) {
60//!         let t = ui.theme();
61//!         ui.with(NodeSpec::column().fill().center().gap(16.0).bg(t.bg), |ui| {
62//!             ui.text(&self.count.to_string(), TextStyle::new(56.0).color(t.fg));
63//!             ui.with(NodeSpec::row().gap(8.0), |ui| {
64//!                 widgets::button(ui, "-1", Msg::Dec);
65//!                 widgets::button(ui, "+1", Msg::Inc);
66//!             });
67//!         });
68//!     }
69//!
70//!     fn on_event(&mut self, ev: UiEvent) {
71//!         match ev.message::<Msg>() {
72//!             Some(Msg::Inc) => self.count += 1,
73//!             Some(Msg::Dec) => self.count -= 1,
74//!             // Not ours: a resize, a focus change, a window event.
75//!             None => {}
76//!         }
77//!     }
78//! }
79//!
80//! fn main() -> Result<(), Box<dyn std::error::Error>> {
81//!     kui_native::app("Counter").size(360.0, 240.0).run(Counter::default())
82//! }
83//! ```
84//!
85//! The book walks from here through controls, lists, the keyboard, floating
86//! windows, motion, effects and tests, one step per chapter.
87//!
88//! # Where to look
89//!
90//! - [`App`]: the trait an app implements; `view`, `on_event`, `setup`,
91//!   `teardown`.
92//! - [`app`] and [`Launcher`]: title, size, chrome, icon, extensions, then
93//!   [`Launcher::run`]; [`Launcher::open`] gives a host that owns the loop a
94//!   [`PumpRunner`] instead.
95//! - [`Ui`]: what `view` is given; [`Ui::with`], [`Ui::text`],
96//!   [`Ui::window`].
97//! - [`NodeSpec`] and [`TextStyle`]: a box's layout, look and handlers; a
98//!   text's size, font and colour.
99//! - [`widgets`]: buttons, checkboxes, text inputs, selects, sliders,
100//!   lists, tables and menus.
101//! - [`UiEvent`]: what `on_event` receives, with the readers on it.
102//! - [`testing`]: a headless driver for an `App`, for `cargo test`.
103//! - From kui-core: [`theme`], [`anim`] (transitions and springs),
104//!   [`window`] (several windows), [`message`] and [`Value`] (the payload
105//!   behind a message), [`Core`] (one window's runtime, for hosts and
106//!   tests).
107//!
108//! # Windows
109//!
110//! One event loop, any number of windows. The launcher opens the main
111//! window; a frame that declares another with [`Ui::window`] opens it on the
112//! same session and GPU device, and a frame that stops declaring it closes
113//! it. `view` runs once per open window per frame, [`Ui::window_name`] says
114//! which (`"main"` for the launcher's), and every event carries its window's
115//! [`WindowId`].
116//!
117//! # Features
118//!
119//! - `audio` (default): plays sounds through kira and cpal. Off, audio
120//!   commands are dropped and no audio library is linked (ALSA on Linux).
121//! - `accesskit` (default): exposes the access tree to screen readers
122//!   through AccessKit. Off, the tree is still built and nothing reaches
123//!   the OS.
124//! - `derive` (default): `#[derive(Message)]`. Off, messages are built and
125//!   matched as [`Value`]s by hand.
126//! - `dialogs` (default): the platform's file dialogs through rfd (the XDG
127//!   portal on Linux). Off, every dialog answers at once as cancelled.
128//! - `smoke`: honours `KUI_SMOKE_FRAMES=n`, which closes the window after
129//!   `n` frames, in a release build. A debug build honours it regardless.
130//!
131//! # Linux
132//!
133//! Building needs `pkg-config` and ALSA's headers (`libasound2-dev` on
134//! Debian and Ubuntu). A window loads the rest at run time: `libxkbcommon`
135//! (and `libxkbcommon-x11` on X11); `libX11`, `libXcursor`, `libXrandr` and
136//! `libXi` on X11 or `libwayland-client` on Wayland; `libvulkan` with a
137//! driver, or `libEGL`. A missing one fails when the window opens, naming
138//! the library. macOS and Windows need only a Rust toolchain.
139//!
140//! The book: <https://kui-book.qxuken.dev>. Repository and design records
141//! (under `docs/adr`): <https://github.com/qxuken/kui>.
142
143use std::sync::Arc;
144
145pub use kui_core::widgets;
146pub use kui_core::*;
147
148mod access_bridge;
149pub mod audio;
150mod axis_lock;
151/// What shows through the window: a blur, a tint, the desktop, or nothing.
152mod backdrop;
153mod clipboard;
154mod decode;
155pub use decode::{
156    Animation, AnimationFrame, MAX_ANIMATION_BYTES, Pixels, Showing, decode_animation, decode_image,
157};
158mod dialogs;
159/// The wallpaper a `Tinted` backdrop draws where the OS has no material.
160mod ground;
161mod icon;
162mod keys;
163/// A `Blur` backdrop asked of a Linux compositor.
164#[cfg(not(any(target_os = "macos", target_os = "windows")))]
165mod linux_blur;
166/// The traffic lights' keep-out and the OS titlebar's height, measured.
167#[cfg(target_os = "macos")]
168mod macos_chrome;
169/// Files dragged in from the Finder, with where they are.
170#[cfg(target_os = "macos")]
171mod macos_drop;
172#[cfg(target_os = "macos")]
173mod macos_force;
174/// A non-activating window that refuses to become key (the popup flick).
175#[cfg(target_os = "macos")]
176mod macos_key;
177/// The platform's own context menu, where there is one.
178#[cfg(target_os = "macos")]
179mod macos_menu;
180/// The documents the Finder, the Dock and `open -a` hand the app.
181#[cfg(target_os = "macos")]
182mod macos_open;
183/// The palette's and dictation's inserts, which winit's view drops.
184#[cfg(target_os = "macos")]
185mod macos_text_input;
186mod menus;
187mod pacer;
188mod pane;
189mod popups;
190mod retarget;
191mod retry;
192mod scroll_gesture;
193mod secure_input;
194pub mod testing;
195mod windows;
196
197use pane::{
198    Pane, appearance_of, apply_ime_off, ime_off_change, level_change, level_supported,
199    option_as_alt_change, sync_env, theme_appearance,
200};
201/// The OS settings winit has no call for, asked once and re-asked when the
202/// user has evidently been in a settings app.
203mod system_env;
204/// The installed fonts changing while the app runs: the platform's signal,
205/// turned into a rescan (`Core::reload_system_fonts`).
206mod system_fonts;
207#[cfg(target_os = "windows")]
208mod windows_anim;
209/// The terminal a `windows_subsystem = "windows"` app was launched from,
210/// given back to it (`attach_parent`).
211#[cfg(target_os = "windows")]
212mod windows_console;
213#[cfg(target_os = "windows")]
214mod windows_nc;
215/// The OS's light/dark switch reaching winit while the app runs.
216#[cfg(target_os = "windows")]
217mod windows_theme;
218
219use winit::application::ApplicationHandler;
220use winit::dpi::{LogicalPosition, LogicalSize};
221use winit::event::{ElementState, Ime, MouseButton as WinitButton, WindowEvent};
222use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
223use winit::keyboard::{Key as WinitKey, ModifiersState, NamedKey};
224// `WindowId` is `kui_core`'s here (re-exported above); winit's own is the
225// OS handle the event loop routes by, and only this file names it.
226use winit::window::{CursorIcon, ResizeDirection, Window, WindowId as WinitWindowId};
227
228/// What a kui application implements: a `view` that declares each frame
229/// and an `on_event` that answers what the frame produced.
230///
231/// Only [`view`](Self::view) is required. Hand a value of the type to
232/// [`Launcher::run`], or to [`testing::Drive`] in a test.
233pub trait App {
234    /// Builds one window's frame. Called once per open window per frame;
235    /// `ui.window_name()` says which (`"main"` for the launcher's).
236    fn view(&mut self, ui: &mut Ui<'_>);
237    /// Receives one event the frame produced: a click's message, a key
238    /// press, a resize, a window event. Called after the frame, once per
239    /// event; [`UiEvent::message`] reads a `#[derive(Message)]` back out.
240    /// The default ignores every event.
241    fn on_event(&mut self, _ev: UiEvent) {}
242    /// [`Self::on_event`] with the [`Core`] of the window the event came
243    /// from. Override it when answering an event means calling the core:
244    /// writing the clipboard, asking for a paste, moving focus, revealing a
245    /// row, asking for a frame. A clipboard write goes out with this turn's
246    /// actions; a focus move or a reveal is seen by the next frame. The
247    /// default calls `on_event`, so an app that overrides only that one is
248    /// unchanged.
249    fn on_event_with(&mut self, ev: UiEvent, _core: &mut Core) {
250        self.on_event(ev)
251    }
252    /// Called once, before the window opens, with a [`Waker`] the app may
253    /// clone into any thread. The loop parks between events, so a thread
254    /// that changed what `view` will show (a PTY reader, a file watcher, a
255    /// socket) calls [`Waker::wake`] to get a frame drawn. The default
256    /// drops the waker.
257    fn setup(&mut self, _waker: Waker) {}
258    /// Called once, when the main window is going for good (its close
259    /// button, Quit, `WindowCommand::Close` on it, a pumped runner ending),
260    /// before `run` returns. Save a session, a draft or a position here;
261    /// the frame is over, there is no `Ui` and nothing draws. A crash under
262    /// `run` does not reach it; a panic unwinding through a [`PumpRunner`]
263    /// does, and a `teardown` that panics there aborts. The default does
264    /// nothing.
265    fn teardown(&mut self) {}
266}
267
268/// A handle into the event loop that any thread may hold; [`Waker::wake`]
269/// asks for a frame from wherever the app's data arrived. Cheap to clone,
270/// and harmless after the loop has ended (a wake nobody hears is dropped).
271/// An app gets one in [`App::setup`], a host from [`PumpRunner::waker`].
272#[derive(Clone)]
273pub struct Waker(EventLoopProxy<access_bridge::UserEvent>);
274
275impl Waker {
276    /// Asks every window for a frame: the loop wakes, `view` runs and the
277    /// frame is drawn. Safe from any thread and at any rate, since wakes
278    /// coalesce into the loop's next turn rather than queueing frames.
279    pub fn wake(&self) {
280        let _ = self.0.send_event(access_bridge::UserEvent::Wake);
281    }
282
283    /// Asks every window for a frame at `at` (backlog F135): the loop
284    /// sleeps until then and `view` runs, with no thread waiting for it.
285    /// The earliest time asked for wins; one already past is a wake now.
286    /// From the view itself `Ui::request_frame_at`, on the frame clock,
287    /// says the same thing without a `Waker`.
288    pub fn wake_at(&self, at: std::time::Instant) {
289        let _ = self.0.send_event(access_bridge::UserEvent::WakeAt(at));
290    }
291}
292
293impl std::fmt::Debug for Waker {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        f.write_str("Waker")
296    }
297}
298
299/// Who draws the window chrome.
300#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
301pub enum Chrome {
302    /// The OS titlebar and buttons (the default).
303    #[default]
304    Native,
305    /// The app draws its own titlebar (`widgets::titlebar`). On macOS the
306    /// native traffic lights stay, overlaid on the content (their rect is
307    /// reported in `env.window.native_controls`); elsewhere the window is
308    /// undecorated and the runner synthesizes edge resizing, double-click
309    /// maximize, and applies the `WindowCommand`s chrome nodes produce.
310    Custom,
311    /// No decorations and no chrome expectations (splash screens, popups).
312    Borderless,
313}
314
315/// How tall the titlebar strip is under [`Chrome::Custom`] (backlog
316/// W22): on macOS where the traffic lights sit in it, and everywhere the
317/// height `widgets::titlebar` draws at.
318///
319/// On macOS the lights are AppKit's to place: a taller titlebar is
320/// AppKit's own taller one, made by giving the window an empty toolbar,
321/// and AppKit keeps the lights in it through resizing, fullscreen and
322/// focus. Measured on macOS 27, the strip about the lights and the close
323/// button's top-left corner:
324///
325/// | | strip | lights at |
326/// |---|---|---|
327/// | `Standard` | 32 | (9, 9) |
328/// | `Medium` | 40 | (12, 13) |
329/// | `Tall` | 52 | (19, 19) |
330///
331/// The numbers are the OS's, and moved between releases before; what the
332/// window got is `env.window.native_controls`, measured,
333/// `widgets::titlebar` lays out against it, and the window's
334/// `titlebar_h` metric is the measured height too, `Standard` included.
335///
336/// On Windows and Linux the strip under custom chrome is the app's, as
337/// tall as the `titlebar_h` metric: `Medium` and `Tall` make that
338/// window's platform height 40 and 52 ([`Titlebar::strip_h`]) in place
339/// of the caption's 32 or 34, so `ui.metrics().titlebar_h`, `$titlebar_h`
340/// and the drawn buttons, which grow to the strip, all read it. An app's
341/// own metrics keep it while their `titlebar_h` is the stock number
342/// (`Metrics::compact()`, Node's `base`); a number of the app's own wins
343/// (`Core::set_platform_titlebar_h`).
344///
345/// Nothing under [`Chrome::Native`] or [`Chrome::Borderless`], on a
346/// popup or on the devtools' window.
347#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
348pub enum Titlebar {
349    /// The titlebar a window without a toolbar has (the default).
350    #[default]
351    Standard,
352    /// A compact toolbar's (`NSWindowToolbarStyle::UnifiedCompact`).
353    Medium,
354    /// A full toolbar's (`NSWindowToolbarStyle::Unified`).
355    Tall,
356}
357
358impl Titlebar {
359    /// The strip's height off macOS, where the strip under custom chrome
360    /// is the app's: the height the macOS titlebar of the same name has
361    /// on macOS 27, so one ask draws one strip on all three. `None` for
362    /// `Standard`, which is the platform's own caption height.
363    pub const fn strip_h(self) -> Option<f32> {
364        match self {
365            Titlebar::Standard => None,
366            Titlebar::Medium => Some(40.0),
367            Titlebar::Tall => Some(52.0),
368        }
369    }
370
371    /// Every name [`Titlebar::from_name`] reads, in order.
372    pub const NAMES: [&'static str; 3] = ["standard", "medium", "tall"];
373
374    /// The variant a binding names in lower case (`"medium"`).
375    pub fn from_name(name: &str) -> Option<Self> {
376        match name {
377            "standard" => Some(Self::Standard),
378            "medium" => Some(Self::Medium),
379            "tall" => Some(Self::Tall),
380            _ => None,
381        }
382    }
383}
384
385/// How outline glyphs are antialiased.
386#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
387pub enum TextAa {
388    /// LCD subpixel coverage when the GPU can blend per channel, grayscale
389    /// otherwise (the default). `KUI_TEXT_AA=gray|subpixel` overrides.
390    #[default]
391    Auto,
392    Grayscale,
393    Subpixel,
394}
395
396/// Starts a [`Launcher`] for a window titled `title`.
397///
398/// Chain the window's options, then [`Launcher::run`]:
399///
400/// ```rust,no_run
401/// # use kui_native::{App, Ui};
402/// # struct Mine;
403/// # impl App for Mine { fn view(&mut self, _ui: &mut Ui<'_>) {} }
404/// kui_native::app("Mine")
405///     .size(800.0, 600.0)
406///     .min_size(320.0, 240.0)
407///     .devtools(true)
408///     .run(Mine)
409///     .unwrap();
410/// ```
411///
412/// The default window is 960 by 640 logical pixels with the OS's own
413/// chrome. `KUI_WINDOW=WxH` in the environment overrides the size.
414pub fn app(title: &str) -> Launcher {
415    Launcher {
416        title: title.to_string(),
417        chrome: Chrome::Native,
418        size: (960.0, 640.0),
419        min_size: None,
420        max_size: None,
421        extensions: Extensions::new(),
422        text_aa: TextAa::Auto,
423        diagnostics: None,
424        core: None,
425        setup_core: Vec::new(),
426        deferred_events: false,
427        system: SystemEnv::default(),
428        icon: None,
429        icon_resource: None,
430        frame_latency: kui_wgpu::DEFAULT_FRAME_LATENCY,
431        backdrop: Backdrop::Opaque,
432        titlebar: Titlebar::Standard,
433    }
434}
435
436/// One [`Launcher::setup_core`] step.
437type CoreSetup = Box<dyn FnOnce(&mut Core)>;
438
439/// The windowed runner's builder, made by [`app`].
440///
441/// Every method takes and returns the launcher; [`Launcher::run`] opens the
442/// window and runs the event loop, [`Launcher::open`] opens it and hands the
443/// loop back as a [`PumpRunner`].
444pub struct Launcher {
445    title: String,
446    chrome: Chrome,
447    size: (f64, f64),
448    /// Inner-size bounds (logical px) handed to the OS, which enforces them
449    /// for user resizing; `None` leaves that side unbounded.
450    min_size: Option<(f64, f64)>,
451    max_size: Option<(f64, f64)>,
452    extensions: Extensions,
453    text_aa: TextAa,
454    /// The main window's core, when the host made it ahead
455    /// ([`Launcher::core`]); the launcher makes one otherwise.
456    core: Option<Core>,
457    /// What to do to the main core before its first frame
458    /// ([`Launcher::setup_core`]).
459    setup_core: Vec<CoreSetup>,
460    /// Whether the core's diagnostics run (see `kui_core::diag`); None =
461    /// on in debug builds, off in release.
462    diagnostics: Option<bool>,
463    /// Whether the host answers events after the loop has handed them over
464    /// (see [`Launcher::deferred_events`]).
465    deferred_events: bool,
466    /// The OS settings the app pinned ([`Launcher::system`]); unknown is
467    /// not pinned.
468    system: SystemEnv,
469    /// The windows' icon ([`Launcher::icon`]), checked when it was given.
470    icon: Option<winit::window::Icon>,
471    /// The executable's icon resource on Windows
472    /// ([`Launcher::icon_resource`]).
473    icon_resource: Option<u16>,
474    /// Frames queued ahead of the one on screen
475    /// ([`Launcher::frame_latency`]).
476    frame_latency: u32,
477    /// What shows through the app's windows ([`Launcher::backdrop`]).
478    backdrop: Backdrop,
479    /// How tall the macOS titlebar is under custom chrome
480    /// ([`Launcher::titlebar`]).
481    titlebar: Titlebar,
482}
483
484impl Launcher {
485    /// Who draws the window chrome; see [`Chrome`]. The OS by default.
486    pub fn chrome(mut self, chrome: Chrome) -> Self {
487        self.chrome = chrome;
488        self
489    }
490
491    /// Glyph antialiasing; see [`TextAa`].
492    pub fn text_aa(mut self, aa: TextAa) -> Self {
493        self.text_aa = aa;
494        self
495    }
496
497    /// How many frames may be queued ahead of the one on screen, for every
498    /// window. Two by default ([`kui_wgpu::DEFAULT_FRAME_LATENCY`]), which
499    /// keeps every vsync fed at light load; one on Windows, where one
500    /// already does. On macOS 14+ frames are paced to the display, so the
501    /// second queued frame costs no latency; on Linux and under a pumped
502    /// runner it reaches the screen one vsync later. `KUI_FRAME_LATENCY`
503    /// overrides the value and `KUI_FRAME_PACING=0` turns the pacing off.
504    /// Values below one are one.
505    pub fn frame_latency(mut self, frames: u32) -> Self {
506        self.frame_latency = frames.max(1);
507        self
508    }
509
510    /// Whether the core looks for silent misconfigurations and the runner
511    /// prints them to stderr (see [`diag`]). On in debug builds and off in
512    /// release unless set, so a shipped app stays quiet.
513    pub fn diagnostics(mut self, on: bool) -> Self {
514        self.diagnostics = Some(on);
515        self
516    }
517
518    /// Opens the app with the devtools panel docked beside it: the event
519    /// stream, the runtime's facts and the node tree. `KUI_DEVTOOLS=1` in
520    /// the environment asks the same of an app that never called this.
521    /// Sugar for `setup_core(|c| c.set_devtools(on))`.
522    pub fn devtools(self, on: bool) -> Self {
523        self.setup_core(move |core| core.set_devtools(on))
524    }
525
526    /// Respells the chord that moves the keyboard into the devtools panel
527    /// and back, and brings a hidden panel back, from its default
528    /// `Ctrl+Shift+I`: `Accel::parse("f12")`, `"mod+shift+d"`, any spelling
529    /// a menu item takes. The panel's other chords stay
530    /// `Ctrl+Shift+<letter>`, and `Ctrl+Shift+I` is the app's again. Sugar
531    /// for `setup_core(|c| c.set_devtools_key(key))`.
532    pub fn devtools_key(self, key: Accel) -> Self {
533        self.setup_core(move |core| core.set_devtools_key(key))
534    }
535
536    /// Runs `f` on the main window's [`Core`] before its first frame: the
537    /// place for what a core is told rather than declared in a view, such
538    /// as a pinned theme, `set_native_menus` or the devtools. Every call
539    /// adds one; they run in order.
540    pub fn setup_core(mut self, f: impl FnOnce(&mut Core) + 'static) -> Self {
541        self.setup_core.push(Box::new(f));
542        self
543    }
544
545    /// Opens the main window on `core` rather than on one the launcher
546    /// makes. Everything registered on it beforehand (fonts, images,
547    /// sounds, tokens, a pinned theme, menus) reaches the window, and its
548    /// session becomes the app's, so a second window joins it and handles
549    /// minted by an earlier headless frame keep drawing. The launcher's own
550    /// settings still apply on top, in order: [`Launcher::diagnostics`],
551    /// then `KUI_DEVTOOLS`, then every [`Launcher::setup_core`]. For a host
552    /// that drew headless first, or built the core through a binding.
553    pub fn core(mut self, core: Core) -> Self {
554        self.core = Some(core);
555        self
556    }
557
558    /// Declares that the app answers an event after `on_event` returns,
559    /// which only a host driving the loop itself can do ([`Launcher::open`],
560    /// [`PumpRunner`]): `on_event` keeps the event, the pump returns, the
561    /// host runs its handler and submits the next view.
562    ///
563    /// With it set, an input whose events reached the app does not ask for
564    /// a frame itself; the host asks with [`PumpRunner::request_redraw`]
565    /// once its handler has run, so a button's release and what the release
566    /// did land in one frame instead of two. Nothing else changes: a
567    /// transition, a caret blink, a [`Waker`] wake or the OS's own repaint
568    /// still paint when they ask, and an input that reached nobody still
569    /// asks for its own frame.
570    pub fn deferred_events(mut self) -> Self {
571        self.deferred_events = true;
572        self
573    }
574
575    /// Pins part of `env.system` for this app's windows. Every field of
576    /// `pinned` that is not "cannot tell" is what the views read, over
577    /// whatever the OS says, for as long as the app runs; the fields left
578    /// at their default keep following the OS, and a change to one of
579    /// those still arrives as the `system` event.
580    ///
581    /// ```rust,no_run
582    /// # use kui_native::{SystemEnv, MotionPref};
583    /// kui_native::app("mine").system(SystemEnv { motion: MotionPref::Reduced, ..Default::default() });
584    /// ```
585    ///
586    /// Use it to see the window a user who asked for less motion, or a
587    /// dark appearance, would get. It is on the launcher rather than an
588    /// environment variable so that a shipped app's motion is decided in
589    /// its own code; a headless core takes the same reading through
590    /// `core.env.system`.
591    pub fn system(mut self, pinned: SystemEnv) -> Self {
592        self.system = pinned;
593        self
594    }
595
596    /// The icon every window of the app is created with: `rgba` is
597    /// `width` by `height` pixels, four bytes each, row by row from the top
598    /// left, alpha not premultiplied. Windows shows it in the title bar,
599    /// Alt-Tab and the taskbar, X11 in the window manager's; macOS and
600    /// Wayland take the app's icon from the bundle or the `.desktop` file
601    /// and ignore this. Pass something a taskbar can shrink cleanly, 64 to
602    /// 256 px. Panics when the pixels are not that size;
603    /// [`Launcher::try_icon`] returns the reason instead.
604    ///
605    /// ```rust,no_run
606    /// # let rgba = vec![0u8; 64 * 64 * 4];
607    /// kui_native::app("mine").icon(rgba, 64, 64);
608    /// ```
609    ///
610    /// On Windows, [`Launcher::icon_resource`] names the icon linked into
611    /// the executable and wins over these pixels.
612    pub fn icon(self, rgba: Vec<u8>, width: u32, height: u32) -> Self {
613        match self.try_icon(rgba, width, height) {
614            Ok(this) => this,
615            Err(e) => panic!("kui: {e}"),
616        }
617    }
618
619    /// [`Launcher::icon`] for pixels that came from outside the program,
620    /// refused with the reason rather than a panic. The launcher is
621    /// consumed either way.
622    pub fn try_icon(mut self, rgba: Vec<u8>, width: u32, height: u32) -> Result<Self, String> {
623        self.icon = Some(icon::from_rgba(rgba, width, height)?);
624        Ok(self)
625    }
626
627    /// [`Launcher::icon`] from an image file's bytes — a PNG, JPEG, WebP
628    /// or GIF, decoded by [`decode_image`] (backlog F138):
629    /// `.icon_bytes(include_bytes!("icon.png"))`. Panics, naming why, on
630    /// bytes that are not an image; [`Launcher::try_icon_bytes`] returns
631    /// the reason instead.
632    pub fn icon_bytes(self, bytes: &[u8]) -> Self {
633        match self.try_icon_bytes(bytes) {
634            Ok(this) => this,
635            Err(e) => panic!("kui: {e}"),
636        }
637    }
638
639    /// [`Launcher::icon_bytes`] for a file that came from outside the
640    /// program, refused with the reason rather than a panic.
641    pub fn try_icon_bytes(self, bytes: &[u8]) -> Result<Self, String> {
642        let p = decode_image(bytes).map_err(|e| format!("an icon: {e}"))?;
643        self.try_icon(p.rgba, p.width, p.height)
644    }
645
646    /// The executable's icon resource `id` as every window's icon, on
647    /// Windows: the `.ico` a `1 ICON "app.ico"` line in the program's `.rc`
648    /// links in, from which the title bar and the taskbar each load the
649    /// frame drawn for their size. A resource the executable does not have
650    /// is reported once on stderr, and [`Launcher::icon`]'s pixels are used
651    /// if there are any. Nothing on other platforms, so an app passes both
652    /// and each OS takes its own.
653    pub fn icon_resource(mut self, id: u16) -> Self {
654        self.icon_resource = Some(id);
655        self
656    }
657
658    /// What shows through the app's windows where a frame paints nothing,
659    /// or paints with alpha (backlog F126), by effect: `Transparent` (the
660    /// desktop as it is), `Blur` (a live blur of what is behind the
661    /// window), `Tinted` (the desktop's colour, steady). `Backdrop::Opaque`,
662    /// the default, is the window every app had before. Which regions show
663    /// it is the app's: paint them with alpha and the rest opaque.
664    ///
665    /// ```rust,no_run
666    /// # use kui_native::{App, Backdrop, Ui};
667    /// # struct Notes;
668    /// # impl App for Notes { fn view(&mut self, _ui: &mut Ui<'_>) {} }
669    /// kui_native::app("Notes")
670    ///     .custom_titlebar()
671    ///     .backdrop(Backdrop::Blur)
672    ///     .run(Notes)
673    ///     .unwrap();
674    /// ```
675    ///
676    /// How each platform gives it:
677    ///
678    /// | | `Blur` | `Tinted` |
679    /// |---|---|---|
680    /// | macOS | `NSVisualEffectView`, behind-window (the sidebar material) | `NSVisualEffectView`, the window-background material |
681    /// | Windows 11 22H2+ | Acrylic | Mica |
682    /// | KDE Plasma | the compositor's blur (`ext-background-effect-v1`, `org_kde_kwin_blur`, `_KDE_NET_WM_BLUR_BEHIND_REGION`) | the wallpaper, drawn by kui |
683    /// | GNOME, other Linux, Windows 10 | the wallpaper, drawn by kui (reads `Tinted`) | the wallpaper, drawn by kui |
684    ///
685    /// "The wallpaper, drawn by kui" is the desktop's picture — GNOME's
686    /// `picture-uri` (or `-dark`), Plasma's config, Windows'
687    /// `SPI_GETDESKWALLPAPER` — read on a thread, scaled down and blurred
688    /// once, and drawn under the frame aligned to where the window sits on
689    /// its monitor (centred on Wayland, which does not say); with no
690    /// wallpaper to read, the window is opaque. Hyprland, SwayFX and picom
691    /// blur translucent windows themselves when configured to: ask for
692    /// `Transparent` there. `KUI_BACKDROP_EMULATE=1` draws the wallpaper
693    /// for `Blur` and `Tinted` on Windows and Linux, to look at it (macOS
694    /// reads no wallpaper, so the window there is `Opaque`).
695    ///
696    /// Asking changes three things. Where the OS draws the effect the
697    /// frame is cleared to nothing rather than to the theme's `bg`, so what
698    /// a view paints is all that sits over it: a region with a translucent
699    /// `bg` (`theme.bg.with_alpha(0.6)`) shows the effect through it, and a
700    /// region with an opaque `bg` does not. Glyphs are antialiased in
701    /// grayscale under `TextAa::Auto`, since an LCD mask cannot be
702    /// composited over what the window cannot see. And on Windows the
703    /// device presents through DirectComposition, the one D3D12 swapchain
704    /// that takes alpha.
705    ///
706    /// What the platform gave is `ui.env().window.backdrop`, every frame,
707    /// and is what a view decides by — less where the platform has less
708    /// (a `Blur` asked of GNOME reads `Tinted`, or `Opaque` with no
709    /// wallpaper). Paint the translucent regions opaque when it reads
710    /// `Opaque`:
711    ///
712    /// ```rust
713    /// # use kui_native::{Backdrop, Color, Env};
714    /// fn pane_bg(env: &Env, base: Color) -> Color {
715    ///     match env.window.backdrop {
716    ///         Backdrop::Opaque => base,
717    ///         // The desktop unblurred: text over it is a lottery.
718    ///         Backdrop::Transparent => base.with_alpha(0.92),
719    ///         Backdrop::Blur | Backdrop::Tinted => base.with_alpha(0.55),
720    ///     }
721    /// }
722    /// # let _ = pane_bg(&Env::default(), Color::hex(0x202020ff));
723    /// ```
724    ///
725    /// It applies to the main window and to every window the app's frames
726    /// declare (`Ui::window`) — not to a popup, which is a menu surface,
727    /// nor to the devtools' window.
728    pub fn backdrop(mut self, backdrop: Backdrop) -> Self {
729        self.backdrop = backdrop;
730        self
731    }
732
733    /// How tall the titlebar strip is under [`Chrome::Custom`] — on macOS
734    /// AppKit's titlebar and so where the traffic lights sit, elsewhere
735    /// the `titlebar_h` metric; see [`Titlebar`]. Applies to the main
736    /// window and every window the app's frames declare, as
737    /// [`Launcher::chrome`] does.
738    ///
739    /// ```rust,no_run
740    /// # use kui_native::{App, Titlebar, Ui};
741    /// # struct Notes;
742    /// # impl App for Notes { fn view(&mut self, ui: &mut Ui<'_>) {
743    /// #     kui_native::widgets::titlebar(ui, "Notes");
744    /// # } }
745    /// kui_native::app("Notes")
746    ///     .custom_titlebar()
747    ///     .titlebar(Titlebar::Tall)
748    ///     .run(Notes)
749    ///     .unwrap();
750    /// ```
751    pub fn titlebar(mut self, titlebar: Titlebar) -> Self {
752        self.titlebar = titlebar;
753        self
754    }
755
756    /// Shorthand for `.chrome(Chrome::Custom)`.
757    pub fn custom_titlebar(self) -> Self {
758        self.chrome(Chrome::Custom)
759    }
760
761    /// Shorthand for `.chrome(Chrome::Borderless)`.
762    pub fn borderless(self) -> Self {
763        self.chrome(Chrome::Borderless)
764    }
765
766    /// Initial inner size, logical px (`KUI_WINDOW=WxH` still overrides).
767    /// Clamped into the `min_size`/`max_size` bounds, as the OS would.
768    pub fn size(mut self, w: f64, h: f64) -> Self {
769        self.size = (w, h);
770        self
771    }
772
773    /// Smallest inner size the user may resize the window to, logical px.
774    /// The OS enforces it; the initial size is clamped up into it.
775    pub fn min_size(mut self, w: f64, h: f64) -> Self {
776        self.min_size = Some((w, h));
777        self
778    }
779
780    /// Largest inner size the user may resize the window to, logical px.
781    /// A bound below the matching `min_size` loses to it, as on the OS side.
782    pub fn max_size(mut self, w: f64, h: f64) -> Self {
783        self.max_size = Some((w, h));
784        self
785    }
786
787    /// Loads `ext` under its own name as its namespace. The slots it fills
788    /// are declared as `ui.slot("<name>/<slot>")`. Panics when the name is
789    /// already another extension's namespace; two of one name need
790    /// [`Launcher::extension_as`].
791    pub fn extension(self, ext: impl Extension + 'static) -> Self {
792        let ns = ext.name().to_owned();
793        self.extension_as(ns, ext)
794    }
795
796    /// Loads `ext` under `namespace`, so the same extension loaded twice is
797    /// two namespaces with two sets of slots and params. Panics on a
798    /// namespace already taken, an empty one, or an extension whose slot
799    /// names contain `/`.
800    pub fn extension_as(self, namespace: impl Into<String>, ext: impl Extension + 'static) -> Self {
801        match self.try_extension_as(namespace, ext) {
802            Ok(this) => this,
803            Err(e) => panic!("kui: {e}"),
804        }
805    }
806
807    /// [`Launcher::extension_as`] for a caller that reports the refusal
808    /// rather than panicking, such as a plugin named from outside the
809    /// program. The launcher is consumed either way.
810    pub fn try_extension_as(
811        mut self,
812        namespace: impl Into<String>,
813        ext: impl Extension + 'static,
814    ) -> Result<Self, String> {
815        self.extensions.push_as(namespace, Box::new(ext))?;
816        Ok(self)
817    }
818
819    /// Takes an [`Extensions`] list already loaded, replacing any
820    /// `extension` calls before it. For a host that built the list
821    /// elsewhere, as the C binding does.
822    pub fn with_extensions(mut self, extensions: Extensions) -> Self {
823        self.extensions = extensions;
824        self
825    }
826
827    /// [`Launcher::extension`] for each, in order.
828    pub fn extensions(mut self, exts: Vec<Box<dyn Extension>>) -> Self {
829        for ext in exts {
830            if let Err(e) = self.extensions.push(ext) {
831                panic!("kui: {e}");
832            }
833        }
834        self
835    }
836
837    /// The shell for `app`, boxed: the one generic step between an app
838    /// and the runner, kept to moving fields. Everything after it
839    /// takes the box unsized, as a [`DynShell`].
840    fn shell<A: App>(mut self, app: A) -> Box<Shell<A>> {
841        let (diagnostics, session, core) = self.main_core();
842        Box::new(Shell {
843            title: self.title,
844            icon: icon::AppIcon::new(self.icon, self.icon_resource),
845            chrome: self.chrome,
846            size: clamp_size(self.size, self.min_size, self.max_size),
847            min_size: self.min_size,
848            max_size: self.max_size,
849            text_aa: self.text_aa,
850            frame_latency: wanted_frame_latency(self.frame_latency),
851            backdrop: self.backdrop,
852            titlebar: self.titlebar,
853            diagnostics,
854            subpixel: false,
855            extensions: self.extensions,
856            session,
857            main_core: Some(core),
858            panes: Vec::new(),
859            gpu: None,
860            reopened: None,
861            reopen_owed: false,
862            pretended_loss: false,
863            locks: kui_core::KeyLocks::default(),
864            script: kui_core::LayoutScript::default(),
865            epoch: std::time::Instant::now(),
866            system: system_env::query(),
867            pinned_system: self.system,
868            clipboard: arboard::Clipboard::new().ok(),
869            #[cfg(target_os = "macos")]
870            native_menu: macos_menu::MacMenu::new(),
871            #[cfg(target_os = "macos")]
872            menu_shown: false,
873            #[cfg(target_os = "macos")]
874            native_menu_bar: macos_menu::MacMenuBar::new(),
875            #[cfg(target_os = "macos")]
876            applied_menu_bar: None,
877            audio: audio::Audio::new(),
878            audio_touch: std::time::Instant::now(),
879            smoke_frames: Self::smoke_frames(),
880            frames_drawn: 0,
881            exit_requested: false,
882            startup_error: None,
883            torn_down: false,
884            secure_input: secure_input::SecureInput::default(),
885            pumped: false,
886            opened: false,
887            primary_down: None,
888            armed: Vec::new(),
889            swallowed_press: None,
890            proxy: None,
891            next_deadline: None,
892            wake_at: None,
893            saw_event: false,
894            woke: false,
895            deferred_events: self.deferred_events,
896            owed: std::cell::Cell::new(false),
897            app,
898        })
899    }
900
901    /// A shell as the runner sees it, for the tests.
902    #[cfg(test)]
903    fn dyn_shell<A: App + 'static>(self, app: A) -> Box<DynShell<'static>> {
904        self.shell(app)
905    }
906
907    /// Whether diagnostics are on, the session, and the main window's core
908    /// with the launcher's setup applied — the part of `shell` that does
909    /// not need the app's type.
910    fn main_core(&mut self) -> (bool, Session, Core) {
911        // Before anything prints: a windows-subsystem app started from a
912        // shell has no stdout until it takes its parent's, and the
913        // diagnostics, the panics and `report_faults` are all worth
914        // reading there (`mod windows_console`).
915        #[cfg(target_os = "windows")]
916        windows_console::attach_parent();
917        // Diagnostics are a development aid: on in debug builds unless the
918        // launcher says otherwise, so a shipped app pays and prints nothing.
919        let diagnostics = self.diagnostics.unwrap_or(cfg!(debug_assertions));
920        // A handed core brings its session; a made one gets a fresh one.
921        let (session, mut core) = match self.core.take() {
922            Some(core) => (core.session().clone(), core),
923            None => {
924                let session = Session::new();
925                let core = Core::new_in(&session);
926                (session, core)
927            }
928        };
929        core.set_diagnostics(diagnostics);
930        // `KUI_DEVTOOLS=1` opens the panel for a program that never asked
931        // (ADR 0024); read here, for a window, and never by a headless
932        // core. What the launcher was told comes after, and wins.
933        core.devtools_from_env();
934        for f in std::mem::take(&mut self.setup_core) {
935            f(&mut core);
936        }
937        (diagnostics, session, core)
938    }
939
940    /// `KUI_SMOKE_FRAMES=n`, in a build that honours it.
941    ///
942    /// A development aid, gated the way `shell` gates the diagnostics: an app you
943    /// ship should not close its own window because something in the
944    /// environment it was launched from happened to set a variable, and
945    /// the app's author never asked for that behaviour. Live in a dev
946    /// build, which is where it is used by hand (`KUI_SMOKE_FRAMES=120
947    /// cargo run --example fragment`), and in a release build that asks
948    /// for it with `--features smoke` — which is what
949    /// the smoke round passes under `--release`
950    /// (examples/devtools/src/bin/smoke.rs), and
951    /// the whole reason this is a feature rather than `debug_assertions`
952    /// alone: the round is worth running against what actually ships.
953    fn smoke_frames() -> Option<u32> {
954        if !(cfg!(debug_assertions) || cfg!(feature = "smoke")) {
955            return None;
956        }
957        std::env::var("KUI_SMOKE_FRAMES")
958            .ok()
959            .and_then(|s| s.parse().ok())
960    }
961
962    /// Opens the main window and runs the event loop until the main window
963    /// closes. Returns `Ok` then, or the error when the window or its
964    /// renderer could not be made. One event loop per process: a process
965    /// that has used [`Launcher::open`] opens again rather than calling
966    /// `run`.
967    pub fn run<A: App>(self, app: A) -> Result<(), Box<dyn std::error::Error>> {
968        let event_loop = run_loop()?;
969        run_shell(event_loop, self.shell(app))
970    }
971
972    /// Opens the window but keeps the event loop in the caller's hands: the
973    /// returned [`PumpRunner`] processes OS events only when
974    /// [`PumpRunner::pump`] is called, so a foreign loop (libuv, a game
975    /// loop, a test harness) can interleave with winit on the main thread.
976    /// winit allows one event loop per process, but any number of runners
977    /// in turn: a runner whose main window has closed parks the loop, and
978    /// the next `open` on the thread takes it back.
979    pub fn open<A: App>(self, app: A) -> Result<PumpRunner<A>, Box<dyn std::error::Error>> {
980        let event_loop = take_event_loop()?;
981        let mut shell = self.shell(app);
982        let state = PumpState::open(event_loop, &mut *shell);
983        let mut runner = PumpRunner {
984            state,
985            shell: std::mem::ManuallyDrop::new(shell),
986        };
987        if !runner.state.alive {
988            runner.retire();
989        }
990        // Retired above, so the loop is parked and the next `open` can
991        // try again (backlog RG47).
992        if let Some(why) = runner.shell.startup_error.take() {
993            return Err(why.into());
994        }
995        Ok(runner)
996    }
997}
998
999/// The event loop `run` owns. Not a parked loop: a pumped runner leaves
1000/// winit's loop *running* (it never exits it — see `PARKED_LOOP`), and
1001/// `run_app` on a running loop is a `debug_assert` in winit's macOS path.
1002/// `run` is the one-shot runner; a process that has pumped opens again.
1003fn run_loop() -> Result<EventLoop<access_bridge::UserEvent>, Box<dyn std::error::Error>> {
1004    if PARKED_LOOP.with(|p| p.borrow().is_some()) {
1005        return Err(
1006            "kui: `run` cannot follow a pumped runner in this process — a loop \
1007                    a `PumpRunner` parked is still running; open another `PumpRunner`"
1008                .into(),
1009        );
1010    }
1011    Ok(EventLoop::<access_bridge::UserEvent>::with_user_event().build()?)
1012}
1013
1014/// `Launcher::run` past the one generic step: compiled here, once.
1015fn run_shell(
1016    event_loop: EventLoop<access_bridge::UserEvent>,
1017    mut shell: Box<DynShell<'_>>,
1018) -> Result<(), Box<dyn std::error::Error>> {
1019    event_loop.set_control_flow(ControlFlow::Wait);
1020    shell.attach(&event_loop);
1021    event_loop.run_app(&mut Handler(&mut shell))?;
1022    match shell.startup_error.take() {
1023        Some(why) => Err(why.into()),
1024        None => Ok(()),
1025    }
1026}
1027
1028impl DynShell<'_> {
1029    /// The main window, or its renderer, could not be made: `why` becomes
1030    /// the error `run` or `open` returns, and the runner ends. `run`'s
1031    /// loop is asked to exit; a pumped one is not — it is parked for the
1032    /// next runner, as when a window closes.
1033    fn fail_open(&mut self, event_loop: &ActiveEventLoop, why: String) {
1034        self.startup_error = Some(why);
1035        self.exit_requested = true;
1036        if !self.pumped {
1037            event_loop.exit();
1038        }
1039    }
1040
1041    /// What a shell takes from the loop it runs on before the first
1042    /// event: the proxy, the app's waker, and the wakers of the platform
1043    /// paths no winit event carries.
1044    fn attach(&mut self, event_loop: &EventLoop<access_bridge::UserEvent>) {
1045        self.proxy = Some(event_loop.create_proxy());
1046        self.app.setup(Waker(event_loop.create_proxy()));
1047        // A menu-bar item chosen by its ⌘-shortcut is the whole of the
1048        // event as far as winit is concerned — AppKit consumed the key —
1049        // so the bar rings the loop itself.
1050        #[cfg(target_os = "macos")]
1051        if let Some(bar) = &self.native_menu_bar {
1052            bar.set_waker(Waker(event_loop.create_proxy()));
1053        }
1054        // And so does an insert the platform makes from the palette or
1055        // dictation: no winit event carries it.
1056        #[cfg(target_os = "macos")]
1057        macos_text_input::set_waker(Waker(event_loop.create_proxy()));
1058        // And a file drag's position (ADR 0031), which winit's do not.
1059        #[cfg(target_os = "macos")]
1060        macos_drop::set_waker(Waker(event_loop.create_proxy()));
1061        // And the documents the OS asks the app to open (backlog F124),
1062        // which winit's application delegate does not answer: installed
1063        // here, before `run_app` launches the app, since the launch-time
1064        // ones come inside `finishLaunching`.
1065        #[cfg(target_os = "macos")]
1066        {
1067            macos_open::set_waker(Waker(event_loop.create_proxy()));
1068            macos_open::install();
1069        }
1070        // And the installed fonts changing, which winit has no event for.
1071        system_fonts::watch(event_loop.create_proxy());
1072    }
1073}
1074
1075thread_local! {
1076    /// The process's one event loop, parked between runners. winit refuses
1077    /// to build a second (`EventLoopError::RecreationAttempt`, a static
1078    /// flag it never clears), so a host that opens a window, closes it
1079    /// and opens another — a smoke test running two configurations in
1080    /// sequence, an app whose second launch is in-process — needs the
1081    /// first runner to hand the loop back rather than drop it. The pump
1082    /// path never calls winit's `exit()` for the same reason: an exited
1083    /// loop answers every later pump with `Exit` and nothing public clears
1084    /// that; the runner ends itself on `exit_requested` instead, and the
1085    /// loop stays live for the next shell.
1086    static PARKED_LOOP: std::cell::RefCell<Option<EventLoop<access_bridge::UserEvent>>> =
1087        const { std::cell::RefCell::new(None) };
1088}
1089
1090/// The parked loop if an earlier runner left one, else a new one — which
1091/// winit allows once per process. For `open` only: `run` builds its own
1092/// (above), since a parked loop is a running one.
1093fn take_event_loop() -> Result<EventLoop<access_bridge::UserEvent>, winit::error::EventLoopError> {
1094    if let Some(parked) = PARKED_LOOP.with(|p| p.borrow_mut().take()) {
1095        return Ok(parked);
1096    }
1097    EventLoop::<access_bridge::UserEvent>::with_user_event().build()
1098}
1099
1100/// Whether this input is one whose own visible effect is finished, so the
1101/// app's answer to it belongs in the same frame.
1102///
1103/// A press or a release changes what the core itself paints — the button
1104/// goes down, the button comes up — and that change reads as the whole of
1105/// what happened, so a frame showing the button let go with the count
1106/// unchanged is a frame that lies. A keystroke, an IME commit and an
1107/// assistive-technology action are discrete the same way.
1108///
1109/// A pointer moving, a wheel turning and a preedit being revised are not:
1110/// they arrive as a stream, every frame during one is superseded by the
1111/// next, and an app's content trailing the pointer by a frame is what
1112/// every toolkit does. Waiting on those would halve the frame rate of a
1113/// drag for nothing — measured at 18 frames against 36 in a 578 ms drag —
1114/// so they never wait.
1115/// Puts a copy on the system clipboard, with the formatting beside the
1116/// words where there is any.
1117///
1118/// Both flavours or neither: `set_html` writes the HTML *and* the plain
1119/// text it is given as an alternative, so an app that understands one
1120/// takes it and everything else takes the words. A clipboard holding only
1121/// HTML pastes markup into every plain-text field on the machine, which is
1122/// the failure mode this shape exists to avoid.
1123fn set_clipboard(clipboard: Option<&mut arboard::Clipboard>, text: String, html: Option<String>) {
1124    let Some(cb) = clipboard else { return };
1125    match html {
1126        Some(html) => {
1127            let _ = cb.set_html(html, Some(text));
1128        }
1129        None => {
1130            let _ = cb.set_text(text);
1131        }
1132    }
1133}
1134
1135fn input_completes(ev: &InputEvent) -> bool {
1136    match ev {
1137        InputEvent::MouseDown { .. }
1138        | InputEvent::MouseUp { .. }
1139        // A force click is one moment and one answer, like a press.
1140        | InputEvent::ForceClick(_)
1141        | InputEvent::Key(..)
1142        | InputEvent::KeyDown(_)
1143        | InputEvent::KeyUp(_)
1144        | InputEvent::Text(_)
1145        | InputEvent::Commit(_)
1146        | InputEvent::Paste { .. }
1147        | InputEvent::Access(_)
1148        // A drop is a release; a cancel ends the drag the same way.
1149        | InputEvent::DropFiles { .. }
1150        | InputEvent::DragCancel
1151        // A dialog's answer is one moment, as a paste is, and so are the
1152        // documents the OS hands over.
1153        | InputEvent::Files(_)
1154        | InputEvent::Open(_) => true,
1155        InputEvent::CursorMoved(_)
1156        | InputEvent::CursorLeft
1157        | InputEvent::Scroll(_)
1158        | InputEvent::ScrollGesture { .. }
1159        | InputEvent::Preedit(..)
1160        | InputEvent::Modifiers(_)
1161        // Files moving over the window is the pointer moving.
1162        | InputEvent::DragFiles { .. } => false,
1163    }
1164}
1165
1166/// Whether a redraw waits for the host's answer instead of painting now.
1167///
1168/// Two conditions, and the second is the one that makes this safe. `owed`
1169/// says an input reached an app that answers later, so what would be
1170/// painted predates that input ([`Launcher::deferred_events`]).
1171/// `deferred_last` says this window's previous redraw already waited — and
1172/// a frame never waits twice running.
1173///
1174/// That bound is not a nicety. Waiting until the host says otherwise is
1175/// the obvious rule and it starves the window: winit hands a pump its
1176/// input before that pump's redraw, so under a stream of input that
1177/// reaches the app — a drag, an auto-repeating key — each pump re-arms the
1178/// wait before the frame the host just asked for is delivered, and the
1179/// window paints **nothing** until the stream ends (measured: one frame in
1180/// a 578 ms drag). The same unbounded rule freezes a window for a whole
1181/// title-bar drag, since a platform's modal move loop never returns to the
1182/// host that would end the wait. Never twice running costs at worst half
1183/// the frame rate under continuous input, and bounds every one of those to
1184/// a single frame.
1185fn frame_waits_for_host(owed: bool, deferred_last: bool) -> bool {
1186    owed && !deferred_last
1187}
1188
1189fn pump_once(
1190    event_loop: &mut EventLoop<access_bridge::UserEvent>,
1191    shell: &mut DynShell<'_>,
1192) -> bool {
1193    use winit::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus};
1194    match event_loop.pump_app_events(Some(std::time::Duration::ZERO), &mut Handler(shell)) {
1195        PumpStatus::Continue => !shell.exit_requested,
1196        PumpStatus::Exit(_) => false,
1197    }
1198}
1199
1200/// A windowed runner driven from outside, from [`Launcher::open`]: the
1201/// same runner as [`Launcher::run`] (input mapping, IME, clipboard, chrome,
1202/// caret blink), but the host calls [`pump`](Self::pump) or
1203/// [`pump_until`](Self::pump_until) on its own cadence instead of parking
1204/// in the event loop.
1205///
1206/// Ask [`next_deadline`](Self::next_deadline) after each pump for how long
1207/// to sleep, and [`request_redraw`](Self::request_redraw) after changing
1208/// what `view` will produce. The runner ends when the main window closes;
1209/// dropping it earlier closes the windows and parks the loop for the next
1210/// `open`.
1211pub struct PumpRunner<A: App> {
1212    state: PumpState,
1213    /// Dropped by hand, unsized (`drop_shell`): as a plain field its drop
1214    /// glue — every window, core and store the shell owns — was generated
1215    /// in the app's crate.
1216    shell: std::mem::ManuallyDrop<Box<Shell<A>>>,
1217}
1218
1219/// What a [`PumpRunner`] keeps beside its shell, and does to it, without
1220/// its app's type.
1221struct PumpState {
1222    /// `None` once the runner has retired: the loop is parked for the next
1223    /// runner on this thread (see `PARKED_LOOP`).
1224    event_loop: Option<EventLoop<access_bridge::UserEvent>>,
1225    alive: bool,
1226    /// Every turn this runner has taken — [`pump`](PumpRunner::pump) and
1227    /// [`pump_until`](PumpRunner::pump_until) alike, the first one that
1228    /// opened the window included. What a driver's backoff is measured in
1229    ///: the runner knows how often it pumped where the app
1230    /// could only read a process monitor.
1231    pumps: u64,
1232    /// The turns among `pumps` whose batch carried an OS event or a wake
1233    ///: what `saw_event` marks, counted once per turn.
1234    woken_pumps: u64,
1235}
1236
1237impl PumpState {
1238    fn open(
1239        mut event_loop: EventLoop<access_bridge::UserEvent>,
1240        shell: &mut DynShell<'_>,
1241    ) -> PumpState {
1242        event_loop.set_control_flow(ControlFlow::Wait);
1243        shell.pumped = true;
1244        shell.attach(&event_loop);
1245        // First pump delivers `resumed`, creating the window + renderer —
1246        // or, on a loop taken back from an earlier runner, `about_to_wait`
1247        // does, since winit's init events came and went with the first.
1248        let alive = pump_once(&mut event_loop, shell);
1249        let mut state = PumpState {
1250            event_loop: Some(event_loop),
1251            alive,
1252            pumps: 1,
1253            woken_pumps: 0,
1254        };
1255        state.tally(shell);
1256        state
1257    }
1258
1259    /// Counts the turn that just ran as woken if its batch saw an event.
1260    /// The flag is taken, so the quiet turns after it are not counted too.
1261    fn tally(&mut self, shell: &mut DynShell<'_>) {
1262        if std::mem::take(&mut shell.woke) {
1263            self.woken_pumps += 1;
1264        }
1265    }
1266
1267    fn pump(&mut self, shell: &mut DynShell<'_>) -> bool {
1268        let Some(event_loop) = &mut self.event_loop else {
1269            return false;
1270        };
1271        self.pumps += 1;
1272        self.alive = pump_once(event_loop, shell);
1273        self.tally(shell);
1274        if !self.alive {
1275            self.retire(shell);
1276        }
1277        self.alive
1278    }
1279
1280    /// The end of this runner: every window closed (dropping the panes
1281    /// is what closes them — the pump path never asks winit to exit) and
1282    /// the loop handed back for the next `Launcher::open` on the thread.
1283    fn retire(&mut self, shell: &mut DynShell<'_>) {
1284        self.alive = false;
1285        shell.teardown_once();
1286        // The main core outlives its window, as it predated it: a host
1287        // still reads events, warnings and the tree off a runner that
1288        // has ended (`core_mut`), and the Node driver does so for the
1289        // pump that returned false.
1290        let mut panes = std::mem::take(&mut shell.panes);
1291        // The facts the platform's text input reads are keyed by the
1292        // view's address, which the next window's view may get.
1293        #[cfg(target_os = "macos")]
1294        for pane in &panes {
1295            macos_text_input::detach(&pane.window);
1296            macos_drop::detach(&pane.window);
1297        }
1298        if !panes.is_empty() {
1299            shell.main_core = Some(panes.remove(0).core);
1300        }
1301        if let Some(event_loop) = self.event_loop.take() {
1302            PARKED_LOOP.with(|p| *p.borrow_mut() = Some(event_loop));
1303        }
1304    }
1305
1306    fn pump_until(&mut self, shell: &mut DynShell<'_>, deadline: std::time::Instant) -> bool {
1307        use winit::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus};
1308        let Some(event_loop) = &mut self.event_loop else {
1309            return false;
1310        };
1311        let timeout = deadline.saturating_duration_since(std::time::Instant::now());
1312        self.pumps += 1;
1313        self.alive = match event_loop.pump_app_events(Some(timeout), &mut Handler(shell)) {
1314            PumpStatus::Continue => !shell.exit_requested,
1315            PumpStatus::Exit(_) => false,
1316        };
1317        self.tally(shell);
1318        if !self.alive {
1319            self.retire(shell);
1320        }
1321        self.alive
1322    }
1323
1324    fn waker(&self, shell: &DynShell<'_>) -> Waker {
1325        match &self.event_loop {
1326            Some(event_loop) => Waker(event_loop.create_proxy()),
1327            // Retired: the proxy the shell kept still names the loop.
1328            None => Waker(shell.proxy.clone().expect("a pump runner keeps its proxy")),
1329        }
1330    }
1331}
1332
1333impl DynShell<'_> {
1334    fn core_mut_of(&mut self, id: WindowId) -> Option<&mut Core> {
1335        if id == WindowId::MAIN {
1336            return Some(self.core_mut());
1337        }
1338        self.panes
1339            .iter_mut()
1340            .find(|p| p.id == id)
1341            .map(|p| &mut p.core)
1342    }
1343
1344    fn window_id(&mut self, name: &str) -> Option<WindowId> {
1345        self.core_mut()
1346            .windows()
1347            .into_iter()
1348            .find(|(_, n)| &**n == name)
1349            .map(|(id, _)| id)
1350    }
1351
1352    fn request_redraw(&self) {
1353        self.owed.set(false);
1354        for p in &self.panes {
1355            p.redraw_for(FrameCause::HOST);
1356        }
1357    }
1358}
1359
1360impl<A: App> Drop for PumpRunner<A> {
1361    /// A runner dropped while alive — a host that let go of it without
1362    /// pumping to the end — parks the loop too, so the next `open` on the
1363    /// thread is not refused for its sake.
1364    fn drop(&mut self) {
1365        // `retire` reaches `App::teardown` too, so a runner a panic unwinds
1366        // through hears the window go (backlog RG1); a second panic out of
1367        // that `teardown` is an abort, as any panic in a drop is.
1368        self.retire();
1369        // SAFETY: taken once, here, and the field is not read again.
1370        let shell: Box<Shell<A>> = unsafe { std::mem::ManuallyDrop::take(&mut self.shell) };
1371        drop_shell(shell);
1372    }
1373}
1374
1375/// Drops a shell unsized, so its drop glue is kui's (see `PumpRunner`).
1376fn drop_shell(shell: Box<DynShell<'_>>) {
1377    drop(shell);
1378}
1379
1380impl<A: App> PumpRunner<A> {
1381    fn shell(&self) -> &DynShell<'_> {
1382        &**self.shell
1383    }
1384
1385    fn shell_mut(&mut self) -> &mut DynShell<'_> {
1386        &mut **self.shell
1387    }
1388
1389    fn retire(&mut self) {
1390        self.state.retire(&mut **self.shell);
1391    }
1392
1393    /// Processes all pending OS events without blocking. Returns false once
1394    /// the main window has closed — and from that pump on the windows are
1395    /// gone and the loop is parked for the next runner; further pumps are
1396    /// no-ops.
1397    pub fn pump(&mut self) -> bool {
1398        self.state.pump(&mut **self.shell)
1399    }
1400
1401    /// How many turns this runner has taken, the one that opened the
1402    /// window included: every `pump` and `pump_until` that ran, not the
1403    /// no-ops after it retired. Monotonic, so two readings a second apart
1404    /// give the pump rate over that second. How many of them something
1405    /// outside the app caused is [`woken_pumps`](Self::woken_pumps).
1406    pub fn pumps(&self) -> u64 {
1407        self.state.pumps
1408    }
1409
1410    /// How many of those [`pumps`](Self::pumps) found an OS event or a
1411    /// wake in their batch: any window event but a redraw (a key, the
1412    /// pointer crossing the window, a focus change, a resize, the window
1413    /// moved or occluded), or anything that came through the loop's proxy
1414    /// (a [`Waker::wake`], assistive technology asking, a file dialog's
1415    /// answer). Monotonic, and never more than `pumps`.
1416    ///
1417    /// Unmoved over a second, the desktop left the window alone and
1418    /// whatever it drew was the app's own doing; moved, the desktop or a
1419    /// person reached in, so an idle-window measurement should restart
1420    /// rather than fail.
1421    pub fn woken_pumps(&self) -> u64 {
1422        self.state.woken_pumps
1423    }
1424
1425    /// [`pump`](Self::pump), but parked until an OS event, a
1426    /// [`Waker::wake`] or `deadline`, whichever comes first, so a host that
1427    /// owns the loop blocks on all three instead of polling on a timer.
1428    /// Returns false once the main window has closed.
1429    pub fn pump_until(&mut self, deadline: std::time::Instant) -> bool {
1430        self.state.pump_until(&mut **self.shell, deadline)
1431    }
1432
1433    /// When the runner next needs pumping, as the last [`pump`](Self::pump)
1434    /// left it: a caret blink, a tick, a transition's next frame, the audio
1435    /// poll or a first-frame retry. `None` when nothing it knows about is
1436    /// due. Ask after each pump and sleep until the answer rather than
1437    /// pumping on a fixed interval.
1438    ///
1439    /// It does not say whether an OS event is waiting, since nothing short
1440    /// of pumping can, so a driver still needs a ceiling of its own; this
1441    /// only ever says to come back sooner.
1442    pub fn next_deadline(&self) -> Option<std::time::Instant> {
1443        self.shell.next_deadline
1444    }
1445
1446    /// A [`Waker`] for this loop, to clone into the threads the host's
1447    /// data arrives on.
1448    pub fn waker(&self) -> Waker {
1449        self.state.waker(&**self.shell)
1450    }
1451
1452    /// The app, for a host that reads or changes its state between pumps.
1453    pub fn app_mut(&mut self) -> &mut A {
1454        &mut self.shell.app
1455    }
1456
1457    /// Delivers `events` the way this runner's own loop does: an
1458    /// extension's event to the extension, and the rest, with whatever the
1459    /// extension replied, to `to_app`. Use it for events the loop never
1460    /// saw, such as those a host produced by driving the core directly
1461    /// (`core_mut().press`, an access action, a drained
1462    /// `take_pending_events`); pushing those straight at the app would
1463    /// hand it an extension's clicks and leave the extension deaf to them.
1464    ///
1465    /// `to_app` is lent the core of the window each event came from, as
1466    /// [`App::on_event_with`] is.
1467    pub fn route_events(
1468        &mut self,
1469        events: impl IntoIterator<Item = UiEvent>,
1470        mut to_app: impl FnMut(&mut A, UiEvent, &mut Core),
1471    ) {
1472        self.shell_mut().stamp_clock();
1473        let Shell {
1474            extensions,
1475            app,
1476            panes,
1477            main_core,
1478            ..
1479        } = &mut **self.shell;
1480        extensions.route(events, |ev| {
1481            if let Some(core) = window_core(panes, main_core, ev.window) {
1482                to_app(app, ev, core);
1483            }
1484        });
1485    }
1486
1487    /// The main window's core. Every window of the app shares its session,
1488    /// so resources registered through it draw in all of them, and
1489    /// `Core::windows` on it lists them.
1490    pub fn core_mut(&mut self) -> &mut Core {
1491        self.shell_mut().core_mut()
1492    }
1493
1494    /// The core of the window `id` names, if that window is open; the main
1495    /// window's for `WindowId::MAIN`. Everything that is one window's
1496    /// rather than the session's (its focus, its editors' text, its scroll
1497    /// offsets, its tokens) is answered by this core and no other.
1498    pub fn core_mut_of(&mut self, id: WindowId) -> Option<&mut Core> {
1499        self.shell_mut().core_mut_of(id)
1500    }
1501
1502    /// The id of the open window named `name` (`"main"` for the launcher's),
1503    /// or `None` while no window of that name is open — before its first
1504    /// frame's diff, or after the user closed it.
1505    pub fn window_id(&mut self, name: &str) -> Option<WindowId> {
1506        self.shell_mut().window_id(name)
1507    }
1508
1509    /// The main window's inner size (logical px) and its scale factor.
1510    /// Unlike `core_mut().viewport()` this is known before the first frame,
1511    /// so a host can size its model at setup. `core_mut().host_area(size)`
1512    /// is what the next frame lays out against: the window less the
1513    /// devtools' dock while the panel is docked, the window otherwise.
1514    pub fn window_size(&self) -> (Size, f32) {
1515        self.shell().window_size()
1516    }
1517
1518    /// Schedules a redraw of every window; call it after changing what
1519    /// `view` will produce. Under [`Launcher::deferred_events`] it also
1520    /// ends a frame's wait: the host is saying its view is current, so the
1521    /// waiting frame is painted with the answer in it.
1522    pub fn request_redraw(&self) {
1523        self.shell().request_redraw();
1524    }
1525
1526    /// Asks the app to close; the next `pump` observes it and returns false.
1527    pub fn request_exit(&mut self) {
1528        self.shell.exit_requested = true;
1529    }
1530
1531    /// Hands the core's queued audio commands to the device now rather than
1532    /// at the next pump, for a host that calls `Core::play` between pumps
1533    /// and wants the sound to start at once.
1534    pub fn flush_audio(&mut self) {
1535        self.shell_mut().apply_audio();
1536    }
1537}
1538
1539/// `app(title).extensions(extensions).run(application)` in one call.
1540pub fn run<A: App>(
1541    title: &str,
1542    application: A,
1543    extensions: Vec<Box<dyn Extension>>,
1544) -> Result<(), Box<dyn std::error::Error>> {
1545    app(title).extensions(extensions).run(application)
1546}
1547
1548/// Clamps a requested inner size into `min`/`max` (logical px), the way the
1549/// OS clamps a resize once the window exists — so a host reading
1550/// `PumpRunner::window_size` before the first frame sees the real size.
1551/// `min` wins where the two bounds cross, matching the platforms.
1552fn clamp_size(size: (f64, f64), min: Option<(f64, f64)>, max: Option<(f64, f64)>) -> (f64, f64) {
1553    let (mut w, mut h) = size;
1554    if let Some((mw, mh)) = max {
1555        w = w.min(mw);
1556        h = h.min(mh);
1557    }
1558    if let Some((mw, mh)) = min {
1559        w = w.max(mw);
1560        h = h.max(mh);
1561    }
1562    (w, h)
1563}
1564
1565/// Width of the invisible resize band synthesized on undecorated windows.
1566const RESIZE_BAND: f32 = 6.0;
1567/// A second titlebar press within this window toggles maximize.
1568#[cfg(not(target_os = "macos"))]
1569const DOUBLE_CLICK_MS: u128 = 350;
1570/// Presses within this window (and `MULTI_CLICK_SLOP` px) count up the
1571/// multi-click sent with `InputEvent::MouseDown` (double = word select).
1572const MULTI_CLICK_MS: u128 = 400;
1573const MULTI_CLICK_SLOP: f32 = 4.0;
1574/// Caret blink half-period while an edit widget is focused.
1575const BLINK_INTERVAL: std::time::Duration = std::time::Duration::from_millis(500);
1576/// How often the loop wakes to notice a playing sound finishing.
1577const AUDIO_POLL: std::time::Duration = std::time::Duration::from_millis(50);
1578
1579/// How long the output device is held open after the last input or sound.
1580/// An open device is a real-time thread the OS runs ~94 times a second
1581/// whether or not anything is playing, which is the entire idle CPU cost of
1582/// an app that owns a sound — the counter example sat at 0.3% doing nothing.
1583/// Held rather than closed at once because the point of opening early is
1584/// that a click finds it open (the ~90 ms open used to stall the counter's
1585/// first click), and any input at all re-warms it: the pointer moving
1586/// towards a button is minutes of warning before the button is pressed.
1587const AUDIO_IDLE_CLOSE: std::time::Duration = std::time::Duration::from_secs(5);
1588/// How long after one try at opening a new device (`Shell::reopen_device`)
1589/// the next may be made. A device that will not open — a driver still
1590/// being installed, an adapter gone — is tried once a second, and the
1591/// loop sleeps until then rather than asking for frames that could only
1592/// ask again.
1593const REOPEN_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
1594/// Frames in a row a surface may refuse for being configured wrong, each
1595/// answered by configuring it again, before it is given up with its device.
1596const SURFACE_TRIES: u8 = 3;
1597
1598/// When a new device asked for at `now` may be opened: at once if none
1599/// has been tried, else `REOPEN_INTERVAL` after the last try and never
1600/// sooner. A time not after `now` means now; a later one is the wake the
1601/// loop sleeps until (`about_to_wait`), since nothing else is bound to ask
1602/// — an idle app's windows draw nothing on their own, and an animating
1603/// one's frames, with no surface to present to, are not paced by vsync
1604/// and would spin the loop until the second was up.
1605fn reopen_due(last_try: Option<std::time::Instant>, now: std::time::Instant) -> std::time::Instant {
1606    last_try.map_or(now, |t| (t + REOPEN_INTERVAL).max(now))
1607}
1608
1609/// What a frame does with a surface refused for being configured wrong.
1610#[derive(Debug, PartialEq, Eq)]
1611enum Refused {
1612    /// Configure it to the window's size and draw again at once.
1613    Retry,
1614    /// Give it up with its device (`Shell::reopen_device`); `say` on the
1615    /// first frame past the tries, not on each one refused after it while
1616    /// the reopen waits out its second.
1617    GiveUp { say: bool },
1618}
1619
1620/// Counts one more refusal in `tries` — saturating, since while a reopen
1621/// is owed every frame input asks for is refused again and counted, and
1622/// a `u8` that wrapped would start the retries over (or, in a debug
1623/// build, panic) — and says what the frame does with it: the first
1624/// `SURFACE_TRIES` are retried, the rest give the surface up.
1625fn surface_refused(tries: &mut u8) -> Refused {
1626    *tries = tries.saturating_add(1);
1627    if *tries <= SURFACE_TRIES {
1628        Refused::Retry
1629    } else {
1630        Refused::GiveUp {
1631            say: *tries == SURFACE_TRIES + 1,
1632        }
1633    }
1634}
1635
1636/// Whether text is drawn with LCD subpixel masks: what was asked for
1637/// (`KUI_TEXT_AA` over the launcher's `text_aa`, `wanted_text_aa`), and
1638/// for `Auto` or `Subpixel` only where the device blends per channel —
1639/// a mask the blend cannot split draws coloured fringes. Decided with each
1640/// device, the first and every one opened after a loss, since a reopen can
1641/// land on another adapter.
1642fn subpixel_on(wanted: TextAa, dual_source: bool) -> bool {
1643    match wanted {
1644        TextAa::Grayscale => false,
1645        TextAa::Subpixel | TextAa::Auto => dual_source,
1646    }
1647}
1648
1649/// The antialiasing `Auto` means for an app that asked for a backdrop
1650/// (backlog F126): grayscale, since an LCD mask is a blend against the
1651/// pixels under it and the window cannot see what the OS composites
1652/// behind a translucent one — subpixel text over a sidebar's material
1653/// draws colour fringes the size of a stem. One decision for every
1654/// window of the app, made before any knows what it got. A `Subpixel`
1655/// the app or `KUI_TEXT_AA` named is kept.
1656fn aa_under(wanted: TextAa, backdrop: Backdrop) -> TextAa {
1657    match wanted {
1658        TextAa::Auto if backdrop.is_translucent() => TextAa::Grayscale,
1659        other => other,
1660    }
1661}
1662
1663/// The frame latency asked for: `KUI_FRAME_LATENCY` if it is a positive
1664/// number, for comparing without a rebuild, else the launcher's.
1665fn wanted_frame_latency(launcher: u32) -> u32 {
1666    std::env::var("KUI_FRAME_LATENCY")
1667        .ok()
1668        .and_then(|v| v.trim().parse::<u32>().ok())
1669        .filter(|n| *n >= 1)
1670        .unwrap_or(launcher)
1671}
1672
1673/// The text antialiasing asked for: `KUI_TEXT_AA` if set, for quick A/B
1674/// comparisons, else what the launcher was told.
1675fn wanted_text_aa(launcher: TextAa) -> TextAa {
1676    match std::env::var("KUI_TEXT_AA").ok().as_deref() {
1677        Some("gray") | Some("grayscale") => TextAa::Grayscale,
1678        Some("subpixel") | Some("lcd") => TextAa::Subpixel,
1679        _ => launcher,
1680    }
1681}
1682
1683/// The core's derived pointer shape in winit's vocabulary. One-to-one:
1684/// `CursorShape` is spelled after the platform names on purpose.
1685/// Chromeless, in the spelling each platform actually honours.
1686///
1687/// Everywhere but macOS that is `with_decorations(false)`. On macOS it is
1688/// **not**: `with_decorations(false)` gives a window with the borderless
1689/// style mask, and AppKit never sends `mouseUp:` to one — every press in it
1690/// lands and never releases, so nothing in the window can be clicked, no
1691/// drag ever ends, and a pressed style never clears. A hidden titlebar over
1692/// a fullsize content view looks the same, is a real window, and is what
1693/// `Chrome::Custom` already asks for; chromeless is that plus the traffic
1694/// lights hidden. It keeps the rounded corners, the drop shadow and the
1695/// native edge-resizing that a borderless window has none of.
1696///
1697/// Found by pressing a menu item and watching nothing happen;
1698/// the popup surface and `Chrome::Borderless` share this because they were
1699/// separately wrong in the same way.
1700fn undecorated(attrs: winit::window::WindowAttributes) -> winit::window::WindowAttributes {
1701    #[cfg(target_os = "macos")]
1702    {
1703        use winit::platform::macos::WindowAttributesExtMacOS;
1704        attrs
1705            .with_titlebar_transparent(true)
1706            .with_fullsize_content_view(true)
1707            .with_title_hidden(true)
1708            .with_titlebar_buttons_hidden(true)
1709    }
1710    #[cfg(not(target_os = "macos"))]
1711    {
1712        attrs.with_decorations(false)
1713    }
1714}
1715
1716/// A popup this press is about : one that opened while the primary button was down, or the
1717/// one the button went down inside. For the rest of that press the owner's
1718/// moves are retargeted into it and its release is classified against it.
1719struct Armed {
1720    /// The popup. Its owner and its anchor are the pane's, so nothing here
1721    /// can go stale against the window it names.
1722    id: WindowId,
1723    /// Whether the last retargeted move landed inside this popup, so that
1724    /// dragging off the list costs one `CursorLeft` and staying off it
1725    /// costs nothing.
1726    inside: bool,
1727}
1728
1729/// The runner's state for one app, over every window it opens.
1730///
1731/// Generic only in its last field: everything is written against
1732/// [`DynShell`] and compiled once, here; a `Shell<A>` is only built,
1733/// boxed, and handed over unsized. Written `impl<A: App> Shell<A>`,
1734/// the whole runner was instantiated and optimised again inside every app
1735/// crate, on every edit: the counter's release rebuild went from 1.20 s
1736/// to 1.57 s as the runner grew.
1737struct Shell<A: App + ?Sized> {
1738    title: String,
1739    /// What every window is created with (`Launcher::icon`).
1740    icon: icon::AppIcon,
1741    chrome: Chrome,
1742    /// Initial inner size (logical px), already clamped into the bounds.
1743    size: (f64, f64),
1744    min_size: Option<(f64, f64)>,
1745    max_size: Option<(f64, f64)>,
1746    text_aa: TextAa,
1747    /// What every renderer is configured with: `Launcher::frame_latency`
1748    /// under `KUI_FRAME_LATENCY`.
1749    frame_latency: u32,
1750    /// What the app asked to show through its windows
1751    /// (`Launcher::backdrop`); what each window got is its pane's.
1752    backdrop: Backdrop,
1753    /// The titlebar's height under custom chrome (`Launcher::titlebar`),
1754    /// for every window opened with it.
1755    titlebar: Titlebar,
1756    /// What every core is created with; see `Launcher::diagnostics`.
1757    diagnostics: bool,
1758    /// Whether the GPU blends per channel, decided by the first renderer,
1759    /// again by each device opened after a loss (`reopen_device`), and
1760    /// applied to every core.
1761    subpixel: bool,
1762    /// Each under the namespace the host gave it; their `Fill` is what fills
1763    /// the slots a view declares.
1764    extensions: Extensions,
1765    /// What every window shares: fonts, images, sounds, the audio queue and
1766    /// the declared window set.
1767    session: Session,
1768    /// The main window's core until `resumed` moves it into its pane, so
1769    /// `PumpRunner::core_mut` has an answer before the first pump.
1770    main_core: Option<Core>,
1771    /// The open windows, main first.
1772    panes: Vec<Pane>,
1773    /// The device every window renders with, from the first renderer.
1774    gpu: Option<kui_wgpu::Gpu>,
1775    /// When the device was last opened again after being lost
1776    /// (`reopen_device`), so a device that will not open is tried once a
1777    /// second rather than once a frame.
1778    reopened: Option<std::time::Instant>,
1779    /// A new device was asked for inside the second after the last try,
1780    /// or the last try left a window without a renderer: `about_to_wait`
1781    /// opens it when `reopen_due` says, and sleeps until then — the ask
1782    /// is kept here rather than in a frame asked for again, which in an
1783    /// idle app nothing would ask for and in an animating one would be
1784    /// asked for every turn with no vsync to pace it.
1785    reopen_owed: bool,
1786    /// Whether `KUI_LOSE_DEVICE` has had its one loss.
1787    pretended_loss: bool,
1788    /// Caps Lock and Num Lock as the lock keys' presses have turned them,
1789    /// in any window — what a press reports where the OS is not asked
1790    /// (`keys::lock_state`). One keyboard, one record: it
1791    /// was each pane's, so a popup began at off and a toggle in one window
1792    /// never reached another.
1793    locks: kui_core::KeyLocks,
1794    /// The layout's script as the letter keys have shown it — what a
1795    /// press goes by where the OS is not asked (`keys::layout_script`).
1796    /// One keyboard, one record, as `locks`.
1797    script: kui_core::LayoutScript,
1798    /// Origin of the frame clock handed to the cores for transitions.
1799    epoch: std::time::Instant,
1800    /// What the OS was asked for at startup — the accent colour, the
1801    /// reduce-motion setting and the language (`mod system_env`) — pushed
1802    /// into every pane's env each frame and re-asked when the user has
1803    /// evidently been somewhere else. The appearance is not here: it is
1804    /// per-window and comes off the window itself.
1805    system: system_env::Queried,
1806    /// What the app pinned over it (`Launcher::system`); merged in
1807    /// `sync_env`, so it is never lost to the per-frame write.
1808    pinned_system: SystemEnv,
1809    clipboard: Option<arboard::Clipboard>,
1810    /// The platform's context menu, where the platform has one.
1811    /// `None` on every other platform and on a macOS build that
1812    /// could not reach the main thread, and then the core draws its own.
1813    #[cfg(target_os = "macos")]
1814    native_menu: Option<macos_menu::MacMenu>,
1815    /// Whether a menu the core opened has been handed to the platform and
1816    /// is waiting for an answer. One per app: only one menu can be up.
1817    /// Beside `native_menu` because it means nothing without one.
1818    #[cfg(target_os = "macos")]
1819    menu_shown: bool,
1820    /// The platform's application menu bar, where the platform has one
1821    ///. One per process,
1822    /// because that is what macOS has: it carries the declaration of the
1823    /// window that has the keyboard, or of whichever window made one.
1824    #[cfg(target_os = "macos")]
1825    native_menu_bar: Option<macos_menu::MacMenuBar>,
1826    /// What it currently carries: a declaration — the window it came from
1827    /// and that core's `menu_bar_revision` — or the standard bar. The diff
1828    /// that keeps a re-declared bar from being rebuilt sixty times a second.
1829    #[cfg(target_os = "macos")]
1830    applied_menu_bar: Option<AppliedBar>,
1831    /// The audio device the core's audio commands drive; see `audio`.
1832    audio: audio::Audio,
1833    /// When the app was last doing something that could lead to a sound:
1834    /// any input, or any audio command. The device is warmed while this is
1835    /// recent and let go once it is not — see `AUDIO_IDLE_CLOSE`. Starts at
1836    /// launch, so a session holding sounds still opens the device before
1837    /// its first click the way it always did.
1838    audio_touch: std::time::Instant,
1839    /// `KUI_SMOKE_FRAMES=n`: quit after the main window has presented `n`
1840    /// frames, so an example is a self-terminating check — a real window
1841    /// on a real GPU, driven by the real loop, that exits 0 when it drew
1842    /// and non-zero when it did not. It is what the smoke round
1843    /// (examples/devtools/src/bin/smoke.rs) runs; the wgpu validation
1844    /// error that made `fragments` panic on
1845    /// first paint (an alignment the adapter and the device disagreed
1846    /// about) is exactly the class of bug no headless test can see.
1847    /// `None` — unset, or unparseable — is the ordinary endless run.
1848    smoke_frames: Option<u32>,
1849    /// Main-window frames presented so far, counted only while
1850    /// `smoke_frames` is set.
1851    frames_drawn: u32,
1852    /// Set by `WindowCommand::Close` on the main window; honored at the end
1853    /// of the event.
1854    exit_requested: bool,
1855    /// Why the main window or its renderer could not be made, when they
1856    /// could not: `run` and `open` return it as their error. It was an
1857    /// `expect`, and a panic there aborted a Node process outright — the
1858    /// unwind cannot cross the addon's boundary — when a compositor that
1859    /// had just gone away refused the window.
1860    startup_error: Option<String>,
1861    /// Whether `App::teardown` has run: once, whichever of the loop's
1862    /// exit and the runner's retirement comes first.
1863    torn_down: bool,
1864    /// The one count of secure keyboard entry this runner may hold, moved
1865    /// at the end of every batch to whether a window whose frame asked
1866    /// (`Ui::secure_input`) has the keyboard, given back at teardown and
1867    /// on drop (`mod secure_input`).
1868    secure_input: secure_input::SecureInput,
1869    /// Driven by a `PumpRunner` rather than `run_app`: the main window's
1870    /// close ends the runner (`exit_requested`) instead of exiting winit's
1871    /// loop, which the next runner on this thread reuses.
1872    pumped: bool,
1873    /// Whether `resumed` has opened the main window — once per shell, so a
1874    /// reused loop that delivers no `resumed` opens it from `about_to_wait`
1875    /// and a main window the user closed is not reopened from there.
1876    opened: bool,
1877    /// Which pane the primary button is down in, if any. A
1878    /// popup is armed against it: a non-activating popup that opens while this is
1879    /// set joins that press, which is the observable form of "the drag
1880    /// whose press opened the popup" and needs no geometry.
1881    primary_down: Option<WindowId>,
1882    /// The popups armed into the press `primary_down` names, in opening
1883    /// order — where two overlap the last is on top. Emptied by the release
1884    /// that classifies it, and by `close_pane` for a window that goes
1885    /// first.
1886    armed: Vec<Armed>,
1887    /// A primary press that dismissed a non-activating popup and was
1888    /// consumed rather than dispatched, by the pane
1889    /// it landed in. Its release is swallowed with it: the core never saw
1890    /// the `down`, so nothing should see the `up`.
1891    swallowed_press: Option<WindowId>,
1892    /// Hands AccessKit a way back into the loop; set before the window
1893    /// exists.
1894    proxy: Option<EventLoopProxy<access_bridge::UserEvent>>,
1895    /// What the last `about_to_wait` decided the control flow should be, kept
1896    /// so a host that owns the loop can read it (`PumpRunner::next_deadline`).
1897    /// `None` is `ControlFlow::Wait`: nothing the shell knows about is due.
1898    next_deadline: Option<std::time::Instant>,
1899    /// The earliest `Waker::wake_at` not yet reached (backlog F135).
1900    wake_at: Option<std::time::Instant>,
1901    /// Whether this batch carried an OS event the shell acted on. A driver
1902    /// cannot see most of them — a pointer crossing a window that declares no
1903    /// hover produces no *app* event at all, and neither does a key nothing
1904    /// is listening for — so a driver pacing itself on what the app saw
1905    /// concludes that a window being moved across is idle. It is the reason
1906    /// `next_deadline` reports "now" after an event: the shell asked for a
1907    /// redraw and wants pumping to present it, and that is also the honest
1908    /// signal for "somebody is using this window".
1909    saw_event: bool,
1910    /// `saw_event` as `about_to_wait` took it, left for the pump runner to
1911    /// take in turn and count (`PumpRunner::woken_pumps`). A
1912    /// loop that owns itself never reads it.
1913    woke: bool,
1914    /// The host answers events after the loop hands them over
1915    /// (`Launcher::deferred_events`).
1916    deferred_events: bool,
1917    /// An input reached the app and the host has not answered yet, so the
1918    /// next frame would be painted from a view that predates that input.
1919    /// Set only under `deferred_events`; cleared when the host says its
1920    /// view is current ([`PumpRunner::request_redraw`], which every
1921    /// `setView` and every drained `pollEvents` reaches).
1922    ///
1923    /// A `Cell` because the clearing is the host's `&self` call, and the
1924    /// alternative — taking `&mut self` there — is a signature break for
1925    /// what is bookkeeping.
1926    owed: std::cell::Cell<bool>,
1927    /// Last, so a `Box<Shell<A>>` unsizes to a `Box<DynShell>`.
1928    app: A,
1929}
1930
1931/// A shell over any app: what the runner is written against. Generic only
1932/// in a lifetime, which is erased, so its code is kui's and not the app
1933/// crate's, and an app that borrows is still an app.
1934type DynShell<'a> = Shell<dyn App + 'a>;
1935
1936/// The core of window `id`, from a shell's two homes for one: the panes,
1937/// and the main window's core before its pane exists — the main window's
1938/// for a window that has closed since its event was made. Over the fields
1939/// rather than `&mut Shell`, so the app can be lent beside it.
1940fn window_core<'a>(
1941    panes: &'a mut [Pane],
1942    main_core: &'a mut Option<Core>,
1943    id: WindowId,
1944) -> Option<&'a mut Core> {
1945    let at = panes
1946        .iter()
1947        .position(|p| p.id == id)
1948        .or_else(|| panes.iter().position(|p| p.id == WindowId::MAIN));
1949    match at {
1950        Some(i) => Some(&mut panes[i].core),
1951        None => main_core.as_mut(),
1952    }
1953}
1954
1955/// What winit's loop drives: it wants a sized handler, and a `DynShell`
1956/// is not one.
1957struct Handler<'s, 'a>(&'s mut DynShell<'a>);
1958
1959impl ApplicationHandler<access_bridge::UserEvent> for Handler<'_, '_> {
1960    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1961        self.0.resumed(event_loop);
1962    }
1963
1964    fn window_event(
1965        &mut self,
1966        event_loop: &ActiveEventLoop,
1967        window: WinitWindowId,
1968        event: WindowEvent,
1969    ) {
1970        self.0.window_event(event_loop, window, event);
1971    }
1972
1973    fn exiting(&mut self, event_loop: &ActiveEventLoop) {
1974        self.0.exiting(event_loop);
1975    }
1976
1977    fn user_event(&mut self, event_loop: &ActiveEventLoop, event: access_bridge::UserEvent) {
1978        self.0.user_event(event_loop, event);
1979    }
1980
1981    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
1982        self.0.about_to_wait(event_loop);
1983    }
1984}
1985
1986impl DynShell<'_> {
1987    /// Brings every core's frame clock up to now (backlog F139). The
1988    /// frame stamps it too, but the loop parks between frames, so input
1989    /// handled after an idle stretch and the handlers it reaches would
1990    /// otherwise read the last frame's time: a toast stamped in a click
1991    /// handler would expire early, a type-ahead would extend a search
1992    /// typed seconds ago. Called before input is handled and before
1993    /// events reach the app. The clock only moves forward, and a frame's
1994    /// own readings go by the time its frame began (`AnimStore`'s
1995    /// `drove_at`), so stamping between frames changes no drawing.
1996    fn stamp_clock(&mut self) {
1997        let now = self.epoch.elapsed().as_secs_f64();
1998        for p in &mut self.panes {
1999            p.core.set_time(now);
2000        }
2001        if let Some(core) = &mut self.main_core {
2002            core.set_time(now);
2003        }
2004    }
2005
2006    /// The main window's core: its pane's once it exists, the one the
2007    /// launcher built before that.
2008    fn core_mut(&mut self) -> &mut Core {
2009        match self.panes.first_mut() {
2010            Some(p) => &mut p.core,
2011            None => self
2012                .main_core
2013                .as_mut()
2014                .expect("the main core exists until its pane takes it"),
2015        }
2016    }
2017
2018    /// What the device is opened for: surfaces that can take alpha when
2019    /// the app asked for a backdrop, which on Windows is another D3D12
2020    /// swapchain (backlog F126) — so only then.
2021    fn gpu_options(&self) -> kui_wgpu::GpuOptions {
2022        kui_wgpu::GpuOptions::transparent(self.backdrop.is_translucent())
2023    }
2024
2025    /// Main inner size in logical px plus the scale factor; the launcher's
2026    /// requested size until the window exists.
2027    fn window_size(&self) -> (Size, f32) {
2028        match self.panes.first() {
2029            Some(p) => p.size(),
2030            None => (Size::new(self.size.0 as f32, self.size.1 as f32), 1.0),
2031        }
2032    }
2033
2034    /// `App::teardown`, once: from `exiting` — the loop's
2035    /// last word, which the OS's Quit reaches too, on macOS through
2036    /// `applicationWillTerminate` where the process ends without `run`
2037    /// ever returning — and from a pumped runner's retirement, whichever
2038    /// comes first.
2039    fn teardown_once(&mut self) {
2040        // The keyboard is given back before the app's teardown runs, and
2041        // on every path here: a process that ends from `exiting` never
2042        // drops the runner (backlog F85).
2043        self.secure_input.set(false);
2044        if !self.torn_down {
2045            self.torn_down = true;
2046            self.app.teardown();
2047        }
2048    }
2049
2050    /// The main window is done: `run_app`'s loop exits; a pumped loop is
2051    /// left running for the next runner, and the runner ends itself on
2052    /// the flag (see `PARKED_LOOP`).
2053    fn exit_main(&mut self, event_loop: &ActiveEventLoop) {
2054        self.exit_requested = true;
2055        if !self.pumped {
2056            event_loop.exit();
2057        }
2058    }
2059
2060    /// Whether the platform's own drag callbacks are answering for every
2061    /// window, so winit's positionless file events are the duplicates.
2062    fn file_drag_is_overridden(&self) -> bool {
2063        #[cfg(target_os = "macos")]
2064        {
2065            macos_drop::installed()
2066        }
2067        #[cfg(not(target_os = "macos"))]
2068        {
2069            false
2070        }
2071    }
2072
2073    /// The file drags the platform reported since the last turn:
2074    /// on macOS the delegate override's messages, each with the
2075    /// position AppKit gave it, dispatched to the pane whose delegate
2076    /// spoke and followed by the stamp that delegate answers the OS from;
2077    /// elsewhere the batch winit's per-file events built, at the pane's
2078    /// last cursor.
2079    fn pump_file_drag(&mut self, event_loop: &ActiveEventLoop) {
2080        #[cfg(target_os = "macos")]
2081        for (delegate, msg) in macos_drop::take_messages() {
2082            let Some(i) = self
2083                .panes
2084                .iter()
2085                .position(|p| macos_drop::delegate_ptr(&p.window) == Some(delegate))
2086            else {
2087                continue;
2088            };
2089            let ev = match msg {
2090                macos_drop::DragMsg::Over(paths, at) => InputEvent::DragFiles { paths, at },
2091                macos_drop::DragMsg::Drop(paths, at) => InputEvent::DropFiles { paths, at },
2092                macos_drop::DragMsg::Cancel => InputEvent::DragCancel,
2093            };
2094            if let Some(i) = self.dispatch(event_loop, i, ev) {
2095                let pane = &self.panes[i];
2096                macos_drop::stamp(&pane.window, pane.core.drop_target().is_some());
2097            }
2098        }
2099        // Collected first, then dispatched by window id: a drop's handler
2100        // may close its window, and a pane's index is not its identity
2101        // (backlog AR39).
2102        let batches: Vec<(WindowId, InputEvent)> = self
2103            .panes
2104            .iter_mut()
2105            .filter_map(|pane| {
2106                let dropped = pane.file_drag_pending.take()?;
2107                let paths = std::mem::take(&mut pane.file_drag);
2108                let at = pane.cursor;
2109                let ev = if dropped {
2110                    InputEvent::DropFiles { paths, at }
2111                } else {
2112                    InputEvent::DragFiles { paths, at }
2113                };
2114                Some((pane.id, ev))
2115            })
2116            .collect();
2117        for (id, ev) in batches {
2118            if let Some(i) = self.pane_of(id) {
2119                self.dispatch(event_loop, i, ev);
2120            }
2121        }
2122    }
2123
2124    /// The documents the OS asked the app to open since the last turn
2125    /// (backlog F124), each list as one `InputEvent::Open` to the main
2126    /// window's core. Before that window has opened — the launch-time
2127    /// open, which AppKit sends inside `finishLaunching` — they stay
2128    /// queued for the first turn after it has. Nothing sends them off
2129    /// macOS: there the documents are in the process's arguments.
2130    fn pump_open_documents(&mut self, event_loop: &ActiveEventLoop) {
2131        #[cfg(target_os = "macos")]
2132        {
2133            if !macos_open::pending() || self.pane_of(WindowId::MAIN).is_none() {
2134                return;
2135            }
2136            for paths in macos_open::take() {
2137                // Looked up per list: a handler may close a window.
2138                let Some(i) = self.pane_of(WindowId::MAIN) else {
2139                    // Gone between two lists: the rest wait, as the
2140                    // launch-time ones do, for a main window to hear them.
2141                    macos_open::requeue(paths);
2142                    continue;
2143                };
2144                self.dispatch(event_loop, i, InputEvent::Open(paths));
2145            }
2146        }
2147        #[cfg(not(target_os = "macos"))]
2148        let _ = event_loop;
2149    }
2150
2151    /// Hands one input to pane `i`'s core and does what followed from it:
2152    /// the events routed, the menu actions, the window commands, the
2153    /// audio. Returns where that pane is afterwards — the commands may
2154    /// have closed a window, and a close in front of it moves it down,
2155    /// so its index is not its identity; `None` when the
2156    /// input closed the pane itself. A caller that goes on addressing the
2157    /// pane goes on with the returned index.
2158    fn dispatch(
2159        &mut self,
2160        event_loop: &ActiveEventLoop,
2161        i: usize,
2162        ev: InputEvent,
2163    ) -> Option<usize> {
2164        let t0 = std::time::Instant::now();
2165        // Someone is using the app, so a sound may be moments away: keep
2166        // the device warm (`AUDIO_IDLE_CLOSE`).
2167        self.audio_touch = t0;
2168        let completes = input_completes(&ev);
2169        let here = self.panes[i].id;
2170        self.stamp_clock();
2171        let events = self.panes[i].core.handle_input(ev);
2172        let reached_app = self.route_events(events);
2173        self.owe_for(reached_app && completes);
2174        self.apply_menu_actions(event_loop, i);
2175        self.apply_window_commands(event_loop);
2176        self.apply_audio();
2177        self.pump_native_menu(event_loop);
2178        let i = self.pane_of(here)?;
2179        let pane = &mut self.panes[i];
2180        pane.apply_cursor();
2181        // Hover styling depends on input too, so any input redraws. A
2182        // damage pass can tighten this later.
2183        pane.core.stats.pending_input_ms += t0.elapsed().as_secs_f32() * 1e3;
2184        pane.window.request_redraw();
2185        Some(i)
2186    }
2187
2188    /// Hands the session's queued audio commands to the device. A session
2189    /// that holds a sound is going to play one: the device starts opening
2190    /// here, on its own thread, so the first play finds it open instead of
2191    /// stalling the frame for the ~90 ms the open takes.
2192    fn apply_audio(&mut self) {
2193        let core = self.core_mut();
2194        let cmds = core.take_audio_commands();
2195        let resources = core.resources.clone();
2196        if !cmds.is_empty() {
2197            self.audio_touch = std::time::Instant::now();
2198        }
2199        // Warmed while the app is being used and let go when it is not.
2200        // Not every frame: a frame is drawn for the caret, for a
2201        // transition, for a window moving over the top — none of which is
2202        // anybody about to play anything, and re-warming on one of those
2203        // would reopen the device the moment `about_to_wait` closed it.
2204        if resources.has_sounds() && self.audio_touch.elapsed() < AUDIO_IDLE_CLOSE {
2205            self.audio.warm();
2206        }
2207        // Two things only the device knows come back here. A `Stop` it found
2208        // still playing is a one-shot cut off, which the core turns into
2209        // `truncated-playback` on the node that went away; a refused play
2210        // never starts and so never ends, so the core turns that into a
2211        // `refused` event and a warning, or a tagged node waits on an
2212        // `ended` that cannot come.
2213        let answered = self.audio.apply(cmds, &resources);
2214        if answered.truncated.is_empty() && answered.refused.is_empty() {
2215            return;
2216        }
2217        let core = self.core_mut();
2218        for (playback, at) in answered.truncated {
2219            core.audio_truncated(playback, at);
2220        }
2221        for playback in answered.refused {
2222            core.audio_refused(playback);
2223        }
2224        self.route_playback_events();
2225    }
2226
2227    /// Folds playbacks that finished on their own back into the core, and
2228    /// routes the `sound` events tagged ones become.
2229    fn poll_audio(&mut self) {
2230        let ended = self.audio.poll_ended();
2231        if ended.is_empty() {
2232            return;
2233        }
2234        let core = self.core_mut();
2235        for playback in ended {
2236            core.audio_ended(playback);
2237        }
2238        self.route_playback_events();
2239    }
2240
2241    /// Routes whatever the audio fold-back queued (`ended`, `refused`) and
2242    /// redraws for what handling it changed.
2243    fn route_playback_events(&mut self) {
2244        let pending = self.core_mut().take_pending_events();
2245        if pending.is_empty() {
2246            return;
2247        }
2248        self.route_events(pending);
2249        for p in &self.panes {
2250            p.redraw_for(FrameCause::AUDIO);
2251        }
2252    }
2253
2254    /// An input's events reached the app: under `deferred_events` the next
2255    /// frame owes the host's answer (see `frame_waits_for_host`).
2256    fn owe_for(&self, reached_app: bool) {
2257        if reached_app && self.deferred_events {
2258            self.owed.set(true);
2259        }
2260    }
2261
2262    /// Returns whether anything reached the app (as against an extension
2263    /// answering for itself).
2264    fn route_events(&mut self, events: Vec<UiEvent>) -> bool {
2265        self.stamp_clock();
2266        let mut reached_app = false;
2267        // An extension's replies go to whoever declared its slot (ADR 0014
2268        // decision 6): not routed by origin — a reply is addressed by being
2269        // one — and carrying the extension's origin, the window and the key
2270        // of the event it answered, so the receiver knows who spoke and
2271        // from where. For every extension this host placed itself, the
2272        // receiver is this host; for one a guest placed, it is the guest,
2273        // and `route` is the walk up.
2274        let Shell {
2275            extensions,
2276            app,
2277            panes,
2278            main_core,
2279            ..
2280        } = self;
2281        extensions.route(events, |ev| {
2282            reached_app = true;
2283            // Lent the core of the window the event came from (ADR 0036).
2284            match window_core(panes, main_core, ev.window) {
2285                Some(core) => app.on_event_with(ev, core),
2286                None => app.on_event(ev),
2287            }
2288        });
2289        // One app, one model, N windows: a handler that ran in answer to
2290        // input in *this* window can change what *another* window declares
2291        // — choosing an item in a popup is the app closing the popup, and
2292        // the declaration that closes it lives in the window that opened
2293        // it. So anything that reached the app redraws every window; the
2294        // caller has already redrawn the one the input landed in. Guarded
2295        // on there being more than one, so the single-window path — every
2296        // hover, every keystroke — is exactly what it was.
2297        if reached_app && self.panes.len() > 1 {
2298            for p in &self.panes {
2299                p.redraw_for(FrameCause::ELSEWHERE);
2300            }
2301        }
2302        reached_app
2303    }
2304
2305    /// Direct edits (cut) mutate the document outside `handle_input`, so
2306    /// the `changed` the core queued for it is routed here, and the window
2307    /// redrawn. The core posts the event (AR15: this used to build one by
2308    /// hand, and the menu's Cut posted none), so it is stamped and routed
2309    /// like the ones a keystroke makes.
2310    fn after_direct_edit(&mut self, i: usize) {
2311        let events = self.panes[i].core.take_pending_events();
2312        // A cut is input too: the frame that shows the text gone should
2313        // show what the app made of `changed`.
2314        let reached_app = self.route_events(events);
2315        self.owe_for(reached_app);
2316        // The one caller is the runner's own ⌘X, which the core never
2317        // hears as a key.
2318        self.panes[i].redraw_for(FrameCause::KEY);
2319    }
2320
2321    /// Builds and draws one window's frame.
2322    fn redraw(&mut self, i: usize) {
2323        let Shell {
2324            app,
2325            extensions,
2326            panes,
2327            min_size,
2328            max_size,
2329            epoch,
2330            system,
2331            pinned_system,
2332            audio,
2333            pretended_loss,
2334            ..
2335        } = self;
2336        let pane = &mut panes[i];
2337        // The wallpaper kui draws where the OS has no material, found and
2338        // read on a thread: an answer that there is none to draw, or none
2339        // that decodes, is said once, and the window is opaque from then
2340        // on — before the view runs, so this frame's already reads it.
2341        if let Some(g) = pane.ground.as_mut()
2342            && let Err(why) = g.settle()
2343        {
2344            eprintln!("kui: no wallpaper to draw behind the window: {why}");
2345            pane.ground = None;
2346            pane.backdrop = Backdrop::Opaque;
2347        }
2348        // What the driver knows and the view only reads, refreshed for
2349        // this frame (it was already filled in when the pane opened).
2350        sync_env(pane, system, *pinned_system, audio.env());
2351        let window = &pane.window;
2352        // The core turns a changed viewport into a `resize` event, routed
2353        // with the rest of the pending events after this frame.
2354        let (viewport, scale) = pane.size();
2355        let size = window.inner_size();
2356        let t_view = std::time::Instant::now();
2357        pane.core.set_time(epoch.elapsed().as_secs_f64());
2358        // Why the runner asked, beside the input the core recorded: the
2359        // view reads both as `frame_cause` (backlog F111).
2360        pane.core.note_frame_cause(pane.cause.take());
2361        // The extensions fill the slots the host's view declares, in place
2362        // (`Ui::slot`), and `"root"` after it unless the host placed that
2363        // too — `finish` below does the latter and reports slots nobody
2364        // declared (ADR 0014). The core numbers their origins.
2365        let mut ui = pane.core.frame_with(viewport, scale, extensions);
2366        app.view(&mut ui);
2367        let view_ms = t_view.elapsed().as_secs_f32() * 1e3;
2368
2369        let t_layout = std::time::Instant::now();
2370        ui.finish();
2371        let layout_ms = t_layout.elapsed().as_secs_f32() * 1e3;
2372
2373        // Silent misconfigurations the core noticed (a grow weight with
2374        // nothing to split against, a transition on a positional key, two
2375        // nodes on one key): each once, to stderr, so they stop looking
2376        // like "the feature is broken".
2377        for w in pane.core.take_warnings() {
2378            eprintln!(
2379                "kui: warning [{}] node {:016x}: {}",
2380                w.code, w.key.0, w.message
2381            );
2382        }
2383
2384        // Mirror this frame's hit regions into the WM_NCHITTEST answerer.
2385        #[cfg(target_os = "windows")]
2386        if let Some(nc) = &pane.nc {
2387            nc.update(
2388                scale,
2389                window.is_maximized(),
2390                pane.core
2391                    .interaction
2392                    .hits()
2393                    .iter()
2394                    .map(|h| (h.window, h.rect.intersect(&h.clip))),
2395            );
2396        }
2397
2398        if let Some(t) = pane.core.window_title()
2399            && t != pane.applied_title
2400        {
2401            pane.applied_title = t.to_string();
2402            window.set_title(&pane.applied_title);
2403        }
2404
2405        // The level, the same way (backlog C30): a per-frame fact, applied
2406        // when it differs from what this pane has, and a popup's is never
2407        // touched. What the OS made of it is `env.window.always_on_top`
2408        // on the next `sync_env`, from the record `level_change` keeps.
2409        if let Some(level) = level_change(
2410            pane.kind,
2411            pane.core.always_on_top(),
2412            &mut pane.applied_on_top,
2413        ) {
2414            window.set_window_level(level);
2415        }
2416
2417        // Which Option keys are Alt, the same way (backlog F113): applied
2418        // when it differs from what this window has, never per frame. A
2419        // popup's own ask is applied too and never read — it cannot
2420        // become key on macOS, so its keys come through its owner.
2421        if let Some(_option_as_alt) =
2422            option_as_alt_change(pane.core.option_as_alt(), &mut pane.applied_option_as_alt)
2423        {
2424            #[cfg(target_os = "macos")]
2425            {
2426                use winit::platform::macos::{OptionAsAlt as Winit, WindowExtMacOS};
2427                window.set_option_as_alt(match _option_as_alt {
2428                    kui_core::OptionAsAlt::None => Winit::None,
2429                    kui_core::OptionAsAlt::Left => Winit::OnlyLeft,
2430                    kui_core::OptionAsAlt::Right => Winit::OnlyRight,
2431                    kui_core::OptionAsAlt::Both => Winit::Both,
2432                });
2433            }
2434        }
2435
2436        // The input method, the same way (backlog F125). Off, winit's
2437        // `keyDown:` stops calling `interpretKeyEvents:`, and everything
2438        // the OS composes there goes with it — an IME, a dead key, and
2439        // macOS's press-and-hold, which is an input method that swallows a
2440        // held letter's repeats; the key still arrives with the layout's
2441        // character as its text. A key still held from a press the IME
2442        // never saw keeps it off until it comes up (`Pane::ime_off_held`).
2443        if let Some(off) = ime_off_change(pane.ime_off_wanted(), &mut pane.applied_ime_off) {
2444            apply_ime_off(window, off);
2445        }
2446
2447        // The floor the app declared is a floor on the *app*: while the
2448        // devtools are docked in the main window, the pane's extent goes
2449        // on top of it, so the OS stops the window where the app is at
2450        // its minimum and not where the app less the dock is. After the
2451        // frame, since the handle's drag and the placement buttons land
2452        // in one; on change only, since it is a window-manager call.
2453        if pane.id == WindowId::MAIN
2454            && let Some((mw, mh)) = *min_size
2455        {
2456            let inset = pane.core.devtools_inset();
2457            let want = clamp_size((mw + inset.w as f64, mh + inset.h as f64), None, *max_size);
2458            if pane.applied_min != Some(want) {
2459                pane.applied_min = Some(want);
2460                window.set_min_inner_size(Some(LogicalSize::new(want.0, want.1)));
2461            }
2462        }
2463
2464        // Anchor the OS IME candidate window at the focused caret.
2465        if let Some(r) = pane.core.ime_rect() {
2466            window.set_ime_cursor_area(
2467                winit::dpi::LogicalPosition::new(r.x, r.y),
2468                winit::dpi::LogicalSize::new(r.w.max(1.0), r.h),
2469            );
2470        }
2471        // And say what the platform's own text input may ask the view:
2472        // whether there is somewhere to type, and where the caret is.
2473        #[cfg(target_os = "macos")]
2474        macos_text_input::stamp(window, &pane.core);
2475
2476        let t_render = std::time::Instant::now();
2477        // KUI_LOSE_DEVICE=SECS marks the device lost that long after
2478        // launch, once, to see the reopening below happen without a
2479        // driver update to cause it.
2480        if let Some(secs) = lose_device_at()
2481            && !*pretended_loss
2482            && epoch.elapsed().as_secs_f64() >= secs
2483            && let Some(r) = pane.renderer.as_ref()
2484        {
2485            *pretended_loss = true;
2486            r.gpu().mark_lost();
2487        }
2488        // The ground under the frame is a theme role like any other
2489        // (ADR 0019). Without this a view that paints no root background
2490        // — every example that lets the window show through — would show
2491        // the renderer's own near-black on a light desktop, which is the
2492        // one surface a `bg` prop cannot reach.
2493        // Written straight through: the renderer asks for a non-sRGB
2494        // surface, so a clear component is the byte it lands as, the same
2495        // way a quad's colour is.
2496        // Over a backdrop the ground is nothing at all (backlog F126): what
2497        // the view paints is what covers the material, and a view that
2498        // paints no root background shows it whole.
2499        let ground = pane.core.theme().bg;
2500        // The wallpaper kui draws where the OS has no material: handed to
2501        // the renderer once it has loaded, and the part behind the window
2502        // set when the window moved.
2503        if let (Some(g), Some(r)) = (pane.ground.as_mut(), pane.renderer.as_mut()) {
2504            g.prepare(window, r);
2505        }
2506        let clear = if pane.backdrop.is_translucent() && pane.ground.is_none() {
2507            kui_wgpu::wgpu::Color::TRANSPARENT
2508        } else {
2509            kui_wgpu::wgpu::Color {
2510                r: ground.r as f64,
2511                g: ground.g as f64,
2512                b: ground.b as f64,
2513                a: ground.a as f64,
2514            }
2515        };
2516        let (dl, atlas) = pane.core.output();
2517        let mut wait_ms = 0.0;
2518        let mut reopen = false;
2519        let drawn = pane.renderer.as_mut().map(|r| {
2520            r.clear_color = clear;
2521            r.render(dl, atlas)
2522        });
2523        match drawn {
2524            Some(Ok(report)) => {
2525                wait_ms = report.vsync_wait_ms;
2526                pane.pacer
2527                    .presented(std::time::Instant::now(), (size.width, size.height));
2528                pane.retry.presented();
2529                pane.surface_tries = 0;
2530                pane.awaits_device = false;
2531            }
2532            Some(Err(kui_wgpu::RenderError::Reconfigure)) => {
2533                if let Some(r) = pane.renderer.as_mut() {
2534                    r.resize(size.width, size.height);
2535                }
2536                pane.redraw_for(FrameCause::RETRY);
2537            }
2538            // The surface is configured wrong for the window — a size the
2539            // platform never told us, a swapchain a driver update left
2540            // behind: configure it to the window's size and draw again;
2541            // a surface that stays wrong is given up with its device.
2542            // Past the tries the frame asks for no other: the reopen is
2543            // what asks for the next one, and if it has to wait out its
2544            // second, a frame asked for now would only be refused again —
2545            // at once, with no vsync to pace it. Said once per surface
2546            // given up, not once per refused frame.
2547            Some(Err(kui_wgpu::RenderError::Validation)) => {
2548                match surface_refused(&mut pane.surface_tries) {
2549                    Refused::Retry => {
2550                        if let Some(r) = pane.renderer.as_mut() {
2551                            r.resize(size.width, size.height);
2552                        }
2553                        pane.redraw_for(FrameCause::RETRY);
2554                    }
2555                    Refused::GiveUp { say } => {
2556                        if say {
2557                            eprintln!("kui: the surface stays invalid; reopening the device");
2558                        }
2559                        reopen = true;
2560                    }
2561                }
2562            }
2563            // Occluded or timed out: nothing to present, and the frame is
2564            // owed — *scheduled*, since nothing else may ask for one. A
2565            // window ordered front, or brought back with ⌘-Tab, reports
2566            // itself occluded for a beat or two before the platform
2567            // catches up; its frame dropped, it showed the one from
2568            // before until a stray mouse move woke it (F102). Only for a
2569            // bounded number of tries, and not while the platform says
2570            // the window is covered, so a hidden window does not spin.
2571            Some(Err(kui_wgpu::RenderError::Skip)) => {
2572                let now = std::time::Instant::now();
2573                pane.retry.skipped(now);
2574                // A skipped frame paces the next as a presented one does:
2575                // what asks while the surface skips (a waker, the host) is
2576                // held for the display, or its fallback, instead of drawn
2577                // at once into another skip (backlog RG98).
2578                pane.pacer.presented(now, (size.width, size.height));
2579            }
2580            // The device is gone (a driver update, a GPU reset), or a
2581            // frame ago it was and no new one could be opened: open one.
2582            Some(Err(kui_wgpu::RenderError::DeviceLost)) | None => reopen = true,
2583        }
2584        pane.awaits_device |= reopen;
2585        let render_ms = (t_render.elapsed().as_secs_f32() * 1e3 - wait_ms).max(0.0);
2586
2587        pane.core.stats.push(FrameSample {
2588            input_ms: 0.0,
2589            view_ms,
2590            layout_ms,
2591            render_ms,
2592            wait_ms,
2593        });
2594        if reopen {
2595            self.reopen_device();
2596        }
2597    }
2598
2599    /// The device is gone — a driver update or a GPU reset took it, and
2600    /// every swapchain with it — so one new device is opened, and a
2601    /// renderer on it for every window, each window's old one dropped
2602    /// before its new surface is made (DXGI gives a window one flip-model
2603    /// swapchain). The windows keep their cores: the next frame draws
2604    /// what the last one would have. A window whose renderer cannot be
2605    /// made has none, and the device is tried again a second later; a
2606    /// device that cannot be opened is tried, and said, once a second, not
2607    /// once a frame. Asked for inside that second, the ask is owed
2608    /// (`reopen_owed`) and `about_to_wait` makes it when the second is up.
2609    fn reopen_device(&mut self) {
2610        let now = std::time::Instant::now();
2611        if reopen_due(self.reopened, now) > now {
2612            self.reopen_owed = true;
2613            return;
2614        }
2615        self.reopened = Some(now);
2616        // Dropped, not leaked: the old swapchain has to be released for
2617        // DXGI to allow the window a new one (leaked, the new surface's
2618        // configure fails with "invalid surface").
2619        for pane in &mut self.panes {
2620            pane.renderer = None;
2621            pane.surface_tries = 0;
2622        }
2623        self.gpu = None;
2624        let mut gpu: Option<kui_wgpu::Gpu> = None;
2625        let options = self.gpu_options();
2626        for pane in &mut self.panes {
2627            let px = pane.window.inner_size();
2628            let made = match &gpu {
2629                None => pollster::block_on(kui_wgpu::Renderer::new_with(
2630                    pane.window.clone(),
2631                    px.width,
2632                    px.height,
2633                    options,
2634                )),
2635                Some(gpu) => kui_wgpu::Renderer::new_in_with(
2636                    gpu,
2637                    pane.window.clone(),
2638                    px.width,
2639                    px.height,
2640                    pane.backdrop.is_translucent(),
2641                ),
2642            };
2643            match made {
2644                Ok(mut r) => {
2645                    r.set_frame_latency(self.frame_latency);
2646                    // The window keeps its backdrop where the new surface
2647                    // takes alpha too; one that will not any more leaves
2648                    // the window opaque, and says so.
2649                    if pane.backdrop.is_translucent() && pane.ground.is_none() && !r.transparent() {
2650                        pane.backdrop = Backdrop::Opaque;
2651                    }
2652                    // A wallpaper drawn by kui goes to the new renderer too.
2653                    if let Some(g) = pane.ground.as_mut() {
2654                        g.renderer_replaced();
2655                    }
2656                    let opened = gpu.is_none();
2657                    gpu.get_or_insert_with(|| r.gpu().clone());
2658                    if opened {
2659                        eprintln!("kui: device reopened");
2660                    }
2661                    pane.renderer = Some(r);
2662                    pane.redraw_for(FrameCause::DEVICE);
2663                }
2664                Err(err) => eprintln!("kui: cannot reopen window {}: {err}", pane.id.0),
2665            }
2666            pane.awaits_device = pane.renderer.is_none();
2667        }
2668        // A window left without a renderer — or every window, when no
2669        // device would open — is tried again a second from now, whether
2670        // or not anything asks it for a frame.
2671        self.reopen_owed = self.panes.iter().any(|p| p.awaits_device);
2672        // The new device may be another adapter's, with or without the
2673        // per-channel blend LCD masks need: decided again, for every core,
2674        // or a core keeps rasterising masks the new pipeline cannot split.
2675        // A changed decision empties each core's atlas (`set_subpixel_text`),
2676        // which the fresh renderer was going to upload whole anyway.
2677        if let Some(gpu) = &gpu {
2678            let wanted = aa_under(wanted_text_aa(self.text_aa), self.backdrop);
2679            let on = subpixel_on(wanted, gpu.dual_source());
2680            if on != self.subpixel {
2681                self.subpixel = on;
2682                for pane in &mut self.panes {
2683                    pane.core.set_subpixel_text(on);
2684                }
2685            }
2686        }
2687        self.gpu = gpu;
2688    }
2689}
2690
2691/// Which bar `pump_menu_bar` last handed the platform.
2692#[cfg(target_os = "macos")]
2693#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2694enum AppliedBar {
2695    /// The standard bar: no window declared one, or the front
2696    /// window draws its own declaration and the platform's is the standard.
2697    Standard,
2698    /// A window's declaration, at that revision.
2699    Declared(WindowId, u64),
2700}
2701
2702impl ApplicationHandler<access_bridge::UserEvent> for DynShell<'_> {
2703    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
2704        if !self.panes.is_empty() || self.opened {
2705            return;
2706        }
2707        self.opened = true;
2708        // KUI_WINDOW=WxH overrides the initial size (useful for testing).
2709        let size = std::env::var("KUI_WINDOW")
2710            .ok()
2711            .and_then(|s| {
2712                let (w, h) = s.split_once('x')?;
2713                Some((w.parse().ok()?, h.parse().ok()?))
2714            })
2715            .map(|s| clamp_size(s, self.min_size, self.max_size))
2716            .unwrap_or(self.size);
2717        let mut attrs = self.window_attrs(&self.title.clone(), size, self.chrome, self.backdrop);
2718        if let Some((mw, mh)) = self.min_size {
2719            attrs = attrs.with_min_inner_size(LogicalSize::new(mw, mh));
2720        }
2721        if let Some((mw, mh)) = self.max_size {
2722            attrs = attrs.with_max_inner_size(LogicalSize::new(mw, mh));
2723        }
2724        // KUI_WINDOW_AT=X,Y places it, logical px from the screen's top
2725        // left: the smoke round's `--jobs` cascades its windows by it, so
2726        // none is wholly covered — a covered window is `Occluded` and
2727        // presents nothing, and the round counts presents.
2728        if let Some((x, y)) = std::env::var("KUI_WINDOW_AT").ok().and_then(|s| {
2729            let (x, y) = s.split_once(',')?;
2730            Some((x.parse::<f64>().ok()?, y.parse::<f64>().ok()?))
2731        }) {
2732            attrs = attrs.with_position(LogicalPosition::new(x, y));
2733        }
2734        let window = match event_loop.create_window(attrs) {
2735            Ok(w) => Arc::new(w),
2736            Err(e) => return self.fail_open(event_loop, format!("cannot open the window: {e}")),
2737        };
2738        self.icon.set_on(&window);
2739        let px = window.inner_size();
2740        let renderer = pollster::block_on(kui_wgpu::Renderer::new_with(
2741            window.clone(),
2742            px.width,
2743            px.height,
2744            self.gpu_options(),
2745        ));
2746        let mut renderer = match renderer {
2747            Ok(r) => r,
2748            Err(e) => return self.fail_open(event_loop, format!("cannot draw in the window: {e}")),
2749        };
2750        renderer.set_frame_latency(self.frame_latency);
2751        // What shows through it, now that there is a surface to present
2752        // with alpha (backlog F126); before it is shown.
2753        let backdrop = backdrop::apply(&window, &renderer, self.backdrop);
2754        // Subpixel text only where the renderer blends per channel; the
2755        // env var wins over the builder for quick A/B comparisons.
2756        self.subpixel = subpixel_on(
2757            aa_under(wanted_text_aa(self.text_aa), self.backdrop),
2758            renderer.subpixel_text(),
2759        );
2760        self.gpu = Some(renderer.gpu().clone());
2761        let mut core = self
2762            .main_core
2763            .take()
2764            .expect("the main core is built once, by the launcher");
2765        core.set_subpixel_text(self.subpixel);
2766        core.env.window.id = WindowId::MAIN;
2767        self.push_pane(
2768            event_loop,
2769            WindowId::MAIN,
2770            WindowConfig::default(),
2771            WindowId::MAIN,
2772            self.chrome,
2773            core,
2774            window,
2775            renderer,
2776            backdrop,
2777        );
2778        self.panes[0].applied_title = self.title.clone();
2779        self.panes[0].applied_min = self.min_size;
2780    }
2781
2782    fn window_event(
2783        &mut self,
2784        event_loop: &ActiveEventLoop,
2785        id: WinitWindowId,
2786        event: WindowEvent,
2787    ) {
2788        let Some(i) = self.pane_index(id) else { return };
2789        // Everything but the redraw itself: a redraw is the *answer* to an
2790        // event, so counting it would make a frame its own reason for the
2791        // next one and an animation would report activity for ever.
2792        if !matches!(event, WindowEvent::RedrawRequested) {
2793            self.saw_event = true;
2794        }
2795        {
2796            let pane = &mut self.panes[i];
2797            if let Some(bridge) = &mut pane.access {
2798                bridge.process_event(&pane.window, &event);
2799            }
2800        }
2801        match event {
2802            WindowEvent::CloseRequested => {
2803                if self.panes[i].id == WindowId::MAIN {
2804                    self.exit_main(event_loop);
2805                } else {
2806                    let id = self.panes[i].id;
2807                    self.close_pane(event_loop, id);
2808                }
2809            }
2810            // On Windows minimizing and restoring are each a `WM_SIZE`:
2811            // the first is where the window went dark, the second the
2812            // frame that brings it back (backlog RG97).
2813            WindowEvent::Resized(size) => {
2814                let pane = &mut self.panes[i];
2815                if let Some(r) = pane.renderer.as_mut() {
2816                    r.resize(size.width, size.height);
2817                }
2818                if let Some(g) = pane.ground.as_mut() {
2819                    g.moved();
2820                }
2821                if pane.minimized() {
2822                    pane.cause.went_dark();
2823                    pane.redraw_for(FrameCause::RESIZE);
2824                } else if pane.cause.is_dark() && cfg!(target_os = "windows") {
2825                    pane.came_back(FrameCause::RESIZE);
2826                } else {
2827                    // Elsewhere a window resized while covered is still
2828                    // covered; `Occluded(false)` is its way back.
2829                    pane.redraw_for(FrameCause::RESIZE);
2830                }
2831            }
2832            WindowEvent::ScaleFactorChanged { .. } => {
2833                self.panes[i].redraw_for(FrameCause::SCALE);
2834            }
2835            // The wallpaper kui draws behind a window stays put on the
2836            // screen as the window moves over it (backlog F126); nothing
2837            // else here cares where the window is.
2838            WindowEvent::Moved(_) => {
2839                let pane = &mut self.panes[i];
2840                if let Some(g) = pane.ground.as_mut() {
2841                    g.moved();
2842                    pane.redraw_for(FrameCause::RESIZE);
2843                }
2844            }
2845            WindowEvent::CursorMoved { position, .. } => {
2846                let pane = &mut self.panes[i];
2847                let scale = pane.window.scale_factor() as f32;
2848                let p = Vec2::new(position.x as f32 / scale, position.y as f32 / scale);
2849                if pane.synthesizes_resize() {
2850                    pane.resize_edge = pane.resize_edge_at(p);
2851                }
2852                if pane.cursor != p {
2853                    pane.scroll_gesture.pointer_moved();
2854                }
2855                pane.cursor = p;
2856                let from = pane.id;
2857                self.dispatch(event_loop, i, InputEvent::CursorMoved(p));
2858                // And then, if this pane is holding a press that a popup
2859                // joined, the same move again in that popup's coordinates.
2860                self.retarget_move(event_loop, from, p);
2861            }
2862            WindowEvent::CursorLeft { .. } => {
2863                self.dispatch(event_loop, i, InputEvent::CursorLeft);
2864            }
2865            // Files dragged in from the OS, as winit reports them: one
2866            // event per file and no position (ADR 0031, decision 5). On
2867            // macOS the delegate override supersedes all three with the
2868            // position AppKit has (`mod macos_drop`); elsewhere the files
2869            // are collected per batch and dispatched at its end, at the
2870            // pane's last reported cursor — the point at enter and at
2871            // release, since no platform here reports the drag moving.
2872            WindowEvent::HoveredFile(path) => {
2873                if !self.file_drag_is_overridden() {
2874                    let pane = &mut self.panes[i];
2875                    pane.file_drag.push(path.to_string_lossy().into_owned());
2876                    pane.file_drag_pending = Some(false);
2877                }
2878            }
2879            WindowEvent::DroppedFile(path) => {
2880                if !self.file_drag_is_overridden() {
2881                    let pane = &mut self.panes[i];
2882                    // A hover batch still waiting in the same turn is the
2883                    // same files: the drop's list starts over.
2884                    if pane.file_drag_pending != Some(true) {
2885                        pane.file_drag.clear();
2886                    }
2887                    pane.file_drag.push(path.to_string_lossy().into_owned());
2888                    pane.file_drag_pending = Some(true);
2889                }
2890            }
2891            WindowEvent::HoveredFileCancelled => {
2892                if !self.file_drag_is_overridden() {
2893                    let pane = &mut self.panes[i];
2894                    pane.file_drag.clear();
2895                    pane.file_drag_pending = None;
2896                    self.dispatch(event_loop, i, InputEvent::DragCancel);
2897                }
2898            }
2899            // Recorded, not acted on: what a view reads is derived from
2900            // every window's copy at the end of the batch (`settle_focus`),
2901            // because focus *moving* is two events and neither alone is the
2902            // answer.
2903            WindowEvent::Focused(focused) => {
2904                self.panes[i].os_focused = focused;
2905                // A modifier let go elsewhere never comes up here: what
2906                // was down is forgotten with the keyboard (backlog F108).
2907                if !focused {
2908                    self.panes[i].modifier_keys_down.clear();
2909                    // And so is a key held through a mode change: its
2910                    // release lands elsewhere, so the IME it was keeping
2911                    // off is the frame's to decide again.
2912                    self.panes[i].ime_off_held.clear();
2913                    self.panes[i].sync_ime();
2914                }
2915                // Coming back to the app is the cheap, reliable sign that
2916                // the user may have been in a settings app: the accent and
2917                // the reduce-motion setting have no event to subscribe to
2918                // here, and re-asking costs microseconds against something
2919                // that happens when a human switches windows.
2920                if focused {
2921                    let before = (self.system, self.panes[i].appearance);
2922                    self.system = system_env::query();
2923                    // And the window's own setting, in case the platform
2924                    // changed it without an event while we were away.
2925                    self.panes[i].appearance = appearance_of(&self.panes[i].window);
2926                    // A change found this way has no event of its own
2927                    // behind it, so ask for the frame that reports it:
2928                    // `begin_frame` turns the difference into a `system`
2929                    // event, and a host that only draws on input would
2930                    // otherwise not learn of it until it drew for
2931                    // something else (F40).
2932                    if before != (self.system, self.panes[i].appearance) {
2933                        for p in &self.panes {
2934                            p.redraw_for(FrameCause::APPEARANCE);
2935                        }
2936                    }
2937                }
2938            }
2939            // Covered or uncovered — where the platform says (macOS, X11):
2940            // uncovered, the window shows what it presented before it was
2941            // covered, so a frame is owed now (F102); covered, frames the
2942            // surface skips are not retried.
2943            // Covered is where a window goes dark, minimized on macOS
2944            // included, and uncovered where it comes back (RG97).
2945            WindowEvent::Occluded(covered) => {
2946                self.panes[i]
2947                    .retry
2948                    .occluded(covered, std::time::Instant::now());
2949                if covered {
2950                    self.panes[i].cause.went_dark();
2951                } else {
2952                    self.panes[i].came_back(FrameCause::OCCLUSION);
2953                }
2954            }
2955            // The theme changing is the other one, and the only one that
2956            // arrives while the app is in front. The next frame reads the
2957            // appearance off the window anyway; the redraw is what makes
2958            // there *be* a next frame in an app that only draws on input.
2959            WindowEvent::ThemeChanged(theme) => {
2960                self.panes[i].appearance = theme_appearance(Some(theme));
2961                self.system = system_env::query();
2962                self.panes[i].redraw_for(FrameCause::APPEARANCE);
2963            }
2964            // The four keyboard events go to `key_target`, which is this
2965            // pane unless it is lending its keyboard to a non-activating
2966            // popup. The modifier mirror follows them, or the popup would
2967            // read a stale Shift.
2968            WindowEvent::ModifiersChanged(m) => {
2969                // The keyboard's state is one fact for the pair: the OS
2970                // delivers the edge to the owner, the target reads it for
2971                // the keys it borrows, and the owner keeps reading it once
2972                // the popup is gone — written to the target alone, a Shift
2973                // released while a menu was up left the owner's next key a
2974                // Shift chord (AR23).
2975                let t = self.key_target(i);
2976                self.panes[i].modifiers = m.state();
2977                self.panes[t].modifiers = m.state();
2978                use winit::keyboard::ModifiersKeyState::Pressed;
2979                let alt = (m.lalt_state() == Pressed, m.ralt_state() == Pressed);
2980                self.panes[i].alt_held = alt;
2981                self.panes[t].alt_held = alt;
2982                let kmods = self.panes[t].kmods();
2983                self.dispatch(event_loop, t, InputEvent::Modifiers(kmods));
2984            }
2985            // Not a press winit made up: on Windows and X11 a window
2986            // gaining focus is handed a press of every key already held —
2987            // made in another window or another app — and a key whose
2988            // press closed a window pressed again in the one beneath it
2989            // (backlog RG100). The releases it makes up as a window loses
2990            // focus are kept: they are what lets go of a key a sink held.
2991            WindowEvent::KeyboardInput {
2992                event,
2993                is_synthetic,
2994                ..
2995            } => {
2996                if !(is_synthetic && event.state == ElementState::Pressed) {
2997                    let t = self.key_target(i);
2998                    self.on_key(event_loop, i, t, event)
2999                }
3000            }
3001            WindowEvent::Ime(Ime::Commit(text)) => {
3002                // Its own channel, not `Text`: a sink hears a commit as a
3003                // `text` event and a keystroke as a `key` event, once each
3004                // (backlog C17); a stock editor takes both the same way.
3005                self.panes[i].preedit_open = false;
3006                let t = self.key_target(i);
3007                self.dispatch(event_loop, t, InputEvent::Commit(text));
3008            }
3009            WindowEvent::Ime(Ime::Preedit(text, cursor)) => {
3010                self.panes[i].preedit_open = !text.is_empty();
3011                let t = self.key_target(i);
3012                self.dispatch(event_loop, t, InputEvent::Preedit(text, cursor));
3013            }
3014            // The IME went away — the frame turned it off (`imeOff`), or
3015            // the user switched input source — and winit's contract is
3016            // that a pending preedit is cleared: a composition still open
3017            // ends without a commit, as an empty preedit, the way an IME
3018            // that cancels says it.
3019            WindowEvent::Ime(Ime::Disabled) => {
3020                if std::mem::take(&mut self.panes[i].preedit_open) {
3021                    let t = self.key_target(i);
3022                    self.dispatch(event_loop, t, InputEvent::Preedit(String::new(), None));
3023                }
3024            }
3025            // Force Touch: stage 2 is the deepened press macOS calls a
3026            // force click. winit reports the whole ramp, and only the
3027            // *edge* into stage 2 is the gesture — a stage that stays at 2
3028            // while the finger presses harder is the same click still
3029            // happening (ADR 0017, decision 6). macOS-only: no other
3030            // winit backend reports pressure at all.
3031            WindowEvent::TouchpadPressure { stage, .. } => {
3032                let pane = &mut self.panes[i];
3033                let was = std::mem::replace(&mut pane.pressure_stage, stage);
3034                if stage >= 2 && was < 2 {
3035                    let at = pane.cursor;
3036                    self.dispatch(event_loop, i, InputEvent::ForceClick(at));
3037                }
3038            }
3039            WindowEvent::MouseWheel { delta, phase, .. } => {
3040                let started = phase == winit::event::TouchPhase::Started;
3041                let pane = &mut self.panes[i];
3042                let scale = pane.window.scale_factor() as f32;
3043                let now = std::time::Instant::now();
3044                let (d, kind) = match delta {
3045                    winit::event::MouseScrollDelta::LineDelta(x, y) => {
3046                        pane.axis_lock.end();
3047                        (
3048                            Vec2::new(Core::lines_to_px(x), Core::lines_to_px(y)),
3049                            scroll_gesture::Kind::Line,
3050                        )
3051                    }
3052                    // A trackpad's swipe keeps to its axis (`mod axis_lock`),
3053                    // and a finger put down begins the next, glide or no
3054                    // glide (backlog F117).
3055                    winit::event::MouseScrollDelta::PixelDelta(p) => {
3056                        if pane.scroll_gesture.phase(phase, now) {
3057                            pane.axis_lock.end();
3058                        }
3059                        (
3060                            pane.axis_lock
3061                                .pixel(Vec2::new(p.x as f32 / scale, p.y as f32 / scale), now),
3062                            scroll_gesture::Kind::Pixel,
3063                        )
3064                    }
3065                };
3066                // The gesture it is part of keeps the targets it began
3067                // with (`mod scroll_gesture`, backlog F107).
3068                if d != Vec2::ZERO {
3069                    let begins = pane.scroll_gesture.begins(kind, started, now);
3070                    self.dispatch(
3071                        event_loop,
3072                        i,
3073                        InputEvent::ScrollGesture { delta: d, begins },
3074                    );
3075                } else {
3076                    pane.scroll_gesture.note(kind, started, now);
3077                }
3078            }
3079            WindowEvent::MouseInput { state, button, .. } => {
3080                let button = match button {
3081                    WinitButton::Left => MouseButton::Primary,
3082                    WinitButton::Right => MouseButton::Secondary,
3083                    WinitButton::Middle => MouseButton::Middle,
3084                    // `onButton` hears these as `Other`'s code (backlog
3085                    // F105), so the numbering has to be stable: back,
3086                    // forward, then whatever the platform reports beyond
3087                    // them.
3088                    WinitButton::Back => MouseButton::Other(0),
3089                    WinitButton::Forward => MouseButton::Other(1),
3090                    WinitButton::Other(n) => {
3091                        MouseButton::Other(n.saturating_add(2).min(u8::MAX as u16) as u8)
3092                    }
3093                };
3094                let primary = button == MouseButton::Primary;
3095                let here = self.panes[i].id;
3096                // Which pane holds a primary press, for ADR 0009's arming.
3097                // Set for a consumed press too: the button is down whatever
3098                // the core was told, and the release below clears it.
3099                if primary {
3100                    self.primary_down = (state == ElementState::Pressed).then_some(here);
3101                    if state == ElementState::Pressed {
3102                        // A consumed press whose release never arrived — the
3103                        // window lost the keyboard mid-gesture, say — must
3104                        // not swallow the next one instead.
3105                        self.swallowed_press = None;
3106                    }
3107                }
3108                // A press anywhere but inside a popup is "outside" it —
3109                // the owner's own window included, which is exactly the
3110                // line a separate surface draws. Reported before the press
3111                // is dispatched, so an app that stops declaring the popup
3112                // on the dismissal still sees the click it was dismissed
3113                // by, the way a modal's dismissal works (ADR 0003).
3114                //
3115                // And then a primary press that dismissed a
3116                // **non-activating** popup is **consumed** — not dispatched
3117                // to the window it landed in, and its release swallowed with
3118                // it (ADR 0009 decision 5). The pass-through this replaces
3119                // claimed to mirror ADR 0003 and had it backwards: under an
3120                // in-window modal everything outside emits no hit region, so
3121                // the outside press dismisses and lands on nothing, while a
3122                // popup's owner is live (ADR 0004 decision 10) and the press
3123                // dismisses *and* acts. Invisible with click-to-open;
3124                // decisive with open-on-press, where a press on the open
3125                // field would otherwise dismiss and reopen in one gesture,
3126                // and no ordering of the two events lets the app tell that
3127                // press from the first. An **activating** popup — a tear-off
3128                // panel — keeps the pass-through, since someone working in a
3129                // panel beside the app expects a click in the app to act.
3130                if state == ElementState::Pressed {
3131                    let mut consumed = false;
3132                    let mut reached_app = false;
3133                    for (id, activates) in self.popups_outside(i) {
3134                        reached_app |= self.dismiss(id, DismissReason::Outside);
3135                        consumed |= primary && !activates;
3136                    }
3137                    if consumed {
3138                        self.swallowed_press = Some(here);
3139                        // Choosing in a menu closes it *and* does what it
3140                        // says: one frame, so this one waits for the host.
3141                        self.owe_for(reached_app);
3142                        // The press the core never saw.
3143                        for p in &self.panes {
3144                            p.redraw_for(FrameCause::BUTTON);
3145                        }
3146                        return;
3147                    }
3148                }
3149                // A press inside a non-activating popup arms that popup for
3150                // its own release (ADR 0009 decision 4's last paragraph):
3151                // the OS keeps the drag here whatever it wanders over, so a
3152                // drag off the top edge and a release on the desktop has to
3153                // be classified rather than ignored — the measured case in
3154                // backlog W2. It gets no retargeted moves, having the real
3155                // ones already.
3156                if primary
3157                    && state == ElementState::Pressed
3158                    && self.panes[i].kind == WindowKind::Popup
3159                    && !self.panes[i].activates
3160                    && !self.armed.iter().any(|a| a.id == here)
3161                {
3162                    self.armed.push(Armed {
3163                        id: here,
3164                        inside: true,
3165                    });
3166                }
3167                if state == ElementState::Released && primary {
3168                    // The release of a press the driver ate goes with it:
3169                    // the core never saw the `down`, so nothing should see
3170                    // this `up`.
3171                    if self.swallowed_press == Some(here) {
3172                        self.swallowed_press = None;
3173                        self.primary_down = None;
3174                        return;
3175                    }
3176                    let at = self.panes[i].cursor;
3177                    self.classify_release(event_loop, here, at);
3178                }
3179                // The frame the classification ran may have closed a window
3180                // and moved this one down the list.
3181                let Some(i) = self.pane_of(here) else { return };
3182                let pane = &mut self.panes[i];
3183                // A press on the synthesized resize band starts an OS resize
3184                // instead of reaching the UI.
3185                if primary
3186                    && state == ElementState::Pressed
3187                    && let Some(dir) = pane.resize_edge
3188                {
3189                    let _ = pane.window.drag_resize_window(dir);
3190                    return;
3191                }
3192                let ev = match state {
3193                    ElementState::Pressed => {
3194                        // Multi-click is the primary button's: a right
3195                        // press between two left ones does not break the
3196                        // run, and never counts up one of its own.
3197                        let clicks = if primary {
3198                            let now = std::time::Instant::now();
3199                            let clicks = match pane.last_click {
3200                                Some((t, p, n))
3201                                    if now.duration_since(t).as_millis() < MULTI_CLICK_MS
3202                                        && (p.x - pane.cursor.x).abs() < MULTI_CLICK_SLOP
3203                                        && (p.y - pane.cursor.y).abs() < MULTI_CLICK_SLOP =>
3204                                {
3205                                    // Cycle 1 → 2 → 3 → 1 like most editors.
3206                                    n % 3 + 1
3207                                }
3208                                _ => 1,
3209                            };
3210                            pane.last_click = Some((now, pane.cursor, clicks));
3211                            clicks
3212                        } else {
3213                            1
3214                        };
3215                        InputEvent::MouseDown { button, clicks }
3216                    }
3217                    ElementState::Released => InputEvent::MouseUp { button },
3218                };
3219                self.dispatch(event_loop, i, ev);
3220            }
3221            WindowEvent::RedrawRequested
3222                if frame_waits_for_host(self.owed.get(), self.panes[i].deferred_frame) =>
3223            {
3224                // What would be painted predates an input the app has been
3225                // told about and not yet answered — the release of a
3226                // button, with the count still at its old value. The host
3227                // asks again the moment its handler has run, and that
3228                // frame carries both. Never twice running, so nothing here
3229                // can stop a window painting.
3230                self.panes[i].deferred_frame = true;
3231            }
3232            // A frame asked for while frames run back to back waits for
3233            // the display, which asks again at the vsync (`mod pacer`).
3234            WindowEvent::RedrawRequested if !self.panes[i].admit_frame() => {}
3235            WindowEvent::RedrawRequested => {
3236                self.panes[i].deferred_frame = false;
3237                self.redraw(i);
3238                // KUI_SMOKE_FRAMES: count what the main window actually
3239                // landed and quit at the target. Counted only once a
3240                // present succeeded, so a window that never draws never
3241                // counts and the run fails on the caller's timeout rather
3242                // than passing quietly. Asking for the next
3243                // one keeps an app that paints only on input painting, so
3244                // every example reaches the count at the same speed.
3245                if let Some(n) = self.smoke_frames
3246                    && let Some(p) = self.panes.get(i)
3247                    && p.id == WindowId::MAIN
3248                    && p.retry.presented_once()
3249                {
3250                    self.frames_drawn += 1;
3251                    if self.frames_drawn >= n {
3252                        self.exit_requested = true;
3253                    } else {
3254                        self.panes[i].redraw_for(FrameCause::SMOKE);
3255                    }
3256                }
3257                self.panes[i].publish_access();
3258                // Views can declare windows and window commands too
3259                // (ui.window, ui.window_command); apply them the same frame
3260                // they were declared. Likewise the sounds a frame started
3261                // (audio nodes, ui.play), and the clipboard work a view
3262                // queued (ui.set_clipboard, ui.request_paste — backlog
3263                // C33), which until now only an input's drain reached.
3264                self.apply_menu_actions(event_loop, i);
3265                self.apply_window_commands(event_loop);
3266                self.apply_audio();
3267                // The frame may have closed this very pane.
3268                let Some(i) = self.pane_index(id) else { return };
3269                // A new frame can put something else under a still cursor.
3270                self.panes[i].apply_cursor();
3271                // A frame can resize the viewport, and can change what sits
3272                // under a still cursor; route the resulting resize / hover
3273                // events now rather than with the next input, and redraw for
3274                // what they change.
3275                let pending = self.panes[i].core.take_pending_events();
3276                if !pending.is_empty() {
3277                    self.route_events(pending);
3278                    if let Some(p) = self.panes.get(i) {
3279                        p.redraw_for(FrameCause::AFTER_FRAME);
3280                    }
3281                }
3282                // Windows moves and resizes a window inside its own modal
3283                // loop, where `about_to_wait` — the pacing that asks for
3284                // the next frame of an animation — does not run, so a
3285                // transition froze for as long as the title bar was held
3286                // (backlog W3). The pane's own timer is what answers that,
3287                // and this is where it learns whether there is anything to
3288                // animate; `mod windows_anim` is why it is a timer and not
3289                // a redraw asked for from right here. Not while the window
3290                // waits for a device: `about_to_wait` asks it for no
3291                // animation frames then (RG29), and the timer asked for 64
3292                // a second, each a view built for no renderer (RG40). The
3293                // reopen's own `request_redraw` arms it again. Nor while
3294                // it is minimized (RG45); the restore's `Resized` does.
3295                #[cfg(target_os = "windows")]
3296                if let Some(p) = self.panes.get_mut(i) {
3297                    let animating = p.animates_now();
3298                    if let Some(t) = &mut p.anim_timer {
3299                        t.set(animating);
3300                    }
3301                }
3302            }
3303            _ => {}
3304        }
3305        if self.exit_requested {
3306            self.exit_main(event_loop);
3307        }
3308    }
3309
3310    /// AccessKit's side of the conversation: assistive technology attaching
3311    /// (send it the tree, and let the view know), detaching, or asking for
3312    /// an action (input).
3313    /// The loop's last event: the app's `teardown`, before the process
3314    /// goes (a Quit from the OS) or `run` returns (the main window
3315    /// closed).
3316    fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
3317        self.teardown_once();
3318    }
3319
3320    fn user_event(&mut self, event_loop: &ActiveEventLoop, event: access_bridge::UserEvent) {
3321        // A wake is the app saying "what `view` shows has changed": every
3322        // window draws, as after any input. Coalesced by the platform's
3323        // queue, so a thread waking a thousand times a frame costs one.
3324        // A wake for later is a deadline, not a frame: the loop sleeps to
3325        // it in `about_to_wait`.
3326        if let access_bridge::UserEvent::WakeAt(at) = event {
3327            self.wake_at = Some(self.wake_at.map_or(at, |w| w.min(at)));
3328            return;
3329        }
3330        self.saw_event = true;
3331        if matches!(event, access_bridge::UserEvent::Wake) {
3332            for pane in &self.panes {
3333                pane.redraw_for(FrameCause::WAKE);
3334            }
3335            return;
3336        }
3337        // The installed fonts changed: the session scans again, through
3338        // any one window's core since every window shares it, and the
3339        // windows draw — each shapes its text again on that frame
3340        // (`weights_rev`), fallback being free to land on a new face. A
3341        // signal that found nothing new (a second window's copy of a
3342        // Windows broadcast) changes nothing and draws nothing.
3343        if matches!(event, access_bridge::UserEvent::FontsChanged) {
3344            system_fonts::handled();
3345            let changed = self
3346                .panes
3347                .first_mut()
3348                .map_or(0, |pane| pane.core.reload_system_fonts());
3349            if changed > 0 {
3350                for pane in &self.panes {
3351                    pane.redraw_for(FrameCause::APPEARANCE);
3352                }
3353            }
3354            return;
3355        }
3356        let Some(i) = access_bridge::window_of(&event).and_then(|w| self.pane_index(w)) else {
3357            return;
3358        };
3359        // A file dialog's answer, from the thread that waited on it: the
3360        // window that asked hears it as input.
3361        let event = match event {
3362            access_bridge::UserEvent::Files { paths, .. } => {
3363                self.dispatch(event_loop, i, InputEvent::Files(paths));
3364                return;
3365            }
3366            other => other,
3367        };
3368        let Some(bridge) = &mut self.panes[i].access else {
3369            return;
3370        };
3371        let was_listening = bridge.active();
3372        let req = bridge.on_event(event);
3373        // A client attaching or leaving is a fact the view reads
3374        // (`env.system.assistive`, backlog F48): `sync_env` writes it on
3375        // the next frame and the core reports it as a `system` event, so
3376        // the frame has to happen — an app that only redraws on input
3377        // would otherwise hear it with the next click.
3378        if bridge.active() != was_listening {
3379            self.panes[i].redraw_for(FrameCause::APPEARANCE);
3380        }
3381        if let Some(req) = req {
3382            self.dispatch(event_loop, i, InputEvent::Access(req));
3383        }
3384        if let Some(pane) = self.panes.get_mut(i) {
3385            pane.publish_access();
3386        }
3387    }
3388
3389    /// Runs after every event batch (including timer wake-ups): the caret
3390    /// blink clock, the transition clock, and the audio poll. Any caret
3391    /// activity re-arms the blink timer with the caret solid; each expiry
3392    /// toggles the phase and schedules the next. A transition mid-flight
3393    /// asks for the next frame right away (vsync paces it). While a sound
3394    /// plays, the loop wakes every `AUDIO_POLL` to notice it finishing.
3395    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
3396        // A loop taken back from an earlier runner delivered its init
3397        // events — `resumed` among them — to that runner's shell; this one
3398        // opens its window from the first turn it gets instead.
3399        if self.pumped && !self.opened && !self.exit_requested {
3400            self.resumed(event_loop);
3401        }
3402        // The platform's menu comes and goes between turns of the loop, so
3403        // this is where its answer is collected. The menu bar is handed
3404        // over and read back from the same place, for the same reason.
3405        self.pump_native_menu(event_loop);
3406        self.pump_menu_bar(event_loop);
3407        self.pump_text_input(event_loop);
3408        self.pump_file_drag(event_loop);
3409        self.pump_open_documents(event_loop);
3410        self.settle_focus();
3411        self.apply_secure_input();
3412        self.dismiss_popups_if_deactivated();
3413        self.poll_audio();
3414        self.apply_audio();
3415        let now = std::time::Instant::now();
3416        let mut deadline: Option<std::time::Instant> = None;
3417        // A frame asked for at a time (backlog F135): by a `Waker` for
3418        // every window, by a view for its own (`Core::next_frame_at`, on
3419        // the frame clock, whose origin is `epoch`). Drawn once it is due,
3420        // and slept to until then.
3421        if let Some(at) = self.wake_at {
3422            if at <= now {
3423                self.wake_at = None;
3424                for pane in &self.panes {
3425                    pane.redraw_for(FrameCause::WAKE);
3426                }
3427            } else {
3428                deadline = Some(deadline.map_or(at, |d| d.min(at)));
3429            }
3430        }
3431        for pane in &self.panes {
3432            let Some(secs) = pane.core.next_frame_at().filter(|s| s.is_finite()) else {
3433                continue;
3434            };
3435            let at = self.epoch + std::time::Duration::from_secs_f64(secs.max(0.0));
3436            if at <= now {
3437                pane.redraw_for(FrameCause::WAKE);
3438            } else {
3439                deadline = Some(deadline.map_or(at, |d| d.min(at)));
3440            }
3441        }
3442        // A new device owed (`reopen_owed`): made now if its second is
3443        // up, and otherwise — or when this try left a window without one —
3444        // woken for when it is. This deadline is the only thing that
3445        // brings the loop back to it: a window waiting for a device asks
3446        // for no frames of its own, below.
3447        if self.reopen_owed {
3448            if reopen_due(self.reopened, now) <= now {
3449                self.reopen_device();
3450            }
3451            if self.reopen_owed {
3452                let at = reopen_due(self.reopened, now);
3453                deadline = Some(deadline.map_or(at, |d| d.min(at)));
3454            }
3455        }
3456        for pane in &mut self.panes {
3457            // Not while the window waits for a device: with nothing to
3458            // present to there is no vsync to pace the frame, and each one
3459            // would only find the device still owed. Nor while it is
3460            // minimized or covered (`Pane::animates_now`, RG45, RG98).
3461            // Nor while the pacer holds one for the display: the link
3462            // brings that frame back at the next vsync, `overdue` below
3463            // is the wake if it does not, and asking again in between is
3464            // held again at once — the loop went round as fast as it
3465            // could until the vsync came (backlog F103). Nor straight
3466            // after a frame the surface skipped: that returned at once,
3467            // with no vsync behind it, and the next is asked for a retry
3468            // apart instead of on the loop's next turn (`mod retry`).
3469            if pane.animates_now() && !pane.pacer.holding() {
3470                match pane.retry.animation_waits(now) {
3471                    None => pane.redraw_for(FrameCause::OWED),
3472                    Some(at) => deadline = Some(deadline.map_or(at, |d| d.min(at))),
3473                }
3474            }
3475            // A frame held for a display that stopped firing is drawn
3476            // anyway once it has waited too long (`mod pacer`).
3477            if let Some((at, due)) = pane.pacer.overdue(now) {
3478                if due {
3479                    pane.redraw_for(FrameCause::OVERDUE);
3480                } else {
3481                    deadline = Some(deadline.map_or(at, |d| d.min(at)));
3482                }
3483            }
3484            // A frame the surface skipped asks again — a retry apart, and
3485            // only so many times (`mod retry`).
3486            let (ask, wake) = pane.retry.poll(now);
3487            if ask {
3488                pane.redraw_for(FrameCause::RETRY);
3489            }
3490            if let Some(at) = wake {
3491                deadline = Some(deadline.map_or(at, |d| d.min(at)));
3492            }
3493            // A caret to blink: the stock editor's, or the `caret` a
3494            // custom editor declares on one of its lines (backlog C35).
3495            // None, or a solid one (`caret_solid`, F68): the phase is
3496            // parked on — focused window or not, since the clock owns
3497            // the phase only of a caret it blinks; what a solid caret
3498            // looks like without the keyboard (hollow, as a GUI editor's
3499            // block goes; dimmed; gone) is the view's own reading of
3500            // `env.focused`, which the hiding below is not a substitute
3501            // for (RG14 (j), in F68's entry). Caught mid-blink — Escape
3502            // from insert mode on the off phase — the frame that handled
3503            // the key read the phase off, so one more is asked for; once,
3504            // not a loop.
3505            if !pane.core.has_caret() {
3506                if !pane.blink_visible {
3507                    pane.blink_visible = true;
3508                    pane.core.set_caret_visible(true);
3509                    pane.redraw_for(FrameCause::CARET);
3510                }
3511                pane.blink_deadline = None;
3512                continue;
3513            }
3514            // A caret in a window that does not have the keyboard is not
3515            // blinking on any platform, and the blink is the one thing in
3516            // an idle app that asks for a frame twice a second forever: an
3517            // editor left in the background drew 120 frames a minute for a
3518            // caret nobody could type into. Hidden rather than parked
3519            // solid, because solid is what a *focused* field looks like.
3520            // The caret comes back with the keyboard: `blink_deadline` is
3521            // cleared here, and a cleared deadline is what the arm below
3522            // reads as "start blinking", so the first `about_to_wait`
3523            // after focus returns shows it and re-arms.
3524            if !pane.core.env.focused {
3525                if pane.blink_visible {
3526                    pane.blink_visible = false;
3527                    pane.core.set_caret_visible(false);
3528                    pane.redraw_for(FrameCause::CARET);
3529                }
3530                pane.blink_deadline = None;
3531                continue;
3532            }
3533            let stamp = pane.core.caret_stamp();
3534            if stamp != pane.caret_stamp_seen || pane.blink_deadline.is_none() {
3535                pane.caret_stamp_seen = stamp;
3536                pane.blink_deadline = Some(now + BLINK_INTERVAL);
3537                if !pane.blink_visible {
3538                    pane.blink_visible = true;
3539                    pane.core.set_caret_visible(true);
3540                    pane.redraw_for(FrameCause::CARET);
3541                }
3542            } else if now >= pane.blink_deadline.unwrap() {
3543                pane.blink_visible = !pane.blink_visible;
3544                pane.core.set_caret_visible(pane.blink_visible);
3545                pane.blink_deadline = Some(now + BLINK_INTERVAL);
3546                pane.redraw_for(FrameCause::CARET);
3547            }
3548            if let Some(d) = pane.blink_deadline {
3549                deadline = Some(deadline.map_or(d, |e| e.min(d)));
3550            }
3551        }
3552        if self.audio.active() {
3553            let poll = now + AUDIO_POLL;
3554            deadline = Some(deadline.map_or(poll, |d| d.min(poll)));
3555        }
3556        // An output device nothing has used for `AUDIO_IDLE_CLOSE` is let
3557        // go: it is a real-time thread the OS keeps calling, and it is what
3558        // an idle app that owns a sound spends its whole CPU on. The wake
3559        // this schedules is the point — under `ControlFlow::Wait` a truly
3560        // idle app is never called again, so without a deadline the close
3561        // would be scheduled and never run.
3562        if self.audio.holds_device() {
3563            if !self.audio.active() && self.audio_touch.elapsed() >= AUDIO_IDLE_CLOSE {
3564                self.audio.close();
3565            }
3566            if self.audio.holds_device() {
3567                // Either it is still in use (wake when the hold expires) or
3568                // the close found it mid-open (ask again shortly).
3569                let at = (self.audio_touch + AUDIO_IDLE_CLOSE).max(now + AUDIO_POLL);
3570                deadline = Some(deadline.map_or(at, |d| d.min(at)));
3571            }
3572        }
3573        // A batch that carried an event has left a redraw asked for, so the
3574        // shell wants pumping at once to present it — and a driver reading
3575        // this is also being told that the window is in use, which is the
3576        // one thing it cannot work out from the events *it* was handed.
3577        if std::mem::take(&mut self.saw_event) {
3578            deadline = Some(now);
3579            self.woke = true;
3580        }
3581        self.next_deadline = deadline;
3582        event_loop.set_control_flow(match deadline {
3583            Some(d) => ControlFlow::WaitUntil(d),
3584            None => ControlFlow::Wait,
3585        });
3586    }
3587}
3588
3589// Re-exported so apps can reach the renderer without depending on kui-wgpu.
3590pub use kui_wgpu::{RenderError, Renderer, wgpu};
3591
3592/// `KUI_LOSE_DEVICE=SECS`, read once: when after launch to pretend the
3593/// device was lost (`Shell::redraw`), or never.
3594fn lose_device_at() -> Option<f64> {
3595    static AT: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
3596    *AT.get_or_init(|| std::env::var("KUI_LOSE_DEVICE").ok()?.parse().ok())
3597}
3598
3599#[cfg(test)]
3600mod tests {
3601    use super::*;
3602
3603    struct Empty;
3604    impl App for Empty {
3605        fn view(&mut self, _ui: &mut Ui<'_>) {}
3606    }
3607
3608    /// An app that borrows is still an app: the runner is compiled once
3609    /// against `Shell<dyn App + 'a>`, and neither `run` nor
3610    /// `open` asks for `'static`. Checked by the compiler alone: a loop
3611    /// cannot be built on a test's worker thread.
3612    #[test]
3613    fn an_app_that_borrows_still_runs() {
3614        struct Borrowing<'a>(&'a str);
3615        impl App for Borrowing<'_> {
3616            fn view(&mut self, ui: &mut Ui<'_>) {
3617                ui.text(self.0, TextStyle::new(12.0));
3618            }
3619        }
3620        let title = String::from("t");
3621        let _run = || app("t").run(Borrowing(&title));
3622        let _open = || {
3623            let mut runner = app("t").open(Borrowing(&title))?;
3624            let Borrowing(s) = runner.app_mut();
3625            assert_eq!(*s, "t");
3626            Ok::<_, Box<dyn std::error::Error>>(())
3627        };
3628    }
3629
3630    /// `App::teardown` runs once, whichever of the runner's ends comes
3631    /// first and however many come after:
3632    /// `retire` — what a pump returning false, `request_exit` and the
3633    /// runner's drop all reach — and `teardown_once` itself, which is what
3634    /// the loop's `exiting` calls. The runner is built without a loop
3635    /// here: winit builds its loop on the main thread only, and a test
3636    /// runs on a worker.
3637    #[test]
3638    fn teardown_runs_once_across_retire_and_drop() {
3639        use std::cell::Cell;
3640        use std::rc::Rc;
3641        struct Counting(Rc<Cell<u32>>);
3642        impl App for Counting {
3643            fn view(&mut self, _ui: &mut Ui<'_>) {}
3644            fn teardown(&mut self) {
3645                self.0.set(self.0.get() + 1);
3646            }
3647        }
3648        let count = Rc::new(Cell::new(0));
3649        let runner_of = |app: Counting| PumpRunner {
3650            state: PumpState {
3651                event_loop: None,
3652                alive: true,
3653                pumps: 0,
3654                woken_pumps: 0,
3655            },
3656            shell: std::mem::ManuallyDrop::new(super::app("t").diagnostics(false).shell(app)),
3657        };
3658        let mut runner = runner_of(Counting(count.clone()));
3659        assert_eq!(count.get(), 0, "nothing before the end");
3660        runner.request_exit();
3661        assert_eq!(count.get(), 0, "asking is not the end");
3662        runner.retire();
3663        assert_eq!(count.get(), 1, "retiring is");
3664        assert!(!runner.state.alive);
3665        runner.retire();
3666        runner.shell_mut().teardown_once();
3667        assert_eq!(count.get(), 1, "once, however many ends come after");
3668        drop(runner);
3669        assert_eq!(
3670            count.get(),
3671            1,
3672            "the drop retires again, and it is still once"
3673        );
3674
3675        // The drop alone — a runner let go of while alive — is an end too.
3676        let count = Rc::new(Cell::new(0));
3677        drop(runner_of(Counting(count.clone())));
3678        assert_eq!(count.get(), 1);
3679    }
3680
3681    /// A turn whose batch saw an event is counted once, and the flag is
3682    /// taken with it: left set, every quiet turn after the first event
3683    /// would count, and a test reading `woken_pumps` would
3684    /// see the desktop in every idle second. Driven through `tally`, the
3685    /// step each pump ends with, since `about_to_wait` — which sets the
3686    /// flag — needs a live loop, and winit builds one on the main thread
3687    /// only.
3688    #[test]
3689    fn a_woken_turn_is_counted_once() {
3690        let mut runner = PumpRunner {
3691            state: PumpState {
3692                event_loop: None,
3693                alive: true,
3694                pumps: 0,
3695                woken_pumps: 0,
3696            },
3697            shell: std::mem::ManuallyDrop::new(super::app("t").diagnostics(false).shell(Empty)),
3698        };
3699        let tally = |r: &mut PumpRunner<Empty>| r.state.tally(&mut **r.shell);
3700        tally(&mut runner);
3701        assert_eq!(runner.woken_pumps(), 0, "a quiet turn is not woken");
3702        runner.shell_mut().woke = true;
3703        tally(&mut runner);
3704        assert_eq!(runner.woken_pumps(), 1);
3705        assert!(!runner.shell().woke, "taken by the turn that counted it");
3706        tally(&mut runner);
3707        tally(&mut runner);
3708        assert_eq!(
3709            runner.woken_pumps(),
3710            1,
3711            "and the quiet turns after it are quiet"
3712        );
3713    }
3714
3715    /// A frame waits only for an answer that is actually owed, and never
3716    /// twice running — the bound that keeps a stream of input, or a
3717    /// platform modal loop the host cannot interrupt, from stopping the
3718    /// window altogether.
3719    /// A press and a release finish something the core drew; a pointer
3720    /// moving and a wheel turning do not, and a frame that waited on those
3721    /// would cost a drag half its frames.
3722    #[test]
3723    fn only_a_discrete_input_is_worth_waiting_for() {
3724        assert!(input_completes(&InputEvent::mouse_down(1)));
3725        assert!(input_completes(&InputEvent::mouse_up()));
3726        assert!(input_completes(&InputEvent::Text("x".into())));
3727        assert!(!input_completes(&InputEvent::CursorMoved(Vec2::ZERO)));
3728        assert!(!input_completes(&InputEvent::Scroll(Vec2::ZERO)));
3729        assert!(!input_completes(&InputEvent::CursorLeft));
3730    }
3731
3732    #[test]
3733    fn a_frame_never_waits_twice_running() {
3734        assert!(frame_waits_for_host(true, false));
3735        assert!(!frame_waits_for_host(true, true));
3736        assert!(!frame_waits_for_host(false, false));
3737        assert!(!frame_waits_for_host(false, true));
3738    }
3739
3740    /// The default is an app that answers inside `on_event`; only a host
3741    /// driving the loop itself opts out.
3742    #[test]
3743    fn deferred_events_is_opt_in() {
3744        assert!(!app("t").deferred_events);
3745        assert!(app("t").deferred_events().deferred_events);
3746    }
3747
3748    #[test]
3749    fn clamp_size_honors_each_bound() {
3750        let bounds = (Some((400.0, 300.0)), Some((1200.0, 900.0)));
3751        assert_eq!(
3752            clamp_size((800.0, 600.0), bounds.0, bounds.1),
3753            (800.0, 600.0)
3754        );
3755        assert_eq!(
3756            clamp_size((100.0, 100.0), bounds.0, bounds.1),
3757            (400.0, 300.0)
3758        );
3759        assert_eq!(
3760            clamp_size((4000.0, 4000.0), bounds.0, bounds.1),
3761            (1200.0, 900.0)
3762        );
3763        // Per-axis, and unbounded sides pass through untouched.
3764        assert_eq!(
3765            clamp_size((100.0, 4000.0), bounds.0, bounds.1),
3766            (400.0, 900.0)
3767        );
3768        assert_eq!(clamp_size((10.0, 10.0), None, None), (10.0, 10.0));
3769        // A max below the min loses to it, as the platforms resolve it.
3770        assert_eq!(
3771            clamp_size((800.0, 600.0), Some((500.0, 500.0)), Some((200.0, 200.0))),
3772            (500.0, 500.0)
3773        );
3774    }
3775
3776    #[test]
3777    fn diagnostics_follow_the_build_unless_told_otherwise() {
3778        assert_eq!(
3779            app("t").dyn_shell(Empty).core_mut().diagnostics(),
3780            cfg!(debug_assertions)
3781        );
3782        assert!(
3783            app("t")
3784                .diagnostics(true)
3785                .dyn_shell(Empty)
3786                .core_mut()
3787                .diagnostics()
3788        );
3789        assert!(
3790            !app("t")
3791                .diagnostics(false)
3792                .dyn_shell(Empty)
3793                .core_mut()
3794                .diagnostics()
3795        );
3796    }
3797
3798    /// `Launcher::core`: what the host registered on the
3799    /// core it hands over is the window's, its session is the app's, and
3800    /// what the launcher was told still lands on top, in order.
3801    #[test]
3802    fn a_handed_core_is_the_main_window_s_and_its_session_the_app_s() {
3803        let session = Session::new();
3804        let mut core = Core::new_in(&session);
3805        core.set_devtools(true);
3806        core.set_native_menus(false);
3807        let image = core.resources.add_image(1, 1, vec![0; 4]);
3808        let mut shell = app("t").diagnostics(false).core(core).dyn_shell(Empty);
3809        assert!(shell.session.is(&session), "the session came along");
3810        assert!(shell.core_mut().devtools(), "the devtools door held");
3811        assert!(!shell.core_mut().native_menus(), "so did the menus one");
3812        assert!(
3813            !shell.core_mut().diagnostics(),
3814            "the launcher's own setting lands after"
3815        );
3816        assert_eq!(
3817            shell.core_mut().resources.image_size(image),
3818            Some((1, 1)),
3819            "a resource the host registered draws in the window"
3820        );
3821
3822        // `setup_core` runs on the handed core, last.
3823        let mut core = Core::new();
3824        core.set_devtools(true);
3825        let mut shell = app("t")
3826            .core(core)
3827            .setup_core(|c| c.set_devtools(false))
3828            .dyn_shell(Empty);
3829        assert!(!shell.core_mut().devtools());
3830    }
3831
3832    /// `Launcher::devtools_key` is the `set_devtools_key` door in builder
3833    /// form: the chord lands on the main window's core before its first
3834    /// frame, and the default stands for an app that never asked.
3835    #[test]
3836    fn the_devtools_chord_is_the_launcher_s_to_respell() {
3837        let mut shell = app("t").diagnostics(false).dyn_shell(Empty);
3838        assert_eq!(shell.core_mut().devtools_key().spelling(), "ctrl+shift+i");
3839        let mut shell = app("t")
3840            .diagnostics(false)
3841            .devtools_key(Accel::parse("f12").unwrap())
3842            .dyn_shell(Empty);
3843        assert_eq!(shell.core_mut().devtools_key().spelling(), "f12");
3844    }
3845
3846    /// An icon from a PNG's bytes (backlog F138), and bytes that are not
3847    /// one refused with the reason.
3848    #[test]
3849    fn launcher_takes_an_icon_from_a_png() {
3850        let img = image::RgbaImage::from_pixel(16, 16, image::Rgba([10, 20, 30, 255]));
3851        let mut png = std::io::Cursor::new(Vec::new());
3852        img.write_to(&mut png, image::ImageFormat::Png).unwrap();
3853        let l = app("t").try_icon_bytes(png.get_ref()).unwrap();
3854        assert!(l.icon.is_some());
3855        let e = app("t").try_icon_bytes(b"not a png").err().unwrap();
3856        assert!(e.starts_with("an icon: not an image"), "{e}");
3857    }
3858
3859    #[test]
3860    fn launcher_clamps_the_initial_size_into_the_bounds() {
3861        let shell = app("t")
3862            .size(320.0, 240.0)
3863            .min_size(640.0, 480.0)
3864            .dyn_shell(Empty);
3865        assert_eq!(shell.size, (640.0, 480.0));
3866        assert_eq!(shell.min_size, Some((640.0, 480.0)));
3867
3868        let shell = app("t")
3869            .size(1600.0, 1200.0)
3870            .max_size(800.0, 600.0)
3871            .dyn_shell(Empty);
3872        assert_eq!(shell.size, (800.0, 600.0));
3873        assert_eq!(shell.max_size, Some((800.0, 600.0)));
3874
3875        // Builder order is irrelevant: the clamp happens once, at `shell`.
3876        let shell = app("t")
3877            .min_size(640.0, 480.0)
3878            .size(320.0, 240.0)
3879            .dyn_shell(Empty);
3880        assert_eq!(shell.size, (640.0, 480.0));
3881    }
3882
3883    /// A device that will not open is tried once a second (RG29): the
3884    /// first ask runs at once, an ask inside the second after a try waits
3885    /// for the second to be up — the instant `about_to_wait` sleeps until,
3886    /// not a frame asked for every turn — and one after it runs at once.
3887    #[test]
3888    fn a_reopen_waits_out_the_second_after_the_last_try() {
3889        let now = std::time::Instant::now();
3890        assert_eq!(reopen_due(None, now), now, "the first try is at once");
3891        let recent = now - std::time::Duration::from_millis(300);
3892        assert_eq!(
3893            reopen_due(Some(recent), now),
3894            recent + REOPEN_INTERVAL,
3895            "inside the second: woken when it is up"
3896        );
3897        assert!(reopen_due(Some(recent), now) > now, "and not before");
3898        let old = now - std::time::Duration::from_secs(5);
3899        assert_eq!(reopen_due(Some(old), now), now, "past it: at once");
3900        let edge = now - REOPEN_INTERVAL;
3901        assert_eq!(reopen_due(Some(edge), now), now, "at it: at once");
3902    }
3903
3904    /// A surface refused frame after frame while its reopen waits (RG30):
3905    /// retried `SURFACE_TRIES` times, then given up — said once — and the
3906    /// count saturates rather than wrapping back into retries or, in a
3907    /// debug build, panicking, however many frames input asks for.
3908    #[test]
3909    fn a_refused_surface_is_retried_then_given_up_once_and_the_count_saturates() {
3910        let mut tries = 0u8;
3911        let answers: Vec<Refused> = (0..1000).map(|_| surface_refused(&mut tries)).collect();
3912        let retries = SURFACE_TRIES as usize;
3913        assert!(answers[..retries].iter().all(|a| *a == Refused::Retry));
3914        assert_eq!(answers[retries], Refused::GiveUp { say: true });
3915        assert!(
3916            answers[retries + 1..]
3917                .iter()
3918                .all(|a| *a == Refused::GiveUp { say: false }),
3919            "given up, and said only the once"
3920        );
3921        assert_eq!(tries, u8::MAX);
3922    }
3923
3924    /// Subpixel text follows the device it is drawn on (RG32): asked for
3925    /// or left to `Auto`, it is on only where the device blends per
3926    /// channel — so a reopen onto an adapter without dual-source blending
3927    /// turns it off — and grayscale asked for is grayscale everywhere.
3928    #[test]
3929    fn subpixel_text_is_decided_by_each_device() {
3930        assert!(subpixel_on(TextAa::Auto, true));
3931        assert!(!subpixel_on(TextAa::Auto, false));
3932        assert!(subpixel_on(TextAa::Subpixel, true));
3933        assert!(!subpixel_on(TextAa::Subpixel, false));
3934        assert!(!subpixel_on(TextAa::Grayscale, true));
3935        assert!(!subpixel_on(TextAa::Grayscale, false));
3936    }
3937
3938    /// Over a backdrop `Auto` is grayscale (backlog F126): an LCD mask is
3939    /// a blend against pixels the window cannot see. What the app named
3940    /// stands, and an opaque window keeps `Auto`.
3941    #[test]
3942    fn a_backdrop_takes_auto_to_grayscale() {
3943        assert_eq!(aa_under(TextAa::Auto, Backdrop::Opaque), TextAa::Auto);
3944        for b in [Backdrop::Transparent, Backdrop::Blur, Backdrop::Tinted] {
3945            assert_eq!(aa_under(TextAa::Auto, b), TextAa::Grayscale);
3946            assert_eq!(aa_under(TextAa::Subpixel, b), TextAa::Subpixel);
3947        }
3948        assert!(!subpixel_on(aa_under(TextAa::Auto, Backdrop::Tinted), true));
3949    }
3950
3951    /// An app that never asked opens its device as it always did: no
3952    /// transparent surfaces, so on Windows the swapchain on the window's
3953    /// handle it always had.
3954    #[test]
3955    fn only_a_backdrop_opens_a_transparent_device() {
3956        let shell = app("plain").dyn_shell(Empty);
3957        assert!(!shell.gpu_options().transparent);
3958        let shell = app("glass").backdrop(Backdrop::Blur).dyn_shell(Empty);
3959        assert!(shell.gpu_options().transparent);
3960    }
3961}