Skip to main content

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}