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    /// This is the target's transformed visual rect, not any
371    /// `hit_overflow` band that may also route pointer events to it.
372    pub fn target_rect(&self) -> Option<Rect> {
373        self.target.as_ref().map(|t| t.rect)
374    }
375
376    /// OS-composed text payload for [`UiEventKind::TextInput`].
377    pub fn text(&self) -> Option<&str> {
378        self.text.as_deref()
379    }
380}
381
382/// What kind of event happened.
383///
384/// This enum is non-exhaustive so Aetna can add new input events
385/// without breaking downstream apps. Match the variants you handle and
386/// include a wildcard arm for everything else.
387#[derive(Clone, Copy, Debug, PartialEq, Eq)]
388#[non_exhaustive]
389pub enum UiEventKind {
390    /// Primary-button pointer down + up landed on the same node.
391    Click,
392    /// Primary-button click landed on a text run carrying a
393    /// [`crate::tree::El::text_link`] URL. The URL is in [`UiEvent::key`].
394    /// Apps decide whether to honor it (filtering, confirmation,
395    /// platform-appropriate open via [`App::drain_link_opens`] +
396    /// host-side opener). Aetna doesn't open URLs itself — it surfaces
397    /// the click and lets the app route it.
398    LinkActivated,
399    /// Secondary-button (right-click) pointer down + up landed on the
400    /// same node. Used for context menus.
401    SecondaryClick,
402    /// Middle-button pointer down + up landed on the same node.
403    MiddleClick,
404    /// Focused element was activated by keyboard (Enter/Space).
405    Activate,
406    /// Escape was pressed. Routed to the focused element when present,
407    /// otherwise emitted as a window-level event.
408    Escape,
409    /// A registered hotkey chord matched. `event.key` is the registered
410    /// name (the second element of the `(KeyChord, String)` pair).
411    Hotkey,
412    /// Other keyboard input.
413    KeyDown,
414    /// Composed text input — printable characters from the OS, after
415    /// dead-key composition / IME / shift mapping. Routed to the
416    /// focused element. Distinct from `KeyDown(Character(_))`: the
417    /// latter is the raw key event used for shortcuts and navigation;
418    /// `TextInput` is the grapheme stream a text field should consume.
419    TextInput,
420    /// Pointer moved while the primary button was held down. Routed
421    /// to the originally pressed target so a widget can extend a
422    /// selection / scrub a slider / move a draggable. `event.pointer`
423    /// carries the current logical-pixel position; `event.target` is
424    /// the node where the drag began.
425    Drag,
426    /// Primary pointer button released. Fires regardless of whether
427    /// the up landed on the same node as the down — paired with
428    /// `Click` (which only fires on a same-node match), this lets
429    /// drag-aware widgets always observe drag-end.
430    /// `event.target` is the originally pressed node;
431    /// `event.pointer` is the up position.
432    PointerUp,
433    /// Primary pointer button pressed on a hit-test target. Routed
434    /// before the eventual `Click` (which fires on up-on-same-target).
435    /// Used by widgets like text_input that need to react at
436    /// down-time — e.g., to set the selection anchor before any drag
437    /// extends it. `event.target` is the down-target,
438    /// `event.pointer` is the down position, and `event.modifiers`
439    /// carries the modifier mask (Shift+click for extend-selection).
440    PointerDown,
441    /// The library's selection manager resolved a pointer interaction
442    /// on selectable text and wants the app to update its
443    /// [`crate::selection::Selection`] state. `event.selection`
444    /// carries the new value (an empty `Selection` clears).
445    /// Emitted by `pointer_down`, `pointer_moved` (during a drag),
446    /// and the runtime's escape / dismiss paths.
447    SelectionChanged,
448    /// Pointer crossed onto a keyed hit-test target. Routed to the
449    /// newly hovered leaf — `event.target` is the new hover target,
450    /// `event.pointer` is the current pointer position. Fires
451    /// once per identity change, including the initial hover when the
452    /// pointer first enters a keyed region from nothing.
453    ///
454    /// Use for transition-driven side effects (sound on hover-enter,
455    /// analytics, hover-intent prefetch) — read state via
456    /// [`crate::BuildCx::hovered_key`] /
457    /// [`crate::BuildCx::is_hovering_within`] when you just need to
458    /// branch the build output. Both surfaces stay coherent because
459    /// the runtime debounces redraws and events to the same
460    /// hover-identity transitions.
461    ///
462    /// Always paired with a preceding `PointerLeave` for the previous
463    /// target (when there was one). Apps that want subtree-aware
464    /// behavior (parent stays "hot" while a child is hovered) should
465    /// query `is_hovering_within` rather than tracking enter/leave on
466    /// every keyed descendant.
467    PointerEnter,
468    /// Pointer crossed off a keyed hit-test target — either onto a
469    /// different keyed target (paired with a following `PointerEnter`)
470    /// or off any keyed surface entirely. Routed to the leaf that
471    /// just lost hover — `event.target` is the previous hover target,
472    /// `event.pointer` is the current pointer position (or the last
473    /// known position when the pointer left the window).
474    PointerLeave,
475    /// A file is being dragged over the window (the user hasn't
476    /// released yet). `event.path` carries the file's path; multi-file
477    /// drags fire one event per file, matching the underlying winit
478    /// semantics. `event.target` is the keyed leaf at the current
479    /// pointer position when one was hit, otherwise `None`
480    /// (drop-zone overlays that span the window can match on
481    /// `event.target.is_none()` or filter by their own key).
482    ///
483    /// Apps use this to highlight a drop zone before the drop lands.
484    /// Always paired with either a later `FileHoverCancelled` (the
485    /// user moved off without releasing) or `FileDropped` (the user
486    /// released).
487    FileHovered,
488    /// The user moved a hovered file off the window without releasing,
489    /// or pressed Escape. Window-level event (`event.target` is
490    /// `None`) — apps clear any drop-zone affordance state regardless
491    /// of which keyed leaf was previously highlighted.
492    FileHoverCancelled,
493    /// A file was dropped on the window. `event.path` carries the
494    /// path; multi-file drops fire one event per file. `event.target`
495    /// is the keyed leaf at the drop position, or `None` if the drop
496    /// landed outside any keyed surface — apps that want a global drop
497    /// target match on `target.is_none()` or treat unrouted events as
498    /// hits to a single window-level upload sink.
499    FileDropped,
500}
501
502/// Per-frame, read-only context for [`App::build`].
503///
504/// The runner snapshots the app's [`crate::Theme`] before calling
505/// `build` and exposes it through `cx.theme()` / `cx.palette()` so app
506/// code can branch on the active palette (a custom widget that picks
507/// between two non-token colors based on dark vs. light, for instance).
508/// `BuildCx` is the explicit handle for this — token references inside
509/// widgets resolve through the palette automatically and don't need it.
510///
511/// Future fields like viewport metrics or frame phase will live here so
512/// the API stays additive: adding a new accessor on `BuildCx` doesn't
513/// break apps that ignore the context.
514#[derive(Copy, Clone, Debug)]
515pub struct BuildCx<'a> {
516    theme: &'a crate::Theme,
517    ui_state: Option<&'a crate::state::UiState>,
518    diagnostics: Option<&'a HostDiagnostics>,
519}
520
521/// Why the current frame is being built. Hosts set this before each
522/// `request_redraw` so apps that surface a diagnostic overlay can show
523/// what kind of input is driving the redraw cadence.
524///
525/// `Other` is the conservative default: it covers redraws the host
526/// can't attribute (idle redraws driven by external `request_redraw`
527/// callers, the initial paint, etc.). Specific variants narrow the
528/// reason when the host can.
529#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
530pub enum FrameTrigger {
531    /// Host can't attribute the redraw to a specific cause.
532    #[default]
533    Other,
534    /// Initial paint after surface configuration.
535    Initial,
536    /// Surface resize / DPI change.
537    Resize,
538    /// Pointer move, button, or wheel.
539    Pointer,
540    /// Keyboard / IME input.
541    Keyboard,
542    /// Inside-out animation deadline elapsed (one of the visible
543    /// widgets asked for a future frame via `redraw_within`, or a
544    /// visual animation is still settling). Drives the layout-path
545    /// (full rebuild + prepare).
546    Animation,
547    /// Time-driven shader deadline elapsed (e.g. stock spinner /
548    /// skeleton / progress-indeterminate, or a custom shader
549    /// registered with `samples_time=true`). Drives the paint-only
550    /// path: `frame.time` advances but layout state is unchanged.
551    ShaderPaint,
552    /// Periodic host-config cadence (`HostConfig::redraw_interval`).
553    Periodic,
554}
555
556impl FrameTrigger {
557    /// Short, fixed-width tag for diagnostic overlays.
558    pub fn label(self) -> &'static str {
559        match self {
560            FrameTrigger::Other => "other",
561            FrameTrigger::Initial => "initial",
562            FrameTrigger::Resize => "resize",
563            FrameTrigger::Pointer => "pointer",
564            FrameTrigger::Keyboard => "keyboard",
565            FrameTrigger::Animation => "animation",
566            FrameTrigger::ShaderPaint => "shader-paint",
567            FrameTrigger::Periodic => "periodic",
568        }
569    }
570}
571
572/// Per-frame diagnostic snapshot the host hands the app via
573/// [`BuildCx::diagnostics`]. Apps that surface a debug overlay (e.g.
574/// the showcase status block) read this each build to display the
575/// active backend, frame cadence, and what triggered the redraw.
576///
577/// Hosts populate every field they can; `backend` is a static string
578/// (`"WebGPU"`, `"Vulkan"`, `"Metal"`, `"DX12"`, `"GL"`) so the app
579/// doesn't need to depend on `wgpu` to read it. Time fields use
580/// `std::time::Duration`, which works on both native and wasm32 — only
581/// `Instant::now()` is the wasm-incompatible piece, and that stays on
582/// the host side.
583#[derive(Clone, Debug)]
584pub struct HostDiagnostics {
585    /// Render backend in human-readable form.
586    pub backend: &'static str,
587    /// Current surface size in physical pixels.
588    pub surface_size: (u32, u32),
589    /// Display scale factor (`physical / logical`).
590    pub scale_factor: f32,
591    /// Active MSAA sample count (1 = MSAA off).
592    pub msaa_samples: u32,
593    /// Frame counter; increments every redraw the host actually
594    /// renders. Useful for verifying that an animated source is
595    /// progressing.
596    pub frame_index: u64,
597    /// Wall-clock time between this redraw and the previous one.
598    /// `Duration::ZERO` for the first frame (no prior frame).
599    pub last_frame_dt: std::time::Duration,
600    /// Why the host triggered this frame.
601    pub trigger: FrameTrigger,
602}
603
604impl Default for HostDiagnostics {
605    fn default() -> Self {
606        Self {
607            backend: "?",
608            surface_size: (0, 0),
609            scale_factor: 1.0,
610            msaa_samples: 1,
611            frame_index: 0,
612            last_frame_dt: std::time::Duration::ZERO,
613            trigger: FrameTrigger::default(),
614        }
615    }
616}
617
618impl<'a> BuildCx<'a> {
619    /// Construct a [`BuildCx`] borrowing the supplied theme. Hosts call
620    /// this once per frame after [`App::theme`] and before [`App::build`].
621    /// Hosts that own a [`crate::state::UiState`] should chain
622    /// [`Self::with_ui_state`] so the app can read interaction state
623    /// (hover) during build via [`Self::hovered_key`] /
624    /// [`Self::is_hovering_within`].
625    pub fn new(theme: &'a crate::Theme) -> Self {
626        Self {
627            theme,
628            ui_state: None,
629            diagnostics: None,
630        }
631    }
632
633    /// Attach the runtime's [`crate::state::UiState`] so build-time
634    /// accessors (`hovered_key`, `is_hovering_within`) can answer.
635    /// When omitted, those accessors return `None` / `false` — useful
636    /// for headless rendering paths that don't track interaction
637    /// state.
638    pub fn with_ui_state(mut self, ui_state: &'a crate::state::UiState) -> Self {
639        self.ui_state = Some(ui_state);
640        self
641    }
642
643    /// Attach a [`HostDiagnostics`] snapshot for this frame. Hosts call
644    /// this when they want apps to surface debug overlays (e.g. the
645    /// showcase status block); apps that don't read `diagnostics()`
646    /// pay nothing for it. Headless render paths leave it `None`.
647    pub fn with_diagnostics(mut self, diagnostics: &'a HostDiagnostics) -> Self {
648        self.diagnostics = Some(diagnostics);
649        self
650    }
651
652    /// Per-frame diagnostic snapshot from the host (backend, frame
653    /// cadence, trigger reason, etc.), or `None` when the host did
654    /// not attach one. Apps display this in optional debug overlays.
655    pub fn diagnostics(&self) -> Option<&HostDiagnostics> {
656        self.diagnostics
657    }
658
659    /// The active runtime theme for this frame.
660    pub fn theme(&self) -> &crate::Theme {
661        self.theme
662    }
663
664    /// Shorthand for `self.theme().palette()`.
665    pub fn palette(&self) -> &crate::Palette {
666        self.theme.palette()
667    }
668
669    /// Key of the leaf node currently under the pointer, or `None`
670    /// when nothing is hovered or this `BuildCx` was built without a
671    /// `UiState` (headless rendering paths).
672    ///
673    /// Use for branching the build output on hover state without
674    /// mirroring it via `App::on_event` handlers — e.g., a sidebar
675    /// row that previews details in a side pane based on what's
676    /// currently hovered.
677    ///
678    /// For region-aware queries (parent stays "hot" while a child is
679    /// hovered), prefer [`Self::is_hovering_within`].
680    pub fn hovered_key(&self) -> Option<&str> {
681        self.ui_state?.hovered_key()
682    }
683
684    /// True iff `key`'s node — or any descendant of it — is the
685    /// current hover target. Subtree-aware, matching the semantics of
686    /// [`crate::tree::El::hover_alpha`]. Returns `false` when this
687    /// `BuildCx` has no attached `UiState` or when `key` isn't in the
688    /// current tree.
689    ///
690    /// Reads the underlying tracker, not the eased subtree envelope —
691    /// the boolean flips immediately on hit-test identity change.
692    pub fn is_hovering_within(&self, key: &str) -> bool {
693        self.ui_state
694            .is_some_and(|state| state.is_hovering_within(key))
695    }
696}
697
698/// The application contract. Implement this on your state struct and
699/// pass it to a host runner (e.g., `aetna_winit_wgpu::run`).
700pub trait App {
701    /// Refresh app-owned external state immediately before a frame is
702    /// built.
703    ///
704    /// Hosts call this once per redraw before [`Self::build`]. Use it
705    /// for polling an external source, reconciling optimistic local
706    /// state with a backend snapshot, or advancing host-owned live data
707    /// that should be visible in the next tree. Keep expensive work
708    /// outside the render loop; this hook is still on the frame path.
709    ///
710    /// Default: no-op.
711    fn before_build(&mut self) {}
712
713    /// Project current state into a scene tree. Called whenever the
714    /// host requests a redraw, after [`Self::before_build`]. Prefer to
715    /// keep this pure: read current state and return a fresh tree.
716    ///
717    /// `cx` carries per-frame, read-only context (active theme, future
718    /// viewport / phase metadata). Apps that don't need to branch on
719    /// the theme during construction can ignore the parameter — token
720    /// references in widget code resolve through the palette
721    /// automatically.
722    fn build(&self, cx: &BuildCx) -> El;
723
724    /// Update state in response to a routed event. Default: no-op.
725    fn on_event(&mut self, _event: UiEvent) {}
726
727    /// The application's current text [`crate::selection::Selection`].
728    /// Read by the host once per frame so the library can paint
729    /// highlight bands and resolve `selected_text` for clipboard.
730    /// Apps that own a `Selection` field return a clone here; the
731    /// default returns the empty selection.
732    fn selection(&self) -> crate::selection::Selection {
733        crate::selection::Selection::default()
734    }
735
736    /// App-level hotkey registry. The library matches incoming key
737    /// presses against this list before its own focus-activation
738    /// routing; a match emits a [`UiEvent`] with `kind =
739    /// UiEventKind::Hotkey` and `key = Some(name)`.
740    ///
741    /// Called once per build cycle; the host runner snapshots the list
742    /// alongside `build()` so the chords stay in sync with state.
743    /// Default: no hotkeys.
744    fn hotkeys(&self) -> Vec<(KeyChord, String)> {
745        Vec::new()
746    }
747
748    /// Drain pending toast notifications produced since the last
749    /// frame. The runtime calls this once per `prepare_layout`,
750    /// stamps each spec with a monotonic id and `expires_at = now +
751    /// ttl`, queues it in the runtime toast state, and
752    /// synthesizes a `toast_stack` layer at the El root so the
753    /// rendered tree mirrors the visible state. Apps typically
754    /// accumulate specs in a `Vec<ToastSpec>` field from event
755    /// handlers, then `mem::take` it here.
756    ///
757    /// **Root requirement:** apps that produce toasts (or use
758    /// `.tooltip(text)` on any node) must wrap their
759    /// [`Self::build`] return value in `overlays(main, [])` so the
760    /// runtime can append the floating layer as an overlay sibling
761    /// — same convention used for popovers and modals. Debug
762    /// builds panic if the synthesizer runs against a non-overlay
763    /// root.
764    ///
765    /// Default: no toasts.
766    fn drain_toasts(&mut self) -> Vec<crate::toast::ToastSpec> {
767        Vec::new()
768    }
769
770    /// Drain pending programmatic focus requests produced since the
771    /// last frame. The runtime calls this once per `prepare_layout`,
772    /// after the focus order has been rebuilt from the new tree, and
773    /// resolves each entry against the keyed focusables. Unmatched
774    /// keys (widget absent from the rebuilt tree, or not focusable)
775    /// are dropped silently.
776    ///
777    /// This is the imperative companion to keyboard `Tab` traversal:
778    /// use it for affordances like *Ctrl+F → focus the search input*,
779    /// *jump-to-match → focus the matched row*, or *open inline edit
780    /// → focus the field*. Apps typically accumulate keys in a
781    /// `Vec<String>` field from event handlers and `mem::take` it
782    /// here.
783    ///
784    /// Multiple requests in one frame resolve in order; the last
785    /// successfully-resolved key is the one focused.
786    ///
787    /// Default: no requests.
788    fn drain_focus_requests(&mut self) -> Vec<String> {
789        Vec::new()
790    }
791
792    /// Drain pending programmatic scroll requests. The runtime
793    /// resolves each request during layout, using live viewport rects
794    /// and row-height/content geometry that apps should not duplicate.
795    /// Unmatched keys and out-of-range row indices drop silently.
796    ///
797    /// Use [`crate::scroll::ScrollRequest::ToRow`] for virtual-list
798    /// affordances such as jump-to-search-result, reveal selected row,
799    /// or scroll-to-top-on-tab-change. Use
800    /// [`crate::scroll::ScrollRequest::EnsureVisible`] for widgets
801    /// with an internal scroll viewport, including fixed-height
802    /// [`crate::widgets::text_area`] caret-into-view after accepted
803    /// edit/navigation events. Apps typically accumulate requests in a
804    /// `Vec<ScrollRequest>` field from event handlers and
805    /// `mem::take` it here.
806    ///
807    /// Default: no requests.
808    fn drain_scroll_requests(&mut self) -> Vec<crate::scroll::ScrollRequest> {
809        Vec::new()
810    }
811
812    /// Drain pending URL-open requests produced since the last frame.
813    /// Hosts call this once per frame and route each URL to a
814    /// platform-appropriate opener — `window.open` in the wasm host,
815    /// the `open` crate (or equivalent) on native.
816    ///
817    /// The library emits [`UiEventKind::LinkActivated`] when a click
818    /// lands on a text run carrying a link URL, but it does not act
819    /// on the URL itself: opening a link is an app concern (apps may
820    /// want to confirm, filter by scheme, route through an internal
821    /// router, or no-op entirely). Apps that want the default
822    /// browser-style behavior accumulate URLs from
823    /// [`UiEventKind::LinkActivated`] in their `on_event` handler and
824    /// return them here; apps that don't override this method drop
825    /// link clicks on the floor.
826    ///
827    /// Default: no requests.
828    fn drain_link_opens(&mut self) -> Vec<String> {
829        Vec::new()
830    }
831
832    /// Custom shaders this app needs registered. Each entry carries
833    /// the shader name, its WGSL source, and per-flag opt-ins
834    /// (backdrop sampling, time-driven motion). The host runner
835    /// registers them once at startup via
836    /// `Runner::register_shader_with(name, wgsl, samples_backdrop, samples_time)`.
837    ///
838    /// Backends that don't support backdrop sampling skip entries with
839    /// `samples_backdrop=true`; any node bound to such a shader will
840    /// draw nothing on those backends rather than mis-render.
841    /// `samples_time=true` declares that the shader's output depends
842    /// on `frame.time`, which keeps the host idle loop ticking while
843    /// any node is bound to it.
844    ///
845    /// Default: no shaders.
846    fn shaders(&self) -> Vec<AppShader> {
847        Vec::new()
848    }
849
850    /// Runtime paint theme for this app. Hosts apply it to the renderer
851    /// before preparing each frame so stateful apps can switch global
852    /// material routing without backend-specific calls.
853    fn theme(&self) -> crate::Theme {
854        crate::Theme::default()
855    }
856}
857
858/// One custom shader registration, returned from [`App::shaders`].
859#[derive(Clone, Copy, Debug)]
860pub struct AppShader {
861    pub name: &'static str,
862    pub wgsl: &'static str,
863    /// Reads the prior pass's color target (`@group(2) backdrop_tex`).
864    /// Backends without backdrop support skip these.
865    pub samples_backdrop: bool,
866    /// Reads `frame.time` and so requires continuous redraw whenever
867    /// any node is bound to it. The runtime ORs this into
868    /// `PrepareResult::needs_redraw` per frame.
869    pub samples_time: bool,
870}