Skip to main content

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}