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