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        // 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}