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