Skip to main content

kui_core/
window.rs

1//! Windows as data. A node can declare a chrome role (drag handle,
2//! close/minimize/maximize button); interacting with it produces
3//! [`WindowCommand`]s that the frame driver drains and applies to the real
4//! window. A frame can declare that a *window exists* (`Core::declare_window`,
5//! `docs/adr/0004-multi-window.md`): the core diffs the declared set and the
6//! same queue carries the [`WindowCommand::Open`] / [`WindowCommand::Close`]
7//! the diff produces. A declared window is a [`WindowKind::Normal`] one or
8//! a [`WindowKind::Popup`] — borderless, owned, anchored, non-activating —
9//! and a driver reports a popup dismissed the way it reports one closed
10//! ([`DismissReason`]). Host window facts flow back in through [`WindowEnv`]
11//! on `Env`. The core never touches a window — headless drivers just never
12//! drain.
13
14use crate::geom::{Rect, Size};
15use crate::tree::OriginId;
16
17/// Which OS window something belongs to: an opaque integer the core's
18/// declaration diff assigns when it opens a window, not a handle an app
19/// builds. [`WindowId::MAIN`] is 0 — the window the launcher opens, which is
20/// always live. Apps name windows with a stable string
21/// (`Core::declare_window`); the id is how the driver and the events refer
22/// to the surface that string opened.
23///
24/// It crosses every transport as a plain integer — `UiEvent::window` in
25/// Rust, `window` on a JSX `UiEvent`, `KuiEvent.window` in C — so nothing
26/// has to model window identity twice.
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
28pub struct WindowId(pub u32);
29
30impl WindowId {
31    /// The window the app starts in, and the answer everywhere until a
32    /// driver opens a second one.
33    pub const MAIN: WindowId = WindowId(0);
34}
35
36/// Role a node plays in window chrome (set via `NodeSpec::window_drag` /
37/// `NodeSpec::window_button`). Chrome nodes never emit `UiEvent`s — their
38/// interactions become [`WindowCommand`]s for the driver instead.
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
40pub enum WindowRole {
41    /// Mouse-down here asks the driver to start an OS window drag (drivers
42    /// conventionally promote a quick second press to a maximize toggle).
43    Drag,
44    /// Clicking here emits the button's window command.
45    Button(WindowButton),
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
49pub enum WindowButton {
50    Close,
51    Minimize,
52    Maximize,
53}
54
55impl WindowButton {
56    /// The command a click on this button issues, for the window the
57    /// button was drawn in.
58    pub fn command(self, window: WindowId) -> WindowCommand {
59        match self {
60            WindowButton::Close => WindowCommand::Close(window),
61            WindowButton::Minimize => WindowCommand::Minimize(window),
62            WindowButton::Maximize => WindowCommand::ToggleMaximize(window),
63        }
64    }
65}
66
67/// What kind of OS surface a declared window is
68/// (`docs/adr/0004-multi-window.md`, decision 9).
69#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
70pub enum WindowKind {
71    /// A regular top-level window with the launcher's chrome.
72    #[default]
73    Normal,
74    /// A menu surface: borderless, absent from the taskbar, owned by the
75    /// window that declared it and closed when that window closes, placed
76    /// in screen coordinates against [`WindowConfig::anchor`] rather than
77    /// clamped into a viewport, and **non-activating** by default — it
78    /// must not take OS focus, or opening a combobox would blur the field
79    /// that opened it. While a non-activating popup is up the driver
80    /// routes its owner's keyboard input to it and the owner's
81    /// `env.focused` stays true, so the field still draws focused while
82    /// the arrow keys walk the list.
83    ///
84    /// Reach for it only for the three placements a float cannot make: a
85    /// list taller than the window, a menu near an edge with nowhere
86    /// in-window to go, and a panel the user wants beside the app.
87    /// Everything else is cheaper as a float — see
88    /// [`crate::spec::FloatConfig::fit`] — because a float costs one tree
89    /// and one draw call where this costs an OS surface, a swapchain, a
90    /// `Core` and an accessibility adapter.
91    Popup,
92}
93
94impl WindowKind {
95    /// Every kind, in wire order: the index a binding that spells kinds
96    /// as numbers sends. Append-only.
97    pub const ALL: [WindowKind; 2] = [WindowKind::Normal, WindowKind::Popup];
98
99    /// The wire name, for the bindings and the report.
100    pub fn name(self) -> &'static str {
101        match self {
102            WindowKind::Normal => "normal",
103            WindowKind::Popup => "popup",
104        }
105    }
106
107    /// The kind a wire name spells: the inverse of [`Self::name`].
108    pub fn from_name(name: &str) -> Option<WindowKind> {
109        Self::ALL.into_iter().find(|k| k.name() == name)
110    }
111}
112
113/// What a frame says about a window it declares (`Core::declare_window`).
114/// Plain data by ADR 0004 decision 5 — no title, no callbacks — so a
115/// `WindowCommand` stays `Copy` and equality is derived, which is how the
116/// diff tells two declarations of one name apart.
117///
118/// **Read on the opening edge only.** The config that reaches
119/// [`WindowCommand::Open`] is the one the declaration carried on the frame
120/// it started; a live window's config is never looked at again, so
121/// re-declaring `"palette"` at a new size does not resize it. The user owns
122/// a window's geometry once it exists.
123#[derive(Clone, Copy, Debug, PartialEq)]
124pub struct WindowConfig {
125    pub kind: WindowKind,
126    /// Initial inner size, logical px.
127    pub size: Size,
128    /// Whether opening it takes OS focus. True for a normal window;
129    /// [`WindowConfig::popup`] turns it off, so the field that opened a
130    /// dropdown keeps the ring while the list is up.
131    pub activates: bool,
132    /// [`WindowKind::Popup`] only: what the popup is placed against, as a
133    /// rect in the **declaring window's** logical viewport coordinates —
134    /// which is exactly the rect an `onLayout` node reports, so an app
135    /// needs no new geometry query to fill it. The driver resolves it to
136    /// screen coordinates against the owner's own position and puts the
137    /// popup below it, flipping above when the display's bottom edge is
138    /// nearer than the popup is tall.
139    ///
140    /// Read on the opening edge with the rest of the config and never
141    /// again: a popup that has to follow a moving anchor stops being
142    /// declared and is declared again, which is what a dropdown does when
143    /// its field scrolls away anyway. Ignored by a `Normal` window.
144    pub anchor: Rect,
145}
146
147impl WindowConfig {
148    /// `{kind, width, height, activates, anchor: {x, y, w, h}}`.
149    pub fn to_value(&self) -> crate::value::Value {
150        use crate::value::Value;
151        Value::map([
152            ("kind", Value::str(self.kind.name())),
153            ("width", Value::float(self.size.w)),
154            ("height", Value::float(self.size.h)),
155            ("activates", Value::Bool(self.activates)),
156            ("anchor", self.anchor.to_value()),
157        ])
158    }
159
160    /// The size a declaration that names none gets.
161    pub const DEFAULT_SIZE: Size = Size { w: 640.0, h: 480.0 };
162
163    /// A normal, activating window of `w` x `h` logical px.
164    pub fn sized(w: f32, h: f32) -> Self {
165        Self {
166            size: Size::new(w, h),
167            ..Self::default()
168        }
169    }
170
171    /// A [`WindowKind::Popup`] of `w` x `h` logical px placed against
172    /// `anchor` — the rect, in the declaring window's viewport
173    /// coordinates, that an `onLayout` handler reported for the field or
174    /// button the menu belongs to.
175    ///
176    /// Non-activating, which is the default a popup wants and the reason
177    /// this is a constructor rather than a `kind` you set: a popup that
178    /// takes OS focus blurs whatever opened it. Set `activates` back to
179    /// true afterwards for the rare surface that should steal focus.
180    pub fn popup(anchor: Rect, w: f32, h: f32) -> Self {
181        Self {
182            size: Size::new(w, h),
183            anchor,
184            ..Self::of_kind(WindowKind::Popup)
185        }
186    }
187
188    /// The defaults for a window of `kind`: the one decision the kind
189    /// makes on its own is whether opening it takes OS focus — a popup
190    /// that did would blur the field that opened it, so it does not unless
191    /// asked. Every binding's window entry starts from this.
192    pub fn of_kind(kind: WindowKind) -> Self {
193        Self {
194            kind,
195            activates: kind == WindowKind::Normal,
196            ..Self::default()
197        }
198    }
199
200    /// A declaration from plain data: a bare name (the defaults), or a map
201    /// with `name`, `kind` (`"normal"` | `"popup"`), `width` and `height`
202    /// (both and positive, or the default size), `activates`, and the `anchor` rect
203    /// (`{x, y, w, h}`) a popup is placed against. Returns the name with
204    /// the config. An entry is plain data with a fixed shape, not a node's
205    /// loose prop bag, so a value that does nothing is refused rather than
206    /// dropped: a kind kui does not have would otherwise open a normal
207    /// window and read as the popup having worked.
208    pub fn from_value(v: &crate::value::Value) -> Result<(String, Self), String> {
209        use crate::value::Value;
210        match v {
211            Value::Str(name) => Ok((name.clone(), Self::default())),
212            Value::Map(_) => {
213                let name = v
214                    .get_str("name")
215                    .ok_or("a windows entry needs a name")?
216                    .to_string();
217                let kind = match v.get_str("kind") {
218                    None => WindowKind::Normal,
219                    Some(k) => WindowKind::from_name(k).ok_or_else(|| {
220                        format!(
221                            "windows entry `{name}` has kind {k:?}; the kinds are {}",
222                            WindowKind::ALL
223                                .iter()
224                                .map(|k| format!("{:?}", k.name()))
225                                .collect::<Vec<_>>()
226                                .join(" and ")
227                        )
228                    })?,
229                };
230                let mut cfg = Self::of_kind(kind);
231                let num = |key: &str| v.get(key).and_then(Value::as_float).map(|n| n as f32);
232                // Both, and positive: a zero is the default size, as C's
233                // `kui_window_declare` reads it and the runner opens it —
234                // Node's own decoder said so and this did not (AR43).
235                if let (Some(w), Some(h)) = (num("width"), num("height"))
236                    && w > 0.0
237                    && h > 0.0
238                {
239                    cfg.size = Size::new(w, h);
240                }
241                if let Some(a) = v.get_bool("activates") {
242                    cfg.activates = a;
243                }
244                if let Some(a) = v.get("anchor") {
245                    let f = |key: &str| a.get(key).and_then(Value::as_float).unwrap_or(0.0) as f32;
246                    cfg.anchor = Rect::new(f("x"), f("y"), f("w"), f("h"));
247                }
248                Ok((name, cfg))
249            }
250            other => Err(format!(
251                "a windows entry is a name or a table, not {}",
252                other.type_name()
253            )),
254        }
255    }
256}
257
258impl Default for WindowConfig {
259    fn default() -> Self {
260        Self {
261            kind: WindowKind::Normal,
262            size: Self::DEFAULT_SIZE,
263            activates: true,
264            anchor: Rect::new(0.0, 0.0, 0.0, 0.0),
265        }
266    }
267}
268
269/// A window-level intent for the frame driver, drained via
270/// `Core::take_window_commands` after each input dispatch and each frame.
271/// Three things produce one: input on a chrome node, the declared set's
272/// diff, and an app asking directly (`Core::set_window_size`,
273/// `Core::focus_window`, `Core::push_window_command`). A headless driver
274/// never drains, which is the whole of "the core never touches a window".
275///
276/// Every variant says which window it is about, and every variant is
277/// `Copy` and pointer-free — an `Open` carries no title (the new window's
278/// own first frame declares one through `window_title`) and a `SetSize`
279/// carries two floats — so nothing borrowed ever enters a driver's drain
280/// loop.
281#[derive(Clone, Copy, Debug, PartialEq)]
282pub enum WindowCommand {
283    /// Begin an interactive OS move (the press landed on a `Drag` node).
284    StartDrag(WindowId),
285    /// Close this window: from its chrome close button, or because no
286    /// frame declares it any more. For the main window a driver exits.
287    Close(WindowId),
288    Minimize(WindowId),
289    ToggleMaximize(WindowId),
290    /// Open a window the declared set gained. `id` is the one the core
291    /// assigned and will stamp on the window's events; `origin` is the
292    /// frontend whose declaration won (a host may refuse an extension's);
293    /// `owner` is the window whose frame that declaration came from — what
294    /// a [`WindowKind::Popup`] is anchored against and owned by, and what
295    /// closing takes the popup with it (the owner's declarations leave the
296    /// declared set when the owner does, so the diff closes the child in
297    /// the same pass).
298    Open {
299        id: WindowId,
300        owner: WindowId,
301        origin: OriginId,
302        config: WindowConfig,
303    },
304    /// Resize `window` to `size` (logical px), asked for by the app
305    /// (`Core::set_window_size`). A command and not part of a declaration,
306    /// because the user owns a window's size once it exists — a declared
307    /// size would fight every drag of the window's edge, which is why
308    /// `WindowConfig::size` is read on the opening edge only. The OS may
309    /// answer with a different size (a minimum, a tiling manager); the
310    /// frame that follows posts a `resize` event with whatever it actually
311    /// became, the way every resize already does.
312    SetSize {
313        window: WindowId,
314        size: Size,
315    },
316    /// Give `window` keyboard focus (`Core::focus_window`). Advisory, like
317    /// every focus request an app makes of a window manager.
318    Focus(WindowId),
319    /// Draw `window` again: something another window's frame or input
320    /// changed is shown there (`docs/adr/0024`, decision 7 — the devtools
321    /// window's row hover outlines a node in the main window, and an event
322    /// the main window logs moves the stream in the devtools window). A
323    /// driver that redraws every window on every event may ignore it.
324    Redraw(WindowId),
325}
326
327impl WindowCommand {
328    /// The command's wire name: `startDrag`, `close`, `minimize`,
329    /// `toggleMaximize`, `open`, `setSize`, `focus`, `redraw`.
330    pub fn kind_name(&self) -> &'static str {
331        match self {
332            WindowCommand::StartDrag(_) => "startDrag",
333            WindowCommand::Close(_) => "close",
334            WindowCommand::Minimize(_) => "minimize",
335            WindowCommand::ToggleMaximize(_) => "toggleMaximize",
336            WindowCommand::Open { .. } => "open",
337            WindowCommand::SetSize { .. } => "setSize",
338            WindowCommand::Focus(_) => "focus",
339            WindowCommand::Redraw(_) => "redraw",
340        }
341    }
342
343    /// `{kind, window}` plus what the variant carries: `width`/`height`
344    /// for `setSize`; `owner`, `origin` and `config` for `open`.
345    pub fn to_value(&self) -> crate::value::Value {
346        use crate::value::Value;
347        let mut out = vec![
348            ("kind".to_string(), Value::str(self.kind_name())),
349            ("window".to_string(), Value::Int(self.window().0 as i64)),
350        ];
351        match self {
352            WindowCommand::SetSize { size, .. } => {
353                out.push(("width".into(), Value::float(size.w)));
354                out.push(("height".into(), Value::float(size.h)));
355            }
356            WindowCommand::Open {
357                owner,
358                origin,
359                config,
360                ..
361            } => {
362                out.push(("owner".into(), Value::Int(owner.0 as i64)));
363                out.push(("origin".into(), Value::Int(origin.0 as i64)));
364                out.push(("config".into(), config.to_value()));
365            }
366            _ => {}
367        }
368        Value::Map(out)
369    }
370
371    /// The window the command is about.
372    pub fn window(&self) -> WindowId {
373        match *self {
374            WindowCommand::StartDrag(w)
375            | WindowCommand::Close(w)
376            | WindowCommand::Minimize(w)
377            | WindowCommand::ToggleMaximize(w)
378            | WindowCommand::Focus(w)
379            | WindowCommand::Redraw(w) => w,
380            WindowCommand::Open { id, .. } => id,
381            WindowCommand::SetSize { window, .. } => window,
382        }
383    }
384}
385
386/// Why a window was asked to go away (`Core::dismiss_window`): the same
387/// two reasons ADR 0003 gave a modal node, one level up.
388///
389/// A [`WindowKind::Popup`] is dismissed by the driver, because both facts
390/// are the OS's and not the frame's: a press outside a window lands in
391/// another surface, and Escape reaches a non-activating popup only through
392/// whichever window the OS gave the keyboard to. What the core does with
393/// either is what ADR 0003 decided — raise `{kind:"dismiss", reason}` and
394/// close nothing. The app stops declaring the window on the frame it
395/// decides to, so a dropdown that graduates from a `modal` float to a
396/// popup window changes its declaration and not its handler.
397#[derive(Clone, Copy, Debug, PartialEq, Eq)]
398pub enum DismissReason {
399    /// A press landed outside the window.
400    Outside,
401    /// Escape, wherever the OS delivered it.
402    Escape,
403}
404
405impl DismissReason {
406    /// The string the `dismiss` payload carries, the same spelling a
407    /// modal's `reason` uses.
408    pub fn as_str(self) -> &'static str {
409        match self {
410            DismissReason::Outside => "outside",
411            DismissReason::Escape => "escape",
412        }
413    }
414}
415
416/// What the host knows about its window, pushed into `Env` by the frame
417/// driver. Views (e.g. `widgets::titlebar`) read this to adapt: reserve
418/// space for native controls, pick the maximize/restore glyph, or render
419/// nothing at all under native decorations.
420#[derive(Clone, Copy, Debug, Default, PartialEq)]
421pub struct WindowEnv {
422    /// Which window this core is drawing: the id the `Open` that created
423    /// it carried, written here by the driver (`MAIN` for the launcher's).
424    /// A view reads it here rather than through a query, and it is what the
425    /// core stamps onto every `UiEvent` it hands out — so a driver that
426    /// pushes the rest of these facts has already said where its events
427    /// came from. The window's *name* is `Core::window_name`.
428    pub id: WindowId,
429    /// The host asked the app to draw its own chrome (no native titlebar).
430    pub custom_chrome: bool,
431    pub maximized: bool,
432    pub fullscreen: bool,
433    /// The window is above every other app's: the level the driver set
434    /// after the frame asked (`Core::set_always_on_top`, backlog C30), on
435    /// a platform that has one — where winit has no call for it (Wayland)
436    /// a driver reports false however often the app asks, which is what
437    /// a pin button draws its state from. It is the driver's record of
438    /// what it set and not a query: winit has no level getter, so a level
439    /// the OS dropped afterwards (a fullscreen space, a tiling manager)
440    /// is not seen here (backlog AR49 names the gap).
441    pub always_on_top: bool,
442    /// Area (logical px, window coords) covered by controls the OS still
443    /// draws over our content — macOS traffic lights under custom chrome.
444    /// Views keep out of it; `None` means the OS draws nothing over us.
445    pub native_controls: Option<Rect>,
446}