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;