Skip to main content

kui_native/
lib.rs

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