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