damascene_core/event.rs
1//! Event types and the [`App`] trait.
2//!
3//! State-driven rebuilds, routed events, keyboard input, and automatic
4//! hover/press/focus visuals. See `docs/LIBRARY_VISION.md` for the application
5//! model this fits into.
6//!
7//! This module owns the *types* — what the host's `App::on_event` sees
8//! and what gets registered as hotkeys. The state machine that produces
9//! these events lives in [`crate::state::UiState`]; the routing helpers
10//! live in [`mod@crate::hit_test`] and [`mod@crate::focus`].
11//!
12//! # The model
13//!
14//! ```ignore
15//! use damascene_core::prelude::*;
16//!
17//! struct Counter { value: i32 }
18//!
19//! impl App for Counter {
20//! fn build(&self, _cx: &BuildCx) -> El {
21//! column([
22//! h1(format!("{}", self.value)),
23//! row([
24//! button("-").key("dec"),
25//! button("+").key("inc"),
26//! ]),
27//! ])
28//! }
29//! fn on_event(&mut self, e: UiEvent, _cx: &EventCx) {
30//! if e.is_click_or_activate("inc") {
31//! self.value += 1;
32//! } else if e.is_click_or_activate("dec") {
33//! self.value -= 1;
34//! }
35//! }
36//! }
37//! ```
38//!
39//! - **Identity** is `El::key`. Tag a node with `.key("...")` and it's
40//! hit-testable (and gets automatic hover/press visuals).
41//! - **The build closure is pure.** It reads `&self`, returns a fresh
42//! tree. The library tracks pointer state, hovered key, pressed key
43//! internally and applies visual deltas after build but before layout
44//! completes.
45//! - **Events flow back via `on_event`.** The library hit-tests pointer
46//! events against the most-recently-laid-out tree and emits
47//! [`UiEvent`]s when something is clicked. The host's `App::on_event`
48//! updates state; the renderer reports whether animation state needs
49//! another redraw.
50
51// Lock in full per-item documentation for this module (issue #73).
52#![warn(missing_docs)]
53
54use crate::tree::{El, Rect};
55
56/// Hit-test target metadata. `key` is the author-facing route, while
57/// `node_id` is the stable laid-out tree path used by artifacts.
58///
59/// `tooltip` snapshots the node's tooltip text at the moment the
60/// target was constructed, so the tooltip pass doesn't have to walk
61/// the live tree to resolve it. This is what makes tooltips work on
62/// virtual-list rows: hit-testing reads `last_tree` (where the row
63/// has been realized), and the cached text survives into the next
64/// frame's `synthesize_tooltip` even though that frame's tree hasn't
65/// rebuilt its virtual-list children yet.
66#[derive(Clone, Debug, PartialEq)]
67#[non_exhaustive]
68pub struct UiTarget {
69 /// The [`El::key`][method@El::key] route string of the hit node —
70 /// what app code matches events against.
71 pub key: String,
72 /// Stable laid-out tree path of the node, used by artifacts and
73 /// tooling that must survive key renames. Shares the tree's
74 /// interned id (`Arc<str>`) — targets are rebuilt per frame for
75 /// the focus/selection orders, so this must not allocate.
76 pub node_id: std::sync::Arc<str>,
77 /// The node's laid-out rect in logical pixels, from the layout
78 /// pass this target was hit-tested against.
79 pub rect: Rect,
80 /// Tooltip text snapshotted from the node when the target was
81 /// constructed (see the struct docs for why it's cached).
82 pub tooltip: Option<String>,
83 /// Scroll offset of the deepest scroll subtree inside this hit
84 /// target, in logical pixels. `0.0` for widgets that don't
85 /// contain a scroll. Used by widgets like
86 /// [`crate::widgets::text_area`] to convert a pointer in viewport
87 /// space (what the user clicks) into content space (what
88 /// cosmic-text's `hit_byte` and `caret_xy` work in) — without
89 /// this, clicks after scrolling land on the wrong line because
90 /// the content has been shifted up by `scroll_offset_y` while
91 /// the outer's `rect` hasn't moved.
92 pub scroll_offset_y: f32,
93}
94
95/// Which mouse button (or pointer button) generated a pointer event.
96/// The host backend translates its native button id to one of these
97/// before calling `pointer_down` / `pointer_up`.
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub enum PointerButton {
100 /// Left mouse, primary touch, or pen tip. Drives `Click`.
101 Primary,
102 /// Right mouse or two-finger touch. Drives `SecondaryClick` —
103 /// typically opens a context menu.
104 Secondary,
105 /// Middle mouse / scroll-wheel click. No library default; surfaced
106 /// as `MiddleClick` for apps that want it (autoscroll, paste-on-X).
107 Middle,
108}
109
110impl PointerButton {
111 /// Translate a Linux evdev button code (the `button` field of
112 /// `wl_pointer.button`, `<linux/input-event-codes.h>`'s `BTN_*`)
113 /// to a damascene button: `BTN_LEFT` (0x110) → `Primary`,
114 /// `BTN_RIGHT` (0x111) → `Secondary`, `BTN_MIDDLE` (0x112) →
115 /// `Middle`. Side/extra/task buttons return `None` — not surfaced
116 /// today.
117 ///
118 /// For raw Wayland hosts (layer-shell bars, notification daemons)
119 /// that read pointer buttons off the wire without winit in the
120 /// loop.
121 pub const fn from_linux_button(code: u32) -> Option<Self> {
122 match code {
123 0x110 => Some(Self::Primary),
124 0x111 => Some(Self::Secondary),
125 0x112 => Some(Self::Middle),
126 _ => None,
127 }
128 }
129}
130
131/// Physical kind of pointer that produced an event. Mirrors the DOM
132/// `PointerEvent.pointerType`. Backends without a real signal pass
133/// [`PointerKind::Mouse`].
134///
135/// The runtime uses this to specialize behavior that does not transfer
136/// across modalities — for example, `Touch` has no resting hover state
137/// and gates `PointerEnter`/`PointerLeave` accordingly.
138#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
139pub enum PointerKind {
140 /// Mouse, trackpad, or any device that reports continuous hover.
141 #[default]
142 Mouse,
143 /// Touchscreen. No hover state; contact starts with `pointer_down`.
144 Touch,
145 /// Pen / stylus. Behaves like `Mouse` for hover, but backends may
146 /// surface pressure in [`Pointer::pressure`].
147 Pen,
148}
149
150/// Stable per-pointer identifier within a frame. Mirrors the DOM
151/// `PointerEvent.pointerId`. Backends with only one pointer pass
152/// [`PointerId::PRIMARY`]; multi-touch backends keep IDs stable for the
153/// lifetime of a single contact.
154///
155/// The runtime currently routes only the primary contact; secondary IDs
156/// are reserved for future multi-touch / gesture work.
157#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
158pub struct PointerId(pub u32);
159
160impl PointerId {
161 /// The conventional ID for backends that have only one pointer
162 /// (mouse-only hosts, synthetic test events, the first touch
163 /// contact when multi-touch IDs are not tracked).
164 pub const PRIMARY: PointerId = PointerId(0);
165}
166
167/// One pointer sample, in logical pixels. The argument shape for
168/// [`crate::runtime::RunnerCore::pointer_moved`],
169/// [`crate::runtime::RunnerCore::pointer_down`], and
170/// [`crate::runtime::RunnerCore::pointer_up`].
171///
172/// Modeled on the DOM `PointerEvent` interface so backends that
173/// already speak browser pointer events can map fields directly.
174/// `button` is meaningful on `pointer_down` / `pointer_up` and is
175/// ignored on `pointer_moved`; constructors default it to
176/// [`PointerButton::Primary`] for that case.
177#[derive(Clone, Copy, Debug, PartialEq)]
178pub struct Pointer {
179 /// X coordinate in logical pixels relative to the window origin.
180 pub x: f32,
181 /// Y coordinate in logical pixels relative to the window origin.
182 pub y: f32,
183 /// Which button this event refers to. Ignored by `pointer_moved`.
184 pub button: PointerButton,
185 /// Physical kind of pointer (mouse / touch / pen).
186 pub kind: PointerKind,
187 /// Stable per-pointer ID. Use [`PointerId::PRIMARY`] for
188 /// single-pointer backends.
189 pub id: PointerId,
190 /// Normalized pressure in `0.0..=1.0` when the device reports it
191 /// (pen, force-touch). `None` when unavailable; mouse backends
192 /// always pass `None`.
193 pub pressure: Option<f32>,
194}
195
196impl Pointer {
197 /// A mouse-driven pointer at `(x, y)` for the given button. Use
198 /// from mouse-only hosts and synthetic tests.
199 pub fn mouse(x: f32, y: f32, button: PointerButton) -> Self {
200 Self {
201 x,
202 y,
203 button,
204 kind: PointerKind::Mouse,
205 id: PointerId::PRIMARY,
206 pressure: None,
207 }
208 }
209
210 /// A mouse pointer for `pointer_moved`, where `button` is
211 /// irrelevant. Equivalent to
212 /// [`Pointer::mouse(x, y, PointerButton::Primary)`][Self::mouse].
213 pub fn moving(x: f32, y: f32) -> Self {
214 Self::mouse(x, y, PointerButton::Primary)
215 }
216
217 /// A touch contact at `(x, y)` carrying the given pointer ID.
218 /// Backends translating browser `PointerEvent` should pass the
219 /// browser's `pointerId` directly.
220 pub fn touch(x: f32, y: f32, button: PointerButton, id: PointerId) -> Self {
221 Self {
222 x,
223 y,
224 button,
225 kind: PointerKind::Touch,
226 id,
227 pressure: None,
228 }
229 }
230}
231
232/// The **logical** key — the key's current meaning, layout- and
233/// modifier-dependent, mirroring the W3C UI Events
234/// [`KeyboardEvent.key`](https://www.w3.org/TR/uievents-key/) attribute.
235/// This is the right facet for activation, navigation, and accelerators
236/// that should follow the printed legend (`Ctrl+S` wherever `S` is).
237///
238/// Committed text (IME / dead-key composition) is **not** carried here —
239/// it arrives as a separate [`UiEventKind::TextInput`] event with the
240/// string on [`UiEvent::text`]. So this enum stays the *meaning* of the
241/// key, never the produced text. For layout-independent identity (games,
242/// rebindable hotkeys, numpad disambiguation) use [`KeyPress::physical`].
243#[derive(Clone, Debug, PartialEq, Eq)]
244pub enum LogicalKey {
245 /// A named (non-character-producing) key — `Enter`, `ArrowUp`, `F5`.
246 Named(NamedKey),
247 /// A character-producing key, carrying the logical character(s) it
248 /// stands for (e.g. `"a"`, `"A"`, `"ä"`). Hotkey matching compares
249 /// ASCII case-insensitively, so `Character("f")` and `Character("F")`
250 /// match the same chord. This is the key's *meaning*, not the text it
251 /// commits — typed text flows through [`UiEventKind::TextInput`].
252 Character(String),
253 /// The host could not identify the key (dead keys mid-composition,
254 /// platform keys with no W3C name). Replaces the old `Debug`-string
255 /// fallback — never a stringly host-formatted value.
256 Unidentified,
257}
258
259impl LogicalKey {
260 /// The logical character(s) this key stands for, if it is a
261 /// character-producing key. `None` for named and unidentified keys.
262 pub fn character(&self) -> Option<&str> {
263 match self {
264 LogicalKey::Character(s) => Some(s.as_str()),
265 _ => None,
266 }
267 }
268
269 /// The named key this is, if it is a named key. `None` for character
270 /// and unidentified keys.
271 pub fn named(&self) -> Option<NamedKey> {
272 match self {
273 LogicalKey::Named(n) => Some(*n),
274 _ => None,
275 }
276 }
277}
278
279/// Defines a vocabulary enum together with an `ALL` const listing every
280/// variant in declaration order. The input vocabularies (`NamedKey`,
281/// `PhysicalKey`, [`crate::Cursor`]) are `#[non_exhaustive]`, so an
282/// out-of-tree host mapper cannot use exhaustive `match` to prove it
283/// covers the full set; `ALL` is the enumeration hook its totality test
284/// iterates instead. Generating enum and const from one listing keeps
285/// the two from drifting when the vocabulary grows.
286macro_rules! enum_with_all {
287 (
288 $(#[$meta:meta])*
289 pub enum $name:ident {
290 $( $(#[$vmeta:meta])* $variant:ident ),+ $(,)?
291 }
292 ) => {
293 $(#[$meta])*
294 pub enum $name {
295 $( $(#[$vmeta])* $variant ),+
296 }
297
298 impl $name {
299 /// Every variant, in declaration order.
300 ///
301 /// The enum is `#[non_exhaustive]`, so a host input mapper
302 /// cannot prove coverage with an exhaustive `match`; its
303 /// totality test iterates this slice instead (see
304 /// `damascene-winit`'s tests for the pattern).
305 pub const ALL: &'static [$name] = &[ $( $name::$variant ),+ ];
306 }
307 };
308}
309pub(crate) use enum_with_all;
310
311enum_with_all! {
312/// A named (non-character-producing) key value — the named subset of the
313/// W3C UI Events [`key`](https://www.w3.org/TR/uievents-key/#named-key-attribute-values)
314/// vocabulary (`Enter`, `ArrowUp`, `F5`, `Shift`, …). Hosts map their
315/// platform's named keys onto this; anything outside the set surfaces as
316/// [`LogicalKey::Unidentified`] rather than a host-formatted string, so
317/// the contract never leans on a windowing crate's `Debug` output.
318///
319/// `#[non_exhaustive]`: the W3C set is large and grows; match with a
320/// wildcard arm, and test mapper coverage against [`NamedKey::ALL`].
321/// Per-variant docs are omitted — the names are the W3C spec names and
322/// self-describing.
323#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
324#[non_exhaustive]
325#[allow(missing_docs)]
326pub enum NamedKey {
327 // Modifier keys.
328 Alt,
329 AltGraph,
330 CapsLock,
331 Control,
332 Fn,
333 FnLock,
334 Meta,
335 NumLock,
336 ScrollLock,
337 Shift,
338 Super,
339 Hyper,
340 Symbol,
341 // Whitespace / editing / navigation.
342 Enter,
343 Tab,
344 Space,
345 ArrowDown,
346 ArrowLeft,
347 ArrowRight,
348 ArrowUp,
349 End,
350 Home,
351 PageDown,
352 PageUp,
353 Backspace,
354 Clear,
355 Copy,
356 CrSel,
357 Cut,
358 Delete,
359 EraseEof,
360 ExSel,
361 Insert,
362 Paste,
363 Redo,
364 Undo,
365 // General-purpose / UI.
366 Accept,
367 Again,
368 Cancel,
369 ContextMenu,
370 Escape,
371 Execute,
372 Find,
373 Help,
374 Pause,
375 Play,
376 Props,
377 Select,
378 ZoomIn,
379 ZoomOut,
380 // Device.
381 Eject,
382 Power,
383 PrintScreen,
384 WakeUp,
385 // Common media keys.
386 AudioVolumeDown,
387 AudioVolumeMute,
388 AudioVolumeUp,
389 MediaPlayPause,
390 MediaStop,
391 MediaTrackNext,
392 MediaTrackPrevious,
393 // Function keys.
394 F1,
395 F2,
396 F3,
397 F4,
398 F5,
399 F6,
400 F7,
401 F8,
402 F9,
403 F10,
404 F11,
405 F12,
406 F13,
407 F14,
408 F15,
409 F16,
410 F17,
411 F18,
412 F19,
413 F20,
414 F21,
415 F22,
416 F23,
417 F24,
418}
419}
420
421enum_with_all! {
422/// The **physical** key — the layout-independent position on the board,
423/// mirroring the W3C UI Events
424/// [`KeyboardEvent.code`](https://www.w3.org/TR/uievents-code/) set
425/// (`KeyA`, `Numpad1`, `ShiftRight`, `F14`). Unlike [`LogicalKey`] this
426/// does not change with keyboard layout or held modifiers: the key west
427/// of `KeyS` is `KeyA` on QWERTY, AZERTY, and Dvorak alike.
428///
429/// This is the right facet for rebindable controls, WASD-style movement,
430/// global hotkeys, and telling duplicate keys apart (numpad `Enter` vs
431/// the main `Enter`, `ShiftLeft` vs `ShiftRight`) — none of which the
432/// logical key or the modifier mask can distinguish.
433///
434/// Hosts that cannot report a position use [`PhysicalKey::Unidentified`].
435/// Names follow the W3C `code` spelling (e.g. `MetaLeft`/`MetaRight`, not
436/// winit's `SuperLeft`/`SuperRight`).
437///
438/// `#[non_exhaustive]`: match with a wildcard arm, and test mapper
439/// coverage against [`PhysicalKey::ALL`]. Per-variant docs are omitted —
440/// the names are the W3C `code` spec names and self-describing.
441#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
442#[non_exhaustive]
443#[allow(missing_docs)]
444pub enum PhysicalKey {
445 // Writing-system keys.
446 Backquote,
447 Backslash,
448 BracketLeft,
449 BracketRight,
450 Comma,
451 Digit0,
452 Digit1,
453 Digit2,
454 Digit3,
455 Digit4,
456 Digit5,
457 Digit6,
458 Digit7,
459 Digit8,
460 Digit9,
461 Equal,
462 IntlBackslash,
463 IntlRo,
464 IntlYen,
465 KeyA,
466 KeyB,
467 KeyC,
468 KeyD,
469 KeyE,
470 KeyF,
471 KeyG,
472 KeyH,
473 KeyI,
474 KeyJ,
475 KeyK,
476 KeyL,
477 KeyM,
478 KeyN,
479 KeyO,
480 KeyP,
481 KeyQ,
482 KeyR,
483 KeyS,
484 KeyT,
485 KeyU,
486 KeyV,
487 KeyW,
488 KeyX,
489 KeyY,
490 KeyZ,
491 Minus,
492 Period,
493 Quote,
494 Semicolon,
495 Slash,
496 // Functional keys.
497 AltLeft,
498 AltRight,
499 Backspace,
500 CapsLock,
501 ContextMenu,
502 ControlLeft,
503 ControlRight,
504 Enter,
505 MetaLeft,
506 MetaRight,
507 ShiftLeft,
508 ShiftRight,
509 Space,
510 Tab,
511 // Control pad / arrows.
512 Delete,
513 End,
514 Help,
515 Home,
516 Insert,
517 PageDown,
518 PageUp,
519 ArrowDown,
520 ArrowLeft,
521 ArrowRight,
522 ArrowUp,
523 // Numpad.
524 NumLock,
525 Numpad0,
526 Numpad1,
527 Numpad2,
528 Numpad3,
529 Numpad4,
530 Numpad5,
531 Numpad6,
532 Numpad7,
533 Numpad8,
534 Numpad9,
535 NumpadAdd,
536 NumpadBackspace,
537 NumpadClear,
538 NumpadComma,
539 NumpadDecimal,
540 NumpadDivide,
541 NumpadEnter,
542 NumpadEqual,
543 NumpadMultiply,
544 NumpadParenLeft,
545 NumpadParenRight,
546 NumpadSubtract,
547 // System / function.
548 Escape,
549 PrintScreen,
550 ScrollLock,
551 Pause,
552 F1,
553 F2,
554 F3,
555 F4,
556 F5,
557 F6,
558 F7,
559 F8,
560 F9,
561 F10,
562 F11,
563 F12,
564 F13,
565 F14,
566 F15,
567 F16,
568 F17,
569 F18,
570 F19,
571 F20,
572 F21,
573 F22,
574 F23,
575 F24,
576 /// The host could not report a physical position for this key.
577 Unidentified,
578}
579}
580
581/// OS modifier-key mask. The four fields mirror the platform-standard
582/// modifier set; this struct is intentionally **not** `#[non_exhaustive]`
583/// so callers can use struct-literal syntax with `..Default::default()`
584/// to spell precise modifier combinations.
585#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
586pub struct KeyModifiers {
587 /// Shift key held.
588 pub shift: bool,
589 /// Control key held.
590 pub ctrl: bool,
591 /// Alt / Option key held.
592 pub alt: bool,
593 /// Logo key held — Super / Windows key / Command.
594 pub logo: bool,
595}
596
597/// One keyboard key-down, as delivered on [`UiEvent::key_press`].
598/// Hosts feed the constituent parts through
599/// [`crate::runtime::RunnerCore::key_down`]; the runtime packages them
600/// into this struct on the events it emits.
601///
602/// Two of the three W3C key facets live here: [`logical`](Self::logical)
603/// (the key's meaning) and [`physical`](Self::physical) (its
604/// layout-independent position). The third — committed text — is a
605/// separate [`UiEventKind::TextInput`] event ([`UiEvent::text`]), so this
606/// struct never carries produced text.
607#[derive(Clone, Debug, PartialEq, Eq)]
608#[non_exhaustive]
609pub struct KeyPress {
610 /// The logical key — meaning under the active layout + modifiers. Use
611 /// for activation, navigation, and legend-following accelerators.
612 pub logical: LogicalKey,
613 /// The physical key — layout-independent board position. Use for
614 /// rebindable controls, games, global hotkeys, and disambiguating
615 /// duplicate keys (numpad vs main row). [`PhysicalKey::Unidentified`]
616 /// when the host can't report a position.
617 pub physical: PhysicalKey,
618 /// Modifier mask at the moment of the press.
619 pub modifiers: KeyModifiers,
620 /// True when this press is an OS auto-repeat of a held key rather
621 /// than a fresh key-down.
622 pub repeat: bool,
623}
624
625impl KeyPress {
626 /// Construct a key press from its facets. Hosts call this to feed
627 /// [`crate::runtime::RunnerCore::key_down`]. `KeyPress` is
628 /// `#[non_exhaustive]`, so this constructor is the supported way to
629 /// build one outside the core crate.
630 pub fn new(
631 logical: LogicalKey,
632 physical: PhysicalKey,
633 modifiers: KeyModifiers,
634 repeat: bool,
635 ) -> Self {
636 Self {
637 logical,
638 physical,
639 modifiers,
640 repeat,
641 }
642 }
643}
644
645/// Which facet of a key press a [`KeyChord`] matches against.
646///
647/// Pick the facet by intent: [`Logical`](Self::Logical) for shortcuts
648/// that should follow the printed legend (`Ctrl+S` stays on whichever key
649/// prints `S`); [`Physical`](Self::Physical) for layout-independent
650/// bindings (the WASD cluster stays WASD on AZERTY, where the legends read
651/// ZQSD).
652#[derive(Clone, Debug, PartialEq, Eq)]
653#[non_exhaustive]
654pub enum ChordTrigger {
655 /// Match the logical key — layout- and modifier-dependent.
656 Logical(LogicalKey),
657 /// Match the physical key position — layout-independent.
658 Physical(PhysicalKey),
659}
660
661/// A keyboard chord for app-level hotkey registration. Matches one key
662/// facet with an exact modifier mask: `KeyChord::ctrl('f')` does not also
663/// match `Ctrl+Shift+F`, and `KeyChord::vim('j')` does not match if any
664/// modifier is held.
665///
666/// A chord matches either the [`logical`](ChordTrigger::Logical) key
667/// (follow the legend) or the [`physical`](ChordTrigger::Physical) key
668/// (layout-independent) — see [`ChordTrigger`]. The `vim`/`ctrl`/
669/// `ctrl_shift`/`named` constructors build logical chords; [`physical`]
670/// builds a physical one.
671///
672/// Register chords from [`App::hotkeys`]; the library matches them
673/// against incoming key presses ahead of focus activation routing and
674/// emits a [`UiEvent`] with `kind = UiEventKind::Hotkey` and `key`
675/// equal to the registered name.
676///
677/// [`physical`]: Self::physical
678#[derive(Clone, Debug, PartialEq, Eq)]
679#[non_exhaustive]
680pub struct KeyChord {
681 /// The key facet (logical or physical) the chord matches.
682 pub trigger: ChordTrigger,
683 /// Exact modifier mask that must be held — extra modifiers do not
684 /// match (see [`Self::matches`]).
685 pub modifiers: KeyModifiers,
686}
687
688impl KeyChord {
689 /// A bare logical key with no modifiers (vim-style).
690 /// `KeyChord::vim('j')` matches the logical `j` with no
691 /// Ctrl/Shift/Alt/Logo held.
692 pub fn vim(c: char) -> Self {
693 Self {
694 trigger: ChordTrigger::Logical(LogicalKey::Character(c.to_string())),
695 modifiers: KeyModifiers::default(),
696 }
697 }
698
699 /// `Ctrl+<char>`, matched on the logical key.
700 pub fn ctrl(c: char) -> Self {
701 Self {
702 trigger: ChordTrigger::Logical(LogicalKey::Character(c.to_string())),
703 modifiers: KeyModifiers {
704 ctrl: true,
705 ..Default::default()
706 },
707 }
708 }
709
710 /// `Ctrl+Shift+<char>`, matched on the logical key.
711 pub fn ctrl_shift(c: char) -> Self {
712 Self {
713 trigger: ChordTrigger::Logical(LogicalKey::Character(c.to_string())),
714 modifiers: KeyModifiers {
715 ctrl: true,
716 shift: true,
717 ..Default::default()
718 },
719 }
720 }
721
722 /// A logical key with no modifiers (e.g.
723 /// `KeyChord::named(LogicalKey::Named(NamedKey::Escape))`).
724 pub fn named(key: LogicalKey) -> Self {
725 Self {
726 trigger: ChordTrigger::Logical(key),
727 modifiers: KeyModifiers::default(),
728 }
729 }
730
731 /// A physical key position with no modifiers — layout-independent
732 /// (e.g. `KeyChord::physical(PhysicalKey::KeyW)` binds the WASD `W`
733 /// position regardless of layout).
734 pub fn physical(key: PhysicalKey) -> Self {
735 Self {
736 trigger: ChordTrigger::Physical(key),
737 modifiers: KeyModifiers::default(),
738 }
739 }
740
741 /// Builder-style: replace the chord's modifier mask.
742 pub fn with_modifiers(mut self, modifiers: KeyModifiers) -> Self {
743 self.modifiers = modifiers;
744 self
745 }
746
747 /// Strict match: the chord's facet equals the press's matching facet
748 /// AND the modifier mask is identical. Holding extra modifiers does
749 /// not match a chord that didn't request them. Logical-character
750 /// chords compare ASCII case-insensitively.
751 pub fn matches(
752 &self,
753 logical: &LogicalKey,
754 physical: PhysicalKey,
755 modifiers: KeyModifiers,
756 ) -> bool {
757 self.modifiers == modifiers
758 && match &self.trigger {
759 ChordTrigger::Logical(want) => logical_eq(want, logical),
760 ChordTrigger::Physical(want) => *want == physical,
761 }
762 }
763}
764
765fn logical_eq(a: &LogicalKey, b: &LogicalKey) -> bool {
766 match (a, b) {
767 (LogicalKey::Character(x), LogicalKey::Character(y)) => x.eq_ignore_ascii_case(y),
768 _ => a == b,
769 }
770}
771
772/// User-facing event. The host's [`App::on_event`] receives one of these
773/// per discrete user action.
774///
775/// Most apps should not destructure every field. Prefer the convenience
776/// methods on this type for common routes:
777///
778/// ```
779/// # use damascene_core::prelude::*;
780/// # struct Counter { value: i32 }
781/// # impl App for Counter {
782/// # fn build(&self, _cx: &BuildCx) -> El { button("+").key("inc") }
783/// fn on_event(&mut self, event: UiEvent, _cx: &EventCx) {
784/// if event.is_click_or_activate("inc") {
785/// self.value += 1;
786/// }
787/// }
788/// # }
789/// ```
790#[derive(Clone, Debug)]
791#[non_exhaustive]
792pub struct UiEvent {
793 /// Route string for this event.
794 ///
795 /// For pointer and focus events, this is the [`El::key`][crate::El::key]
796 /// of the target node. For [`UiEventKind::Hotkey`], this is the
797 /// action name returned from [`App::hotkeys`]. For window-level
798 /// keyboard events such as Escape with no focused target, this is
799 /// `None`.
800 ///
801 /// Prefer [`Self::route`] or [`Self::is_click_or_activate`] in app
802 /// code. The field remains public for direct pattern matching.
803 pub key: Option<String>,
804 /// Full hit-test target for events routed to a concrete element.
805 pub target: Option<UiTarget>,
806 /// Pointer position in logical pixels when the event was emitted.
807 pub pointer: Option<(f32, f32)>,
808 /// Keyboard payload for key events.
809 pub key_press: Option<KeyPress>,
810 /// Composed text payload for [`UiEventKind::TextInput`] events.
811 pub text: Option<String>,
812 /// Library-emitted selection state for
813 /// [`UiEventKind::SelectionChanged`] events. Carries the new
814 /// [`crate::selection::Selection`] after the runtime resolved a
815 /// pointer interaction. The app folds this into its
816 /// `Selection` field the same way it folds `apply_event` results
817 /// into a [`crate::widgets::text_input::TextSelection`].
818 pub selection: Option<crate::selection::Selection>,
819 /// Modifier mask captured at the moment this event was emitted. For
820 /// keyboard events this duplicates `key_press.modifiers`; for
821 /// pointer events it's the host-tracked modifier state at the time
822 /// of the click / drag (used by widgets like text_input that need
823 /// to detect Shift+click for "extend selection").
824 pub modifiers: KeyModifiers,
825 /// Click number within a multi-click sequence. Set to 1 for single
826 /// click, 2 for double-click, 3 for triple-click, etc. The runtime
827 /// increments this when consecutive `PointerDown`s land on the same
828 /// target within ~500ms and ~4px of the previous click. `Drag`
829 /// events emitted while the final click is held keep the active
830 /// sequence count so text widgets can preserve word / line
831 /// granularity. `0` means "not applicable" — set on events outside
832 /// pointer click / drag routing.
833 ///
834 /// `text_input` / `text_area` and the static-text selection
835 /// manager read this to map double-click → select word, triple-
836 /// click → select line.
837 pub click_count: u8,
838 /// File system path for [`UiEventKind::FileHovered`] /
839 /// [`UiEventKind::FileDropped`] events. Multi-file drag-drops fire
840 /// one event per file (matching the underlying winit semantics);
841 /// each event carries one path. `PathBuf` rather than `String`
842 /// because Windows wide-char paths and unusual Unix paths aren't
843 /// guaranteed to be UTF-8.
844 pub path: Option<std::path::PathBuf>,
845 /// Modality of the pointer that produced this event. `None` for
846 /// non-pointer events (hotkeys, keyboard activation, file drops
847 /// without a tracked pointer). Apps that need to specialize for
848 /// touch (accessibility, analytics, alternate affordances) read
849 /// this; most app code can ignore it.
850 pub pointer_kind: Option<PointerKind>,
851 /// Wheel delta in logical pixels for [`UiEventKind::PointerWheel`].
852 ///
853 /// Positive `dy` means "scroll down" in the same coordinate system
854 /// used by Damascene's scroll containers. Hosts normalize line-based
855 /// and pixel-based wheel input before setting this field.
856 pub wheel_delta: Option<(f32, f32)>,
857 /// What kind of event happened. See [`UiEventKind`] for the
858 /// per-variant routing contracts.
859 pub kind: UiEventKind,
860}
861
862impl UiEvent {
863 /// Synthesize a click event for the given route key.
864 ///
865 /// Intended for tests, headless automation, and snapshot
866 /// fixtures that drive UI logic without a real pointer history.
867 /// All optional fields default to `None`; modifiers are empty.
868 pub fn synthetic_click(key: impl Into<String>) -> Self {
869 Self {
870 kind: UiEventKind::Click,
871 key: Some(key.into()),
872 target: None,
873 pointer: None,
874 key_press: None,
875 text: None,
876 selection: None,
877 modifiers: KeyModifiers::default(),
878 click_count: 1,
879 path: None,
880 pointer_kind: None,
881 wheel_delta: None,
882 }
883 }
884
885 /// Route string for this event, if any.
886 ///
887 /// For pointer/focus events this is the target element key. For
888 /// hotkeys this is the registered action name.
889 pub fn route(&self) -> Option<&str> {
890 self.key.as_deref()
891 }
892
893 /// Target element key, if this event was routed to an element.
894 ///
895 /// Unlike [`Self::route`], this returns `None` for app-level
896 /// hotkey actions because those do not have a concrete element
897 /// target.
898 pub fn target_key(&self) -> Option<&str> {
899 self.target.as_ref().map(|t| t.key.as_str())
900 }
901
902 /// True when this event's route equals `key`.
903 pub fn is_route(&self, key: &str) -> bool {
904 self.route() == Some(key)
905 }
906
907 /// If this event's route is `prefix:rest`, the rest — see
908 /// [`crate::key::suffix`]. The per-item dispatch shape without the
909 /// hand-rolled `strip_prefix` chain:
910 ///
911 /// ```
912 /// # use damascene_core::UiEvent;
913 /// let event = UiEvent::synthetic_click("thumb:42");
914 /// assert_eq!(event.route_suffix("thumb"), Some("42"));
915 /// assert_eq!(event.route_suffix("row"), None);
916 /// ```
917 pub fn route_suffix(&self, prefix: &str) -> Option<&str> {
918 crate::key::suffix(self.route()?, prefix)
919 }
920
921 /// If this event's route is `prefix:index…`, the first segment
922 /// after the prefix parsed as `T` — see [`crate::key::index`].
923 /// Unlike the hand-rolled `strip_prefix` + `parse().ok()` chain, a
924 /// prefix match whose index fails to parse logs a warning instead
925 /// of silently dropping the event.
926 ///
927 /// ```
928 /// # use damascene_core::UiEvent;
929 /// let event = UiEvent::synthetic_click("thumb:42");
930 /// assert_eq!(event.route_index::<usize>("thumb"), Some(42));
931 /// // Select-style routed keys: the leading id still parses.
932 /// let event = UiEvent::synthetic_click("profile:7:option:3");
933 /// assert_eq!(event.route_index::<u32>("profile"), Some(7));
934 /// ```
935 pub fn route_index<T: std::str::FromStr>(&self, prefix: &str) -> Option<T> {
936 crate::key::index(self.route()?, prefix)
937 }
938
939 /// True for a primary click or keyboard activation on `key`.
940 ///
941 /// This is the most common button/menu route in app code.
942 pub fn is_click_or_activate(&self, key: &str) -> bool {
943 matches!(self.kind, UiEventKind::Click | UiEventKind::Activate) && self.is_route(key)
944 }
945
946 /// True for a registered hotkey action name.
947 pub fn is_hotkey(&self, action: &str) -> bool {
948 self.kind == UiEventKind::Hotkey && self.is_route(action)
949 }
950
951 /// Pointer position in logical pixels, if this event carries one.
952 pub fn pointer_pos(&self) -> Option<(f32, f32)> {
953 self.pointer
954 }
955
956 /// Pointer x coordinate in logical pixels, if this event carries one.
957 pub fn pointer_x(&self) -> Option<f32> {
958 self.pointer.map(|(x, _)| x)
959 }
960
961 /// Pointer y coordinate in logical pixels, if this event carries one.
962 pub fn pointer_y(&self) -> Option<f32> {
963 self.pointer.map(|(_, y)| y)
964 }
965
966 /// Wheel delta in logical pixels, if this is a pointer wheel event.
967 pub fn wheel_delta(&self) -> Option<(f32, f32)> {
968 self.wheel_delta
969 }
970
971 /// Vertical wheel delta in logical pixels, if this is a pointer
972 /// wheel event.
973 pub fn wheel_dy(&self) -> Option<f32> {
974 self.wheel_delta.map(|(_, dy)| dy)
975 }
976
977 /// Rectangle of the routed target from the last layout pass.
978 /// This is the target's transformed visual rect, not any
979 /// `hit_overflow` band that may also route pointer events to it.
980 pub fn target_rect(&self) -> Option<Rect> {
981 self.target.as_ref().map(|t| t.rect)
982 }
983
984 /// OS-composed text payload for [`UiEventKind::TextInput`].
985 pub fn text(&self) -> Option<&str> {
986 self.text.as_deref()
987 }
988}
989
990/// What kind of event happened.
991///
992/// This enum is non-exhaustive so Damascene can add new input events
993/// without breaking downstream apps. Match the variants you handle and
994/// include a wildcard arm for everything else.
995#[derive(Clone, Copy, Debug, PartialEq, Eq)]
996#[non_exhaustive]
997pub enum UiEventKind {
998 /// Primary-button pointer down + up landed on the same node.
999 Click,
1000 /// Primary-button click landed on a text run carrying a
1001 /// [`crate::tree::El::text_link`] URL. The URL is in [`UiEvent::key`].
1002 /// Apps decide whether to honor it (filtering, confirmation,
1003 /// platform-appropriate open via [`App::drain_link_opens`] +
1004 /// host-side opener). Damascene doesn't open URLs itself — it surfaces
1005 /// the click and lets the app route it.
1006 LinkActivated,
1007 /// Secondary-button (right-click) pointer down + up landed on the
1008 /// same node. Used for context menus.
1009 SecondaryClick,
1010 /// Middle-button pointer down + up landed on the same node.
1011 MiddleClick,
1012 /// Focused element was activated by keyboard (Enter/Space).
1013 Activate,
1014 /// Escape was pressed. Routed to the focused element when present,
1015 /// otherwise emitted as a window-level event.
1016 Escape,
1017 /// A registered hotkey chord matched. `event.key` is the registered
1018 /// name (the second element of the `(KeyChord, String)` pair).
1019 Hotkey,
1020 /// Other keyboard input.
1021 KeyDown,
1022 /// Composed text input — printable characters from the OS, after
1023 /// dead-key composition / IME / shift mapping. Routed to the
1024 /// focused element. Distinct from `KeyDown(Character(_))`: the
1025 /// latter is the raw key event used for shortcuts and navigation;
1026 /// `TextInput` is the grapheme stream a text field should consume.
1027 TextInput,
1028 /// Pointer moved while the primary button was held down. Routed
1029 /// to the originally pressed target so a widget can extend a
1030 /// selection / scrub a slider / move a draggable. `event.pointer`
1031 /// carries the current logical-pixel position; `event.target` is
1032 /// the node where the drag began.
1033 Drag,
1034 /// Primary pointer button released. Fires regardless of whether
1035 /// the up landed on the same node as the down — paired with
1036 /// `Click` (which only fires on a same-node match), this lets
1037 /// drag-aware widgets always observe drag-end.
1038 /// `event.target` is the originally pressed node;
1039 /// `event.pointer` is the up position.
1040 PointerUp,
1041 /// Primary pointer button pressed on a hit-test target. Routed
1042 /// before the eventual `Click` (which fires on up-on-same-target).
1043 /// Used by widgets like text_input that need to react at
1044 /// down-time — e.g., to set the selection anchor before any drag
1045 /// extends it. `event.target` is the down-target,
1046 /// `event.pointer` is the down position, and `event.modifiers`
1047 /// carries the modifier mask (Shift+click for extend-selection).
1048 PointerDown,
1049 /// Mouse wheel / trackpad scroll input routed to the keyed element
1050 /// under the pointer. Emitted before Damascene's default scroll
1051 /// handling; apps can consume it by returning `true` from
1052 /// [`App::on_wheel_event`]. `event.wheel_delta` carries the
1053 /// normalized logical-pixel delta.
1054 PointerWheel,
1055 /// The library's selection manager resolved a pointer interaction
1056 /// on selectable text and wants the app to update its
1057 /// [`crate::selection::Selection`] state. `event.selection`
1058 /// carries the new value (an empty `Selection` clears).
1059 /// Emitted by `pointer_down`, `pointer_moved` (during a drag),
1060 /// and the runtime's escape / dismiss paths.
1061 SelectionChanged,
1062 /// Pointer crossed onto a keyed hit-test target. Routed to the
1063 /// newly hovered leaf — `event.target` is the new hover target,
1064 /// `event.pointer` is the current pointer position. Fires
1065 /// once per identity change, including the initial hover when the
1066 /// pointer first enters a keyed region from nothing.
1067 ///
1068 /// Use for transition-driven side effects (sound on hover-enter,
1069 /// analytics, hover-intent prefetch) — read state via
1070 /// [`crate::BuildCx::hovered_key`] /
1071 /// [`crate::BuildCx::is_hovering_within`] when you just need to
1072 /// branch the build output. Both surfaces stay coherent because
1073 /// the runtime debounces redraws and events to the same
1074 /// hover-identity transitions.
1075 ///
1076 /// Always paired with a preceding `PointerLeave` for the previous
1077 /// target (when there was one). Apps that want subtree-aware
1078 /// behavior (parent stays "hot" while a child is hovered) should
1079 /// query `is_hovering_within` rather than tracking enter/leave on
1080 /// every keyed descendant.
1081 PointerEnter,
1082 /// Pointer crossed off a keyed hit-test target — either onto a
1083 /// different keyed target (paired with a following `PointerEnter`)
1084 /// or off any keyed surface entirely. Routed to the leaf that
1085 /// just lost hover — `event.target` is the previous hover target,
1086 /// `event.pointer` is the current pointer position (or the last
1087 /// known position when the pointer left the window).
1088 PointerLeave,
1089 /// The runner is abandoning a press because the gesture became
1090 /// something else — currently only fired when a touch contact's
1091 /// movement crosses the touch-scroll threshold and the press
1092 /// target did not opt in via `consumes_touch_drag`. The contact
1093 /// has *not* lifted; the user is still touching the screen, but
1094 /// from the widget's perspective the press is gone (no
1095 /// subsequent `Drag`, no `Click`, no `PointerUp`). Routed to the
1096 /// originally pressed target — apps that handle `PointerDown`
1097 /// for in-flight visual / state setup should also handle
1098 /// `PointerCancel` to roll it back.
1099 ///
1100 /// Browser-initiated pointer cancels (OS gesture takeover, etc.)
1101 /// currently come through as `PointerUp` rather than this event;
1102 /// that may change.
1103 PointerCancel,
1104 /// A touch contact has been held in place past
1105 /// [`crate::state::LONG_PRESS_DELAY`] without lifting or moving
1106 /// past the gesture threshold. Fired exactly once per qualifying
1107 /// press. For normal targets this is fired immediately after a
1108 /// `PointerCancel` is dispatched to the originally pressed target;
1109 /// the underlying primary press is consumed by the long-press, so
1110 /// no subsequent `Click` or `PointerUp` follows. Capture-keys
1111 /// editable targets keep the press captured so movement after the
1112 /// long-press can emit `Drag` to extend text selection. The
1113 /// eventual finger lift is silently swallowed.
1114 ///
1115 /// `event.target` is the keyed leaf at the press point (same
1116 /// node that received the cancelled `PointerDown`), `event.pointer`
1117 /// is the original press coords (not the current finger position
1118 /// — the contact may have drifted within the gesture-threshold
1119 /// radius before firing), and `event.pointer_kind` is always
1120 /// `PointerKind::Touch`.
1121 ///
1122 /// Mouse and pen pointers never produce this event — right-click
1123 /// goes through `PointerDown` with [`PointerButton::Secondary`]
1124 /// instead, which is the desktop-shape signal for the same
1125 /// "open a context menu here" intent. Apps that want both paths
1126 /// to drive the same menu match on either kind.
1127 LongPress,
1128 /// A file is being dragged over the window (the user hasn't
1129 /// released yet). `event.path` carries the file's path; multi-file
1130 /// drags fire one event per file, matching the underlying winit
1131 /// semantics. `event.target` is the keyed leaf at the current
1132 /// pointer position when one was hit, otherwise `None`
1133 /// (drop-zone overlays that span the window can match on
1134 /// `event.target.is_none()` or filter by their own key).
1135 ///
1136 /// Apps use this to highlight a drop zone before the drop lands.
1137 /// Always paired with either a later `FileHoverCancelled` (the
1138 /// user moved off without releasing) or `FileDropped` (the user
1139 /// released).
1140 FileHovered,
1141 /// The user moved a hovered file off the window without releasing,
1142 /// or pressed Escape. Window-level event (`event.target` is
1143 /// `None`) — apps clear any drop-zone affordance state regardless
1144 /// of which keyed leaf was previously highlighted.
1145 FileHoverCancelled,
1146 /// A file was dropped on the window. `event.path` carries the
1147 /// path; multi-file drops fire one event per file. `event.target`
1148 /// is the keyed leaf at the drop position, or `None` if the drop
1149 /// landed outside any keyed surface — apps that want a global drop
1150 /// target match on `target.is_none()` or treat unrouted events as
1151 /// hits to a single window-level upload sink.
1152 FileDropped,
1153 /// A [`user_resizable`](crate::tree::El::user_resizable) pane's
1154 /// edge drag was released. Routed to the pane's key (unkeyed panes
1155 /// resize fine but emit nothing); fires once per completed drag,
1156 /// not per move. Read the final size via
1157 /// [`EventCx::user_size`]/[`crate::UiState::user_size`] — this is
1158 /// the natural moment to persist it. The size mutation itself is
1159 /// runtime-owned, like scrolling: apps only listen when they want
1160 /// to save the value.
1161 Resized,
1162}
1163
1164/// Per-frame, read-only context for [`App::build`].
1165///
1166/// The runner snapshots the app's [`crate::Theme`] before calling
1167/// `build` and exposes it through `cx.theme()` / `cx.palette()` so app
1168/// code can branch on the active palette (a custom widget that picks
1169/// between two non-token colors based on dark vs. light, for instance).
1170/// `BuildCx` is the explicit handle for this — token references inside
1171/// widgets resolve through the palette automatically and don't need it.
1172///
1173/// Future fields like viewport metrics or frame phase will live here so
1174/// the API stays additive: adding a new accessor on `BuildCx` doesn't
1175/// break apps that ignore the context.
1176#[derive(Copy, Clone, Debug)]
1177pub struct BuildCx<'a> {
1178 theme: &'a crate::Theme,
1179 ui_state: Option<&'a crate::state::UiState>,
1180 diagnostics: Option<&'a HostDiagnostics>,
1181 /// Logical-pixel viewport this frame is being built for, when the
1182 /// host attached one. Apps query this via [`Self::viewport`] /
1183 /// [`Self::viewport_below`] to branch layout on phone-vs-desktop
1184 /// without threading the surface size through their own state.
1185 viewport: Option<(f32, f32)>,
1186 /// Logical-pixel insets the host wants the app to inset its
1187 /// layout by — content underneath these bands is obscured by
1188 /// platform chrome and shouldn't host interactive widgets.
1189 /// Today only the bottom inset is populated, by the web host's
1190 /// VisualViewport listener when the on-screen keyboard appears;
1191 /// the same field will carry status-bar / notch / home-indicator
1192 /// insets when native mobile hosts land.
1193 safe_area: Option<crate::tree::Sides>,
1194}
1195
1196/// Why the current frame is being built. Hosts set this before each
1197/// `request_redraw` so apps that surface a diagnostic overlay can show
1198/// what kind of input is driving the redraw cadence.
1199///
1200/// `Other` is the conservative default: it covers redraws the host
1201/// can't attribute. Specific variants narrow the reason when the
1202/// host can.
1203#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1204pub enum FrameTrigger {
1205 /// Host can't attribute the redraw to a specific cause.
1206 #[default]
1207 Other,
1208 /// Initial paint after surface configuration.
1209 Initial,
1210 /// Surface resize / DPI change.
1211 Resize,
1212 /// Pointer move, button, or wheel.
1213 Pointer,
1214 /// Keyboard / IME input.
1215 Keyboard,
1216 /// Inside-out animation deadline elapsed (one of the visible
1217 /// widgets asked for a future frame via `redraw_within`, or a
1218 /// visual animation is still settling). Drives the layout-path
1219 /// (full rebuild + prepare).
1220 Animation,
1221 /// Time-driven shader deadline elapsed (e.g. stock spinner /
1222 /// skeleton / progress-indeterminate, or a custom shader
1223 /// registered with `samples_time=true`). Drives the paint-only
1224 /// path: `frame.time` advances but layout state is unchanged.
1225 ShaderPaint,
1226 /// Periodic host-config cadence (`HostConfig::redraw_interval`).
1227 Periodic,
1228 /// Application code asked for a frame through the host's external
1229 /// wakeup handle (push-driven event-class data — a chat message
1230 /// arrived, a background task advanced state). Data changed
1231 /// outside the tree, so this drives the layout path (full rebuild
1232 /// + prepare), never paint-only.
1233 External,
1234}
1235
1236impl FrameTrigger {
1237 /// Short, fixed-width tag for diagnostic overlays.
1238 pub fn label(self) -> &'static str {
1239 match self {
1240 FrameTrigger::Other => "other",
1241 FrameTrigger::Initial => "initial",
1242 FrameTrigger::Resize => "resize",
1243 FrameTrigger::Pointer => "pointer",
1244 FrameTrigger::Keyboard => "keyboard",
1245 FrameTrigger::Animation => "animation",
1246 FrameTrigger::ShaderPaint => "shader-paint",
1247 FrameTrigger::Periodic => "periodic",
1248 FrameTrigger::External => "external",
1249 }
1250 }
1251}
1252
1253/// Per-frame diagnostic snapshot the host hands the app via
1254/// [`BuildCx::diagnostics`]. Apps that surface a debug overlay (e.g.
1255/// the showcase status block) read this each build to display the
1256/// active backend, frame cadence, and what triggered the redraw.
1257/// Timing fields describe the last completed rendered frame, not the
1258/// frame currently being built; the host cannot know current layout /
1259/// paint timings until after `App::build` returns.
1260///
1261/// Hosts populate every field they can; `backend` is a static string
1262/// (`"WebGPU"`, `"Vulkan"`, `"Metal"`, `"DX12"`, `"GL"`) so the app
1263/// doesn't need to depend on `wgpu` to read it. Time fields use
1264/// `std::time::Duration`, which works on both native and wasm32 — only
1265/// `Instant::now()` is the wasm-incompatible piece, and that stays on
1266/// the host side.
1267#[derive(Clone, Debug)]
1268pub struct HostDiagnostics {
1269 /// Render backend in human-readable form.
1270 pub backend: &'static str,
1271 /// Current surface size in physical pixels.
1272 pub surface_size: (u32, u32),
1273 /// Display scale factor (`physical / logical`).
1274 pub scale_factor: f32,
1275 /// Active MSAA sample count (1 = MSAA off).
1276 pub msaa_samples: u32,
1277 /// Frame counter; increments every redraw the host actually
1278 /// renders. Useful for verifying that an animated source is
1279 /// progressing.
1280 pub frame_index: u64,
1281 /// Wall-clock time between this redraw and the previous one.
1282 /// `Duration::ZERO` for the first frame (no prior frame).
1283 pub last_frame_dt: std::time::Duration,
1284 /// Time spent in the app's `build` method for the last completed
1285 /// frame. `Duration::ZERO` before the first full frame and on
1286 /// paint-only frames that skipped build.
1287 pub last_build: std::time::Duration,
1288 /// Total time spent in the backend `prepare` call for the last
1289 /// completed frame.
1290 pub last_prepare: std::time::Duration,
1291 /// Sub-stage inside `prepare`: layout pass, focus/selection sync,
1292 /// state application, and animation tick.
1293 pub last_layout: std::time::Duration,
1294 /// Intrinsic-measurement cache hits during the last layout pass.
1295 pub last_layout_intrinsic_cache_hits: u64,
1296 /// Intrinsic-measurement cache misses during the last layout pass.
1297 pub last_layout_intrinsic_cache_misses: u64,
1298 /// Direct scroll children whose descendants were skipped during
1299 /// layout because the child was outside the scroll viewport.
1300 pub last_layout_pruned_subtrees: u64,
1301 /// Descendant nodes assigned zero rects as part of scroll layout
1302 /// pruning during the last layout pass.
1303 pub last_layout_pruned_nodes: u64,
1304 /// Sub-stage inside `prepare`: laid-out tree to backend-neutral
1305 /// `DrawOp` list.
1306 pub last_draw_ops: std::time::Duration,
1307 /// Text draw ops skipped during draw-op generation because their
1308 /// glyph rect did not intersect the inherited clip.
1309 pub last_draw_ops_culled_text_ops: u64,
1310 /// Sub-stage inside `prepare`: paint-stream packing and text
1311 /// shaping/rasterization recording.
1312 pub last_paint: std::time::Duration,
1313 /// Paint ops skipped because their painted rect did not intersect
1314 /// the effective clip/viewport in the last completed frame.
1315 pub last_paint_culled_ops: u64,
1316 /// Sub-stage inside `prepare`: backend-side buffer writes, glyph
1317 /// atlas uploads, and frame uniforms.
1318 pub last_gpu_upload: std::time::Duration,
1319 /// Sub-stage inside `prepare`: clone the laid-out tree for
1320 /// next-frame hit-testing.
1321 pub last_snapshot: std::time::Duration,
1322 /// Time spent encoding/submitting/presenting the last completed
1323 /// frame after `prepare`.
1324 pub last_submit: std::time::Duration,
1325 /// Layout-side text-cache hits during the last completed full
1326 /// prepare.
1327 pub last_text_layout_cache_hits: u64,
1328 /// Layout-side text-cache misses during the last completed full
1329 /// prepare.
1330 pub last_text_layout_cache_misses: u64,
1331 /// Estimated layout-side text-cache evictions during the last
1332 /// completed full prepare.
1333 pub last_text_layout_cache_evictions: u64,
1334 /// Total UTF-8 bytes shaped on layout-cache misses during the last
1335 /// completed full prepare.
1336 pub last_text_layout_shaped_bytes: u64,
1337 /// Why the host triggered this frame.
1338 pub trigger: FrameTrigger,
1339 /// What the renderer composites in. The paint stream converts every
1340 /// [`crate::color::Color`] into this space exactly once at the
1341 /// upload boundary. Defaults to [`crate::color::ColorSpace::SRGB_LINEAR`].
1342 pub working_color_space: crate::color::ColorSpace,
1343 /// Wire-side color-management state the host negotiated with the
1344 /// display server. [`crate::color::ColorManagementStatus::Unavailable`]
1345 /// on hosts without a color-management protocol (X11, plain Wayland,
1346 /// macOS / Windows today). See [`crate::color::ColorPreferences`]
1347 /// for how apps influence the negotiation.
1348 pub color_management: crate::color::ColorManagementStatus,
1349 /// Color-relevant facts about the host's GPU presentation surface —
1350 /// the wgpu / WSI half of color negotiation (advertised formats,
1351 /// chosen swapchain format, present/alpha mode, adapter). `None` on
1352 /// hosts that don't present through a wgpu surface (headless render
1353 /// bins, the vulkano demo). See [`SurfaceColorInfo`].
1354 pub surface_color: Option<SurfaceColorInfo>,
1355}
1356
1357impl Default for HostDiagnostics {
1358 fn default() -> Self {
1359 Self {
1360 backend: "?",
1361 surface_size: (0, 0),
1362 scale_factor: 1.0,
1363 msaa_samples: 1,
1364 frame_index: 0,
1365 last_frame_dt: std::time::Duration::ZERO,
1366 last_build: std::time::Duration::ZERO,
1367 last_prepare: std::time::Duration::ZERO,
1368 last_layout: std::time::Duration::ZERO,
1369 last_layout_intrinsic_cache_hits: 0,
1370 last_layout_intrinsic_cache_misses: 0,
1371 last_layout_pruned_subtrees: 0,
1372 last_layout_pruned_nodes: 0,
1373 last_draw_ops: std::time::Duration::ZERO,
1374 last_draw_ops_culled_text_ops: 0,
1375 last_paint: std::time::Duration::ZERO,
1376 last_paint_culled_ops: 0,
1377 last_gpu_upload: std::time::Duration::ZERO,
1378 last_snapshot: std::time::Duration::ZERO,
1379 last_submit: std::time::Duration::ZERO,
1380 last_text_layout_cache_hits: 0,
1381 last_text_layout_cache_misses: 0,
1382 last_text_layout_cache_evictions: 0,
1383 last_text_layout_shaped_bytes: 0,
1384 trigger: FrameTrigger::default(),
1385 working_color_space: crate::paint::DEFAULT_WORKING_COLOR_SPACE,
1386 color_management: crate::color::ColorManagementStatus::default(),
1387 surface_color: None,
1388 }
1389 }
1390}
1391
1392impl HostDiagnostics {
1393 /// Is this app actually rendering HDR right now — an extended-range
1394 /// swapchain on an output with HDR evidence?
1395 ///
1396 /// This is the check, encoded once so apps never re-derive it:
1397 /// the compositor's preferred description indicates an HDR output
1398 /// ([`CompositorColorTargets::indicates_hdr`]) **and** the
1399 /// negotiated swapchain format can carry extended-range output
1400 /// ([`SurfaceFormatInfo::wide`], e.g. `Rgba16Float` scRGB). Do
1401 /// *not* infer HDR from `ColorManagementStatus::Available {
1402 /// attached }` — on every current host the WSI owns the surface
1403 /// tag and `attached` stays `None` even in full HDR operation.
1404 ///
1405 /// Live: hosts refresh these diagnostics when the compositor's
1406 /// preferred description changes (`preferred_changed2` — window
1407 /// moved to another output, HDR toggled), so this flips with the
1408 /// window. HDR is opt-in via [`crate::color::ColorPreferences`];
1409 /// a default `sdr_only` app reports `false` even on an HDR output.
1410 ///
1411 /// [`CompositorColorTargets::indicates_hdr`]: crate::color::CompositorColorTargets::indicates_hdr
1412 pub fn hdr_active(&self) -> bool {
1413 let crate::color::ColorManagementStatus::Available { targets, .. } = &self.color_management
1414 else {
1415 return false;
1416 };
1417 targets.indicates_hdr()
1418 && self.surface_color.as_ref().is_some_and(|s| {
1419 s.formats
1420 .iter()
1421 .any(|f| f.wide && f.name == s.chosen_format)
1422 })
1423 }
1424}
1425
1426/// Color-relevant facts about the host's GPU presentation surface — the
1427/// wgpu / WSI half of color negotiation. The compositor (via
1428/// [`crate::color::ColorManagementStatus`]) says what it *accepts*; this
1429/// says what the *swapchain* can represent. The intersection is what the
1430/// negotiator can actually pick — e.g. a compositor that ingests linear
1431/// BT.2020 is moot if the surface offers no float format.
1432///
1433/// Strings throughout so `damascene-core` needn't depend on `wgpu`.
1434#[derive(Clone, Debug, Default)]
1435pub struct SurfaceColorInfo {
1436 /// Adapter / device name (e.g. `"Intel Graphics (ADL GT2)"`).
1437 pub adapter: String,
1438 /// Driver name + version, when the backend reports it.
1439 pub driver: String,
1440 /// Color formats the surface advertised, in wgpu's reported order.
1441 pub formats: Vec<SurfaceFormatInfo>,
1442 /// The swapchain format negotiation actually chose.
1443 pub chosen_format: String,
1444 /// Present mode in use.
1445 pub present_mode: String,
1446 /// Composite alpha mode in use.
1447 pub alpha_mode: String,
1448}
1449
1450/// One surface texture format, classified by how it can carry color
1451/// output. See [`SurfaceColorInfo`].
1452#[derive(Clone, Debug)]
1453pub struct SurfaceFormatInfo {
1454 /// wgpu format name (e.g. `"Rgba16Float"`).
1455 pub name: String,
1456 /// Carries an sRGB EOTF in hardware (`*_unorm_srgb`): the GPU encodes
1457 /// linear → sRGB on store.
1458 pub srgb: bool,
1459 /// Can carry wide-gamut / HDR output: a float format (linear-direct —
1460 /// the compositor does the output encode) or a ≥10-bit format (a
1461 /// PQ-encode target). 8-bit unorm formats are SDR-only.
1462 pub wide: bool,
1463}
1464
1465impl<'a> BuildCx<'a> {
1466 /// Construct a [`BuildCx`] borrowing the supplied theme. Hosts call
1467 /// this once per frame after [`App::theme`] and before [`App::build`].
1468 /// Hosts that own a [`crate::state::UiState`] should chain
1469 /// [`Self::with_ui_state`] so the app can read interaction state
1470 /// (hover) during build via [`Self::hovered_key`] /
1471 /// [`Self::is_hovering_within`].
1472 pub fn new(theme: &'a crate::Theme) -> Self {
1473 Self {
1474 theme,
1475 ui_state: None,
1476 diagnostics: None,
1477 viewport: None,
1478 safe_area: None,
1479 }
1480 }
1481
1482 /// Attach the runtime's [`crate::state::UiState`] so build-time
1483 /// accessors (`hovered_key`, `is_hovering_within`) can answer.
1484 /// When omitted, those accessors return `None` / `false` — useful
1485 /// for headless rendering paths that don't track interaction
1486 /// state.
1487 pub fn with_ui_state(mut self, ui_state: &'a crate::state::UiState) -> Self {
1488 self.ui_state = Some(ui_state);
1489 self
1490 }
1491
1492 /// Attach a [`HostDiagnostics`] snapshot for this frame. Hosts call
1493 /// this when they want apps to surface debug overlays (e.g. the
1494 /// showcase status block); apps that don't read `diagnostics()`
1495 /// pay nothing for it. Headless render paths leave it `None`.
1496 pub fn with_diagnostics(mut self, diagnostics: &'a HostDiagnostics) -> Self {
1497 self.diagnostics = Some(diagnostics);
1498 self
1499 }
1500
1501 /// Attach the logical-pixel viewport size for this frame. Hosts
1502 /// chain this so apps can branch on viewport metrics during build
1503 /// (responsive layout, phone-vs-desktop splits) without threading
1504 /// surface size through their own state. Headless render paths
1505 /// without a meaningful viewport leave it unset.
1506 pub fn with_viewport(mut self, width: f32, height: f32) -> Self {
1507 self.viewport = Some((width, height));
1508 self
1509 }
1510
1511 /// Attach the host's reported safe-area insets in logical pixels.
1512 /// Hosts chain this when platform chrome (on-screen keyboard,
1513 /// notch, status bar, home indicator) is obscuring some band of
1514 /// the viewport. Apps read it via [`Self::safe_area`] /
1515 /// [`Self::safe_area_bottom`] and inset their interactive content
1516 /// accordingly. Hosts that don't report safe-area metrics omit
1517 /// this; apps see `Sides::zero()` from the read accessors.
1518 pub fn with_safe_area(mut self, sides: crate::tree::Sides) -> Self {
1519 self.safe_area = Some(sides);
1520 self
1521 }
1522
1523 /// Per-frame diagnostic snapshot from the host (backend, frame
1524 /// cadence, trigger reason, etc.), or `None` when the host did
1525 /// not attach one. Apps display this in optional debug overlays.
1526 pub fn diagnostics(&self) -> Option<&HostDiagnostics> {
1527 self.diagnostics
1528 }
1529
1530 /// The active runtime theme for this frame.
1531 pub fn theme(&self) -> &crate::Theme {
1532 self.theme
1533 }
1534
1535 /// Shorthand for `self.theme().palette()`.
1536 pub fn palette(&self) -> &crate::Palette {
1537 self.theme.palette()
1538 }
1539
1540 /// Logical-pixel viewport `(width, height)` the host attached for
1541 /// this frame, or `None` for headless render paths. Apps use this
1542 /// to branch layout on viewport metrics — see [`Self::viewport_below`]
1543 /// for the common phone-vs-desktop breakpoint case.
1544 pub fn viewport(&self) -> Option<(f32, f32)> {
1545 self.viewport
1546 }
1547
1548 /// Logical-pixel viewport width the host attached for this frame,
1549 /// or `None` when no viewport is available. Convenience for the
1550 /// common single-axis branch (`cx.viewport_width().map_or(false,
1551 /// |w| w < 600.0)`).
1552 pub fn viewport_width(&self) -> Option<f32> {
1553 self.viewport.map(|(w, _)| w)
1554 }
1555
1556 /// Logical-pixel viewport height the host attached for this frame,
1557 /// or `None` when no viewport is available.
1558 pub fn viewport_height(&self) -> Option<f32> {
1559 self.viewport.map(|(_, h)| h)
1560 }
1561
1562 /// True iff the attached viewport's width is strictly less than
1563 /// `threshold` logical pixels. Returns `false` when no viewport is
1564 /// attached so headless / desktop-default paths fall through to
1565 /// the wider branch — apps that want the opposite default can
1566 /// match on [`Self::viewport_width`] directly.
1567 ///
1568 /// Use for the common breakpoint split:
1569 /// ```ignore
1570 /// if cx.viewport_below(600.0) {
1571 /// phone_layout()
1572 /// } else {
1573 /// desktop_layout()
1574 /// }
1575 /// ```
1576 pub fn viewport_below(&self, threshold: f32) -> bool {
1577 self.viewport_width().is_some_and(|w| w < threshold)
1578 }
1579
1580 /// Logical-pixel safe-area insets the host reports for this frame
1581 /// (`Sides::zero()` when nothing was attached). Today this is
1582 /// populated only by damascene-web when the on-screen keyboard
1583 /// shrinks the visual viewport — `bottom` carries the keyboard
1584 /// height; future native mobile hosts will additionally populate
1585 /// `top` for status-bar / notch and `bottom` for home-indicator.
1586 ///
1587 /// Apps inset their root layout (or just the focused-input
1588 /// region) by these amounts so interactive content doesn't sit
1589 /// underneath platform chrome. The runtime does not auto-apply
1590 /// this — apps decide where the inset matters.
1591 pub fn safe_area(&self) -> crate::tree::Sides {
1592 self.safe_area.unwrap_or_default()
1593 }
1594
1595 /// Convenience: just the bottom inset, in logical pixels. Most
1596 /// commonly the soft-keyboard height.
1597 pub fn safe_area_bottom(&self) -> f32 {
1598 self.safe_area().bottom
1599 }
1600
1601 /// Key of the leaf node currently under the pointer, or `None`
1602 /// when nothing is hovered or this `BuildCx` was built without a
1603 /// `UiState` (headless rendering paths).
1604 ///
1605 /// Use for branching the build output on hover state without
1606 /// mirroring it via `App::on_event` handlers — e.g., a sidebar
1607 /// row that previews details in a side pane based on what's
1608 /// currently hovered.
1609 ///
1610 /// For region-aware queries (parent stays "hot" while a child is
1611 /// hovered), prefer [`Self::is_hovering_within`].
1612 pub fn hovered_key(&self) -> Option<&str> {
1613 self.ui_state?.hovered_key()
1614 }
1615
1616 /// True iff `key`'s node — or any descendant of it — is the
1617 /// current hover target. Subtree-aware, matching the semantics of
1618 /// [`crate::tree::El::hover_alpha`]. Returns `false` when this
1619 /// `BuildCx` has no attached `UiState` or when `key` isn't in the
1620 /// current tree.
1621 ///
1622 /// Reads the underlying tracker, not the eased subtree envelope —
1623 /// the boolean flips immediately on hit-test identity change.
1624 pub fn is_hovering_within(&self, key: &str) -> bool {
1625 self.ui_state
1626 .is_some_and(|state| state.is_hovering_within(key))
1627 }
1628
1629 /// The scatter point currently under the cursor in a `chart3d` scene, if
1630 /// any — the 3D analogue of [`hovered_key`](Self::hovered_key).
1631 ///
1632 /// Scene points aren't `El`s, so they can't emit `PointerEnter`/`Leave`
1633 /// like 2D widgets; this surfaces the same hover pick that draws the
1634 /// built-in tooltip chip ([`ScenePointPick`] carries the scene id + mark +
1635 /// point index). Use it to drive a detail panel / highlight / linked view
1636 /// on hover — branch the build on `cx.hovered_scene_point()` without an
1637 /// `on_event` handler. Picked a frame late (fine for hover UI) and honours
1638 /// the chip's depth-occlusion + behind-camera culling.
1639 ///
1640 /// [`ScenePointPick`]: crate::scene::ScenePointPick
1641 pub fn hovered_scene_point(&self) -> Option<&crate::scene::ScenePointPick> {
1642 self.ui_state?.hovered_scene_point()
1643 }
1644
1645 /// The laid-out rect of the keyed node `key` from the *previous*
1646 /// frame's layout, or `None` when the key wasn't in that tree (or
1647 /// this `BuildCx` has no attached `UiState` — headless paths).
1648 ///
1649 /// The damascene analogue of the DOM's
1650 /// `getBoundingClientRect()`: layout geometry is retained between
1651 /// frames, so build code can read where a keyed thing actually
1652 /// landed. One frame stale by construction — `build` runs before
1653 /// this frame's layout — which is fine for the usual uses
1654 /// (branching on a measured-once size, sizing a dependent pane).
1655 /// Same staleness contract as [`Self::hovered_key`]. For
1656 /// event-time decisions prefer [`EventCx::rect_of_key`], which
1657 /// answers at the moment the handler runs.
1658 pub fn rect_of_key(&self, key: &str) -> Option<Rect> {
1659 self.ui_state?.rect_of_key(key)
1660 }
1661
1662 /// The live pose of a keyed scene camera by computed id — see
1663 /// [`UiState::scene_camera`](crate::state::UiState::scene_camera).
1664 /// Combined with the node rect and a pointer position, an app builds a
1665 /// screen→world ray (`view_proj().inverse()`) to pick or place geometry on
1666 /// a plane. The id is a node's computed id, e.g. a build-time
1667 /// [`BuildCx::rect_of_key`]-keyed scene's resolved id.
1668 pub fn scene_camera(&self, id: &str) -> Option<crate::scene::CameraState> {
1669 self.ui_state?.scene_camera(id)
1670 }
1671
1672 /// The half-open row-index range the keyed virtual list realized in
1673 /// the previous frame's layout, or `None` when the key isn't a
1674 /// laid-out virtual list (or no `UiState` is attached). One frame
1675 /// stale by construction, same contract as [`Self::rect_of_key`].
1676 ///
1677 /// The cache-eviction hook for media-heavy lists: rows outside this
1678 /// range are off-screen, so their decoded images/thumbnails can be
1679 /// dropped and recreated by the row builder when scrolled back.
1680 pub fn visible_range(&self, key: &str) -> Option<std::ops::Range<usize>> {
1681 self.ui_state?.visible_range(key)
1682 }
1683
1684 /// The current pan/zoom of the [`viewport`](crate::tree::viewport)
1685 /// keyed `key`, from the last layout — for a zoom-percentage readout
1686 /// or to project content into screen space. `None` until the keyed
1687 /// viewport has been laid out.
1688 pub fn viewport_view(&self, key: &str) -> Option<crate::viewport::ViewportView> {
1689 self.ui_state?.viewport_view_by_key(key)
1690 }
1691
1692 /// The bounding box of the keyed viewport's laid-out content in
1693 /// **content space** (pre-transform), from the last layout — combine
1694 /// with [`Self::viewport_view`] to draw a minimap / overview rect.
1695 /// `None` until the keyed viewport has been laid out with measurable
1696 /// content. See
1697 /// [`UiState::viewport_content_bounds_by_key`](crate::state::UiState::viewport_content_bounds_by_key).
1698 pub fn viewport_content_bounds(&self, key: &str) -> Option<Rect> {
1699 self.ui_state?.viewport_content_bounds_by_key(key)
1700 }
1701
1702 /// Whether the keyed viewport is still at its home framing (the
1703 /// policy fit / last programmatic fit or reset), or the user has
1704 /// taken the view over — for chrome that shows "Fit" vs a concrete
1705 /// zoom percentage, or disables a Reset button at home. `None` when
1706 /// no laid-out node carries `key`. See
1707 /// [`UiState::viewport_at_home`](crate::state::UiState::viewport_at_home).
1708 pub fn viewport_at_home(&self, key: &str) -> Option<bool> {
1709 self.ui_state?.viewport_at_home_by_key(key)
1710 }
1711
1712 /// Whether the keyed viewport is mid-flight on a smooth programmatic
1713 /// navigation
1714 /// ([`ViewportBehavior::Smooth`](crate::viewport::ViewportBehavior::Smooth))
1715 /// — for gating input or chrome during a fly-to. `None` when no
1716 /// laid-out node carries `key`. See
1717 /// [`UiState::viewport_in_flight`](crate::state::UiState::viewport_in_flight).
1718 pub fn viewport_in_flight(&self, key: &str) -> Option<bool> {
1719 self.ui_state?.viewport_in_flight_by_key(key)
1720 }
1721
1722 /// The user-dragged size of a keyed
1723 /// [`user_resizable`](crate::tree::El::user_resizable) pane, in
1724 /// logical pixels. `None` until the user's first drag — the pane
1725 /// is still at its declared size. See
1726 /// [`UiState::user_size`](crate::UiState::user_size).
1727 pub fn user_size(&self, key: &str) -> Option<f32> {
1728 self.ui_state?.user_size(key)
1729 }
1730}
1731
1732/// Read-only context passed to [`App::on_event`] /
1733/// [`App::on_wheel_event`].
1734///
1735/// Event handlers regularly need post-layout geometry to make a
1736/// decision — "which room row is under this drop?", "what size did
1737/// the lightbox body actually get?" — and the handler's state owns no
1738/// node, so it can't have carried the rect itself. `EventCx` is the
1739/// damascene analogue of the DOM's ambient `document`: a handle into
1740/// the retained layout the user is currently looking at, queryable by
1741/// key (`element.getBoundingClientRect()` shape). Geometry answers
1742/// from the last laid-out frame — exactly what's on screen when the
1743/// event fires.
1744///
1745/// Like [`BuildCx`], the struct is opaque so the API stays additive:
1746/// new accessors don't break apps that ignore the context.
1747#[derive(Copy, Clone, Debug, Default)]
1748pub struct EventCx<'a> {
1749 ui_state: Option<&'a crate::state::UiState>,
1750 diagnostics: Option<&'a HostDiagnostics>,
1751 viewport: Option<(f32, f32)>,
1752}
1753
1754impl<'a> EventCx<'a> {
1755 /// Construct an empty context. Headless tests that drive
1756 /// [`App::on_event`] directly use this; real hosts chain
1757 /// [`Self::with_ui_state`] so geometry queries can answer.
1758 pub fn new() -> Self {
1759 Self::default()
1760 }
1761
1762 /// Attach the runtime's [`crate::state::UiState`] so geometry
1763 /// accessors can answer. Hosts call this at every dispatch site;
1764 /// when omitted, the accessors return `None`.
1765 pub fn with_ui_state(mut self, ui_state: &'a crate::state::UiState) -> Self {
1766 self.ui_state = Some(ui_state);
1767 self
1768 }
1769
1770 /// Attach the host's most recent [`HostDiagnostics`] snapshot —
1771 /// the one from the last built frame. Hosts chain this at every
1772 /// dispatch site so handlers can branch on negotiated output
1773 /// state (e.g. [`HostDiagnostics::hdr_active`], working color
1774 /// space) without mirroring it from `build` through app state.
1775 pub fn with_diagnostics(mut self, diagnostics: &'a HostDiagnostics) -> Self {
1776 self.diagnostics = Some(diagnostics);
1777 self
1778 }
1779
1780 /// Attach the logical-pixel viewport size the user is currently
1781 /// looking at. Hosts chain this so handlers that make
1782 /// layout-dependent decisions (grid-column navigation, breakpoint
1783 /// branches) don't have to stash the viewport from `build` in app
1784 /// state.
1785 pub fn with_viewport(mut self, width: f32, height: f32) -> Self {
1786 self.viewport = Some((width, height));
1787 self
1788 }
1789
1790 /// The host's diagnostic snapshot from the last built frame, or
1791 /// `None` when the host did not attach one (headless dispatch).
1792 /// Same data as [`BuildCx::diagnostics`], one frame stale by
1793 /// construction — events fire between frames.
1794 pub fn diagnostics(&self) -> Option<&HostDiagnostics> {
1795 self.diagnostics
1796 }
1797
1798 /// Logical-pixel viewport `(width, height)` at the time this
1799 /// event fires, or `None` when the host attached none. Same
1800 /// contract as [`BuildCx::viewport`].
1801 pub fn viewport(&self) -> Option<(f32, f32)> {
1802 self.viewport
1803 }
1804
1805 /// Logical-pixel viewport width, or `None` when no viewport is
1806 /// attached. Convenience mirroring [`BuildCx::viewport_width`] —
1807 /// the common case is event-time navigation math that must agree
1808 /// with build-time layout (e.g. grid column count).
1809 pub fn viewport_width(&self) -> Option<f32> {
1810 self.viewport.map(|(w, _)| w)
1811 }
1812
1813 /// Logical-pixel viewport height, or `None` when no viewport is
1814 /// attached.
1815 pub fn viewport_height(&self) -> Option<f32> {
1816 self.viewport.map(|(_, h)| h)
1817 }
1818
1819 /// True iff the attached viewport's width is strictly less than
1820 /// `threshold` logical pixels; `false` when no viewport is
1821 /// attached. Same semantics as [`BuildCx::viewport_below`] so
1822 /// build- and event-time breakpoint branches agree.
1823 pub fn viewport_below(&self, threshold: f32) -> bool {
1824 self.viewport_width().is_some_and(|w| w < threshold)
1825 }
1826
1827 /// The laid-out rect of the keyed node `key`, from the layout the
1828 /// user is looking at as this event fires. `None` when the key is
1829 /// absent from that tree (or no `UiState` is attached).
1830 ///
1831 /// This is the first-class shape for "the handler needs to know
1832 /// where a keyed thing landed": resolving a drop target against
1833 /// row rects on `PointerUp`, stepping zoom from a body's fitted
1834 /// size, anchoring app-drawn chrome to a control. The event's own
1835 /// target rect is already on [`UiEvent::target`]; this answers
1836 /// for *other* keys.
1837 pub fn rect_of_key(&self, key: &str) -> Option<Rect> {
1838 self.ui_state?.rect_of_key(key)
1839 }
1840
1841 /// The live pose of a keyed scene camera by computed id (the moment the
1842 /// handler runs) — see
1843 /// [`UiState::scene_camera`](crate::state::UiState::scene_camera).
1844 /// Combined with [`UiEvent::target`]'s rect + pointer, build a
1845 /// screen→world ray for picking/placement; `id` is typically that
1846 /// target's `node_id`.
1847 ///
1848 /// [`UiEvent::target`]: crate::event::UiEvent::target
1849 pub fn scene_camera(&self, id: &str) -> Option<crate::scene::CameraState> {
1850 self.ui_state?.scene_camera(id)
1851 }
1852
1853 /// The half-open row-index range the keyed virtual list realized in
1854 /// the layout the user is looking at — see
1855 /// [`BuildCx::visible_range`].
1856 pub fn visible_range(&self, key: &str) -> Option<std::ops::Range<usize>> {
1857 self.ui_state?.visible_range(key)
1858 }
1859
1860 /// The current pan/zoom of the [`viewport`](crate::tree::viewport)
1861 /// keyed `key`, from the last layout — for a zoom-percentage readout
1862 /// or to project content into screen space. `None` until the keyed
1863 /// viewport has been laid out.
1864 pub fn viewport_view(&self, key: &str) -> Option<crate::viewport::ViewportView> {
1865 self.ui_state?.viewport_view_by_key(key)
1866 }
1867
1868 /// The bounding box of the keyed viewport's laid-out content in
1869 /// content space — see [`BuildCx::viewport_content_bounds`].
1870 pub fn viewport_content_bounds(&self, key: &str) -> Option<Rect> {
1871 self.ui_state?.viewport_content_bounds_by_key(key)
1872 }
1873
1874 /// Whether the keyed viewport is still at its home framing — see
1875 /// [`BuildCx::viewport_at_home`].
1876 pub fn viewport_at_home(&self, key: &str) -> Option<bool> {
1877 self.ui_state?.viewport_at_home_by_key(key)
1878 }
1879
1880 /// Whether the keyed viewport is mid-flight on a smooth programmatic
1881 /// navigation — see [`BuildCx::viewport_in_flight`].
1882 pub fn viewport_in_flight(&self, key: &str) -> Option<bool> {
1883 self.ui_state?.viewport_in_flight_by_key(key)
1884 }
1885
1886 /// The user-dragged size of a keyed
1887 /// [`user_resizable`](crate::tree::El::user_resizable) pane — the
1888 /// value to persist when a [`UiEventKind::Resized`] arrives. See
1889 /// [`BuildCx::user_size`].
1890 pub fn user_size(&self, key: &str) -> Option<f32> {
1891 self.ui_state?.user_size(key)
1892 }
1893}
1894
1895/// The application contract. Implement this on your state struct and
1896/// pass it to a host runner (e.g., `damascene_winit_wgpu::run`).
1897pub trait App {
1898 /// Refresh app-owned external state immediately before a frame is
1899 /// built.
1900 ///
1901 /// Hosts call this once per redraw before [`Self::build`]. Use it
1902 /// for polling an external source, reconciling optimistic local
1903 /// state with a backend snapshot, or advancing host-owned live data
1904 /// that should be visible in the next tree. Keep expensive work
1905 /// outside the render loop; this hook is still on the frame path.
1906 ///
1907 /// This is the drain half of the **mailbox pattern** — background
1908 /// work posts messages over a channel and wakes the host (native:
1909 /// a channel + the winit host's external `Wakeup`; web:
1910 /// `damascene_web::Mailbox`), and this hook folds them into app
1911 /// state so the frame being built sees them. See "Patterns real
1912 /// apps converge on" in the damascene-core README.
1913 ///
1914 /// Default: no-op.
1915 fn before_build(&mut self) {}
1916
1917 /// Project current state into a scene tree. Called whenever the
1918 /// host requests a redraw, after [`Self::before_build`]. Prefer to
1919 /// keep this pure: read current state and return a fresh tree.
1920 ///
1921 /// `cx` carries per-frame, read-only context (active theme, future
1922 /// viewport / phase metadata). Apps that don't need to branch on
1923 /// the theme during construction can ignore the parameter — token
1924 /// references in widget code resolve through the palette
1925 /// automatically.
1926 ///
1927 /// # Page anatomy
1928 ///
1929 /// The returned tree is the *whole window*, and a bare
1930 /// `column([...])` root is almost never what a window wants: it
1931 /// has no padding (content sits flush against window edges and
1932 /// clips under rounded window corners) and no overlay root for
1933 /// `.tooltip()` layers to mount on. Return
1934 /// [`page`](crate::widgets::page::page) — it bakes the window
1935 /// padding + overlay root — and wrap it in
1936 /// [`overlays`](crate::overlays) when the app drives modals or
1937 /// dropdowns:
1938 ///
1939 /// ```ignore
1940 /// fn build(&self, _cx: &BuildCx) -> El {
1941 /// overlays(
1942 /// page([toolbar([...]), content()]),
1943 /// [self.modal_open.then(|| modal("confirm", "Sure?", [...]))],
1944 /// )
1945 /// }
1946 /// ```
1947 ///
1948 /// For custom anatomy (full-bleed canvases, centered Hug-sized
1949 /// cards), compose `stack([background, content])` by hand — see
1950 /// `damascene-fixtures/src/hero.rs` for the expanded idiom.
1951 fn build(&self, cx: &BuildCx) -> El;
1952
1953 /// Update state in response to a routed event. Default: no-op.
1954 ///
1955 /// `cx` carries read-only frame context — most usefully
1956 /// [`EventCx::rect_of_key`], for decisions that depend on where a
1957 /// keyed node landed in the layout the user is looking at
1958 /// (resolving a drop target on `PointerUp`, stepping zoom from a
1959 /// measured size). Handlers that don't consult layout ignore it.
1960 fn on_event(&mut self, _event: UiEvent, _cx: &EventCx) {}
1961
1962 /// Update state in response to routed wheel input.
1963 ///
1964 /// Return `true` to consume the wheel and suppress Damascene's default
1965 /// scroll routing. The default forwards to [`Self::on_event`] and
1966 /// returns `false`, so existing apps can observe wheel events
1967 /// without opting out of normal scrolling.
1968 fn on_wheel_event(&mut self, event: UiEvent, cx: &EventCx) -> bool {
1969 self.on_event(event, cx);
1970 false
1971 }
1972
1973 /// The application's current text [`crate::selection::Selection`].
1974 /// Read by the host once per frame so the library can paint
1975 /// highlight bands and resolve `selected_text` for clipboard.
1976 /// Apps that own a `Selection` field return a clone here; the
1977 /// default returns the empty selection.
1978 fn selection(&self) -> crate::selection::Selection {
1979 crate::selection::Selection::default()
1980 }
1981
1982 /// App-level hotkey registry. The library matches incoming key
1983 /// presses against this list before its own focus-activation
1984 /// routing; a match emits a [`UiEvent`] with `kind =
1985 /// UiEventKind::Hotkey` and `key = Some(name)`.
1986 ///
1987 /// Called once per build cycle; the host runner snapshots the list
1988 /// alongside `build()` so the chords stay in sync with state.
1989 /// Default: no hotkeys.
1990 ///
1991 /// # Multi-window scoping
1992 ///
1993 /// Hotkeys are scoped per `Runner`, and a multi-window host owns
1994 /// one `Runner` per window — so the contract is simply: **feed
1995 /// each window's `Runner` only that window's hotkey list**, and
1996 /// route each window's key events only to its own `Runner` (which
1997 /// a winit host does naturally, keyed by `WindowId`). A chord then
1998 /// fires in the window the OS focused, never globally. There is no
1999 /// cross-window registry to deduplicate or shadow; "global"
2000 /// accelerators are app policy — register the chord in every
2001 /// window's list and treat the resulting per-window `Hotkey` event
2002 /// as the same action.
2003 fn hotkeys(&self) -> Vec<(KeyChord, String)> {
2004 Vec::new()
2005 }
2006
2007 /// Drain pending toast notifications produced since the last
2008 /// frame. The runtime calls this once per `prepare_layout`,
2009 /// stamps each spec with a monotonic id and `expires_at = now +
2010 /// ttl`, queues it in the runtime toast state, and
2011 /// synthesizes a `toast_stack` layer at the El root so the
2012 /// rendered tree mirrors the visible state. Apps typically
2013 /// accumulate specs in a `Vec<ToastSpec>` field from event
2014 /// handlers, then `mem::take` it here.
2015 ///
2016 /// **Root requirement:** apps that produce toasts (or use
2017 /// `.tooltip(text)` on any node) must wrap their
2018 /// [`Self::build`] return value in `overlays(main, [])` so the
2019 /// runtime can append the floating layer as an overlay sibling
2020 /// — same convention used for popovers and modals. Debug
2021 /// builds panic if the synthesizer runs against a non-overlay
2022 /// root.
2023 ///
2024 /// Default: no toasts.
2025 fn drain_toasts(&mut self) -> Vec<crate::toast::ToastSpec> {
2026 Vec::new()
2027 }
2028
2029 /// Drain pending programmatic focus requests produced since the
2030 /// last frame. The runtime calls this once per `prepare_layout`,
2031 /// after the focus order has been rebuilt from the new tree, and
2032 /// resolves each entry against the keyed focusables. Unmatched
2033 /// keys (widget absent from the rebuilt tree, or not focusable)
2034 /// are dropped silently.
2035 ///
2036 /// This is the imperative companion to keyboard `Tab` traversal:
2037 /// use it for affordances like *Ctrl+F → focus the search input*,
2038 /// *jump-to-match → focus the matched row*, or *open inline edit
2039 /// → focus the field*. Apps typically accumulate keys in a
2040 /// `Vec<String>` field from event handlers and `mem::take` it
2041 /// here.
2042 ///
2043 /// Multiple requests in one frame resolve in order; the last
2044 /// successfully-resolved key is the one focused.
2045 ///
2046 /// Default: no requests.
2047 fn drain_focus_requests(&mut self) -> Vec<String> {
2048 Vec::new()
2049 }
2050
2051 /// Drain pending programmatic scroll requests. The runtime
2052 /// resolves each request during layout, using live viewport rects
2053 /// and row-height/content geometry that apps should not duplicate.
2054 /// Unmatched keys and out-of-range row indices drop silently.
2055 ///
2056 /// Use [`crate::scroll::ScrollRequest::ToRow`] for virtual-list
2057 /// affordances such as jump-to-search-result, reveal selected row,
2058 /// or scroll-to-top-on-tab-change. Use
2059 /// [`crate::scroll::ScrollRequest::EnsureVisible`] for widgets
2060 /// with an internal scroll viewport, including fixed-height
2061 /// [`crate::widgets::text_area`] caret-into-view after accepted
2062 /// edit/navigation events. Apps typically accumulate requests in a
2063 /// `Vec<ScrollRequest>` field from event handlers and
2064 /// `mem::take` it here.
2065 ///
2066 /// Default: no requests.
2067 fn drain_scroll_requests(&mut self) -> Vec<crate::scroll::ScrollRequest> {
2068 Vec::new()
2069 }
2070
2071 /// Drain programmatic [`crate::viewport::ViewportRequest`]s produced
2072 /// since the last frame — fit-to-content, reset, or center a
2073 /// [`crate::tree::viewport`] by its `.key(...)`. Hosts call this once
2074 /// per frame and forward to
2075 /// [`crate::runtime::RunnerCore::push_viewport_requests`]; each
2076 /// request is consumed during layout of the matching viewport, where
2077 /// its live rect and content extents are known. Apps accumulate
2078 /// requests from event handlers (e.g. a "Fit" toolbar button) in a
2079 /// `Vec<ViewportRequest>` field and `mem::take` it here, mirroring
2080 /// [`Self::drain_scroll_requests`].
2081 ///
2082 /// Default: no requests.
2083 fn drain_viewport_requests(&mut self) -> Vec<crate::viewport::ViewportRequest> {
2084 Vec::new()
2085 }
2086
2087 /// Drain programmatic [`crate::plot::PlotRequest`]s produced since
2088 /// the last frame — fit-all or pin the X window of a
2089 /// [`crate::tree::plot`] by its `.key(...)`. Hosts call this once per
2090 /// frame and forward to
2091 /// [`crate::runtime::RunnerCore::push_plot_requests`]; each request
2092 /// is consumed during the plot prepare pass, where the live data
2093 /// bounds are known. Apps accumulate requests from event handlers
2094 /// (e.g. a "Fit" button or a "last 60 s" preset) in a
2095 /// `Vec<PlotRequest>` field and `mem::take` it here, mirroring
2096 /// [`Self::drain_viewport_requests`].
2097 ///
2098 /// Default: no requests.
2099 fn drain_plot_requests(&mut self) -> Vec<crate::plot::PlotRequest> {
2100 Vec::new()
2101 }
2102
2103 /// Drain pending URL-open requests produced since the last frame.
2104 /// Hosts call this once per frame and route each URL to a
2105 /// platform-appropriate opener — `window.open` in the wasm host,
2106 /// the `open` crate (or equivalent) on native.
2107 ///
2108 /// The library emits [`UiEventKind::LinkActivated`] when a click
2109 /// lands on a text run carrying a link URL, but it does not act
2110 /// on the URL itself: opening a link is an app concern (apps may
2111 /// want to confirm, filter by scheme, route through an internal
2112 /// router, or no-op entirely). Apps that want the default
2113 /// browser-style behavior accumulate URLs from
2114 /// [`UiEventKind::LinkActivated`] in their `on_event` handler and
2115 /// return them here; apps that don't override this method drop
2116 /// link clicks on the floor.
2117 ///
2118 /// Default: no requests.
2119 fn drain_link_opens(&mut self) -> Vec<String> {
2120 Vec::new()
2121 }
2122
2123 /// Custom shaders this app needs registered. Each entry carries
2124 /// the shader name, its WGSL source, and per-flag opt-ins
2125 /// (backdrop sampling, time-driven motion). The host runner
2126 /// registers them once at startup via
2127 /// `Runner::register_shader_with(name, wgsl, samples_backdrop, samples_time)`.
2128 ///
2129 /// Backends that don't support backdrop sampling skip entries with
2130 /// `samples_backdrop=true`; any node bound to such a shader will
2131 /// draw nothing on those backends rather than mis-render.
2132 /// `samples_time=true` declares that the shader's output depends
2133 /// on `frame.time`, which keeps the host idle loop ticking while
2134 /// any node is bound to it.
2135 ///
2136 /// Default: no shaders.
2137 fn shaders(&self) -> Vec<AppShader> {
2138 Vec::new()
2139 }
2140
2141 /// Runtime paint theme for this app. Hosts apply it to the renderer
2142 /// before preparing each frame so stateful apps can switch global
2143 /// material routing without backend-specific calls.
2144 fn theme(&self) -> crate::Theme {
2145 crate::Theme::default()
2146 }
2147}
2148
2149/// One custom shader registration, returned from [`App::shaders`].
2150#[derive(Clone, Copy, Debug)]
2151pub struct AppShader {
2152 /// Registration name that nodes reference to bind the shader.
2153 pub name: &'static str,
2154 /// WGSL source the host registers with the backend at startup.
2155 pub wgsl: &'static str,
2156 /// Reads the prior pass's color target (`@group(2) backdrop_tex`).
2157 /// Backends without backdrop support skip these.
2158 pub samples_backdrop: bool,
2159 /// Reads `frame.time` and so requires continuous redraw whenever
2160 /// any node is bound to it. The runtime ORs this into
2161 /// `PrepareResult::needs_redraw` per frame.
2162 pub samples_time: bool,
2163}
2164
2165#[cfg(test)]
2166mod tests {
2167 use super::*;
2168
2169 #[test]
2170 fn pointer_button_from_linux_evdev_codes() {
2171 assert_eq!(
2172 PointerButton::from_linux_button(0x110),
2173 Some(PointerButton::Primary)
2174 );
2175 assert_eq!(
2176 PointerButton::from_linux_button(0x111),
2177 Some(PointerButton::Secondary)
2178 );
2179 assert_eq!(
2180 PointerButton::from_linux_button(0x112),
2181 Some(PointerButton::Middle)
2182 );
2183 // BTN_SIDE / BTN_EXTRA — not surfaced.
2184 assert_eq!(PointerButton::from_linux_button(0x113), None);
2185 assert_eq!(PointerButton::from_linux_button(0), None);
2186 }
2187 use crate::Theme;
2188
2189 #[test]
2190 fn viewport_unset_returns_none_and_breakpoint_returns_false() {
2191 let theme = Theme::default();
2192 let cx = BuildCx::new(&theme);
2193 assert!(cx.viewport().is_none());
2194 assert!(cx.viewport_width().is_none());
2195 assert!(!cx.viewport_below(600.0));
2196 }
2197
2198 #[test]
2199 fn build_cx_surfaces_hovered_scene_point() {
2200 use crate::scene::ScenePointPick;
2201 let theme = Theme::default();
2202 let mut ui = crate::state::UiState::new();
2203
2204 // No attached state, and none stored → nothing to surface.
2205 assert!(BuildCx::new(&theme).hovered_scene_point().is_none());
2206 assert!(
2207 BuildCx::new(&theme)
2208 .with_ui_state(&ui)
2209 .hovered_scene_point()
2210 .is_none()
2211 );
2212
2213 // After the runtime stores a pick, the app reads it at build.
2214 ui.set_hovered_scene_point(Some(ScenePointPick {
2215 scene: "scene".into(),
2216 mark: 0,
2217 point: 4,
2218 }));
2219 let cx = BuildCx::new(&theme).with_ui_state(&ui);
2220 let pick = cx.hovered_scene_point().expect("pick surfaced");
2221 assert_eq!(
2222 (pick.scene.as_str(), pick.mark, pick.point),
2223 ("scene", 0, 4)
2224 );
2225 }
2226
2227 #[test]
2228 fn viewport_set_exposes_width_and_height() {
2229 let theme = Theme::default();
2230 let cx = BuildCx::new(&theme).with_viewport(420.0, 800.0);
2231 assert_eq!(cx.viewport(), Some((420.0, 800.0)));
2232 assert_eq!(cx.viewport_width(), Some(420.0));
2233 assert_eq!(cx.viewport_height(), Some(800.0));
2234 }
2235
2236 #[test]
2237 fn hdr_active_needs_output_evidence_and_wide_chosen_format() {
2238 use crate::color::{ColorManagementStatus, CompositorColorTargets, TransferFunction};
2239
2240 let hdr_targets = CompositorColorTargets {
2241 preferred_transfer: Some(TransferFunction::Pq),
2242 ..Default::default()
2243 };
2244 let scrgb_surface = SurfaceColorInfo {
2245 formats: vec![
2246 SurfaceFormatInfo {
2247 name: "Bgra8UnormSrgb".into(),
2248 srgb: true,
2249 wide: false,
2250 },
2251 SurfaceFormatInfo {
2252 name: "Rgba16Float".into(),
2253 srgb: false,
2254 wide: true,
2255 },
2256 ],
2257 chosen_format: "Rgba16Float".into(),
2258 ..Default::default()
2259 };
2260
2261 let mut d = HostDiagnostics::default();
2262 // Default: no protocol, no surface info.
2263 assert!(!d.hdr_active());
2264
2265 // HDR output + scRGB swapchain → active. `attached` stays None
2266 // on the no-attach host — it must not factor in.
2267 d.color_management = ColorManagementStatus::Available {
2268 capabilities: Default::default(),
2269 attached: None,
2270 targets: hdr_targets.clone(),
2271 };
2272 d.surface_color = Some(scrgb_surface.clone());
2273 assert!(d.hdr_active());
2274
2275 // HDR output but the negotiator stayed on 8-bit sRGB (e.g. the
2276 // app is sdr_only) → not active.
2277 d.surface_color = Some(SurfaceColorInfo {
2278 chosen_format: "Bgra8UnormSrgb".into(),
2279 ..scrgb_surface.clone()
2280 });
2281 assert!(!d.hdr_active());
2282
2283 // Wide swapchain but no HDR evidence from the output → not active.
2284 d.color_management = ColorManagementStatus::Available {
2285 capabilities: Default::default(),
2286 attached: None,
2287 targets: CompositorColorTargets::default(),
2288 };
2289 d.surface_color = Some(scrgb_surface);
2290 assert!(!d.hdr_active());
2291 }
2292
2293 #[test]
2294 fn viewport_below_uses_strict_less_than() {
2295 let theme = Theme::default();
2296 let cx = BuildCx::new(&theme).with_viewport(600.0, 800.0);
2297 assert!(!cx.viewport_below(600.0), "boundary is exclusive");
2298 assert!(cx.viewport_below(601.0));
2299 assert!(!cx.viewport_below(599.0));
2300 }
2301}