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