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