aetna_core/state.rs
1//! [`UiState`] — the renderer's interaction-state side store.
2//!
3//! Holds pointer position, hovered/pressed/focused targets, per-node
4//! scroll offsets, the app-supplied hotkey registry, and the per-(node,
5//! prop) animation map. The host doesn't touch this directly; backend
6//! runners such as `aetna_wgpu::Runner` own one and route input events
7//! through it.
8//!
9//! Visual delta application: if `pressed` is set, that node renders with
10//! `state = Press`. Otherwise, if `hovered` is set, that node renders
11//! with `state = Hover`. Press takes precedence so clicking a button
12//! that's also hovered shows the press visual, not the hover visual.
13//! Focus is independent of both — the focus ring is its own envelope.
14
15mod animation;
16mod click;
17mod cursor;
18mod focus;
19mod interaction;
20mod keyboard;
21pub(crate) mod query;
22mod scroll;
23mod selection;
24mod toast;
25mod types;
26mod widget_state;
27
28use std::fmt::Debug;
29// `web_time::Instant` is API-identical to `std::time::Instant` on
30// native and uses `performance.now()` on wasm32 — std's `Instant::now()`
31// panics in the browser because there is no monotonic clock there.
32
33use crate::event::{KeyModifiers, PointerButton, PointerKind, UiTarget};
34
35pub use types::{
36 AnimationMode, EnvelopeKind, LONG_PRESS_DELAY, ScrollMetrics, ThumbDrag, WidgetState,
37};
38pub(crate) use types::{
39 ScrollAnchor, SelectionDrag, SelectionDragGranularity, TOUCH_DRAG_THRESHOLD, TouchGestureState,
40 VirtualAnchor, caret_blink_alpha_for,
41};
42
43use types::{
44 AnimationState, CaretState, ClickState, FocusState, HotkeyState, LayoutState,
45 NodeInteractionState, PopoverFocusState, ScrollState, SelectionState, ToastState, TooltipState,
46 WidgetStateStore,
47};
48
49/// Internal UI state — interaction trackers + the side maps the library
50/// writes during layout / state-apply / animation-tick passes. Owned by
51/// the renderer; the host doesn't interact with this directly.
52///
53/// The side maps replace the per-node bookkeeping fields that used to
54/// live on `El` (computed rect, interaction state, envelope amounts).
55/// Keying is by `El::computed_id`, the path-shaped string assigned by
56/// the layout pass.
57#[derive(Default)]
58pub struct UiState {
59 /// Last known pointer position in **logical** pixels. `None` until
60 /// the pointer enters the window.
61 pub pointer_pos: Option<(f32, f32)>,
62 /// Modality of the most recent pointer ingest. Used to stamp
63 /// emitted [`crate::event::UiEvent::pointer_kind`] and to gate
64 /// hover-only behavior on touch. Defaults to
65 /// [`PointerKind::Mouse`] until the first ingest, which matches
66 /// historical behavior on hosts without touch.
67 pub pointer_kind: PointerKind,
68 /// Touch-gesture state machine. Tracks whether the active touch
69 /// contact is awaiting threshold disambiguation
70 /// ([`TouchGestureState::Pending`]), has committed to scrolling
71 /// ([`TouchGestureState::Scrolling`]), or is idle / committed to
72 /// drag ([`TouchGestureState::None`]). Mouse and pen pointers
73 /// stay at `None`.
74 pub(crate) touch_gesture: TouchGestureState,
75 pub hovered: Option<UiTarget>,
76 pub pressed: Option<UiTarget>,
77 /// Secondary / middle button down-target, kept on a separate
78 /// channel so it doesn't fight the primary `pressed` envelope or
79 /// move focus. Carries the button kind so `pointer_up` knows which
80 /// click variant to emit. Cleared by `pointer_up` matching the
81 /// same button.
82 pub(crate) pressed_secondary: Option<(UiTarget, PointerButton)>,
83 /// URL of the text-link run under a primary press, when present.
84 /// Set by `pointer_down` from `hit_test::link_at`; consumed by
85 /// `pointer_up`, which emits `UiEventKind::LinkActivated` only
86 /// when the up position lands on the same link URL — same
87 /// press-then-confirm contract as a normal `Click`.
88 pub(crate) pressed_link: Option<String>,
89 /// URL of the text-link run currently under the pointer (no
90 /// button press required). Tracked by `pointer_moved` so the
91 /// cursor resolver can return [`crate::cursor::Cursor::Pointer`]
92 /// over links without the text leaves having to be keyed
93 /// hover-test targets. Cleared on `pointer_left`.
94 pub(crate) hovered_link: Option<String>,
95 pub focused: Option<UiTarget>,
96 /// Whether the focused element should display its focus ring.
97 /// Tracks the web platform's `:focus-visible` heuristic: keyboard
98 /// focus (Tab, arrow-nav) raises the flag; pointer-down clears it.
99 /// Widgets where the ring belongs even on click — text inputs and
100 /// text areas, where the ring communicates "this surface is now
101 /// active" beyond the caret alone — opt back in via
102 /// [`crate::tree::El::always_show_focus_ring`].
103 pub focus_visible: bool,
104 pub(crate) focus: FocusState,
105 /// Mirror of the application's current
106 /// [`crate::selection::Selection`]. Set by the host runner once
107 /// per frame from [`crate::event::App::selection`]; read by the
108 /// painter to draw highlight bands and by the selection manager
109 /// to know what's currently active when extending a drag.
110 pub current_selection: crate::selection::Selection,
111 /// Internal selection traversal and drag state.
112 pub(crate) selection: SelectionState,
113 pub(crate) click: ClickState,
114 pub(crate) caret: CaretState,
115 pub(crate) popover_focus: PopoverFocusState,
116 pub(crate) tooltip: TooltipState,
117 pub(crate) scroll: ScrollState,
118 /// Runtime-managed toast notification queue and id allocator.
119 pub(crate) toast: ToastState,
120 /// App-declared keyboard shortcuts and their action names.
121 pub(crate) hotkeys: HotkeyState,
122 /// Visual prop animations, state envelopes, and animation pacing.
123 pub(crate) animation: AnimationState,
124
125 // ---- side maps (formerly El bookkeeping) ----
126 /// Layout-owned rect and key-index side maps.
127 pub(crate) layout: LayoutState,
128 /// Per-node interaction states derived from focused/pressed/hovered
129 /// trackers by [`Self::apply_to_state`].
130 pub(crate) node_states: NodeInteractionState,
131 /// Per-(node, type) widget state buckets. The library owns the
132 /// storage but never reads the values — they're for widget authors
133 /// to stash text-input carets, dropdown open flags, etc. Entries
134 /// are GC'd alongside envelopes/animations when a node leaves the
135 /// tree (see [`Self::tick_visual_animations`]).
136 widget_states: WidgetStateStore,
137 /// Last known keyboard modifier mask. Updated by the host runner
138 /// from winit's `ModifiersChanged`; pointer events stamp this
139 /// value into their `UiEvent.modifiers` so widgets that need to
140 /// detect Shift+click / Ctrl+drag can read it without separate
141 /// plumbing.
142 pub modifiers: KeyModifiers,
143}
144
145impl UiState {
146 pub fn new() -> Self {
147 Self::default()
148 }
149}
150
151impl Debug for UiState {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 f.debug_struct("UiState")
154 .field("pointer_pos", &self.pointer_pos)
155 .field("hovered", &self.hovered)
156 .field("pressed", &self.pressed)
157 .field("focused", &self.focused)
158 .field("focus_visible", &self.focus_visible)
159 .field("focus", &self.focus)
160 .field("popover_focus", &self.popover_focus)
161 .field("click", &self.click)
162 .field("caret", &self.caret)
163 .field("scroll", &self.scroll)
164 .field("toast", &self.toast)
165 .field("tooltip", &self.tooltip)
166 .field("hotkeys", &self.hotkeys)
167 .field("animation", &self.animation)
168 .field("layout", &self.layout)
169 .field("node_states", &self.node_states)
170 .field("modifiers", &self.modifiers)
171 .field("widget_states", &self.widget_states)
172 .finish()
173 }
174}
175
176#[cfg(test)]
177mod tests;