Skip to main content

damascene_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 `damascene_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 camera;
17mod click;
18mod cursor;
19mod focus;
20mod interaction;
21mod keyboard;
22pub(crate) mod query;
23mod scroll;
24mod selection;
25mod toast;
26mod types;
27mod widget_state;
28
29use std::fmt::Debug;
30// `web_time::Instant` is API-identical to `std::time::Instant` on
31// native and uses `performance.now()` on wasm32 — std's `Instant::now()`
32// panics in the browser because there is no monotonic clock there.
33
34use crate::event::{KeyModifiers, PointerButton, PointerKind, UiTarget};
35
36pub use types::{
37    AnimationMode, EnvelopeKind, LONG_PRESS_DELAY, ScrollMetrics, ThumbDrag, WidgetState,
38};
39pub(crate) use types::{
40    ScrollAnchor, SelectionDrag, SelectionDragGranularity, TOUCH_DRAG_THRESHOLD, TouchGestureState,
41    VirtualAnchor, caret_blink_alpha_for,
42};
43
44use types::{
45    AnimationState, CaretState, ClickState, FocusState, HotkeyState, LayoutState,
46    NodeInteractionState, PopoverFocusState, ScrollState, SelectionState, ToastState, TooltipState,
47    WidgetStateStore,
48};
49
50/// Internal UI state — interaction trackers + the side maps the library
51/// writes during layout / state-apply / animation-tick passes. Owned by
52/// the renderer; the host doesn't interact with this directly.
53///
54/// The side maps replace the per-node bookkeeping fields that used to
55/// live on `El` (computed rect, interaction state, envelope amounts).
56/// Keying is by `El::computed_id`, the path-shaped string assigned by
57/// the layout pass.
58#[derive(Default)]
59pub struct UiState {
60    /// Last known pointer position in **logical** pixels. `None` until
61    /// the pointer enters the window.
62    pub pointer_pos: Option<(f32, f32)>,
63    /// Modality of the most recent pointer ingest. Used to stamp
64    /// emitted [`crate::event::UiEvent::pointer_kind`] and to gate
65    /// hover-only behavior on touch. Defaults to
66    /// [`PointerKind::Mouse`] until the first ingest, which matches
67    /// historical behavior on hosts without touch.
68    pub pointer_kind: PointerKind,
69    /// Touch-gesture state machine. Tracks whether the active touch
70    /// contact is awaiting threshold disambiguation
71    /// ([`TouchGestureState::Pending`]), has committed to scrolling
72    /// ([`TouchGestureState::Scrolling`]), or is idle / committed to
73    /// drag ([`TouchGestureState::None`]). Mouse and pen pointers
74    /// stay at `None`.
75    pub(crate) touch_gesture: TouchGestureState,
76    pub hovered: Option<UiTarget>,
77    pub pressed: Option<UiTarget>,
78    /// Secondary / middle button down-target, kept on a separate
79    /// channel so it doesn't fight the primary `pressed` envelope or
80    /// move focus. Carries the button kind so `pointer_up` knows which
81    /// click variant to emit. Cleared by `pointer_up` matching the
82    /// same button.
83    pub(crate) pressed_secondary: Option<(UiTarget, PointerButton)>,
84    /// URL of the text-link run under a primary press, when present.
85    /// Set by `pointer_down` from `hit_test::link_at`; consumed by
86    /// `pointer_up`, which emits `UiEventKind::LinkActivated` only
87    /// when the up position lands on the same link URL — same
88    /// press-then-confirm contract as a normal `Click`.
89    pub(crate) pressed_link: Option<String>,
90    /// URL of the text-link run currently under the pointer (no
91    /// button press required). Tracked by `pointer_moved` so the
92    /// cursor resolver can return [`crate::cursor::Cursor::Pointer`]
93    /// over links without the text leaves having to be keyed
94    /// hover-test targets. Cleared on `pointer_left`.
95    pub(crate) hovered_link: Option<String>,
96    pub focused: Option<UiTarget>,
97    /// Whether the focused element should display its focus ring.
98    /// Tracks the web platform's `:focus-visible` heuristic: keyboard
99    /// focus (Tab, arrow-nav) raises the flag; pointer-down clears it.
100    /// Widgets where the ring belongs even on click — text inputs and
101    /// text areas, where the ring communicates "this surface is now
102    /// active" beyond the caret alone — opt back in via
103    /// [`crate::tree::El::always_show_focus_ring`].
104    pub focus_visible: bool,
105    pub(crate) focus: FocusState,
106    /// Mirror of the application's current
107    /// [`crate::selection::Selection`]. Set by the host runner once
108    /// per frame from [`crate::event::App::selection`]; read by the
109    /// painter to draw highlight bands and by the selection manager
110    /// to know what's currently active when extending a drag.
111    pub current_selection: crate::selection::Selection,
112    /// Internal selection traversal and drag state.
113    pub(crate) selection: SelectionState,
114    pub(crate) click: ClickState,
115    pub(crate) caret: CaretState,
116    pub(crate) popover_focus: PopoverFocusState,
117    pub(crate) tooltip: TooltipState,
118    pub(crate) scroll: ScrollState,
119    /// Per-`Scene3D`-node camera poses (current + goal + spring velocity),
120    /// keyed by `computed_id`. The library-owned interactive camera; see
121    /// [`camera`](self::camera).
122    pub(crate) cameras: camera::CameraStore,
123    /// Per-`Scene3D`-node depth maps captured by the backend, keyed by
124    /// `computed_id`. The draw-op pass reads these to occlude scene-anchored
125    /// labels behind geometry; the backend populates them a frame late via
126    /// [`scene_depth_mut`](Self::scene_depth_mut). See
127    /// [`SceneDepthMap`](crate::scene::SceneDepthMap).
128    scene_depth: std::collections::HashMap<String, crate::scene::SceneDepthMap>,
129    /// Scatter point under the cursor, picked by the draw-op pass from the
130    /// hover-label path and stored a frame late (like [`scene_depth`]). The
131    /// app reads it via [`BuildCx::hovered_scene_point`] to drive its own
132    /// detail UI on hover. `None` when no scene point is hovered.
133    ///
134    /// [`scene_depth`]: Self::scene_depth
135    /// [`BuildCx::hovered_scene_point`]: crate::event::BuildCx::hovered_scene_point
136    hovered_scene_point: Option<crate::scene::ScenePointPick>,
137    /// Runtime-managed toast notification queue and id allocator.
138    pub(crate) toast: ToastState,
139    /// App-declared keyboard shortcuts and their action names.
140    pub(crate) hotkeys: HotkeyState,
141    /// Visual prop animations, state envelopes, and animation pacing.
142    pub(crate) animation: AnimationState,
143
144    // ---- side maps (formerly El bookkeeping) ----
145    /// Layout-owned rect and key-index side maps.
146    pub(crate) layout: LayoutState,
147    /// Per-node interaction states derived from focused/pressed/hovered
148    /// trackers by [`Self::apply_to_state`].
149    pub(crate) node_states: NodeInteractionState,
150    /// Per-(node, type) widget state buckets. The library owns the
151    /// storage but never reads the values — they're for widget authors
152    /// to stash text-input carets, dropdown open flags, etc. Entries
153    /// are GC'd alongside envelopes/animations when a node leaves the
154    /// tree (see [`Self::tick_visual_animations`]).
155    widget_states: WidgetStateStore,
156    /// Last known keyboard modifier mask. Updated by the host runner
157    /// from winit's `ModifiersChanged`; pointer events stamp this
158    /// value into their `UiEvent.modifiers` so widgets that need to
159    /// detect Shift+click / Ctrl+drag can read it without separate
160    /// plumbing.
161    pub modifiers: KeyModifiers,
162}
163
164impl UiState {
165    pub fn new() -> Self {
166        Self::default()
167    }
168
169    /// The captured depth map for a `Scene3D` node, if the backend has
170    /// produced one. `None` until the first map arrives — callers treat
171    /// that as "occlude all labels" (see [`crate::scene::SceneDepthMap`]).
172    pub fn scene_depth(&self, id: &str) -> Option<&crate::scene::SceneDepthMap> {
173        self.scene_depth.get(id)
174    }
175
176    /// Mutable access to the per-node scene depth maps, for the backend to
177    /// install freshly read-back maps and GC nodes that have left the tree.
178    pub fn scene_depth_mut(
179        &mut self,
180    ) -> &mut std::collections::HashMap<String, crate::scene::SceneDepthMap> {
181        &mut self.scene_depth
182    }
183
184    /// Iterate the captured `(id, map)` scene depth maps. Exposed for
185    /// backend integration tests; app code reads a single map via
186    /// [`scene_depth`](Self::scene_depth).
187    #[doc(hidden)]
188    pub fn scene_depth_maps(&self) -> impl Iterator<Item = (&str, &crate::scene::SceneDepthMap)> {
189        self.scene_depth.iter().map(|(k, v)| (k.as_str(), v))
190    }
191
192    /// The scatter point currently under the cursor, if any. Picked by the
193    /// draw-op pass and refreshed each prepare (a frame late). App code reads
194    /// this through [`BuildCx::hovered_scene_point`](crate::event::BuildCx::hovered_scene_point).
195    pub fn hovered_scene_point(&self) -> Option<&crate::scene::ScenePointPick> {
196        self.hovered_scene_point.as_ref()
197    }
198
199    /// Install the hovered-point pick computed by the draw-op pass. Called by
200    /// the runtime right after `draw_ops`; `None` clears a stale pick.
201    pub(crate) fn set_hovered_scene_point(&mut self, pick: Option<crate::scene::ScenePointPick>) {
202        self.hovered_scene_point = pick;
203    }
204}
205
206impl Debug for UiState {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        f.debug_struct("UiState")
209            .field("pointer_pos", &self.pointer_pos)
210            .field("hovered", &self.hovered)
211            .field("pressed", &self.pressed)
212            .field("focused", &self.focused)
213            .field("focus_visible", &self.focus_visible)
214            .field("focus", &self.focus)
215            .field("popover_focus", &self.popover_focus)
216            .field("click", &self.click)
217            .field("caret", &self.caret)
218            .field("scroll", &self.scroll)
219            .field("toast", &self.toast)
220            .field("tooltip", &self.tooltip)
221            .field("hotkeys", &self.hotkeys)
222            .field("animation", &self.animation)
223            .field("layout", &self.layout)
224            .field("node_states", &self.node_states)
225            .field("modifiers", &self.modifiers)
226            .field("widget_states", &self.widget_states)
227            .finish()
228    }
229}
230
231#[cfg(test)]
232mod tests;