Skip to main content

aetna_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 aetna_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/// Keyboard key values normalized by the core library. This keeps the
98/// core independent from host/windowing crates while covering the
99/// navigation and activation keys the library owns.
100#[derive(Clone, Debug, PartialEq, Eq)]
101pub enum UiKey {
102    Enter,
103    Escape,
104    Tab,
105    Space,
106    ArrowUp,
107    ArrowDown,
108    ArrowLeft,
109    ArrowRight,
110    /// Backspace — deletes the grapheme before the caret.
111    Backspace,
112    /// Forward delete — deletes the grapheme after the caret.
113    Delete,
114    /// Home — caret to start of line.
115    Home,
116    /// End — caret to end of line.
117    End,
118    /// PageUp — coarse-step navigation (sliders adjust by a larger
119    /// amount; lists scroll a viewport).
120    PageUp,
121    /// PageDown — coarse-step navigation (sliders adjust by a larger
122    /// amount; lists scroll a viewport).
123    PageDown,
124    Character(String),
125    Other(String),
126}
127
128/// OS modifier-key mask. The four fields mirror the platform-standard
129/// modifier set; this struct is intentionally **not** `#[non_exhaustive]`
130/// so callers can use struct-literal syntax with `..Default::default()`
131/// to spell precise modifier combinations.
132#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
133pub struct KeyModifiers {
134    pub shift: bool,
135    pub ctrl: bool,
136    pub alt: bool,
137    pub logo: bool,
138}
139
140#[derive(Clone, Debug, PartialEq, Eq)]
141#[non_exhaustive]
142pub struct KeyPress {
143    pub key: UiKey,
144    pub modifiers: KeyModifiers,
145    pub repeat: bool,
146}
147
148/// A keyboard chord for app-level hotkey registration. Match a key with
149/// an exact modifier mask: `KeyChord::ctrl('f')` does not also match
150/// `Ctrl+Shift+F`, and `KeyChord::vim('j')` does not match if any
151/// modifier is held.
152///
153/// Register chords from [`App::hotkeys`]; the library matches them
154/// against incoming key presses ahead of focus activation routing and
155/// emits a [`UiEvent`] with `kind = UiEventKind::Hotkey` and `key`
156/// equal to the registered name.
157#[derive(Clone, Debug, PartialEq, Eq)]
158#[non_exhaustive]
159pub struct KeyChord {
160    pub key: UiKey,
161    pub modifiers: KeyModifiers,
162}
163
164impl KeyChord {
165    /// A bare key with no modifiers (vim-style). `KeyChord::vim('j')`
166    /// matches the `j` key with no Ctrl/Shift/Alt/Logo held.
167    pub fn vim(c: char) -> Self {
168        Self {
169            key: UiKey::Character(c.to_string()),
170            modifiers: KeyModifiers::default(),
171        }
172    }
173
174    /// `Ctrl+<char>`.
175    pub fn ctrl(c: char) -> Self {
176        Self {
177            key: UiKey::Character(c.to_string()),
178            modifiers: KeyModifiers {
179                ctrl: true,
180                ..Default::default()
181            },
182        }
183    }
184
185    /// `Ctrl+Shift+<char>`.
186    pub fn ctrl_shift(c: char) -> Self {
187        Self {
188            key: UiKey::Character(c.to_string()),
189            modifiers: KeyModifiers {
190                ctrl: true,
191                shift: true,
192                ..Default::default()
193            },
194        }
195    }
196
197    /// A named key with no modifiers (e.g. `KeyChord::named(UiKey::Escape)`).
198    pub fn named(key: UiKey) -> Self {
199        Self {
200            key,
201            modifiers: KeyModifiers::default(),
202        }
203    }
204
205    pub fn with_modifiers(mut self, modifiers: KeyModifiers) -> Self {
206        self.modifiers = modifiers;
207        self
208    }
209
210    /// Strict match: keys equal AND modifier mask is identical. Holding
211    /// extra modifiers does not match a chord that didn't request them.
212    pub fn matches(&self, key: &UiKey, modifiers: KeyModifiers) -> bool {
213        key_eq(&self.key, key) && self.modifiers == modifiers
214    }
215}
216
217fn key_eq(a: &UiKey, b: &UiKey) -> bool {
218    match (a, b) {
219        (UiKey::Character(x), UiKey::Character(y)) => x.eq_ignore_ascii_case(y),
220        _ => a == b,
221    }
222}
223
224/// User-facing event. The host's [`App::on_event`] receives one of these
225/// per discrete user action.
226///
227/// Most apps should not destructure every field. Prefer the convenience
228/// methods on this type for common routes:
229///
230/// ```
231/// # use aetna_core::prelude::*;
232/// # struct Counter { value: i32 }
233/// # impl App for Counter {
234/// # fn build(&self, _cx: &BuildCx) -> El { button("+").key("inc") }
235/// fn on_event(&mut self, event: UiEvent) {
236///     if event.is_click_or_activate("inc") {
237///         self.value += 1;
238///     }
239/// }
240/// # }
241/// ```
242#[derive(Clone, Debug)]
243#[non_exhaustive]
244pub struct UiEvent {
245    /// Route string for this event.
246    ///
247    /// For pointer and focus events, this is the [`El::key`][crate::El::key]
248    /// of the target node. For [`UiEventKind::Hotkey`], this is the
249    /// action name returned from [`App::hotkeys`]. For window-level
250    /// keyboard events such as Escape with no focused target, this is
251    /// `None`.
252    ///
253    /// Prefer [`Self::route`] or [`Self::is_click_or_activate`] in app
254    /// code. The field remains public for direct pattern matching.
255    pub key: Option<String>,
256    /// Full hit-test target for events routed to a concrete element.
257    pub target: Option<UiTarget>,
258    /// Pointer position in logical pixels when the event was emitted.
259    pub pointer: Option<(f32, f32)>,
260    /// Keyboard payload for key events.
261    pub key_press: Option<KeyPress>,
262    /// Composed text payload for [`UiEventKind::TextInput`] events.
263    pub text: Option<String>,
264    /// Library-emitted selection state for
265    /// [`UiEventKind::SelectionChanged`] events. Carries the new
266    /// [`crate::selection::Selection`] after the runtime resolved a
267    /// pointer interaction. The app folds this into its
268    /// `Selection` field the same way it folds `apply_event` results
269    /// into a [`crate::widgets::text_input::TextSelection`].
270    pub selection: Option<crate::selection::Selection>,
271    /// Modifier mask captured at the moment this event was emitted. For
272    /// keyboard events this duplicates `key_press.modifiers`; for
273    /// pointer events it's the host-tracked modifier state at the time
274    /// of the click / drag (used by widgets like text_input that need
275    /// to detect Shift+click for "extend selection").
276    pub modifiers: KeyModifiers,
277    /// Click number within a multi-click sequence. Set to 1 for single
278    /// click, 2 for double-click, 3 for triple-click, etc. The runtime
279    /// increments this when consecutive `PointerDown`s land on the same
280    /// target within ~500ms and ~4px of the previous click. `0` means
281    /// "not applicable" — set on every event other than `PointerDown` /
282    /// `PointerUp` / `Click` (and their secondary / middle siblings,
283    /// which always carry 1).
284    ///
285    /// `text_input` / `text_area` and the static-text selection
286    /// manager read this to map double-click → select word, triple-
287    /// click → select line.
288    pub click_count: u8,
289    /// File system path for [`UiEventKind::FileHovered`] /
290    /// [`UiEventKind::FileDropped`] events. Multi-file drag-drops fire
291    /// one event per file (matching the underlying winit semantics);
292    /// each event carries one path. `PathBuf` rather than `String`
293    /// because Windows wide-char paths and unusual Unix paths aren't
294    /// guaranteed to be UTF-8.
295    pub path: Option<std::path::PathBuf>,
296    pub kind: UiEventKind,
297}
298
299impl UiEvent {
300    /// Synthesize a click event for the given route key.
301    ///
302    /// Intended for tests, headless automation, and snapshot
303    /// fixtures that drive UI logic without a real pointer history.
304    /// All optional fields default to `None`; modifiers are empty.
305    pub fn synthetic_click(key: impl Into<String>) -> Self {
306        Self {
307            kind: UiEventKind::Click,
308            key: Some(key.into()),
309            target: None,
310            pointer: None,
311            key_press: None,
312            text: None,
313            selection: None,
314            modifiers: KeyModifiers::default(),
315            click_count: 1,
316            path: None,
317        }
318    }
319
320    /// Route string for this event, if any.
321    ///
322    /// For pointer/focus events this is the target element key. For
323    /// hotkeys this is the registered action name.
324    pub fn route(&self) -> Option<&str> {
325        self.key.as_deref()
326    }
327
328    /// Target element key, if this event was routed to an element.
329    ///
330    /// Unlike [`Self::route`], this returns `None` for app-level
331    /// hotkey actions because those do not have a concrete element
332    /// target.
333    pub fn target_key(&self) -> Option<&str> {
334        self.target.as_ref().map(|t| t.key.as_str())
335    }
336
337    /// True when this event's route equals `key`.
338    pub fn is_route(&self, key: &str) -> bool {
339        self.route() == Some(key)
340    }
341
342    /// True for a primary click or keyboard activation on `key`.
343    ///
344    /// This is the most common button/menu route in app code.
345    pub fn is_click_or_activate(&self, key: &str) -> bool {
346        matches!(self.kind, UiEventKind::Click | UiEventKind::Activate) && self.is_route(key)
347    }
348
349    /// True for a registered hotkey action name.
350    pub fn is_hotkey(&self, action: &str) -> bool {
351        self.kind == UiEventKind::Hotkey && self.is_route(action)
352    }
353
354    /// Pointer position in logical pixels, if this event carries one.
355    pub fn pointer_pos(&self) -> Option<(f32, f32)> {
356        self.pointer
357    }
358
359    /// Pointer x coordinate in logical pixels, if this event carries one.
360    pub fn pointer_x(&self) -> Option<f32> {
361        self.pointer.map(|(x, _)| x)
362    }
363
364    /// Pointer y coordinate in logical pixels, if this event carries one.
365    pub fn pointer_y(&self) -> Option<f32> {
366        self.pointer.map(|(_, y)| y)
367    }
368
369    /// Rectangle of the routed target from the last layout pass.
370    pub fn target_rect(&self) -> Option<Rect> {
371        self.target.as_ref().map(|t| t.rect)
372    }
373
374    /// OS-composed text payload for [`UiEventKind::TextInput`].
375    pub fn text(&self) -> Option<&str> {
376        self.text.as_deref()
377    }
378}
379
380/// What kind of event happened.
381///
382/// This enum is non-exhaustive so Aetna can add new input events
383/// without breaking downstream apps. Match the variants you handle and
384/// include a wildcard arm for everything else.
385#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386#[non_exhaustive]
387pub enum UiEventKind {
388    /// Primary-button pointer down + up landed on the same node.
389    Click,
390    /// Primary-button click landed on a text run carrying a
391    /// [`crate::tree::El::text_link`] URL. The URL is in [`UiEvent::key`].
392    /// Apps decide whether to honor it (filtering, confirmation,
393    /// platform-appropriate open via [`App::drain_link_opens`] +
394    /// host-side opener). Aetna doesn't open URLs itself — it surfaces
395    /// the click and lets the app route it.
396    LinkActivated,
397    /// Secondary-button (right-click) pointer down + up landed on the
398    /// same node. Used for context menus.
399    SecondaryClick,
400    /// Middle-button pointer down + up landed on the same node.
401    MiddleClick,
402    /// Focused element was activated by keyboard (Enter/Space).
403    Activate,
404    /// Escape was pressed. Routed to the focused element when present,
405    /// otherwise emitted as a window-level event.
406    Escape,
407    /// A registered hotkey chord matched. `event.key` is the registered
408    /// name (the second element of the `(KeyChord, String)` pair).
409    Hotkey,
410    /// Other keyboard input.
411    KeyDown,
412    /// Composed text input — printable characters from the OS, after
413    /// dead-key composition / IME / shift mapping. Routed to the
414    /// focused element. Distinct from `KeyDown(Character(_))`: the
415    /// latter is the raw key event used for shortcuts and navigation;
416    /// `TextInput` is the grapheme stream a text field should consume.
417    TextInput,
418    /// Pointer moved while the primary button was held down. Routed
419    /// to the originally pressed target so a widget can extend a
420    /// selection / scrub a slider / move a draggable. `event.pointer`
421    /// carries the current logical-pixel position; `event.target` is
422    /// the node where the drag began.
423    Drag,
424    /// Primary pointer button released. Fires regardless of whether
425    /// the up landed on the same node as the down — paired with
426    /// `Click` (which only fires on a same-node match), this lets
427    /// drag-aware widgets always observe drag-end.
428    /// `event.target` is the originally pressed node;
429    /// `event.pointer` is the up position.
430    PointerUp,
431    /// Primary pointer button pressed on a hit-test target. Routed
432    /// before the eventual `Click` (which fires on up-on-same-target).
433    /// Used by widgets like text_input that need to react at
434    /// down-time — e.g., to set the selection anchor before any drag
435    /// extends it. `event.target` is the down-target,
436    /// `event.pointer` is the down position, and `event.modifiers`
437    /// carries the modifier mask (Shift+click for extend-selection).
438    PointerDown,
439    /// The library's selection manager resolved a pointer interaction
440    /// on selectable text and wants the app to update its
441    /// [`crate::selection::Selection`] state. `event.selection`
442    /// carries the new value (an empty `Selection` clears).
443    /// Emitted by `pointer_down`, `pointer_moved` (during a drag),
444    /// and the runtime's escape / dismiss paths.
445    SelectionChanged,
446    /// Pointer crossed onto a keyed hit-test target. Routed to the
447    /// newly hovered leaf — `event.target` is the new hover target,
448    /// `event.pointer` is the current pointer position. Fires
449    /// once per identity change, including the initial hover when the
450    /// pointer first enters a keyed region from nothing.
451    ///
452    /// Use for transition-driven side effects (sound on hover-enter,
453    /// analytics, hover-intent prefetch) — read state via
454    /// [`crate::BuildCx::hovered_key`] /
455    /// [`crate::BuildCx::is_hovering_within`] when you just need to
456    /// branch the build output. Both surfaces stay coherent because
457    /// the runtime debounces redraws and events to the same
458    /// hover-identity transitions.
459    ///
460    /// Always paired with a preceding `PointerLeave` for the previous
461    /// target (when there was one). Apps that want subtree-aware
462    /// behavior (parent stays "hot" while a child is hovered) should
463    /// query `is_hovering_within` rather than tracking enter/leave on
464    /// every keyed descendant.
465    PointerEnter,
466    /// Pointer crossed off a keyed hit-test target — either onto a
467    /// different keyed target (paired with a following `PointerEnter`)
468    /// or off any keyed surface entirely. Routed to the leaf that
469    /// just lost hover — `event.target` is the previous hover target,
470    /// `event.pointer` is the current pointer position (or the last
471    /// known position when the pointer left the window).
472    PointerLeave,
473    /// A file is being dragged over the window (the user hasn't
474    /// released yet). `event.path` carries the file's path; multi-file
475    /// drags fire one event per file, matching the underlying winit
476    /// semantics. `event.target` is the keyed leaf at the current
477    /// pointer position when one was hit, otherwise `None`
478    /// (drop-zone overlays that span the window can match on
479    /// `event.target.is_none()` or filter by their own key).
480    ///
481    /// Apps use this to highlight a drop zone before the drop lands.
482    /// Always paired with either a later `FileHoverCancelled` (the
483    /// user moved off without releasing) or `FileDropped` (the user
484    /// released).
485    FileHovered,
486    /// The user moved a hovered file off the window without releasing,
487    /// or pressed Escape. Window-level event (`event.target` is
488    /// `None`) — apps clear any drop-zone affordance state regardless
489    /// of which keyed leaf was previously highlighted.
490    FileHoverCancelled,
491    /// A file was dropped on the window. `event.path` carries the
492    /// path; multi-file drops fire one event per file. `event.target`
493    /// is the keyed leaf at the drop position, or `None` if the drop
494    /// landed outside any keyed surface — apps that want a global drop
495    /// target match on `target.is_none()` or treat unrouted events as
496    /// hits to a single window-level upload sink.
497    FileDropped,
498}
499
500/// Per-frame, read-only context for [`App::build`].
501///
502/// The runner snapshots the app's [`crate::Theme`] before calling
503/// `build` and exposes it through `cx.theme()` / `cx.palette()` so app
504/// code can branch on the active palette (a custom widget that picks
505/// between two non-token colors based on dark vs. light, for instance).
506/// `BuildCx` is the explicit handle for this — token references inside
507/// widgets resolve through the palette automatically and don't need it.
508///
509/// Future fields like viewport metrics or frame phase will live here so
510/// the API stays additive: adding a new accessor on `BuildCx` doesn't
511/// break apps that ignore the context.
512#[derive(Copy, Clone, Debug)]
513pub struct BuildCx<'a> {
514    theme: &'a crate::Theme,
515    ui_state: Option<&'a crate::state::UiState>,
516    diagnostics: Option<&'a HostDiagnostics>,
517}
518
519/// Why the current frame is being built. Hosts set this before each
520/// `request_redraw` so apps that surface a diagnostic overlay can show
521/// what kind of input is driving the redraw cadence.
522///
523/// `Other` is the conservative default: it covers redraws the host
524/// can't attribute (idle redraws driven by external `request_redraw`
525/// callers, the initial paint, etc.). Specific variants narrow the
526/// reason when the host can.
527#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
528pub enum FrameTrigger {
529    /// Host can't attribute the redraw to a specific cause.
530    #[default]
531    Other,
532    /// Initial paint after surface configuration.
533    Initial,
534    /// Surface resize / DPI change.
535    Resize,
536    /// Pointer move, button, or wheel.
537    Pointer,
538    /// Keyboard / IME input.
539    Keyboard,
540    /// Inside-out animation deadline elapsed (one of the visible
541    /// widgets asked for a future frame via `redraw_within`, or a
542    /// visual animation is still settling). Drives the layout-path
543    /// (full rebuild + prepare).
544    Animation,
545    /// Time-driven shader deadline elapsed (e.g. stock spinner /
546    /// skeleton / progress-indeterminate, or a custom shader
547    /// registered with `samples_time=true`). Drives the paint-only
548    /// path: `frame.time` advances but layout state is unchanged.
549    ShaderPaint,
550    /// Periodic host-config cadence (`HostConfig::redraw_interval`).
551    Periodic,
552}
553
554impl FrameTrigger {
555    /// Short, fixed-width tag for diagnostic overlays.
556    pub fn label(self) -> &'static str {
557        match self {
558            FrameTrigger::Other => "other",
559            FrameTrigger::Initial => "initial",
560            FrameTrigger::Resize => "resize",
561            FrameTrigger::Pointer => "pointer",
562            FrameTrigger::Keyboard => "keyboard",
563            FrameTrigger::Animation => "animation",
564            FrameTrigger::ShaderPaint => "shader-paint",
565            FrameTrigger::Periodic => "periodic",
566        }
567    }
568}
569
570/// Per-frame diagnostic snapshot the host hands the app via
571/// [`BuildCx::diagnostics`]. Apps that surface a debug overlay (e.g.
572/// the showcase status block) read this each build to display the
573/// active backend, frame cadence, and what triggered the redraw.
574///
575/// Hosts populate every field they can; `backend` is a static string
576/// (`"WebGPU"`, `"Vulkan"`, `"Metal"`, `"DX12"`, `"GL"`) so the app
577/// doesn't need to depend on `wgpu` to read it. Time fields use
578/// `std::time::Duration`, which works on both native and wasm32 — only
579/// `Instant::now()` is the wasm-incompatible piece, and that stays on
580/// the host side.
581#[derive(Clone, Debug)]
582pub struct HostDiagnostics {
583    /// Render backend in human-readable form.
584    pub backend: &'static str,
585    /// Current surface size in physical pixels.
586    pub surface_size: (u32, u32),
587    /// Display scale factor (`physical / logical`).
588    pub scale_factor: f32,
589    /// Active MSAA sample count (1 = MSAA off).
590    pub msaa_samples: u32,
591    /// Frame counter; increments every redraw the host actually
592    /// renders. Useful for verifying that an animated source is
593    /// progressing.
594    pub frame_index: u64,
595    /// Wall-clock time between this redraw and the previous one.
596    /// `Duration::ZERO` for the first frame (no prior frame).
597    pub last_frame_dt: std::time::Duration,
598    /// Why the host triggered this frame.
599    pub trigger: FrameTrigger,
600}
601
602impl Default for HostDiagnostics {
603    fn default() -> Self {
604        Self {
605            backend: "?",
606            surface_size: (0, 0),
607            scale_factor: 1.0,
608            msaa_samples: 1,
609            frame_index: 0,
610            last_frame_dt: std::time::Duration::ZERO,
611            trigger: FrameTrigger::default(),
612        }
613    }
614}
615
616impl<'a> BuildCx<'a> {
617    /// Construct a [`BuildCx`] borrowing the supplied theme. Hosts call
618    /// this once per frame after [`App::theme`] and before [`App::build`].
619    /// Hosts that own a [`crate::state::UiState`] should chain
620    /// [`Self::with_ui_state`] so the app can read interaction state
621    /// (hover) during build via [`Self::hovered_key`] /
622    /// [`Self::is_hovering_within`].
623    pub fn new(theme: &'a crate::Theme) -> Self {
624        Self {
625            theme,
626            ui_state: None,
627            diagnostics: None,
628        }
629    }
630
631    /// Attach the runtime's [`crate::state::UiState`] so build-time
632    /// accessors (`hovered_key`, `is_hovering_within`) can answer.
633    /// When omitted, those accessors return `None` / `false` — useful
634    /// for headless rendering paths that don't track interaction
635    /// state.
636    pub fn with_ui_state(mut self, ui_state: &'a crate::state::UiState) -> Self {
637        self.ui_state = Some(ui_state);
638        self
639    }
640
641    /// Attach a [`HostDiagnostics`] snapshot for this frame. Hosts call
642    /// this when they want apps to surface debug overlays (e.g. the
643    /// showcase status block); apps that don't read `diagnostics()`
644    /// pay nothing for it. Headless render paths leave it `None`.
645    pub fn with_diagnostics(mut self, diagnostics: &'a HostDiagnostics) -> Self {
646        self.diagnostics = Some(diagnostics);
647        self
648    }
649
650    /// Per-frame diagnostic snapshot from the host (backend, frame
651    /// cadence, trigger reason, etc.), or `None` when the host did
652    /// not attach one. Apps display this in optional debug overlays.
653    pub fn diagnostics(&self) -> Option<&HostDiagnostics> {
654        self.diagnostics
655    }
656
657    /// The active runtime theme for this frame.
658    pub fn theme(&self) -> &crate::Theme {
659        self.theme
660    }
661
662    /// Shorthand for `self.theme().palette()`.
663    pub fn palette(&self) -> &crate::Palette {
664        self.theme.palette()
665    }
666
667    /// Key of the leaf node currently under the pointer, or `None`
668    /// when nothing is hovered or this `BuildCx` was built without a
669    /// `UiState` (headless rendering paths).
670    ///
671    /// Use for branching the build output on hover state without
672    /// mirroring it via `App::on_event` handlers — e.g., a sidebar
673    /// row that previews details in a side pane based on what's
674    /// currently hovered.
675    ///
676    /// For region-aware queries (parent stays "hot" while a child is
677    /// hovered), prefer [`Self::is_hovering_within`].
678    pub fn hovered_key(&self) -> Option<&str> {
679        self.ui_state?.hovered_key()
680    }
681
682    /// True iff `key`'s node — or any descendant of it — is the
683    /// current hover target. Subtree-aware, matching the semantics of
684    /// [`crate::tree::El::hover_alpha`]. Returns `false` when this
685    /// `BuildCx` has no attached `UiState` or when `key` isn't in the
686    /// current tree.
687    ///
688    /// Reads the underlying tracker, not the eased subtree envelope —
689    /// the boolean flips immediately on hit-test identity change.
690    pub fn is_hovering_within(&self, key: &str) -> bool {
691        self.ui_state
692            .is_some_and(|state| state.is_hovering_within(key))
693    }
694}
695
696/// The application contract. Implement this on your state struct and
697/// pass it to a host runner (e.g., `aetna_winit_wgpu::run`).
698pub trait App {
699    /// Refresh app-owned external state immediately before a frame is
700    /// built.
701    ///
702    /// Hosts call this once per redraw before [`Self::build`]. Use it
703    /// for polling an external source, reconciling optimistic local
704    /// state with a backend snapshot, or advancing host-owned live data
705    /// that should be visible in the next tree. Keep expensive work
706    /// outside the render loop; this hook is still on the frame path.
707    ///
708    /// Default: no-op.
709    fn before_build(&mut self) {}
710
711    /// Project current state into a scene tree. Called whenever the
712    /// host requests a redraw, after [`Self::before_build`]. Prefer to
713    /// keep this pure: read current state and return a fresh tree.
714    ///
715    /// `cx` carries per-frame, read-only context (active theme, future
716    /// viewport / phase metadata). Apps that don't need to branch on
717    /// the theme during construction can ignore the parameter — token
718    /// references in widget code resolve through the palette
719    /// automatically.
720    fn build(&self, cx: &BuildCx) -> El;
721
722    /// Update state in response to a routed event. Default: no-op.
723    fn on_event(&mut self, _event: UiEvent) {}
724
725    /// The application's current text [`crate::selection::Selection`].
726    /// Read by the host once per frame so the library can paint
727    /// highlight bands and resolve `selected_text` for clipboard.
728    /// Apps that own a `Selection` field return a clone here; the
729    /// default returns the empty selection.
730    fn selection(&self) -> crate::selection::Selection {
731        crate::selection::Selection::default()
732    }
733
734    /// App-level hotkey registry. The library matches incoming key
735    /// presses against this list before its own focus-activation
736    /// routing; a match emits a [`UiEvent`] with `kind =
737    /// UiEventKind::Hotkey` and `key = Some(name)`.
738    ///
739    /// Called once per build cycle; the host runner snapshots the list
740    /// alongside `build()` so the chords stay in sync with state.
741    /// Default: no hotkeys.
742    fn hotkeys(&self) -> Vec<(KeyChord, String)> {
743        Vec::new()
744    }
745
746    /// Drain pending toast notifications produced since the last
747    /// frame. The runtime calls this once per `prepare_layout`,
748    /// stamps each spec with a monotonic id and `expires_at = now +
749    /// ttl`, queues it in the runtime toast state, and
750    /// synthesizes a `toast_stack` layer at the El root so the
751    /// rendered tree mirrors the visible state. Apps typically
752    /// accumulate specs in a `Vec<ToastSpec>` field from event
753    /// handlers, then `mem::take` it here.
754    ///
755    /// **Root requirement:** apps that produce toasts (or use
756    /// `.tooltip(text)` on any node) must wrap their
757    /// [`Self::build`] return value in `overlays(main, [])` so the
758    /// runtime can append the floating layer as an overlay sibling
759    /// — same convention used for popovers and modals. Debug
760    /// builds panic if the synthesizer runs against a non-overlay
761    /// root.
762    ///
763    /// Default: no toasts.
764    fn drain_toasts(&mut self) -> Vec<crate::toast::ToastSpec> {
765        Vec::new()
766    }
767
768    /// Drain pending programmatic focus requests produced since the
769    /// last frame. The runtime calls this once per `prepare_layout`,
770    /// after the focus order has been rebuilt from the new tree, and
771    /// resolves each entry against the keyed focusables. Unmatched
772    /// keys (widget absent from the rebuilt tree, or not focusable)
773    /// are dropped silently.
774    ///
775    /// This is the imperative companion to keyboard `Tab` traversal:
776    /// use it for affordances like *Ctrl+F → focus the search input*,
777    /// *jump-to-match → focus the matched row*, or *open inline edit
778    /// → focus the field*. Apps typically accumulate keys in a
779    /// `Vec<String>` field from event handlers and `mem::take` it
780    /// here.
781    ///
782    /// Multiple requests in one frame resolve in order; the last
783    /// successfully-resolved key is the one focused.
784    ///
785    /// Default: no requests.
786    fn drain_focus_requests(&mut self) -> Vec<String> {
787        Vec::new()
788    }
789
790    /// Drain pending programmatic scroll requests targeting virtual
791    /// lists. The runtime resolves each request during layout of the
792    /// matching list, using the live viewport rect and row-height
793    /// cache (so first-frame and `virtual_list_dyn` cases work without
794    /// the app duplicating the widget's layout math). Unmatched list
795    /// keys and out-of-range row indices drop silently.
796    ///
797    /// Use for "reveal a row" affordances: jump-to-search-result,
798    /// reveal-selected-item, scroll-to-top-on-tab-change. Apps
799    /// typically accumulate requests in a `Vec<ScrollRequest>` field
800    /// from event handlers and `mem::take` it here.
801    ///
802    /// Default: no requests.
803    fn drain_scroll_requests(&mut self) -> Vec<crate::scroll::ScrollRequest> {
804        Vec::new()
805    }
806
807    /// Drain pending URL-open requests produced since the last frame.
808    /// Hosts call this once per frame and route each URL to a
809    /// platform-appropriate opener — `window.open` in the wasm host,
810    /// the `open` crate (or equivalent) on native.
811    ///
812    /// The library emits [`UiEventKind::LinkActivated`] when a click
813    /// lands on a text run carrying a link URL, but it does not act
814    /// on the URL itself: opening a link is an app concern (apps may
815    /// want to confirm, filter by scheme, route through an internal
816    /// router, or no-op entirely). Apps that want the default
817    /// browser-style behavior accumulate URLs from
818    /// [`UiEventKind::LinkActivated`] in their `on_event` handler and
819    /// return them here; apps that don't override this method drop
820    /// link clicks on the floor.
821    ///
822    /// Default: no requests.
823    fn drain_link_opens(&mut self) -> Vec<String> {
824        Vec::new()
825    }
826
827    /// Custom shaders this app needs registered. Each entry carries
828    /// the shader name, its WGSL source, and per-flag opt-ins
829    /// (backdrop sampling, time-driven motion). The host runner
830    /// registers them once at startup via
831    /// `Runner::register_shader_with(name, wgsl, samples_backdrop, samples_time)`.
832    ///
833    /// Backends that don't support backdrop sampling skip entries with
834    /// `samples_backdrop=true`; any node bound to such a shader will
835    /// draw nothing on those backends rather than mis-render.
836    /// `samples_time=true` declares that the shader's output depends
837    /// on `frame.time`, which keeps the host idle loop ticking while
838    /// any node is bound to it.
839    ///
840    /// Default: no shaders.
841    fn shaders(&self) -> Vec<AppShader> {
842        Vec::new()
843    }
844
845    /// Runtime paint theme for this app. Hosts apply it to the renderer
846    /// before preparing each frame so stateful apps can switch global
847    /// material routing without backend-specific calls.
848    fn theme(&self) -> crate::Theme {
849        crate::Theme::default()
850    }
851}
852
853/// One custom shader registration, returned from [`App::shaders`].
854#[derive(Clone, Copy, Debug)]
855pub struct AppShader {
856    pub name: &'static str,
857    pub wgsl: &'static str,
858    /// Reads the prior pass's color target (`@group(2) backdrop_tex`).
859    /// Backends without backdrop support skip these.
860    pub samples_backdrop: bool,
861    /// Reads `frame.time` and so requires continuous redraw whenever
862    /// any node is bound to it. The runtime ORs this into
863    /// `PrepareResult::needs_redraw` per frame.
864    pub samples_time: bool,
865}