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}