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}