aetna_core/state/interaction.rs
1//! Hover/press/focus interaction-state resolution.
2
3use web_time::Instant;
4
5use crate::event::UiTarget;
6use crate::tree::InteractionState;
7
8use super::UiState;
9
10impl UiState {
11 /// Resolved interaction state for `id`. Returns
12 /// [`InteractionState::Default`] when no tracker matches.
13 pub fn node_state(&self, id: &str) -> InteractionState {
14 self.node_states.nodes.get(id).copied().unwrap_or_default()
15 }
16
17 /// Rebuild the resolved per-node interaction-state side map from
18 /// the current focused/pressed/hovered trackers. Press wins over
19 /// Hover on a same-node match; Hover wins over Focus on a
20 /// same-node match (so a keyboard-auto-focused menu item still
21 /// gets its hover-lighten when the cursor is over it). Focus
22 /// applies on its own when the node isn't pressed or hovered.
23 ///
24 /// Press is gated on the pointer being currently over the
25 /// originally-pressed target — drag the cursor off and the press
26 /// visual decays, drag back on and it returns. Mirrors the HTML /
27 /// Tailwind `:active` behaviour: the visual reflects "would
28 /// release-here activate?", not "was pointer_down captured?".
29 /// Drag events still route to `pressed` regardless of pointer
30 /// position (see `runtime::pointer_moved`); this gating only
31 /// affects the visual envelope.
32 pub fn apply_to_state(&mut self) {
33 self.node_states.nodes.clear();
34 if let Some(target) = &self.focused {
35 self.node_states
36 .nodes
37 .insert(target.node_id.clone(), InteractionState::Focus);
38 }
39 let press_target = match (&self.pressed, &self.hovered) {
40 (Some(pressed), Some(hovered)) if pressed.node_id == hovered.node_id => Some(pressed),
41 _ => None,
42 };
43 if let Some(target) = &self.hovered {
44 let pressed_same = press_target
45 .map(|p| p.node_id == target.node_id)
46 .unwrap_or(false);
47 if !pressed_same {
48 self.node_states
49 .nodes
50 .insert(target.node_id.clone(), InteractionState::Hover);
51 }
52 }
53 if let Some(target) = press_target {
54 self.node_states
55 .nodes
56 .insert(target.node_id.clone(), InteractionState::Press);
57 }
58 }
59
60 /// Update the hovered target. Maintains the hover-stable timer
61 /// the tooltip pass reads — resets to `now` whenever the hovered
62 /// node changes (or hover is gained), clears when it goes away.
63 /// Also clears the per-session "tooltip dismissed by press" flag
64 /// so the next hover starts fresh.
65 ///
66 /// Returns `true` when the hovered identity actually changed —
67 /// used by [`crate::runtime::RunnerCore::pointer_moved`] to decide
68 /// whether the host should redraw (cursor moves *within* the
69 /// same hovered node are visual no-ops).
70 pub(crate) fn set_hovered(&mut self, new: Option<UiTarget>, now: Instant) -> bool {
71 let same = match (&self.hovered, &new) {
72 (Some(a), Some(b)) => a.node_id == b.node_id,
73 (None, None) => true,
74 _ => false,
75 };
76 if !same {
77 self.tooltip.hover_started_at = new.as_ref().map(|_| now);
78 self.tooltip.dismissed_for_hover = false;
79 }
80 self.hovered = new;
81 !same
82 }
83}