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) {
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
51use crate::tree::{El, Rect};
52
53/// Hit-test target metadata. `key` is the author-facing route, while
54/// `node_id` is the stable laid-out tree path used by artifacts.
55///
56/// `tooltip` snapshots the node's tooltip text at the moment the
57/// target was constructed, so the tooltip pass doesn't have to walk
58/// the live tree to resolve it. This is what makes tooltips work on
59/// virtual-list rows: hit-testing reads `last_tree` (where the row
60/// has been realized), and the cached text survives into the next
61/// frame's `synthesize_tooltip` even though that frame's tree hasn't
62/// rebuilt its virtual-list children yet.
63#[derive(Clone, Debug, PartialEq)]
64#[non_exhaustive]
65pub struct UiTarget {
66    pub key: String,
67    pub node_id: String,
68    pub rect: Rect,
69    pub tooltip: Option<String>,
70    /// Scroll offset of the deepest scroll subtree inside this hit
71    /// target, in logical pixels. `0.0` for widgets that don't
72    /// contain a scroll. Used by widgets like
73    /// [`crate::widgets::text_area`] to convert a pointer in viewport
74    /// space (what the user clicks) into content space (what
75    /// cosmic-text's `hit_byte` and `caret_xy` work in) — without
76    /// this, clicks after scrolling land on the wrong line because
77    /// the content has been shifted up by `scroll_offset_y` while
78    /// the outer's `rect` hasn't moved.
79    pub scroll_offset_y: f32,
80}
81
82/// Which mouse button (or pointer button) generated a pointer event.
83/// The host backend translates its native button id to one of these
84/// before calling `pointer_down` / `pointer_up`.
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86pub enum PointerButton {
87    /// Left mouse, primary touch, or pen tip. Drives `Click`.
88    Primary,
89    /// Right mouse or two-finger touch. Drives `SecondaryClick` —
90    /// typically opens a context menu.
91    Secondary,
92    /// Middle mouse / scroll-wheel click. No library default; surfaced
93    /// as `MiddleClick` for apps that want it (autoscroll, paste-on-X).
94    Middle,
95}
96
97/// Physical kind of pointer that produced an event. Mirrors the DOM
98/// `PointerEvent.pointerType`. Backends without a real signal pass
99/// [`PointerKind::Mouse`].
100///
101/// The runtime uses this to specialize behavior that does not transfer
102/// across modalities — for example, `Touch` has no resting hover state
103/// and gates `PointerEnter`/`PointerLeave` accordingly.
104#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
105pub enum PointerKind {
106    /// Mouse, trackpad, or any device that reports continuous hover.
107    #[default]
108    Mouse,
109    /// Touchscreen. No hover state; contact starts with `pointer_down`.
110    Touch,
111    /// Pen / stylus. Behaves like `Mouse` for hover, but backends may
112    /// surface pressure in [`Pointer::pressure`].
113    Pen,
114}
115
116/// Stable per-pointer identifier within a frame. Mirrors the DOM
117/// `PointerEvent.pointerId`. Backends with only one pointer pass
118/// [`PointerId::PRIMARY`]; multi-touch backends keep IDs stable for the
119/// lifetime of a single contact.
120///
121/// The runtime currently routes only the primary contact; secondary IDs
122/// are reserved for future multi-touch / gesture work.
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
124pub struct PointerId(pub u32);
125
126impl PointerId {
127    /// The conventional ID for backends that have only one pointer
128    /// (mouse-only hosts, synthetic test events, the first touch
129    /// contact when multi-touch IDs are not tracked).
130    pub const PRIMARY: PointerId = PointerId(0);
131}
132
133/// One pointer sample, in logical pixels. The argument shape for
134/// [`crate::runtime::RunnerCore::pointer_moved`],
135/// [`crate::runtime::RunnerCore::pointer_down`], and
136/// [`crate::runtime::RunnerCore::pointer_up`].
137///
138/// Modeled on the DOM `PointerEvent` interface so backends that
139/// already speak browser pointer events can map fields directly.
140/// `button` is meaningful on `pointer_down` / `pointer_up` and is
141/// ignored on `pointer_moved`; constructors default it to
142/// [`PointerButton::Primary`] for that case.
143#[derive(Clone, Copy, Debug, PartialEq)]
144pub struct Pointer {
145    /// X coordinate in logical pixels relative to the window origin.
146    pub x: f32,
147    /// Y coordinate in logical pixels relative to the window origin.
148    pub y: f32,
149    /// Which button this event refers to. Ignored by `pointer_moved`.
150    pub button: PointerButton,
151    /// Physical kind of pointer (mouse / touch / pen).
152    pub kind: PointerKind,
153    /// Stable per-pointer ID. Use [`PointerId::PRIMARY`] for
154    /// single-pointer backends.
155    pub id: PointerId,
156    /// Normalized pressure in `0.0..=1.0` when the device reports it
157    /// (pen, force-touch). `None` when unavailable; mouse backends
158    /// always pass `None`.
159    pub pressure: Option<f32>,
160}
161
162impl Pointer {
163    /// A mouse-driven pointer at `(x, y)` for the given button. Use
164    /// from mouse-only hosts and synthetic tests.
165    pub fn mouse(x: f32, y: f32, button: PointerButton) -> Self {
166        Self {
167            x,
168            y,
169            button,
170            kind: PointerKind::Mouse,
171            id: PointerId::PRIMARY,
172            pressure: None,
173        }
174    }
175
176    /// A mouse pointer for `pointer_moved`, where `button` is
177    /// irrelevant. Equivalent to
178    /// [`Pointer::mouse(x, y, PointerButton::Primary)`][Self::mouse].
179    pub fn moving(x: f32, y: f32) -> Self {
180        Self::mouse(x, y, PointerButton::Primary)
181    }
182
183    /// A touch contact at `(x, y)` carrying the given pointer ID.
184    /// Backends translating browser `PointerEvent` should pass the
185    /// browser's `pointerId` directly.
186    pub fn touch(x: f32, y: f32, button: PointerButton, id: PointerId) -> Self {
187        Self {
188            x,
189            y,
190            button,
191            kind: PointerKind::Touch,
192            id,
193            pressure: None,
194        }
195    }
196}
197
198/// Keyboard key values normalized by the core library. This keeps the
199/// core independent from host/windowing crates while covering the
200/// navigation and activation keys the library owns.
201#[derive(Clone, Debug, PartialEq, Eq)]
202pub enum UiKey {
203    Enter,
204    Escape,
205    Tab,
206    Space,
207    ArrowUp,
208    ArrowDown,
209    ArrowLeft,
210    ArrowRight,
211    /// Backspace — deletes the grapheme before the caret.
212    Backspace,
213    /// Forward delete — deletes the grapheme after the caret.
214    Delete,
215    /// Home — caret to start of line.
216    Home,
217    /// End — caret to end of line.
218    End,
219    /// PageUp — coarse-step navigation (sliders adjust by a larger
220    /// amount; lists scroll a viewport).
221    PageUp,
222    /// PageDown — coarse-step navigation (sliders adjust by a larger
223    /// amount; lists scroll a viewport).
224    PageDown,
225    Character(String),
226    Other(String),
227}
228
229/// OS modifier-key mask. The four fields mirror the platform-standard
230/// modifier set; this struct is intentionally **not** `#[non_exhaustive]`
231/// so callers can use struct-literal syntax with `..Default::default()`
232/// to spell precise modifier combinations.
233#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
234pub struct KeyModifiers {
235    pub shift: bool,
236    pub ctrl: bool,
237    pub alt: bool,
238    pub logo: bool,
239}
240
241#[derive(Clone, Debug, PartialEq, Eq)]
242#[non_exhaustive]
243pub struct KeyPress {
244    pub key: UiKey,
245    pub modifiers: KeyModifiers,
246    pub repeat: bool,
247}
248
249/// A keyboard chord for app-level hotkey registration. Match a key with
250/// an exact modifier mask: `KeyChord::ctrl('f')` does not also match
251/// `Ctrl+Shift+F`, and `KeyChord::vim('j')` does not match if any
252/// modifier is held.
253///
254/// Register chords from [`App::hotkeys`]; the library matches them
255/// against incoming key presses ahead of focus activation routing and
256/// emits a [`UiEvent`] with `kind = UiEventKind::Hotkey` and `key`
257/// equal to the registered name.
258#[derive(Clone, Debug, PartialEq, Eq)]
259#[non_exhaustive]
260pub struct KeyChord {
261    pub key: UiKey,
262    pub modifiers: KeyModifiers,
263}
264
265impl KeyChord {
266    /// A bare key with no modifiers (vim-style). `KeyChord::vim('j')`
267    /// matches the `j` key with no Ctrl/Shift/Alt/Logo held.
268    pub fn vim(c: char) -> Self {
269        Self {
270            key: UiKey::Character(c.to_string()),
271            modifiers: KeyModifiers::default(),
272        }
273    }
274
275    /// `Ctrl+<char>`.
276    pub fn ctrl(c: char) -> Self {
277        Self {
278            key: UiKey::Character(c.to_string()),
279            modifiers: KeyModifiers {
280                ctrl: true,
281                ..Default::default()
282            },
283        }
284    }
285
286    /// `Ctrl+Shift+<char>`.
287    pub fn ctrl_shift(c: char) -> Self {
288        Self {
289            key: UiKey::Character(c.to_string()),
290            modifiers: KeyModifiers {
291                ctrl: true,
292                shift: true,
293                ..Default::default()
294            },
295        }
296    }
297
298    /// A named key with no modifiers (e.g. `KeyChord::named(UiKey::Escape)`).
299    pub fn named(key: UiKey) -> Self {
300        Self {
301            key,
302            modifiers: KeyModifiers::default(),
303        }
304    }
305
306    pub fn with_modifiers(mut self, modifiers: KeyModifiers) -> Self {
307        self.modifiers = modifiers;
308        self
309    }
310
311    /// Strict match: keys equal AND modifier mask is identical. Holding
312    /// extra modifiers does not match a chord that didn't request them.
313    pub fn matches(&self, key: &UiKey, modifiers: KeyModifiers) -> bool {
314        key_eq(&self.key, key) && self.modifiers == modifiers
315    }
316}
317
318fn key_eq(a: &UiKey, b: &UiKey) -> bool {
319    match (a, b) {
320        (UiKey::Character(x), UiKey::Character(y)) => x.eq_ignore_ascii_case(y),
321        _ => a == b,
322    }
323}
324
325/// User-facing event. The host's [`App::on_event`] receives one of these
326/// per discrete user action.
327///
328/// Most apps should not destructure every field. Prefer the convenience
329/// methods on this type for common routes:
330///
331/// ```
332/// # use damascene_core::prelude::*;
333/// # struct Counter { value: i32 }
334/// # impl App for Counter {
335/// # fn build(&self, _cx: &BuildCx) -> El { button("+").key("inc") }
336/// fn on_event(&mut self, event: UiEvent) {
337///     if event.is_click_or_activate("inc") {
338///         self.value += 1;
339///     }
340/// }
341/// # }
342/// ```
343#[derive(Clone, Debug)]
344#[non_exhaustive]
345pub struct UiEvent {
346    /// Route string for this event.
347    ///
348    /// For pointer and focus events, this is the [`El::key`][crate::El::key]
349    /// of the target node. For [`UiEventKind::Hotkey`], this is the
350    /// action name returned from [`App::hotkeys`]. For window-level
351    /// keyboard events such as Escape with no focused target, this is
352    /// `None`.
353    ///
354    /// Prefer [`Self::route`] or [`Self::is_click_or_activate`] in app
355    /// code. The field remains public for direct pattern matching.
356    pub key: Option<String>,
357    /// Full hit-test target for events routed to a concrete element.
358    pub target: Option<UiTarget>,
359    /// Pointer position in logical pixels when the event was emitted.
360    pub pointer: Option<(f32, f32)>,
361    /// Keyboard payload for key events.
362    pub key_press: Option<KeyPress>,
363    /// Composed text payload for [`UiEventKind::TextInput`] events.
364    pub text: Option<String>,
365    /// Library-emitted selection state for
366    /// [`UiEventKind::SelectionChanged`] events. Carries the new
367    /// [`crate::selection::Selection`] after the runtime resolved a
368    /// pointer interaction. The app folds this into its
369    /// `Selection` field the same way it folds `apply_event` results
370    /// into a [`crate::widgets::text_input::TextSelection`].
371    pub selection: Option<crate::selection::Selection>,
372    /// Modifier mask captured at the moment this event was emitted. For
373    /// keyboard events this duplicates `key_press.modifiers`; for
374    /// pointer events it's the host-tracked modifier state at the time
375    /// of the click / drag (used by widgets like text_input that need
376    /// to detect Shift+click for "extend selection").
377    pub modifiers: KeyModifiers,
378    /// Click number within a multi-click sequence. Set to 1 for single
379    /// click, 2 for double-click, 3 for triple-click, etc. The runtime
380    /// increments this when consecutive `PointerDown`s land on the same
381    /// target within ~500ms and ~4px of the previous click. `Drag`
382    /// events emitted while the final click is held keep the active
383    /// sequence count so text widgets can preserve word / line
384    /// granularity. `0` means "not applicable" — set on events outside
385    /// pointer click / drag routing.
386    ///
387    /// `text_input` / `text_area` and the static-text selection
388    /// manager read this to map double-click → select word, triple-
389    /// click → select line.
390    pub click_count: u8,
391    /// File system path for [`UiEventKind::FileHovered`] /
392    /// [`UiEventKind::FileDropped`] events. Multi-file drag-drops fire
393    /// one event per file (matching the underlying winit semantics);
394    /// each event carries one path. `PathBuf` rather than `String`
395    /// because Windows wide-char paths and unusual Unix paths aren't
396    /// guaranteed to be UTF-8.
397    pub path: Option<std::path::PathBuf>,
398    /// Modality of the pointer that produced this event. `None` for
399    /// non-pointer events (hotkeys, keyboard activation, file drops
400    /// without a tracked pointer). Apps that need to specialize for
401    /// touch (accessibility, analytics, alternate affordances) read
402    /// this; most app code can ignore it.
403    pub pointer_kind: Option<PointerKind>,
404    /// Wheel delta in logical pixels for [`UiEventKind::PointerWheel`].
405    ///
406    /// Positive `dy` means "scroll down" in the same coordinate system
407    /// used by Damascene's scroll containers. Hosts normalize line-based
408    /// and pixel-based wheel input before setting this field.
409    pub wheel_delta: Option<(f32, f32)>,
410    pub kind: UiEventKind,
411}
412
413impl UiEvent {
414    /// Synthesize a click event for the given route key.
415    ///
416    /// Intended for tests, headless automation, and snapshot
417    /// fixtures that drive UI logic without a real pointer history.
418    /// All optional fields default to `None`; modifiers are empty.
419    pub fn synthetic_click(key: impl Into<String>) -> Self {
420        Self {
421            kind: UiEventKind::Click,
422            key: Some(key.into()),
423            target: None,
424            pointer: None,
425            key_press: None,
426            text: None,
427            selection: None,
428            modifiers: KeyModifiers::default(),
429            click_count: 1,
430            path: None,
431            pointer_kind: None,
432            wheel_delta: None,
433        }
434    }
435
436    /// Route string for this event, if any.
437    ///
438    /// For pointer/focus events this is the target element key. For
439    /// hotkeys this is the registered action name.
440    pub fn route(&self) -> Option<&str> {
441        self.key.as_deref()
442    }
443
444    /// Target element key, if this event was routed to an element.
445    ///
446    /// Unlike [`Self::route`], this returns `None` for app-level
447    /// hotkey actions because those do not have a concrete element
448    /// target.
449    pub fn target_key(&self) -> Option<&str> {
450        self.target.as_ref().map(|t| t.key.as_str())
451    }
452
453    /// True when this event's route equals `key`.
454    pub fn is_route(&self, key: &str) -> bool {
455        self.route() == Some(key)
456    }
457
458    /// True for a primary click or keyboard activation on `key`.
459    ///
460    /// This is the most common button/menu route in app code.
461    pub fn is_click_or_activate(&self, key: &str) -> bool {
462        matches!(self.kind, UiEventKind::Click | UiEventKind::Activate) && self.is_route(key)
463    }
464
465    /// True for a registered hotkey action name.
466    pub fn is_hotkey(&self, action: &str) -> bool {
467        self.kind == UiEventKind::Hotkey && self.is_route(action)
468    }
469
470    /// Pointer position in logical pixels, if this event carries one.
471    pub fn pointer_pos(&self) -> Option<(f32, f32)> {
472        self.pointer
473    }
474
475    /// Pointer x coordinate in logical pixels, if this event carries one.
476    pub fn pointer_x(&self) -> Option<f32> {
477        self.pointer.map(|(x, _)| x)
478    }
479
480    /// Pointer y coordinate in logical pixels, if this event carries one.
481    pub fn pointer_y(&self) -> Option<f32> {
482        self.pointer.map(|(_, y)| y)
483    }
484
485    /// Wheel delta in logical pixels, if this is a pointer wheel event.
486    pub fn wheel_delta(&self) -> Option<(f32, f32)> {
487        self.wheel_delta
488    }
489
490    /// Vertical wheel delta in logical pixels, if this is a pointer
491    /// wheel event.
492    pub fn wheel_dy(&self) -> Option<f32> {
493        self.wheel_delta.map(|(_, dy)| dy)
494    }
495
496    /// Rectangle of the routed target from the last layout pass.
497    /// This is the target's transformed visual rect, not any
498    /// `hit_overflow` band that may also route pointer events to it.
499    pub fn target_rect(&self) -> Option<Rect> {
500        self.target.as_ref().map(|t| t.rect)
501    }
502
503    /// OS-composed text payload for [`UiEventKind::TextInput`].
504    pub fn text(&self) -> Option<&str> {
505        self.text.as_deref()
506    }
507}
508
509/// What kind of event happened.
510///
511/// This enum is non-exhaustive so Damascene can add new input events
512/// without breaking downstream apps. Match the variants you handle and
513/// include a wildcard arm for everything else.
514#[derive(Clone, Copy, Debug, PartialEq, Eq)]
515#[non_exhaustive]
516pub enum UiEventKind {
517    /// Primary-button pointer down + up landed on the same node.
518    Click,
519    /// Primary-button click landed on a text run carrying a
520    /// [`crate::tree::El::text_link`] URL. The URL is in [`UiEvent::key`].
521    /// Apps decide whether to honor it (filtering, confirmation,
522    /// platform-appropriate open via [`App::drain_link_opens`] +
523    /// host-side opener). Damascene doesn't open URLs itself — it surfaces
524    /// the click and lets the app route it.
525    LinkActivated,
526    /// Secondary-button (right-click) pointer down + up landed on the
527    /// same node. Used for context menus.
528    SecondaryClick,
529    /// Middle-button pointer down + up landed on the same node.
530    MiddleClick,
531    /// Focused element was activated by keyboard (Enter/Space).
532    Activate,
533    /// Escape was pressed. Routed to the focused element when present,
534    /// otherwise emitted as a window-level event.
535    Escape,
536    /// A registered hotkey chord matched. `event.key` is the registered
537    /// name (the second element of the `(KeyChord, String)` pair).
538    Hotkey,
539    /// Other keyboard input.
540    KeyDown,
541    /// Composed text input — printable characters from the OS, after
542    /// dead-key composition / IME / shift mapping. Routed to the
543    /// focused element. Distinct from `KeyDown(Character(_))`: the
544    /// latter is the raw key event used for shortcuts and navigation;
545    /// `TextInput` is the grapheme stream a text field should consume.
546    TextInput,
547    /// Pointer moved while the primary button was held down. Routed
548    /// to the originally pressed target so a widget can extend a
549    /// selection / scrub a slider / move a draggable. `event.pointer`
550    /// carries the current logical-pixel position; `event.target` is
551    /// the node where the drag began.
552    Drag,
553    /// Primary pointer button released. Fires regardless of whether
554    /// the up landed on the same node as the down — paired with
555    /// `Click` (which only fires on a same-node match), this lets
556    /// drag-aware widgets always observe drag-end.
557    /// `event.target` is the originally pressed node;
558    /// `event.pointer` is the up position.
559    PointerUp,
560    /// Primary pointer button pressed on a hit-test target. Routed
561    /// before the eventual `Click` (which fires on up-on-same-target).
562    /// Used by widgets like text_input that need to react at
563    /// down-time — e.g., to set the selection anchor before any drag
564    /// extends it. `event.target` is the down-target,
565    /// `event.pointer` is the down position, and `event.modifiers`
566    /// carries the modifier mask (Shift+click for extend-selection).
567    PointerDown,
568    /// Mouse wheel / trackpad scroll input routed to the keyed element
569    /// under the pointer. Emitted before Damascene's default scroll
570    /// handling; apps can consume it by returning `true` from
571    /// [`App::on_wheel_event`]. `event.wheel_delta` carries the
572    /// normalized logical-pixel delta.
573    PointerWheel,
574    /// The library's selection manager resolved a pointer interaction
575    /// on selectable text and wants the app to update its
576    /// [`crate::selection::Selection`] state. `event.selection`
577    /// carries the new value (an empty `Selection` clears).
578    /// Emitted by `pointer_down`, `pointer_moved` (during a drag),
579    /// and the runtime's escape / dismiss paths.
580    SelectionChanged,
581    /// Pointer crossed onto a keyed hit-test target. Routed to the
582    /// newly hovered leaf — `event.target` is the new hover target,
583    /// `event.pointer` is the current pointer position. Fires
584    /// once per identity change, including the initial hover when the
585    /// pointer first enters a keyed region from nothing.
586    ///
587    /// Use for transition-driven side effects (sound on hover-enter,
588    /// analytics, hover-intent prefetch) — read state via
589    /// [`crate::BuildCx::hovered_key`] /
590    /// [`crate::BuildCx::is_hovering_within`] when you just need to
591    /// branch the build output. Both surfaces stay coherent because
592    /// the runtime debounces redraws and events to the same
593    /// hover-identity transitions.
594    ///
595    /// Always paired with a preceding `PointerLeave` for the previous
596    /// target (when there was one). Apps that want subtree-aware
597    /// behavior (parent stays "hot" while a child is hovered) should
598    /// query `is_hovering_within` rather than tracking enter/leave on
599    /// every keyed descendant.
600    PointerEnter,
601    /// Pointer crossed off a keyed hit-test target — either onto a
602    /// different keyed target (paired with a following `PointerEnter`)
603    /// or off any keyed surface entirely. Routed to the leaf that
604    /// just lost hover — `event.target` is the previous hover target,
605    /// `event.pointer` is the current pointer position (or the last
606    /// known position when the pointer left the window).
607    PointerLeave,
608    /// The runner is abandoning a press because the gesture became
609    /// something else — currently only fired when a touch contact's
610    /// movement crosses the touch-scroll threshold and the press
611    /// target did not opt in via `consumes_touch_drag`. The contact
612    /// has *not* lifted; the user is still touching the screen, but
613    /// from the widget's perspective the press is gone (no
614    /// subsequent `Drag`, no `Click`, no `PointerUp`). Routed to the
615    /// originally pressed target — apps that handle `PointerDown`
616    /// for in-flight visual / state setup should also handle
617    /// `PointerCancel` to roll it back.
618    ///
619    /// Browser-initiated pointer cancels (OS gesture takeover, etc.)
620    /// currently come through as `PointerUp` rather than this event;
621    /// that may change.
622    PointerCancel,
623    /// A touch contact has been held in place past
624    /// [`crate::state::LONG_PRESS_DELAY`] without lifting or moving
625    /// past the gesture threshold. Fired exactly once per qualifying
626    /// press. For normal targets this is fired immediately after a
627    /// `PointerCancel` is dispatched to the originally pressed target;
628    /// the underlying primary press is consumed by the long-press, so
629    /// no subsequent `Click` or `PointerUp` follows. Capture-keys
630    /// editable targets keep the press captured so movement after the
631    /// long-press can emit `Drag` to extend text selection. The
632    /// eventual finger lift is silently swallowed.
633    ///
634    /// `event.target` is the keyed leaf at the press point (same
635    /// node that received the cancelled `PointerDown`), `event.pointer`
636    /// is the original press coords (not the current finger position
637    /// — the contact may have drifted within the gesture-threshold
638    /// radius before firing), and `event.pointer_kind` is always
639    /// `PointerKind::Touch`.
640    ///
641    /// Mouse and pen pointers never produce this event — right-click
642    /// goes through `PointerDown` with [`PointerButton::Secondary`]
643    /// instead, which is the desktop-shape signal for the same
644    /// "open a context menu here" intent. Apps that want both paths
645    /// to drive the same menu match on either kind.
646    LongPress,
647    /// A file is being dragged over the window (the user hasn't
648    /// released yet). `event.path` carries the file's path; multi-file
649    /// drags fire one event per file, matching the underlying winit
650    /// semantics. `event.target` is the keyed leaf at the current
651    /// pointer position when one was hit, otherwise `None`
652    /// (drop-zone overlays that span the window can match on
653    /// `event.target.is_none()` or filter by their own key).
654    ///
655    /// Apps use this to highlight a drop zone before the drop lands.
656    /// Always paired with either a later `FileHoverCancelled` (the
657    /// user moved off without releasing) or `FileDropped` (the user
658    /// released).
659    FileHovered,
660    /// The user moved a hovered file off the window without releasing,
661    /// or pressed Escape. Window-level event (`event.target` is
662    /// `None`) — apps clear any drop-zone affordance state regardless
663    /// of which keyed leaf was previously highlighted.
664    FileHoverCancelled,
665    /// A file was dropped on the window. `event.path` carries the
666    /// path; multi-file drops fire one event per file. `event.target`
667    /// is the keyed leaf at the drop position, or `None` if the drop
668    /// landed outside any keyed surface — apps that want a global drop
669    /// target match on `target.is_none()` or treat unrouted events as
670    /// hits to a single window-level upload sink.
671    FileDropped,
672}
673
674/// Per-frame, read-only context for [`App::build`].
675///
676/// The runner snapshots the app's [`crate::Theme`] before calling
677/// `build` and exposes it through `cx.theme()` / `cx.palette()` so app
678/// code can branch on the active palette (a custom widget that picks
679/// between two non-token colors based on dark vs. light, for instance).
680/// `BuildCx` is the explicit handle for this — token references inside
681/// widgets resolve through the palette automatically and don't need it.
682///
683/// Future fields like viewport metrics or frame phase will live here so
684/// the API stays additive: adding a new accessor on `BuildCx` doesn't
685/// break apps that ignore the context.
686#[derive(Copy, Clone, Debug)]
687pub struct BuildCx<'a> {
688    theme: &'a crate::Theme,
689    ui_state: Option<&'a crate::state::UiState>,
690    diagnostics: Option<&'a HostDiagnostics>,
691    /// Logical-pixel viewport this frame is being built for, when the
692    /// host attached one. Apps query this via [`Self::viewport`] /
693    /// [`Self::viewport_below`] to branch layout on phone-vs-desktop
694    /// without threading the surface size through their own state.
695    viewport: Option<(f32, f32)>,
696    /// Logical-pixel insets the host wants the app to inset its
697    /// layout by — content underneath these bands is obscured by
698    /// platform chrome and shouldn't host interactive widgets.
699    /// Today only the bottom inset is populated, by the web host's
700    /// VisualViewport listener when the on-screen keyboard appears;
701    /// the same field will carry status-bar / notch / home-indicator
702    /// insets when native mobile hosts land.
703    safe_area: Option<crate::tree::Sides>,
704}
705
706/// Why the current frame is being built. Hosts set this before each
707/// `request_redraw` so apps that surface a diagnostic overlay can show
708/// what kind of input is driving the redraw cadence.
709///
710/// `Other` is the conservative default: it covers redraws the host
711/// can't attribute (idle redraws driven by external `request_redraw`
712/// callers, the initial paint, etc.). Specific variants narrow the
713/// reason when the host can.
714#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
715pub enum FrameTrigger {
716    /// Host can't attribute the redraw to a specific cause.
717    #[default]
718    Other,
719    /// Initial paint after surface configuration.
720    Initial,
721    /// Surface resize / DPI change.
722    Resize,
723    /// Pointer move, button, or wheel.
724    Pointer,
725    /// Keyboard / IME input.
726    Keyboard,
727    /// Inside-out animation deadline elapsed (one of the visible
728    /// widgets asked for a future frame via `redraw_within`, or a
729    /// visual animation is still settling). Drives the layout-path
730    /// (full rebuild + prepare).
731    Animation,
732    /// Time-driven shader deadline elapsed (e.g. stock spinner /
733    /// skeleton / progress-indeterminate, or a custom shader
734    /// registered with `samples_time=true`). Drives the paint-only
735    /// path: `frame.time` advances but layout state is unchanged.
736    ShaderPaint,
737    /// Periodic host-config cadence (`HostConfig::redraw_interval`).
738    Periodic,
739}
740
741impl FrameTrigger {
742    /// Short, fixed-width tag for diagnostic overlays.
743    pub fn label(self) -> &'static str {
744        match self {
745            FrameTrigger::Other => "other",
746            FrameTrigger::Initial => "initial",
747            FrameTrigger::Resize => "resize",
748            FrameTrigger::Pointer => "pointer",
749            FrameTrigger::Keyboard => "keyboard",
750            FrameTrigger::Animation => "animation",
751            FrameTrigger::ShaderPaint => "shader-paint",
752            FrameTrigger::Periodic => "periodic",
753        }
754    }
755}
756
757/// Per-frame diagnostic snapshot the host hands the app via
758/// [`BuildCx::diagnostics`]. Apps that surface a debug overlay (e.g.
759/// the showcase status block) read this each build to display the
760/// active backend, frame cadence, and what triggered the redraw.
761/// Timing fields describe the last completed rendered frame, not the
762/// frame currently being built; the host cannot know current layout /
763/// paint timings until after `App::build` returns.
764///
765/// Hosts populate every field they can; `backend` is a static string
766/// (`"WebGPU"`, `"Vulkan"`, `"Metal"`, `"DX12"`, `"GL"`) so the app
767/// doesn't need to depend on `wgpu` to read it. Time fields use
768/// `std::time::Duration`, which works on both native and wasm32 — only
769/// `Instant::now()` is the wasm-incompatible piece, and that stays on
770/// the host side.
771#[derive(Clone, Debug)]
772pub struct HostDiagnostics {
773    /// Render backend in human-readable form.
774    pub backend: &'static str,
775    /// Current surface size in physical pixels.
776    pub surface_size: (u32, u32),
777    /// Display scale factor (`physical / logical`).
778    pub scale_factor: f32,
779    /// Active MSAA sample count (1 = MSAA off).
780    pub msaa_samples: u32,
781    /// Frame counter; increments every redraw the host actually
782    /// renders. Useful for verifying that an animated source is
783    /// progressing.
784    pub frame_index: u64,
785    /// Wall-clock time between this redraw and the previous one.
786    /// `Duration::ZERO` for the first frame (no prior frame).
787    pub last_frame_dt: std::time::Duration,
788    /// Time spent in the app's `build` method for the last completed
789    /// frame. `Duration::ZERO` before the first full frame and on
790    /// paint-only frames that skipped build.
791    pub last_build: std::time::Duration,
792    /// Total time spent in the backend `prepare` call for the last
793    /// completed frame.
794    pub last_prepare: std::time::Duration,
795    /// Sub-stage inside `prepare`: layout pass, focus/selection sync,
796    /// state application, and animation tick.
797    pub last_layout: std::time::Duration,
798    /// Intrinsic-measurement cache hits during the last layout pass.
799    pub last_layout_intrinsic_cache_hits: u64,
800    /// Intrinsic-measurement cache misses during the last layout pass.
801    pub last_layout_intrinsic_cache_misses: u64,
802    /// Direct scroll children whose descendants were skipped during
803    /// layout because the child was outside the scroll viewport.
804    pub last_layout_pruned_subtrees: u64,
805    /// Descendant nodes assigned zero rects as part of scroll layout
806    /// pruning during the last layout pass.
807    pub last_layout_pruned_nodes: u64,
808    /// Sub-stage inside `prepare`: laid-out tree to backend-neutral
809    /// `DrawOp` list.
810    pub last_draw_ops: std::time::Duration,
811    /// Text draw ops skipped during draw-op generation because their
812    /// glyph rect did not intersect the inherited clip.
813    pub last_draw_ops_culled_text_ops: u64,
814    /// Sub-stage inside `prepare`: paint-stream packing and text
815    /// shaping/rasterization recording.
816    pub last_paint: std::time::Duration,
817    /// Paint ops skipped because their painted rect did not intersect
818    /// the effective clip/viewport in the last completed frame.
819    pub last_paint_culled_ops: u64,
820    /// Sub-stage inside `prepare`: backend-side buffer writes, glyph
821    /// atlas uploads, and frame uniforms.
822    pub last_gpu_upload: std::time::Duration,
823    /// Sub-stage inside `prepare`: clone the laid-out tree for
824    /// next-frame hit-testing.
825    pub last_snapshot: std::time::Duration,
826    /// Time spent encoding/submitting/presenting the last completed
827    /// frame after `prepare`.
828    pub last_submit: std::time::Duration,
829    /// Layout-side text-cache hits during the last completed full
830    /// prepare.
831    pub last_text_layout_cache_hits: u64,
832    /// Layout-side text-cache misses during the last completed full
833    /// prepare.
834    pub last_text_layout_cache_misses: u64,
835    /// Estimated layout-side text-cache evictions during the last
836    /// completed full prepare.
837    pub last_text_layout_cache_evictions: u64,
838    /// Total UTF-8 bytes shaped on layout-cache misses during the last
839    /// completed full prepare.
840    pub last_text_layout_shaped_bytes: u64,
841    /// Why the host triggered this frame.
842    pub trigger: FrameTrigger,
843    /// What the renderer composites in. The paint stream converts every
844    /// [`crate::color::Color`] into this space exactly once at the
845    /// upload boundary. Defaults to [`crate::color::ColorSpace::SRGB_LINEAR`].
846    pub working_color_space: crate::color::ColorSpace,
847    /// Wire-side color-management state the host negotiated with the
848    /// display server. [`crate::color::ColorManagementStatus::Unavailable`]
849    /// on hosts without a color-management protocol (X11, plain Wayland,
850    /// macOS / Windows today). See [`crate::color::ColorPreferences`]
851    /// for how apps influence the negotiation.
852    pub color_management: crate::color::ColorManagementStatus,
853    /// Color-relevant facts about the host's GPU presentation surface —
854    /// the wgpu / WSI half of color negotiation (advertised formats,
855    /// chosen swapchain format, present/alpha mode, adapter). `None` on
856    /// hosts that don't present through a wgpu surface (headless render
857    /// bins, the vulkano demo). See [`SurfaceColorInfo`].
858    pub surface_color: Option<SurfaceColorInfo>,
859}
860
861impl Default for HostDiagnostics {
862    fn default() -> Self {
863        Self {
864            backend: "?",
865            surface_size: (0, 0),
866            scale_factor: 1.0,
867            msaa_samples: 1,
868            frame_index: 0,
869            last_frame_dt: std::time::Duration::ZERO,
870            last_build: std::time::Duration::ZERO,
871            last_prepare: std::time::Duration::ZERO,
872            last_layout: std::time::Duration::ZERO,
873            last_layout_intrinsic_cache_hits: 0,
874            last_layout_intrinsic_cache_misses: 0,
875            last_layout_pruned_subtrees: 0,
876            last_layout_pruned_nodes: 0,
877            last_draw_ops: std::time::Duration::ZERO,
878            last_draw_ops_culled_text_ops: 0,
879            last_paint: std::time::Duration::ZERO,
880            last_paint_culled_ops: 0,
881            last_gpu_upload: std::time::Duration::ZERO,
882            last_snapshot: std::time::Duration::ZERO,
883            last_submit: std::time::Duration::ZERO,
884            last_text_layout_cache_hits: 0,
885            last_text_layout_cache_misses: 0,
886            last_text_layout_cache_evictions: 0,
887            last_text_layout_shaped_bytes: 0,
888            trigger: FrameTrigger::default(),
889            working_color_space: crate::paint::DEFAULT_WORKING_COLOR_SPACE,
890            color_management: crate::color::ColorManagementStatus::default(),
891            surface_color: None,
892        }
893    }
894}
895
896/// Color-relevant facts about the host's GPU presentation surface — the
897/// wgpu / WSI half of color negotiation. The compositor (via
898/// [`crate::color::ColorManagementStatus`]) says what it *accepts*; this
899/// says what the *swapchain* can represent. The intersection is what the
900/// negotiator can actually pick — e.g. a compositor that ingests linear
901/// BT.2020 is moot if the surface offers no float format.
902///
903/// Strings throughout so `damascene-core` needn't depend on `wgpu`.
904#[derive(Clone, Debug, Default)]
905pub struct SurfaceColorInfo {
906    /// Adapter / device name (e.g. `"Intel Graphics (ADL GT2)"`).
907    pub adapter: String,
908    /// Driver name + version, when the backend reports it.
909    pub driver: String,
910    /// Color formats the surface advertised, in wgpu's reported order.
911    pub formats: Vec<SurfaceFormatInfo>,
912    /// The swapchain format negotiation actually chose.
913    pub chosen_format: String,
914    /// Present mode in use.
915    pub present_mode: String,
916    /// Composite alpha mode in use.
917    pub alpha_mode: String,
918}
919
920/// One surface texture format, classified by how it can carry color
921/// output. See [`SurfaceColorInfo`].
922#[derive(Clone, Debug)]
923pub struct SurfaceFormatInfo {
924    /// wgpu format name (e.g. `"Rgba16Float"`).
925    pub name: String,
926    /// Carries an sRGB EOTF in hardware (`*_unorm_srgb`): the GPU encodes
927    /// linear → sRGB on store.
928    pub srgb: bool,
929    /// Can carry wide-gamut / HDR output: a float format (linear-direct —
930    /// the compositor does the output encode) or a ≥10-bit format (a
931    /// PQ-encode target). 8-bit unorm formats are SDR-only.
932    pub wide: bool,
933}
934
935impl<'a> BuildCx<'a> {
936    /// Construct a [`BuildCx`] borrowing the supplied theme. Hosts call
937    /// this once per frame after [`App::theme`] and before [`App::build`].
938    /// Hosts that own a [`crate::state::UiState`] should chain
939    /// [`Self::with_ui_state`] so the app can read interaction state
940    /// (hover) during build via [`Self::hovered_key`] /
941    /// [`Self::is_hovering_within`].
942    pub fn new(theme: &'a crate::Theme) -> Self {
943        Self {
944            theme,
945            ui_state: None,
946            diagnostics: None,
947            viewport: None,
948            safe_area: None,
949        }
950    }
951
952    /// Attach the runtime's [`crate::state::UiState`] so build-time
953    /// accessors (`hovered_key`, `is_hovering_within`) can answer.
954    /// When omitted, those accessors return `None` / `false` — useful
955    /// for headless rendering paths that don't track interaction
956    /// state.
957    pub fn with_ui_state(mut self, ui_state: &'a crate::state::UiState) -> Self {
958        self.ui_state = Some(ui_state);
959        self
960    }
961
962    /// Attach a [`HostDiagnostics`] snapshot for this frame. Hosts call
963    /// this when they want apps to surface debug overlays (e.g. the
964    /// showcase status block); apps that don't read `diagnostics()`
965    /// pay nothing for it. Headless render paths leave it `None`.
966    pub fn with_diagnostics(mut self, diagnostics: &'a HostDiagnostics) -> Self {
967        self.diagnostics = Some(diagnostics);
968        self
969    }
970
971    /// Attach the logical-pixel viewport size for this frame. Hosts
972    /// chain this so apps can branch on viewport metrics during build
973    /// (responsive layout, phone-vs-desktop splits) without threading
974    /// surface size through their own state. Headless render paths
975    /// without a meaningful viewport leave it unset.
976    pub fn with_viewport(mut self, width: f32, height: f32) -> Self {
977        self.viewport = Some((width, height));
978        self
979    }
980
981    /// Attach the host's reported safe-area insets in logical pixels.
982    /// Hosts chain this when platform chrome (on-screen keyboard,
983    /// notch, status bar, home indicator) is obscuring some band of
984    /// the viewport. Apps read it via [`Self::safe_area`] /
985    /// [`Self::safe_area_bottom`] and inset their interactive content
986    /// accordingly. Hosts that don't report safe-area metrics omit
987    /// this; apps see `Sides::zero()` from the read accessors.
988    pub fn with_safe_area(mut self, sides: crate::tree::Sides) -> Self {
989        self.safe_area = Some(sides);
990        self
991    }
992
993    /// Per-frame diagnostic snapshot from the host (backend, frame
994    /// cadence, trigger reason, etc.), or `None` when the host did
995    /// not attach one. Apps display this in optional debug overlays.
996    pub fn diagnostics(&self) -> Option<&HostDiagnostics> {
997        self.diagnostics
998    }
999
1000    /// The active runtime theme for this frame.
1001    pub fn theme(&self) -> &crate::Theme {
1002        self.theme
1003    }
1004
1005    /// Shorthand for `self.theme().palette()`.
1006    pub fn palette(&self) -> &crate::Palette {
1007        self.theme.palette()
1008    }
1009
1010    /// Logical-pixel viewport `(width, height)` the host attached for
1011    /// this frame, or `None` for headless render paths. Apps use this
1012    /// to branch layout on viewport metrics — see [`Self::viewport_below`]
1013    /// for the common phone-vs-desktop breakpoint case.
1014    pub fn viewport(&self) -> Option<(f32, f32)> {
1015        self.viewport
1016    }
1017
1018    /// Logical-pixel viewport width the host attached for this frame,
1019    /// or `None` when no viewport is available. Convenience for the
1020    /// common single-axis branch (`cx.viewport_width().map_or(false,
1021    /// |w| w < 600.0)`).
1022    pub fn viewport_width(&self) -> Option<f32> {
1023        self.viewport.map(|(w, _)| w)
1024    }
1025
1026    /// Logical-pixel viewport height the host attached for this frame,
1027    /// or `None` when no viewport is available.
1028    pub fn viewport_height(&self) -> Option<f32> {
1029        self.viewport.map(|(_, h)| h)
1030    }
1031
1032    /// True iff the attached viewport's width is strictly less than
1033    /// `threshold` logical pixels. Returns `false` when no viewport is
1034    /// attached so headless / desktop-default paths fall through to
1035    /// the wider branch — apps that want the opposite default can
1036    /// match on [`Self::viewport_width`] directly.
1037    ///
1038    /// Use for the common breakpoint split:
1039    /// ```ignore
1040    /// if cx.viewport_below(600.0) {
1041    ///     phone_layout()
1042    /// } else {
1043    ///     desktop_layout()
1044    /// }
1045    /// ```
1046    pub fn viewport_below(&self, threshold: f32) -> bool {
1047        self.viewport_width().is_some_and(|w| w < threshold)
1048    }
1049
1050    /// Logical-pixel safe-area insets the host reports for this frame
1051    /// (`Sides::zero()` when nothing was attached). Today this is
1052    /// populated only by damascene-web when the on-screen keyboard
1053    /// shrinks the visual viewport — `bottom` carries the keyboard
1054    /// height; future native mobile hosts will additionally populate
1055    /// `top` for status-bar / notch and `bottom` for home-indicator.
1056    ///
1057    /// Apps inset their root layout (or just the focused-input
1058    /// region) by these amounts so interactive content doesn't sit
1059    /// underneath platform chrome. The runtime does not auto-apply
1060    /// this — apps decide where the inset matters.
1061    pub fn safe_area(&self) -> crate::tree::Sides {
1062        self.safe_area.unwrap_or_default()
1063    }
1064
1065    /// Convenience: just the bottom inset, in logical pixels. Most
1066    /// commonly the soft-keyboard height.
1067    pub fn safe_area_bottom(&self) -> f32 {
1068        self.safe_area().bottom
1069    }
1070
1071    /// Key of the leaf node currently under the pointer, or `None`
1072    /// when nothing is hovered or this `BuildCx` was built without a
1073    /// `UiState` (headless rendering paths).
1074    ///
1075    /// Use for branching the build output on hover state without
1076    /// mirroring it via `App::on_event` handlers — e.g., a sidebar
1077    /// row that previews details in a side pane based on what's
1078    /// currently hovered.
1079    ///
1080    /// For region-aware queries (parent stays "hot" while a child is
1081    /// hovered), prefer [`Self::is_hovering_within`].
1082    pub fn hovered_key(&self) -> Option<&str> {
1083        self.ui_state?.hovered_key()
1084    }
1085
1086    /// True iff `key`'s node — or any descendant of it — is the
1087    /// current hover target. Subtree-aware, matching the semantics of
1088    /// [`crate::tree::El::hover_alpha`]. Returns `false` when this
1089    /// `BuildCx` has no attached `UiState` or when `key` isn't in the
1090    /// current tree.
1091    ///
1092    /// Reads the underlying tracker, not the eased subtree envelope —
1093    /// the boolean flips immediately on hit-test identity change.
1094    pub fn is_hovering_within(&self, key: &str) -> bool {
1095        self.ui_state
1096            .is_some_and(|state| state.is_hovering_within(key))
1097    }
1098
1099    /// The scatter point currently under the cursor in a `chart3d` scene, if
1100    /// any — the 3D analogue of [`hovered_key`](Self::hovered_key).
1101    ///
1102    /// Scene points aren't `El`s, so they can't emit `PointerEnter`/`Leave`
1103    /// like 2D widgets; this surfaces the same hover pick that draws the
1104    /// built-in tooltip chip ([`ScenePointPick`] carries the scene id + mark +
1105    /// point index). Use it to drive a detail panel / highlight / linked view
1106    /// on hover — branch the build on `cx.hovered_scene_point()` without an
1107    /// `on_event` handler. Picked a frame late (fine for hover UI) and honours
1108    /// the chip's depth-occlusion + behind-camera culling.
1109    ///
1110    /// [`ScenePointPick`]: crate::scene::ScenePointPick
1111    pub fn hovered_scene_point(&self) -> Option<&crate::scene::ScenePointPick> {
1112        self.ui_state?.hovered_scene_point()
1113    }
1114}
1115
1116/// The application contract. Implement this on your state struct and
1117/// pass it to a host runner (e.g., `damascene_winit_wgpu::run`).
1118pub trait App {
1119    /// Refresh app-owned external state immediately before a frame is
1120    /// built.
1121    ///
1122    /// Hosts call this once per redraw before [`Self::build`]. Use it
1123    /// for polling an external source, reconciling optimistic local
1124    /// state with a backend snapshot, or advancing host-owned live data
1125    /// that should be visible in the next tree. Keep expensive work
1126    /// outside the render loop; this hook is still on the frame path.
1127    ///
1128    /// Default: no-op.
1129    fn before_build(&mut self) {}
1130
1131    /// Project current state into a scene tree. Called whenever the
1132    /// host requests a redraw, after [`Self::before_build`]. Prefer to
1133    /// keep this pure: read current state and return a fresh tree.
1134    ///
1135    /// `cx` carries per-frame, read-only context (active theme, future
1136    /// viewport / phase metadata). Apps that don't need to branch on
1137    /// the theme during construction can ignore the parameter — token
1138    /// references in widget code resolve through the palette
1139    /// automatically.
1140    fn build(&self, cx: &BuildCx) -> El;
1141
1142    /// Update state in response to a routed event. Default: no-op.
1143    fn on_event(&mut self, _event: UiEvent) {}
1144
1145    /// Update state in response to routed wheel input.
1146    ///
1147    /// Return `true` to consume the wheel and suppress Damascene's default
1148    /// scroll routing. The default forwards to [`Self::on_event`] and
1149    /// returns `false`, so existing apps can observe wheel events
1150    /// without opting out of normal scrolling.
1151    fn on_wheel_event(&mut self, event: UiEvent) -> bool {
1152        self.on_event(event);
1153        false
1154    }
1155
1156    /// The application's current text [`crate::selection::Selection`].
1157    /// Read by the host once per frame so the library can paint
1158    /// highlight bands and resolve `selected_text` for clipboard.
1159    /// Apps that own a `Selection` field return a clone here; the
1160    /// default returns the empty selection.
1161    fn selection(&self) -> crate::selection::Selection {
1162        crate::selection::Selection::default()
1163    }
1164
1165    /// App-level hotkey registry. The library matches incoming key
1166    /// presses against this list before its own focus-activation
1167    /// routing; a match emits a [`UiEvent`] with `kind =
1168    /// UiEventKind::Hotkey` and `key = Some(name)`.
1169    ///
1170    /// Called once per build cycle; the host runner snapshots the list
1171    /// alongside `build()` so the chords stay in sync with state.
1172    /// Default: no hotkeys.
1173    fn hotkeys(&self) -> Vec<(KeyChord, String)> {
1174        Vec::new()
1175    }
1176
1177    /// Drain pending toast notifications produced since the last
1178    /// frame. The runtime calls this once per `prepare_layout`,
1179    /// stamps each spec with a monotonic id and `expires_at = now +
1180    /// ttl`, queues it in the runtime toast state, and
1181    /// synthesizes a `toast_stack` layer at the El root so the
1182    /// rendered tree mirrors the visible state. Apps typically
1183    /// accumulate specs in a `Vec<ToastSpec>` field from event
1184    /// handlers, then `mem::take` it here.
1185    ///
1186    /// **Root requirement:** apps that produce toasts (or use
1187    /// `.tooltip(text)` on any node) must wrap their
1188    /// [`Self::build`] return value in `overlays(main, [])` so the
1189    /// runtime can append the floating layer as an overlay sibling
1190    /// — same convention used for popovers and modals. Debug
1191    /// builds panic if the synthesizer runs against a non-overlay
1192    /// root.
1193    ///
1194    /// Default: no toasts.
1195    fn drain_toasts(&mut self) -> Vec<crate::toast::ToastSpec> {
1196        Vec::new()
1197    }
1198
1199    /// Drain pending programmatic focus requests produced since the
1200    /// last frame. The runtime calls this once per `prepare_layout`,
1201    /// after the focus order has been rebuilt from the new tree, and
1202    /// resolves each entry against the keyed focusables. Unmatched
1203    /// keys (widget absent from the rebuilt tree, or not focusable)
1204    /// are dropped silently.
1205    ///
1206    /// This is the imperative companion to keyboard `Tab` traversal:
1207    /// use it for affordances like *Ctrl+F → focus the search input*,
1208    /// *jump-to-match → focus the matched row*, or *open inline edit
1209    /// → focus the field*. Apps typically accumulate keys in a
1210    /// `Vec<String>` field from event handlers and `mem::take` it
1211    /// here.
1212    ///
1213    /// Multiple requests in one frame resolve in order; the last
1214    /// successfully-resolved key is the one focused.
1215    ///
1216    /// Default: no requests.
1217    fn drain_focus_requests(&mut self) -> Vec<String> {
1218        Vec::new()
1219    }
1220
1221    /// Drain pending programmatic scroll requests. The runtime
1222    /// resolves each request during layout, using live viewport rects
1223    /// and row-height/content geometry that apps should not duplicate.
1224    /// Unmatched keys and out-of-range row indices drop silently.
1225    ///
1226    /// Use [`crate::scroll::ScrollRequest::ToRow`] for virtual-list
1227    /// affordances such as jump-to-search-result, reveal selected row,
1228    /// or scroll-to-top-on-tab-change. Use
1229    /// [`crate::scroll::ScrollRequest::EnsureVisible`] for widgets
1230    /// with an internal scroll viewport, including fixed-height
1231    /// [`crate::widgets::text_area`] caret-into-view after accepted
1232    /// edit/navigation events. Apps typically accumulate requests in a
1233    /// `Vec<ScrollRequest>` field from event handlers and
1234    /// `mem::take` it here.
1235    ///
1236    /// Default: no requests.
1237    fn drain_scroll_requests(&mut self) -> Vec<crate::scroll::ScrollRequest> {
1238        Vec::new()
1239    }
1240
1241    /// Drain pending URL-open requests produced since the last frame.
1242    /// Hosts call this once per frame and route each URL to a
1243    /// platform-appropriate opener — `window.open` in the wasm host,
1244    /// the `open` crate (or equivalent) on native.
1245    ///
1246    /// The library emits [`UiEventKind::LinkActivated`] when a click
1247    /// lands on a text run carrying a link URL, but it does not act
1248    /// on the URL itself: opening a link is an app concern (apps may
1249    /// want to confirm, filter by scheme, route through an internal
1250    /// router, or no-op entirely). Apps that want the default
1251    /// browser-style behavior accumulate URLs from
1252    /// [`UiEventKind::LinkActivated`] in their `on_event` handler and
1253    /// return them here; apps that don't override this method drop
1254    /// link clicks on the floor.
1255    ///
1256    /// Default: no requests.
1257    fn drain_link_opens(&mut self) -> Vec<String> {
1258        Vec::new()
1259    }
1260
1261    /// Custom shaders this app needs registered. Each entry carries
1262    /// the shader name, its WGSL source, and per-flag opt-ins
1263    /// (backdrop sampling, time-driven motion). The host runner
1264    /// registers them once at startup via
1265    /// `Runner::register_shader_with(name, wgsl, samples_backdrop, samples_time)`.
1266    ///
1267    /// Backends that don't support backdrop sampling skip entries with
1268    /// `samples_backdrop=true`; any node bound to such a shader will
1269    /// draw nothing on those backends rather than mis-render.
1270    /// `samples_time=true` declares that the shader's output depends
1271    /// on `frame.time`, which keeps the host idle loop ticking while
1272    /// any node is bound to it.
1273    ///
1274    /// Default: no shaders.
1275    fn shaders(&self) -> Vec<AppShader> {
1276        Vec::new()
1277    }
1278
1279    /// Runtime paint theme for this app. Hosts apply it to the renderer
1280    /// before preparing each frame so stateful apps can switch global
1281    /// material routing without backend-specific calls.
1282    fn theme(&self) -> crate::Theme {
1283        crate::Theme::default()
1284    }
1285}
1286
1287/// One custom shader registration, returned from [`App::shaders`].
1288#[derive(Clone, Copy, Debug)]
1289pub struct AppShader {
1290    pub name: &'static str,
1291    pub wgsl: &'static str,
1292    /// Reads the prior pass's color target (`@group(2) backdrop_tex`).
1293    /// Backends without backdrop support skip these.
1294    pub samples_backdrop: bool,
1295    /// Reads `frame.time` and so requires continuous redraw whenever
1296    /// any node is bound to it. The runtime ORs this into
1297    /// `PrepareResult::needs_redraw` per frame.
1298    pub samples_time: bool,
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303    use super::*;
1304    use crate::Theme;
1305
1306    #[test]
1307    fn viewport_unset_returns_none_and_breakpoint_returns_false() {
1308        let theme = Theme::default();
1309        let cx = BuildCx::new(&theme);
1310        assert!(cx.viewport().is_none());
1311        assert!(cx.viewport_width().is_none());
1312        assert!(!cx.viewport_below(600.0));
1313    }
1314
1315    #[test]
1316    fn build_cx_surfaces_hovered_scene_point() {
1317        use crate::scene::ScenePointPick;
1318        let theme = Theme::default();
1319        let mut ui = crate::state::UiState::new();
1320
1321        // No attached state, and none stored → nothing to surface.
1322        assert!(BuildCx::new(&theme).hovered_scene_point().is_none());
1323        assert!(
1324            BuildCx::new(&theme)
1325                .with_ui_state(&ui)
1326                .hovered_scene_point()
1327                .is_none()
1328        );
1329
1330        // After the runtime stores a pick, the app reads it at build.
1331        ui.set_hovered_scene_point(Some(ScenePointPick {
1332            scene: "scene".into(),
1333            mark: 0,
1334            point: 4,
1335        }));
1336        let cx = BuildCx::new(&theme).with_ui_state(&ui);
1337        let pick = cx.hovered_scene_point().expect("pick surfaced");
1338        assert_eq!(
1339            (pick.scene.as_str(), pick.mark, pick.point),
1340            ("scene", 0, 4)
1341        );
1342    }
1343
1344    #[test]
1345    fn viewport_set_exposes_width_and_height() {
1346        let theme = Theme::default();
1347        let cx = BuildCx::new(&theme).with_viewport(420.0, 800.0);
1348        assert_eq!(cx.viewport(), Some((420.0, 800.0)));
1349        assert_eq!(cx.viewport_width(), Some(420.0));
1350        assert_eq!(cx.viewport_height(), Some(800.0));
1351    }
1352
1353    #[test]
1354    fn viewport_below_uses_strict_less_than() {
1355        let theme = Theme::default();
1356        let cx = BuildCx::new(&theme).with_viewport(600.0, 800.0);
1357        assert!(!cx.viewport_below(600.0), "boundary is exclusive");
1358        assert!(cx.viewport_below(601.0));
1359        assert!(!cx.viewport_below(599.0));
1360    }
1361}