Skip to main content

kui_core/
window.rs

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