kui_core/runtime/windows.rs
1//! The window this core draws and the windows a frame declares:
2//! the declared set and its diff into
3//! `Open` / `Close`, the command queue drivers drain, the title and the
4//! window level.
5
6use super::*;
7
8impl Core {
9 // -- Declared windows ---------------------------------------------------
10 // `docs/adr/0004-multi-window.md`, decisions 4-6: a window's existence
11 // is declared the way a title is, the session diffs the union of what
12 // every live window's frame declared, and the diff's `Open` / `Close`
13 // go out through the same queue chrome commands do.
14
15 /// Declares that a window named `name` exists this frame.
16 ///
17 /// The window opens on the first frame any core declares it — that
18 /// frame's `finish_frame` queues a [`crate::WindowCommand::Open`] with
19 /// the id the core assigned and raises `{kind:"window",
20 /// phase:"opened", name, id}` — and closes, with a `Close` and a
21 /// `phase:"closed"`, on the first frame none does. Declared every
22 /// frame it costs nothing after the first.
23 ///
24 /// `config` is read on that opening edge and never again: re-declaring
25 /// a live window at another size changes nothing, because the user
26 /// owns its geometry once it exists. Where two declarations of one
27 /// name disagree on that edge, the lowest declaring window's first
28 /// declaration wins and [`crate::diag::DUPLICATE_WINDOW_CONFIG`]
29 /// says so. A window the user closed (see [`Self::window_closed`])
30 /// does not reopen while it is still declared — the declaration has
31 /// to stop and start again — and keeps raising
32 /// [`crate::diag::WINDOW_DECLARED_WHILE_CLOSED`] until it does.
33 /// `"main"` names the window the launcher opened and is always live.
34 pub fn declare_window(&mut self, name: &str, mut config: WindowConfig) {
35 self.taint_kept("it declared a window");
36 // A popup's anchor is declared in the host's coordinates and
37 // resolved by the driver against the window's (ADR 0024): the
38 // dock's offset goes back on here.
39 let shift = self.dt_shift();
40 config.anchor.x += shift.x;
41 config.anchor.y += shift.y;
42 if let Some(d) = self.declared_windows.iter_mut().find(|d| &*d.name == name) {
43 // First declaration wins within a frame; a disagreement is
44 // remembered for the opening edge to report.
45 d.conflict |= d.config != config;
46 return;
47 }
48 self.declared_windows.push(WindowDecl {
49 name: Rc::from(name),
50 config,
51 origin: self.origin,
52 conflict: false,
53 });
54 }
55
56 /// The windows declared while the current frame was built, for the
57 /// scene corpus's coverage derivation: a declaration leaves no node.
58 #[cfg(feature = "conformance")]
59 pub(crate) fn declared_windows(&self) -> &[WindowDecl] {
60 &self.declared_windows
61 }
62
63 /// The name of the window this core draws: `"main"` for
64 /// `WindowId::MAIN`, else the name the declaration that opened
65 /// `env.window.id` used. A driver that gave a core an id the session
66 /// never opened gets the id spelled out, so the answer is never empty.
67 pub fn window_name(&self) -> Rc<str> {
68 let id = self.env.window.id;
69 if id == WindowId::MAIN {
70 return Rc::from(MAIN_WINDOW_NAME);
71 }
72 self.session
73 .state()
74 .windows
75 .name_of(id)
76 .unwrap_or_else(|| Rc::from(format!("window-{}", id.0).as_str()))
77 }
78
79 /// Every window of the session that is open right now — main first,
80 /// then in the order they opened — as `(id, name)`. What the
81 /// declaration diff has asked for, not what the driver has shown:
82 /// the two agree once the driver drains its commands.
83 pub fn windows(&self) -> Vec<(WindowId, Rc<str>)> {
84 self.session.state().windows.live()
85 }
86
87 /// The driver reports that the OS closed window `id` — its close
88 /// button, a keyboard shortcut, the window manager. The window is gone
89 /// and stays gone while its name is still declared (the app has to stop
90 /// declaring it and start again to reopen it; see
91 /// [`Self::declare_window`]); its own declarations leave the union, so
92 /// whatever only it declared closes too. Raises `{kind:"window",
93 /// phase:"closed", name, id}` for the app, pending like a resize.
94 /// Nothing happens for the main window (closing it ends the app) or
95 /// for a window the diff already closed.
96 pub fn window_closed(&mut self, id: WindowId) {
97 let mut changes = Vec::new();
98 let name = {
99 let sess = &mut *self.session.state();
100 let Some(name) = sess.windows.os_closed(id) else {
101 return;
102 };
103 sess.windows.diff(&mut changes);
104 name
105 };
106 self.push_window_event("closed", &name, id);
107 self.release_window_audio(id);
108 self.apply_window_changes(changes);
109 }
110
111 /// A window that will finish no more frames leaves its `audio` mounts
112 /// behind: they are reconciled against that window's frames alone,
113 /// so nothing else would ever stop them — a popup's looped bed
114 /// played on after the popup closed. Reconciling the window against
115 /// the nothing it now declares stops each mount by the rule a removed
116 /// node follows (`finish` releases a one-shot, a loop stops).
117 fn release_window_audio(&mut self, id: WindowId) {
118 self.session.state().audio.reconcile(id);
119 }
120
121 /// The driver reports that window `id` was asked to go away — a press
122 /// landed outside it, or Escape reached it (see
123 /// [`crate::window::DismissReason`]). Raises `{kind:"dismiss", reason,
124 /// name, id}` on the root, pending like a resize, and **closes
125 /// nothing**: only the app can stop declaring the window, and it does
126 /// that on the frame it decides to, the way a modal closes. So an app that
127 /// graduates a dropdown from a `modal` float
128 /// to a popup window changes its declaration and keeps its handler.
129 ///
130 /// Both facts behind it are the OS's — a press outside a window lands
131 /// in another surface, and a non-activating popup never holds the
132 /// keyboard — so the core cannot notice either; a driver reports them
133 /// the way it reports a close ([`Self::window_closed`]). Nothing
134 /// happens for a window the session has not opened.
135 pub fn dismiss_window(&mut self, id: WindowId, reason: crate::window::DismissReason) {
136 let Some(name) = self.session.state().windows.name_of(id) else {
137 return;
138 };
139 self.pending.push(UiEvent {
140 origin: OriginId::HOST,
141 window: WindowId::MAIN,
142 key: Key::ROOT,
143 payload: Value::map([
144 ("kind", Value::str("dismiss")),
145 ("reason", Value::str(reason.as_str())),
146 ("name", Value::str(&*name)),
147 ("id", Value::Int(id.0 as i64)),
148 ]),
149 slot: None,
150 });
151 }
152
153 /// Hands this frame's declarations to the session and takes back what
154 /// the union's diff decided. Runs at `finish_frame`; skipped whole when
155 /// this frame declared what the last one did and no window is sitting
156 /// closed-but-declared, which is every frame of a single-window app.
157 pub(crate) fn sync_windows(&mut self) {
158 let changed = self.declared_windows != self.declared_windows_last;
159 let mut changes = Vec::new();
160 let mut still_closed: Vec<Rc<str>> = Vec::new();
161 {
162 let sess = &mut *self.session.state();
163 let reg = &mut sess.windows;
164 if !changed && !reg.any_closed() {
165 return;
166 }
167 if changed {
168 reg.set_slot(self.env.window.id, &self.declared_windows);
169 reg.diff(&mut changes);
170 }
171 still_closed.extend(reg.closed_among(&self.declared_windows));
172 }
173 for name in still_closed {
174 self.diag
175 .raise(crate::diag::window_declared_while_closed(&name));
176 }
177 self.apply_window_changes(changes);
178 }
179
180 /// Turns the diff's decisions into what a driver and an app see: a
181 /// command in the queue, a `{kind:"window"}` event, and the conflict
182 /// warning where the opening edge found one.
183 fn apply_window_changes(&mut self, changes: Vec<WindowChange>) {
184 for change in changes {
185 match change {
186 WindowChange::Opened {
187 id,
188 name,
189 owner,
190 origin,
191 config,
192 conflict,
193 } => {
194 self.interaction
195 .window_commands
196 .push(crate::window::WindowCommand::Open {
197 id,
198 owner,
199 origin,
200 config,
201 });
202 self.push_window_event("opened", &name, id);
203 if conflict {
204 self.diag.raise(crate::diag::duplicate_window_config(&name));
205 }
206 }
207 WindowChange::Closed { id, name } => {
208 self.interaction
209 .window_commands
210 .push(crate::window::WindowCommand::Close(id));
211 self.push_window_event("closed", &name, id);
212 self.release_window_audio(id);
213 }
214 }
215 }
216 }
217
218 /// `{kind:"window", phase, name, id}` on the root, pending for the
219 /// driver to route after the frame (or with the next input). `id` is
220 /// in the payload because `UiEvent::window` says which core reported
221 /// it, and the diff runs on whichever core finished its frame.
222 pub(crate) fn push_window_event(&mut self, phase: &str, name: &str, id: WindowId) {
223 self.pending.push(UiEvent {
224 origin: OriginId::HOST,
225 window: WindowId::MAIN,
226 key: Key::ROOT,
227 payload: Value::map([
228 ("kind", Value::str("window")),
229 ("phase", Value::str(phase)),
230 ("name", Value::str(name)),
231 ("id", Value::Int(id.0 as i64)),
232 ]),
233 slot: None,
234 });
235 }
236
237 /// Queues a window command as if chrome had produced it, so apps can
238 /// close/minimize/maximize from a keymap or command line. Drained by
239 /// the frame driver with the rest.
240 pub fn push_window_command(&mut self, cmd: crate::window::WindowCommand) {
241 self.interaction.window_commands.push(cmd);
242 }
243
244 /// Asks the driver to resize `window` to `size` (logical px). Queued
245 /// the way `reveal` and `play` queue theirs: a request the driver
246 /// applies on its next pump — after this input dispatch if called from
247 /// a handler, after this frame if called from a view — and one a
248 /// headless driver never applies, since it never drains. The window
249 /// answers through the ordinary `resize` event, with the size it
250 /// actually became.
251 pub fn set_window_size(&mut self, window: crate::window::WindowId, size: crate::geom::Size) {
252 self.interaction
253 .window_commands
254 .push(crate::window::WindowCommand::SetSize { window, size });
255 }
256
257 /// Asks the driver to give `window` keyboard focus; queued like
258 /// [`Core::set_window_size`]. Whether the window manager agrees shows
259 /// up as `env.focused` on the frames that follow, not as a reply.
260 pub fn focus_window(&mut self, window: crate::window::WindowId) {
261 self.interaction
262 .window_commands
263 .push(crate::window::WindowCommand::Focus(window));
264 }
265
266 /// Drains window intents queued since the last drain — by chrome nodes,
267 /// by `push_window_command`, `set_window_size` and `focus_window` — in
268 /// the order they were queued. Frame drivers call this after each input
269 /// dispatch and each frame and apply the commands to the real window;
270 /// headless drivers may simply never call.
271 pub fn take_window_commands(&mut self) -> Vec<crate::window::WindowCommand> {
272 std::mem::take(&mut self.interaction.window_commands)
273 }
274
275 /// Declares this frame's window title. Like all frame state it's data:
276 /// the driver diffs against what's applied and only then touches the
277 /// window. Undeclared frames leave the title alone; last writer wins.
278 pub fn set_window_title(&mut self, title: &str) {
279 self.taint_kept("it declared the window's title");
280 self.window_title = Some(title.to_string());
281 }
282
283 /// The title declared this frame, if any (for the frame driver).
284 pub fn window_title(&self) -> Option<&str> {
285 self.window_title.as_deref()
286 }
287
288 /// Declares that this frame wants the window above every other app's:
289 /// a floating palette, a picture-in-picture player, a
290 /// timer. Frame state like the title, and the driver applies it the
291 /// same way — `set_window_level` when it differs from what is applied,
292 /// nothing when it does not — but it defaults to `false` rather than
293 /// "leave as-is", so a frame that stops declaring it lowers the window
294 /// again and a pin button is a toggle on the app's own state. Whether
295 /// the platform has a level to set is `env.window.always_on_top`: the
296 /// driver's record of what it set, false on Wayland (where winit has
297 /// no call for it) however often the app asks — and not a query, so a
298 /// level the OS dropped afterwards (a fullscreen space, a tiling
299 /// manager) is not reported. A popup's level is its own whatever its
300 /// owner declares.
301 pub fn set_always_on_top(&mut self, on_top: bool) {
302 self.taint_kept("it declared always-on-top");
303 self.always_on_top = on_top;
304 }
305
306 /// Whether this frame asked for the window to stay on top (for the
307 /// frame driver); false for a frame that never said.
308 pub fn always_on_top(&self) -> bool {
309 self.always_on_top
310 }
311
312 /// Declares that this frame wants the keyboard to this window kept
313 /// from other processes while the window has it — macOS's Secure
314 /// Keyboard Entry, what a terminal turns on at a password prompt.
315 /// Frame state like `always_on_top`: a frame that stops
316 /// declaring it turns it off, so an app asks on every frame the
317 /// prompt is up and never has to remember to undo it.
318 ///
319 /// The runner owns the platform call and its balance: it enables
320 /// secure input only while a window whose frame asked has the
321 /// keyboard, and disables it when that window loses the keyboard,
322 /// closes, stops asking, or the app exits — Apple's rule for it,
323 /// since while it is on no other process can read the keyboard at
324 /// all (a launcher's hotkey, a text expander, an accessibility tool).
325 /// `EnableSecureEventInput` is process-wide and counted, and the
326 /// runner holds at most one count however many windows ask. Nothing
327 /// happens on other platforms, which have no such switch.
328 pub fn set_secure_input(&mut self, on: bool) {
329 self.taint_kept("it declared secure input");
330 self.secure_input = on;
331 }
332
333 /// Whether this frame asked for secure keyboard entry (for the frame
334 /// driver); false for a frame that never said.
335 pub fn secure_input(&self) -> bool {
336 self.secure_input
337 }
338
339 /// Declares which Option keys act as Alt in this window on macOS:
340 /// a dead key under that Option — ⌥u, ⌥e, ⌥i, ⌥n,
341 /// ⌥\` — then arrives as the chord `<A-u>` rather than starting an
342 /// accent the app never hears, and a key under it types nothing, as
343 /// under Control. Frame state like `always_on_top`, default
344 /// [`OptionAsAlt::None`](crate::OptionAsAlt::None): a frame that stops
345 /// declaring it gives the Option keys back to the layout, so an app
346 /// declares it on every frame — from a setting, say — and never has
347 /// to undo it.
348 ///
349 /// The runner applies it to the window on change and never per frame.
350 /// A popup never has the keyboard on macOS — its keys come through its
351 /// owner, which stays key — so the owner's declaration is the one a
352 /// popup's keys are read under. Nothing happens on other platforms,
353 /// whose Alt composes nothing.
354 pub fn set_option_as_alt(&mut self, option_as_alt: crate::OptionAsAlt) {
355 self.taint_kept("it declared option-as-alt");
356 self.option_as_alt = option_as_alt;
357 }
358
359 /// Which Option keys this frame asked to act as Alt (for the frame
360 /// driver); `None` for a frame that never said.
361 pub fn option_as_alt(&self) -> crate::OptionAsAlt {
362 self.option_as_alt
363 }
364
365 /// Declares that this window takes the keyboard as keys, with the
366 /// platform's input method off: no composition and no candidate
367 /// window, and on a Mac no dead key waiting for the next one and no
368 /// press-and-hold — which is an input method too, so a held letter
369 /// repeats instead of opening the accent picker, whatever the user's
370 /// `ApplePressAndHoldEnabled` says. A key's `text` is still the
371 /// layout's character; what goes is everything the OS would have
372 /// composed from it. What a modal editor's normal mode wants, where
373 /// `jjjj` is how one moves and a Japanese IME left on eats the
374 /// keymap; its insert mode stops declaring it and gets both back.
375 /// Frame state like `always_on_top`, default off: a frame that stops
376 /// declaring it gives the window its input method back, so an app
377 /// declares it on every frame its mode wants it and never has to
378 /// undo it.
379 ///
380 /// The runner applies it to the window on change and never per
381 /// frame (winit's `set_ime_allowed`); a composition in progress when
382 /// it turns off is ended without a commit, as an empty `preedit`. It
383 /// is the window's, not a node's: a stock editor focused under it
384 /// composes nothing either. A popup's keys arrive through its owner,
385 /// so the owner's declaration is the one they are read under. On
386 /// Windows and Linux the window's IME is disabled the same way, and
387 /// that is all: their dead keys are the layout's (`WM_DEADCHAR`, xkb
388 /// compose), winit composes them whatever the IME says, and they
389 /// still compose.
390 pub fn set_ime_off(&mut self, off: bool) {
391 self.taint_kept("it declared the input method off");
392 self.ime_off = off;
393 }
394
395 /// Whether this frame asked for the input method off (for the frame
396 /// driver); false for a frame that never said.
397 pub fn ime_off(&self) -> bool {
398 self.ime_off
399 }
400}