Skip to main content

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;