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