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