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}