1
//! Hover state management for tracking mouse and touch hover history
2
//!
3
//! The HoverManager records hit test results for multiple input points
4
//! (mouse, touch, pen) over multiple frames to enable gesture detection
5
//! (like DragStart) that requires analyzing hover patterns over time
6
//! rather than just the current frame.
7

            
8
use std::collections::{BTreeMap, VecDeque};
9

            
10
use crate::hit_test::FullHitTest;
11

            
12
/// Maximum number of frames to keep in hover history
13
const MAX_HOVER_HISTORY: usize = 5;
14

            
15
/// Identifier for an input point (mouse, touch, pen, etc.)
16
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
17
pub enum InputPointId {
18
    /// Mouse cursor
19
    Mouse,
20
    /// Touch point with unique ID (from TouchEvent.id)
21
    Touch(u64),
22
}
23

            
24
/// Manages hover state history for all input points
25
///
26
/// Records hit test results for mouse and touch inputs over multiple frames:
27
/// - DragStart detection (requires movement threshold over multiple frames)
28
/// - Hover-over event detection
29
/// - Multi-touch gesture detection
30
/// - Input path analysis
31
///
32
/// The manager maintains a separate history for each active input point.
33
#[derive(Debug, Clone, PartialEq)]
34
pub struct HoverManager {
35
    /// Hit test history for each input point
36
    /// Each point has its own ring buffer of the last N frames
37
    hover_histories: BTreeMap<InputPointId, VecDeque<FullHitTest>>,
38
}
39

            
40
impl HoverManager {
41
    /// Create a new empty HoverManager
42
2507
    pub fn new() -> Self {
43
2507
        Self {
44
2507
            hover_histories: BTreeMap::new(),
45
2507
        }
46
2507
    }
47

            
48
    /// (input points, total history entries across all points). Used by
49
    /// `AZ_E2E_TEST` to watch for unbounded growth.
50
    pub fn debug_counts(&self) -> (usize, usize) {
51
        let points = self.hover_histories.len();
52
        let total: usize = self.hover_histories.values().map(|h| h.len()).sum();
53
        (points, total)
54
    }
55

            
56
    /// Push a new hit test result for a specific input point
57
    ///
58
    /// The most recent result is always at index 0 for that input point.
59
    /// If the history is full, the oldest frame is dropped.
60
490
    pub fn push_hit_test(&mut self, input_id: InputPointId, hit_test: FullHitTest) {
61
490
        let history = self
62
490
            .hover_histories
63
490
            .entry(input_id)
64
490
            .or_insert_with(|| VecDeque::with_capacity(MAX_HOVER_HISTORY));
65

            
66
        // Add to front (most recent)
67
490
        history.push_front(hit_test);
68

            
69
        // Remove oldest if we exceed the limit
70
490
        if history.len() > MAX_HOVER_HISTORY {
71
70
            history.pop_back();
72
420
        }
73
490
    }
74

            
75
    /// Remove an input point's history (e.g., when touch ends)
76
35
    pub fn remove_input_point(&mut self, input_id: &InputPointId) {
77
35
        self.hover_histories.remove(input_id);
78
35
    }
79

            
80
    /// Get the most recent hit test result for an input point
81
    ///
82
    /// Returns None if no hit tests have been recorded for this input point.
83
222
    pub fn get_current(&self, input_id: &InputPointId) -> Option<&FullHitTest> {
84
222
        self.hover_histories
85
222
            .get(input_id)
86
222
            .and_then(|history| history.front())
87
222
    }
88

            
89
    /// Get the most recent mouse cursor hit test (convenience method)
90
47
    pub fn get_current_mouse(&self) -> Option<&FullHitTest> {
91
47
        self.get_current(&InputPointId::Mouse)
92
47
    }
93

            
94
    /// Get the hit test result from N frames ago for an input point
95
    /// (0 = current frame)
96
    ///
97
    /// Returns None if the requested frame is not in history.
98
22
    pub fn get_frame(&self, input_id: &InputPointId, frames_ago: usize) -> Option<&FullHitTest> {
99
22
        self.hover_histories
100
22
            .get(input_id)
101
22
            .and_then(|history| history.get(frames_ago))
102
22
    }
103

            
104
    /// Get the entire hover history for an input point (most recent first)
105
    pub fn get_history(&self, input_id: &InputPointId) -> Option<&VecDeque<FullHitTest>> {
106
        self.hover_histories.get(input_id)
107
    }
108

            
109
    /// Get all currently tracked input points
110
35
    pub fn get_active_input_points(&self) -> Vec<InputPointId> {
111
35
        self.hover_histories.keys().copied().collect()
112
35
    }
113

            
114
    /// Get the number of frames in history for an input point
115
280
    pub fn frame_count(&self, input_id: &InputPointId) -> usize {
116
280
        self.hover_histories
117
280
            .get(input_id)
118
280
            .map(|h| h.len())
119
280
            .unwrap_or(0)
120
280
    }
121

            
122
    /// Clear all hover history for all input points
123
    pub fn clear(&mut self) {
124
        self.hover_histories.clear();
125
    }
126

            
127
    /// Clear history for a specific input point
128
    pub fn clear_input_point(&mut self, input_id: &InputPointId) {
129
        if let Some(history) = self.hover_histories.get_mut(input_id) {
130
            history.clear();
131
        }
132
    }
133

            
134
    /// Check if we have enough frames for gesture detection on an input point
135
    ///
136
    /// DragStart detection requires analyzing movement over multiple frames.
137
    /// This returns true if we have at least 2 frames of history.
138
105
    pub fn has_sufficient_history_for_gestures(&self, input_id: &InputPointId) -> bool {
139
105
        self.frame_count(input_id) >= 2
140
105
    }
141

            
142
    /// Check if any input point has enough history for gesture detection
143
70
    pub fn any_has_sufficient_history_for_gestures(&self) -> bool {
144
70
        self.hover_histories
145
70
            .iter()
146
70
            .any(|(_, history)| history.len() >= 2)
147
70
    }
148

            
149
    /// Get the deepest hovered node from the current mouse hit test.
150
    ///
151
    /// Returns the NodeId of the most specific (deepest in DOM tree) node
152
    /// that the mouse cursor is currently over, or None if not hovering anything.
153
    ///
154
    /// NOTE: Assumes single-DOM architecture (uses `DomId { inner: 0 }`).
155
12
    pub fn current_hover_node(&self) -> Option<azul_core::id::NodeId> {
156
12
        let current = self.get_current_mouse()?;
157
        let dom_id = azul_core::dom::DomId { inner: 0 };
158
        let ht = current.hovered_nodes.get(&dom_id)?;
159
        ht.regular_hit_test_nodes.keys().last().copied()
160
12
    }
161

            
162
    /// Get the deepest hovered node from the previous frame's mouse hit test.
163
    ///
164
    /// Returns the NodeId from one frame ago, or None if not hovering anything
165
    /// or no previous frame exists.
166
    ///
167
    /// NOTE: Assumes single-DOM architecture (uses `DomId { inner: 0 }`).
168
1
    pub fn previous_hover_node(&self) -> Option<azul_core::id::NodeId> {
169
1
        let history = self.hover_histories.get(&InputPointId::Mouse)?;
170
        let previous = history.get(1)?; // index 1 = one frame ago
171
        let dom_id = azul_core::dom::DomId { inner: 0 };
172
        let ht = previous.hovered_nodes.get(&dom_id)?;
173
        ht.regular_hit_test_nodes.keys().last().copied()
174
1
    }
175

            
176
    /// Remap NodeIds in all hover histories after DOM reconciliation.
177
    ///
178
    /// When the DOM is regenerated, NodeIds can change. This method updates
179
    /// all stored NodeIds in hover histories using the old→new mapping from
180
    /// reconciliation. Nodes not found in the map are removed from hit tests.
181
    pub fn remap_node_ids(
182
        &mut self,
183
        dom_id: azul_core::dom::DomId,
184
        node_id_map: &std::collections::BTreeMap<azul_core::id::NodeId, azul_core::id::NodeId>,
185
    ) {
186
        for history in self.hover_histories.values_mut() {
187
            for hit_test in history.iter_mut() {
188
                if let Some(ht) = hit_test.hovered_nodes.get_mut(&dom_id) {
189
                    remap_btreemap(&mut ht.regular_hit_test_nodes, node_id_map);
190
                    remap_btreemap(&mut ht.scroll_hit_test_nodes, node_id_map);
191
                    remap_btreemap(&mut ht.cursor_hit_test_nodes, node_id_map);
192

            
193
                    // Remap scrollbar_hit_test_nodes (ScrollbarHitId contains NodeId)
194
                    let old_sb: Vec<_> = ht.scrollbar_hit_test_nodes.keys().cloned().collect();
195
                    let mut new_sb = std::collections::BTreeMap::new();
196
                    for old_key in old_sb {
197
                        let new_key = remap_scrollbar_hit_id(&old_key, dom_id, node_id_map);
198
                        if let Some(item) = ht.scrollbar_hit_test_nodes.remove(&old_key) {
199
                            new_sb.insert(new_key, item);
200
                        }
201
                    }
202
                    ht.scrollbar_hit_test_nodes = new_sb;
203
                }
204
            }
205
        }
206
    }
207
}
208

            
209
impl Default for HoverManager {
210
    fn default() -> Self {
211
        Self::new()
212
    }
213
}
214

            
215
/// Remap all keys in a BTreeMap<NodeId, V> using the reconciliation map.
216
/// Entries whose old NodeId is not in the map are dropped.
217
fn remap_btreemap<V>(
218
    map: &mut std::collections::BTreeMap<azul_core::id::NodeId, V>,
219
    node_id_map: &std::collections::BTreeMap<azul_core::id::NodeId, azul_core::id::NodeId>,
220
) {
221
    let old_keys: Vec<_> = map.keys().cloned().collect();
222
    let mut new_map = std::collections::BTreeMap::new();
223
    for old_nid in old_keys {
224
        if let Some(&new_nid) = node_id_map.get(&old_nid) {
225
            if let Some(item) = map.remove(&old_nid) {
226
                new_map.insert(new_nid, item);
227
            }
228
        }
229
    }
230
    *map = new_map;
231
}
232

            
233
/// Remap a ScrollbarHitId's NodeId using the reconciliation map.
234
/// If the NodeId's DomId doesn't match, or the NodeId isn't in the map, returns unchanged.
235
fn remap_scrollbar_hit_id(
236
    id: &azul_core::hit_test::ScrollbarHitId,
237
    dom_id: azul_core::dom::DomId,
238
    node_id_map: &std::collections::BTreeMap<azul_core::id::NodeId, azul_core::id::NodeId>,
239
) -> azul_core::hit_test::ScrollbarHitId {
240
    use azul_core::hit_test::ScrollbarHitId;
241
    match id {
242
        ScrollbarHitId::VerticalTrack(d, n) if *d == dom_id => {
243
            ScrollbarHitId::VerticalTrack(*d, *node_id_map.get(n).unwrap_or(n))
244
        }
245
        ScrollbarHitId::VerticalThumb(d, n) if *d == dom_id => {
246
            ScrollbarHitId::VerticalThumb(*d, *node_id_map.get(n).unwrap_or(n))
247
        }
248
        ScrollbarHitId::HorizontalTrack(d, n) if *d == dom_id => {
249
            ScrollbarHitId::HorizontalTrack(*d, *node_id_map.get(n).unwrap_or(n))
250
        }
251
        ScrollbarHitId::HorizontalThumb(d, n) if *d == dom_id => {
252
            ScrollbarHitId::HorizontalThumb(*d, *node_id_map.get(n).unwrap_or(n))
253
        }
254
        other => other.clone(),
255
    }
256
}