Skip to main content

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}