1use std::collections::{BTreeMap, VecDeque};
9
10use crate::hit_test::FullHitTest;
11
12const MAX_HOVER_HISTORY: usize = 5;
14
15fn deepest_node_across_doms(ht: &FullHitTest) -> Option<azul_core::dom::DomNodeId> {
21 for (dom_id, hit) in ht.hovered_nodes.iter().rev() {
22 if let Some(node_id) = hit.regular_hit_test_nodes.keys().last().copied() {
23 return Some(azul_core::dom::DomNodeId {
24 dom: *dom_id,
25 node: azul_core::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(
26 node_id,
27 )),
28 });
29 }
30 }
31 None
32}
33
34#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub enum InputPointId {
37 Mouse,
39 Touch(u64),
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct HoverManager {
54 hover_histories: BTreeMap<InputPointId, VecDeque<FullHitTest>>,
57}
58
59impl HoverManager {
60 #[must_use] pub const fn new() -> Self {
62 Self {
63 hover_histories: BTreeMap::new(),
64 }
65 }
66
67 #[must_use] pub fn debug_counts(&self) -> (usize, usize) {
70 let points = self.hover_histories.len();
71 let total: usize = self.hover_histories.values().map(VecDeque::len).sum();
72 (points, total)
73 }
74
75 pub fn push_hit_test(&mut self, input_id: InputPointId, hit_test: FullHitTest) {
80 let history = self
81 .hover_histories
82 .entry(input_id)
83 .or_insert_with(|| VecDeque::with_capacity(MAX_HOVER_HISTORY));
84
85 history.push_front(hit_test);
87
88 if history.len() > MAX_HOVER_HISTORY {
90 history.pop_back();
91 }
92 }
93
94 pub fn remove_input_point(&mut self, input_id: &InputPointId) {
96 self.hover_histories.remove(input_id);
97 }
98
99 #[must_use] pub fn get_current(&self, input_id: &InputPointId) -> Option<&FullHitTest> {
103 self.hover_histories
104 .get(input_id)
105 .and_then(|history| history.front())
106 }
107
108 #[must_use] pub fn get_current_mouse(&self) -> Option<&FullHitTest> {
110 self.get_current(&InputPointId::Mouse)
111 }
112
113 #[must_use] pub fn get_frame(&self, input_id: &InputPointId, frames_ago: usize) -> Option<&FullHitTest> {
118 self.hover_histories
119 .get(input_id)
120 .and_then(|history| history.get(frames_ago))
121 }
122
123 #[must_use] pub fn get_history(&self, input_id: &InputPointId) -> Option<&VecDeque<FullHitTest>> {
125 self.hover_histories.get(input_id)
126 }
127
128 #[must_use] pub fn get_active_input_points(&self) -> Vec<InputPointId> {
130 self.hover_histories.keys().copied().collect()
131 }
132
133 #[must_use] pub fn frame_count(&self, input_id: &InputPointId) -> usize {
135 self.hover_histories
136 .get(input_id)
137 .map_or(0, VecDeque::len)
138 }
139
140 pub fn purge_dom(&mut self, dom_id: &azul_core::dom::DomId) {
153 for history in self.hover_histories.values_mut() {
154 for frame in history.iter_mut() {
155 frame.hovered_nodes.remove(dom_id);
156 }
157 }
158 }
159
160 pub fn clear(&mut self) {
162 self.hover_histories.clear();
163 }
164
165 pub(crate) fn clear_input_point(&mut self, input_id: &InputPointId) {
167 if let Some(history) = self.hover_histories.get_mut(input_id) {
168 history.clear();
169 }
170 }
171
172 #[must_use] pub fn has_sufficient_history_for_gestures(&self, input_id: &InputPointId) -> bool {
177 self.frame_count(input_id) >= 2
178 }
179
180 #[must_use] pub fn any_has_sufficient_history_for_gestures(&self) -> bool {
182 self.hover_histories
183 .iter()
184 .any(|(_, history)| history.len() >= 2)
185 }
186
187 #[must_use] pub fn current_hover_node(&self) -> Option<azul_core::id::NodeId> {
194 let current = self.get_current_mouse()?;
195 let dom_id = azul_core::dom::DomId { inner: 0 };
196 let ht = current.hovered_nodes.get(&dom_id)?;
197 ht.regular_hit_test_nodes.keys().last().copied()
198 }
199
200 #[must_use] pub fn previous_hover_node(&self) -> Option<azul_core::id::NodeId> {
207 let history = self.hover_histories.get(&InputPointId::Mouse)?;
208 let previous = history.get(1)?; let dom_id = azul_core::dom::DomId { inner: 0 };
210 let ht = previous.hovered_nodes.get(&dom_id)?;
211 ht.regular_hit_test_nodes.keys().last().copied()
212 }
213
214 #[must_use] pub fn current_hover_node_full(&self) -> Option<azul_core::dom::DomNodeId> {
227 deepest_node_across_doms(self.get_current_mouse()?)
228 }
229
230 #[must_use] pub fn previous_hover_node_full(&self) -> Option<azul_core::dom::DomNodeId> {
232 let history = self.hover_histories.get(&InputPointId::Mouse)?;
233 deepest_node_across_doms(history.get(1)?)
234 }
235
236}
237
238impl crate::managers::NodeIdRemap for HoverManager {
239 fn remap_node_ids(&mut self, dom_id: azul_core::dom::DomId, map: &crate::managers::NodeIdMap) {
245 let node_id_map = map.as_btree_map();
246 for history in self.hover_histories.values_mut() {
247 for hit_test in history.iter_mut() {
248 if let Some(ht) = hit_test.hovered_nodes.get_mut(&dom_id) {
249 crate::managers::remap_keys(&mut ht.regular_hit_test_nodes, map);
250 crate::managers::remap_keys(&mut ht.scroll_hit_test_nodes, map);
251 crate::managers::remap_keys(&mut ht.cursor_hit_test_nodes, map);
252
253 let old_sb: Vec<_> = ht.scrollbar_hit_test_nodes.keys().copied().collect();
255 let mut new_sb = BTreeMap::new();
256 for old_key in old_sb {
257 let Some(new_key) = remap_scrollbar_hit_id(&old_key, dom_id, node_id_map)
258 else {
259 ht.scrollbar_hit_test_nodes.remove(&old_key);
261 continue;
262 };
263 if let Some(item) = ht.scrollbar_hit_test_nodes.remove(&old_key) {
264 new_sb.insert(new_key, item);
265 }
266 }
267 ht.scrollbar_hit_test_nodes = new_sb;
268 }
269 }
270 }
271 }
272}
273
274impl Default for HoverManager {
275 fn default() -> Self {
276 Self::new()
277 }
278}
279
280fn remap_scrollbar_hit_id(
284 id: &azul_core::hit_test::ScrollbarHitId,
285 dom_id: azul_core::dom::DomId,
286 node_id_map: &BTreeMap<azul_core::id::NodeId, azul_core::id::NodeId>,
287) -> Option<azul_core::hit_test::ScrollbarHitId> {
288 use azul_core::hit_test::ScrollbarHitId;
289 Some(match id {
290 ScrollbarHitId::VerticalTrack(d, n) if *d == dom_id => {
291 ScrollbarHitId::VerticalTrack(*d, *node_id_map.get(n)?)
292 }
293 ScrollbarHitId::VerticalThumb(d, n) if *d == dom_id => {
294 ScrollbarHitId::VerticalThumb(*d, *node_id_map.get(n)?)
295 }
296 ScrollbarHitId::HorizontalTrack(d, n) if *d == dom_id => {
297 ScrollbarHitId::HorizontalTrack(*d, *node_id_map.get(n)?)
298 }
299 ScrollbarHitId::HorizontalThumb(d, n) if *d == dom_id => {
300 ScrollbarHitId::HorizontalThumb(*d, *node_id_map.get(n)?)
301 }
302 other => *other,
303 })
304}
305
306#[cfg(test)]
307mod autotest_generated {
308 use azul_core::{
309 dom::{DomId, DomNodeId, ScrollbarOrientation},
310 geom::LogicalPosition,
311 hit_test::{
312 CursorHitTestItem, CursorType, HitTest, HitTestItem, OverflowingScrollNode,
313 ScrollHitTestItem, ScrollbarHitId, ScrollbarHitTestItem,
314 },
315 id::NodeId,
316 styled_dom::NodeHierarchyItemId,
317 };
318
319 use super::*;
320 use crate::managers::{NodeIdMap, NodeIdRemap};
321
322 fn hit_item(depth: u32) -> HitTestItem {
325 HitTestItem {
326 point_in_viewport: LogicalPosition::zero(),
327 point_relative_to_item: LogicalPosition::zero(),
328 is_focusable: false,
329 is_virtual_view_hit: None,
330 hit_depth: depth,
331 }
332 }
333
334 fn scroll_item() -> ScrollHitTestItem {
335 ScrollHitTestItem {
336 point_in_viewport: LogicalPosition::zero(),
337 point_relative_to_item: LogicalPosition::zero(),
338 scroll_node: OverflowingScrollNode::default(),
339 }
340 }
341
342 fn cursor_item() -> CursorHitTestItem {
343 CursorHitTestItem {
344 cursor_type: CursorType::Text,
345 hit_depth: 0,
346 point_in_viewport: LogicalPosition::zero(),
347 }
348 }
349
350 fn scrollbar_item() -> ScrollbarHitTestItem {
351 ScrollbarHitTestItem {
352 point_in_viewport: LogicalPosition::zero(),
353 point_relative_to_item: LogicalPosition::zero(),
354 orientation: ScrollbarOrientation::Vertical,
355 }
356 }
357
358 fn dom(inner: usize) -> DomId {
359 DomId { inner }
360 }
361
362 fn hits(entries: &[(usize, &[usize])]) -> FullHitTest {
365 let mut full = FullHitTest::empty(None);
366 for (dom_inner, nodes) in entries {
367 let ht = full
368 .hovered_nodes
369 .entry(dom(*dom_inner))
370 .or_insert_with(HitTest::empty);
371 for n in *nodes {
372 ht.regular_hit_test_nodes
373 .insert(NodeId::new(*n), hit_item(0));
374 }
375 }
376 full
377 }
378
379 fn dom_node(dom_inner: usize, node: usize) -> DomNodeId {
381 DomNodeId {
382 dom: dom(dom_inner),
383 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node))),
384 }
385 }
386
387 fn mouse_history(frames: Vec<FullHitTest>) -> HoverManager {
390 let mut hm = HoverManager::new();
391 for f in frames {
392 hm.push_hit_test(InputPointId::Mouse, f);
393 }
394 hm
395 }
396
397 #[test]
400 fn deepest_node_across_doms_empty_returns_none() {
401 assert_eq!(deepest_node_across_doms(&FullHitTest::empty(None)), None);
402 }
403
404 #[test]
405 fn deepest_node_across_doms_uses_nodeid_order_not_insertion_order() {
406 let ht = hits(&[(0, &[2, 9, 7])]);
408 assert_eq!(deepest_node_across_doms(&ht), Some(dom_node(0, 9)));
409 }
410
411 #[test]
412 fn deepest_node_across_doms_prefers_highest_dom_even_if_its_node_is_shallower() {
413 let ht = hits(&[(0, &[99]), (3, &[1])]);
415 assert_eq!(deepest_node_across_doms(&ht), Some(dom_node(3, 1)));
416 }
417
418 #[test]
419 fn deepest_node_across_doms_skips_dom_with_no_regular_hits() {
420 let mut ht = hits(&[(0, &[4])]);
423 let mut empty_regular = HitTest::empty();
424 empty_regular
425 .scroll_hit_test_nodes
426 .insert(NodeId::new(1), scroll_item());
427 empty_regular
428 .cursor_hit_test_nodes
429 .insert(NodeId::new(1), cursor_item());
430 empty_regular.scrollbar_hit_test_nodes.insert(
431 ScrollbarHitId::VerticalThumb(dom(5), NodeId::new(1)),
432 scrollbar_item(),
433 );
434 ht.hovered_nodes.insert(dom(5), empty_regular);
435
436 assert_eq!(deepest_node_across_doms(&ht), Some(dom_node(0, 4)));
437 }
438
439 #[test]
440 fn deepest_node_across_doms_all_doms_empty_returns_none() {
441 let mut ht = FullHitTest::empty(None);
442 ht.hovered_nodes.insert(dom(0), HitTest::empty());
443 ht.hovered_nodes.insert(dom(usize::MAX), HitTest::empty());
444 assert_eq!(deepest_node_across_doms(&ht), None);
445 }
446
447 #[test]
448 fn deepest_node_across_doms_extreme_ids_survive_the_nodeid_encoding() {
449 let max_node = usize::MAX - 1;
452 let ht = hits(&[(usize::MAX, &[0, max_node])]);
453 let got = deepest_node_across_doms(&ht).expect("a hit exists");
454 assert_eq!(got.dom, dom(usize::MAX));
455 assert_eq!(got.node.into_crate_internal(), Some(NodeId::new(max_node)));
457 }
458
459 #[test]
462 fn new_manager_is_empty_and_every_getter_is_none_or_zero() {
463 let hm = HoverManager::new();
464 let mouse = InputPointId::Mouse;
465
466 assert_eq!(hm.debug_counts(), (0, 0));
467 assert!(hm.get_active_input_points().is_empty());
468 assert!(hm.get_current(&mouse).is_none());
469 assert!(hm.get_current_mouse().is_none());
470 assert!(hm.get_history(&mouse).is_none());
471 assert_eq!(hm.frame_count(&mouse), 0);
472 assert!(!hm.has_sufficient_history_for_gestures(&mouse));
473 assert!(!hm.any_has_sufficient_history_for_gestures());
474 assert!(hm.current_hover_node().is_none());
475 assert!(hm.previous_hover_node().is_none());
476 assert!(hm.current_hover_node_full().is_none());
477 assert!(hm.previous_hover_node_full().is_none());
478 assert!(hm.get_frame(&mouse, 0).is_none());
480 assert!(hm.get_frame(&mouse, usize::MAX).is_none());
481 }
482
483 #[test]
484 fn default_matches_new() {
485 assert_eq!(HoverManager::default(), HoverManager::new());
486 }
487
488 #[test]
491 fn push_hit_test_index_zero_is_the_newest_frame() {
492 let hm = mouse_history(vec![hits(&[(0, &[1])]), hits(&[(0, &[2])])]);
493
494 assert_eq!(hm.frame_count(&InputPointId::Mouse), 2);
495 assert_eq!(hm.get_frame(&InputPointId::Mouse, 0), Some(&hits(&[(0, &[2])])));
496 assert_eq!(hm.get_frame(&InputPointId::Mouse, 1), Some(&hits(&[(0, &[1])])));
497 assert_eq!(hm.get_current_mouse(), Some(&hits(&[(0, &[2])])));
498 }
499
500 #[test]
501 fn push_hit_test_ring_buffer_never_exceeds_max_hover_history() {
502 let mut hm = HoverManager::new();
503 for i in 0..1000_usize {
504 hm.push_hit_test(InputPointId::Mouse, hits(&[(0, &[i])]));
505 }
506
507 assert_eq!(hm.frame_count(&InputPointId::Mouse), MAX_HOVER_HISTORY);
508 assert_eq!(hm.debug_counts(), (1, MAX_HOVER_HISTORY));
509 for ago in 0..MAX_HOVER_HISTORY {
511 assert_eq!(
512 hm.get_frame(&InputPointId::Mouse, ago),
513 Some(&hits(&[(0, &[999 - ago])])),
514 "frame {ago} frames ago"
515 );
516 }
517 assert!(hm.get_frame(&InputPointId::Mouse, MAX_HOVER_HISTORY).is_none());
519 }
520
521 #[test]
522 fn get_frame_out_of_range_index_returns_none_without_overflow() {
523 let hm = mouse_history(vec![hits(&[(0, &[1])])]);
524 let mouse = InputPointId::Mouse;
525
526 assert!(hm.get_frame(&mouse, 0).is_some());
527 assert!(hm.get_frame(&mouse, 1).is_none());
528 assert!(hm.get_frame(&mouse, usize::MAX).is_none());
529 assert!(hm.get_frame(&mouse, usize::MAX / 2).is_none());
530 assert!(hm.get_frame(&InputPointId::Touch(u64::MAX), usize::MAX).is_none());
532 }
533
534 #[test]
535 fn touch_ids_at_u64_boundaries_are_distinct_histories() {
536 let mut hm = HoverManager::new();
537 hm.push_hit_test(InputPointId::Touch(u64::MIN), hits(&[(0, &[1])]));
538 hm.push_hit_test(InputPointId::Touch(u64::MAX), hits(&[(0, &[2])]));
539 hm.push_hit_test(InputPointId::Mouse, hits(&[(0, &[3])]));
540
541 assert_eq!(hm.debug_counts(), (3, 3));
542 assert_eq!(
543 hm.get_current(&InputPointId::Touch(u64::MIN)),
544 Some(&hits(&[(0, &[1])]))
545 );
546 assert_eq!(
547 hm.get_current(&InputPointId::Touch(u64::MAX)),
548 Some(&hits(&[(0, &[2])]))
549 );
550 assert_eq!(hm.get_current_mouse(), Some(&hits(&[(0, &[3])])));
551 assert_eq!(
553 hm.get_active_input_points(),
554 vec![
555 InputPointId::Mouse,
556 InputPointId::Touch(0),
557 InputPointId::Touch(u64::MAX),
558 ]
559 );
560 }
561
562 #[test]
563 fn debug_counts_stays_bounded_under_a_flood_of_points_and_frames() {
564 let mut hm = HoverManager::new();
565 for point in 0..100_u64 {
566 for frame in 0..50_usize {
567 hm.push_hit_test(InputPointId::Touch(point), hits(&[(0, &[frame])]));
568 }
569 }
570 assert_eq!(hm.debug_counts(), (100, 100 * MAX_HOVER_HISTORY));
572 }
573
574 #[test]
575 fn push_hit_test_stores_the_value_verbatim_including_focused_node() {
576 let focused = dom_node(0, 7);
577 let mut ht = FullHitTest::empty(Some(focused));
578 ht.hovered_nodes.insert(dom(0), HitTest::empty());
579
580 let mut hm = HoverManager::new();
581 hm.push_hit_test(InputPointId::Mouse, ht.clone());
582
583 assert_eq!(hm.get_current_mouse(), Some(&ht));
584 assert_eq!(
585 hm.get_current_mouse().map(|h| h.focused_node),
586 Some(Some(focused).into())
587 );
588 assert!(hm.current_hover_node().is_none());
590 assert!(hm.current_hover_node_full().is_none());
591 }
592
593 #[test]
594 fn get_history_returns_all_frames_newest_first() {
595 let hm = mouse_history(vec![
596 hits(&[(0, &[1])]),
597 hits(&[(0, &[2])]),
598 hits(&[(0, &[3])]),
599 ]);
600 let history = hm.get_history(&InputPointId::Mouse).expect("history exists");
601
602 assert_eq!(history.len(), 3);
603 assert_eq!(history[0], hits(&[(0, &[3])]));
604 assert_eq!(history[2], hits(&[(0, &[1])]));
605 assert!(hm.get_history(&InputPointId::Touch(0)).is_none());
606 }
607
608 #[test]
611 fn remove_absent_input_point_is_a_noop() {
612 let mut hm = mouse_history(vec![hits(&[(0, &[1])])]);
613 let before = hm.clone();
614
615 hm.remove_input_point(&InputPointId::Touch(0));
616 hm.remove_input_point(&InputPointId::Touch(u64::MAX));
617
618 assert_eq!(hm, before);
619 }
620
621 #[test]
622 fn remove_input_point_only_drops_the_target_point() {
623 let mut hm = HoverManager::new();
624 hm.push_hit_test(InputPointId::Mouse, hits(&[(0, &[1])]));
625 hm.push_hit_test(InputPointId::Touch(3), hits(&[(0, &[2])]));
626
627 hm.remove_input_point(&InputPointId::Touch(3));
628
629 assert_eq!(hm.debug_counts(), (1, 1));
630 assert_eq!(hm.get_active_input_points(), vec![InputPointId::Mouse]);
631 assert!(hm.get_current(&InputPointId::Touch(3)).is_none());
632 assert_eq!(hm.frame_count(&InputPointId::Touch(3)), 0);
633 assert!(hm.get_current_mouse().is_some());
634 }
635
636 #[test]
637 fn remove_then_push_restarts_the_history_from_scratch() {
638 let mut hm = mouse_history(vec![hits(&[(0, &[1])]), hits(&[(0, &[2])])]);
639 assert!(hm.has_sufficient_history_for_gestures(&InputPointId::Mouse));
640
641 hm.remove_input_point(&InputPointId::Mouse);
642 hm.push_hit_test(InputPointId::Mouse, hits(&[(0, &[3])]));
643
644 assert_eq!(hm.frame_count(&InputPointId::Mouse), 1);
645 assert!(!hm.has_sufficient_history_for_gestures(&InputPointId::Mouse));
646 assert!(hm.previous_hover_node().is_none());
647 }
648
649 #[test]
650 fn clear_drops_every_point() {
651 let mut hm = HoverManager::new();
652 hm.push_hit_test(InputPointId::Mouse, hits(&[(0, &[1])]));
653 hm.push_hit_test(InputPointId::Touch(9), hits(&[(0, &[2])]));
654
655 hm.clear();
656
657 assert_eq!(hm, HoverManager::new());
658 assert_eq!(hm.debug_counts(), (0, 0));
659 assert!(!hm.any_has_sufficient_history_for_gestures());
660 hm.clear();
662 assert_eq!(hm.debug_counts(), (0, 0));
663 }
664
665 #[test]
666 fn clear_input_point_empties_history_but_keeps_the_point_registered() {
667 let mut hm = mouse_history(vec![hits(&[(0, &[1])]), hits(&[(0, &[2])])]);
668
669 hm.clear_input_point(&InputPointId::Mouse);
670
671 assert_eq!(hm.debug_counts(), (1, 0));
673 assert_eq!(hm.get_active_input_points(), vec![InputPointId::Mouse]);
674 assert_eq!(hm.frame_count(&InputPointId::Mouse), 0);
675 assert!(hm.get_current_mouse().is_none());
676 assert!(hm.get_history(&InputPointId::Mouse).is_some());
677 assert!(!hm.has_sufficient_history_for_gestures(&InputPointId::Mouse));
678 assert!(!hm.any_has_sufficient_history_for_gestures());
679 assert!(hm.current_hover_node().is_none());
680 assert!(hm.previous_hover_node().is_none());
681 }
682
683 #[test]
684 fn clear_input_point_on_an_absent_point_is_a_noop() {
685 let mut hm = mouse_history(vec![hits(&[(0, &[1])])]);
686 let before = hm.clone();
687
688 hm.clear_input_point(&InputPointId::Touch(u64::MAX));
689
690 assert_eq!(hm, before);
691 }
692
693 #[test]
696 fn has_sufficient_history_needs_at_least_two_frames() {
697 let mouse = InputPointId::Mouse;
698 let mut hm = HoverManager::new();
699 assert!(!hm.has_sufficient_history_for_gestures(&mouse));
700
701 hm.push_hit_test(mouse, hits(&[(0, &[1])]));
702 assert!(!hm.has_sufficient_history_for_gestures(&mouse), "1 frame is not enough");
703
704 hm.push_hit_test(mouse, hits(&[(0, &[2])]));
705 assert!(hm.has_sufficient_history_for_gestures(&mouse), "2 frames is the threshold");
706
707 for i in 0..10 {
708 hm.push_hit_test(mouse, hits(&[(0, &[i])]));
709 }
710 assert!(hm.has_sufficient_history_for_gestures(&mouse), "stays true when saturated");
711 }
712
713 #[test]
714 fn any_has_sufficient_history_is_an_or_across_points() {
715 let mut hm = HoverManager::new();
716 for id in [
718 InputPointId::Mouse,
719 InputPointId::Touch(0),
720 InputPointId::Touch(u64::MAX),
721 ] {
722 hm.push_hit_test(id, hits(&[(0, &[1])]));
723 }
724 assert!(!hm.any_has_sufficient_history_for_gestures());
725
726 hm.push_hit_test(InputPointId::Touch(u64::MAX), hits(&[(0, &[2])]));
728 assert!(hm.any_has_sufficient_history_for_gestures());
729
730 hm.clear_input_point(&InputPointId::Touch(u64::MAX));
732 assert!(!hm.any_has_sufficient_history_for_gestures());
733 }
734
735 #[test]
738 fn current_hover_node_returns_the_deepest_node_of_dom_zero() {
739 let hm = mouse_history(vec![hits(&[(0, &[3, 8, 5])])]);
740
741 assert_eq!(hm.current_hover_node(), Some(NodeId::new(8)));
742 assert_eq!(hm.current_hover_node_full(), Some(dom_node(0, 8)));
744 }
745
746 #[test]
747 fn current_hover_node_ignores_non_zero_doms_but_full_does_not() {
748 let hm = mouse_history(vec![hits(&[(2, &[4])])]);
750
751 assert_eq!(hm.current_hover_node(), None);
752 assert_eq!(hm.current_hover_node_full(), Some(dom_node(2, 4)));
753 }
754
755 #[test]
756 fn current_hover_node_full_prefers_the_front_most_child_dom() {
757 let hm = mouse_history(vec![hits(&[(0, &[9]), (1, &[2])])]);
758
759 assert_eq!(hm.current_hover_node(), Some(NodeId::new(9)));
761 assert_eq!(hm.current_hover_node_full(), Some(dom_node(1, 2)));
763 }
764
765 #[test]
766 fn previous_hover_node_is_none_until_a_second_frame_exists() {
767 let hm = mouse_history(vec![hits(&[(0, &[1])])]);
768
769 assert_eq!(hm.current_hover_node(), Some(NodeId::new(1)));
770 assert_eq!(hm.previous_hover_node(), None);
771 assert_eq!(hm.previous_hover_node_full(), None);
772 }
773
774 #[test]
775 fn previous_hover_node_reads_frame_one_not_the_oldest_frame() {
776 let hm = mouse_history((0..6).map(|i| hits(&[(0, &[i])])).collect());
778
779 assert_eq!(hm.current_hover_node(), Some(NodeId::new(5)));
780 assert_eq!(hm.previous_hover_node(), Some(NodeId::new(4)));
781 assert_eq!(hm.previous_hover_node_full(), Some(dom_node(0, 4)));
782 }
783
784 #[test]
785 fn previous_hover_node_full_sees_child_doms_of_the_previous_frame() {
786 let hm = mouse_history(vec![hits(&[(0, &[1]), (7, &[3])]), hits(&[(0, &[2])])]);
787
788 assert_eq!(hm.previous_hover_node(), Some(NodeId::new(1)));
789 assert_eq!(hm.previous_hover_node_full(), Some(dom_node(7, 3)));
790 assert_eq!(hm.current_hover_node_full(), Some(dom_node(0, 2)));
791 }
792
793 #[test]
794 fn hover_node_getters_are_none_when_the_frame_hit_nothing() {
795 let hm = mouse_history(vec![FullHitTest::empty(None), FullHitTest::empty(None)]);
796
797 assert!(hm.current_hover_node().is_none());
798 assert!(hm.previous_hover_node().is_none());
799 assert!(hm.current_hover_node_full().is_none());
800 assert!(hm.previous_hover_node_full().is_none());
801 }
802
803 #[test]
804 fn hover_node_getters_ignore_touch_history_entirely() {
805 let mut hm = HoverManager::new();
806 hm.push_hit_test(InputPointId::Touch(1), hits(&[(0, &[5])]));
807 hm.push_hit_test(InputPointId::Touch(1), hits(&[(0, &[6])]));
808
809 assert!(hm.current_hover_node().is_none());
810 assert!(hm.previous_hover_node().is_none());
811 assert!(hm.current_hover_node_full().is_none());
812 assert!(hm.previous_hover_node_full().is_none());
813 assert!(hm.any_has_sufficient_history_for_gestures());
814 }
815
816 #[test]
819 fn purge_dom_removes_that_dom_from_every_frame_of_every_point() {
820 let mut hm = HoverManager::new();
821 for id in [InputPointId::Mouse, InputPointId::Touch(2)] {
822 hm.push_hit_test(id, hits(&[(0, &[1]), (1, &[2])]));
823 hm.push_hit_test(id, hits(&[(0, &[3]), (1, &[4])]));
824 }
825
826 hm.purge_dom(&dom(1));
827
828 assert_eq!(hm.debug_counts(), (2, 4));
830 for id in [InputPointId::Mouse, InputPointId::Touch(2)] {
831 let history = hm.get_history(&id).expect("history exists");
832 for frame in history {
833 assert!(!frame.hovered_nodes.contains_key(&dom(1)));
834 assert!(frame.hovered_nodes.contains_key(&dom(0)));
835 }
836 }
837 assert_eq!(hm.current_hover_node_full(), Some(dom_node(0, 3)));
838 assert_eq!(hm.previous_hover_node_full(), Some(dom_node(0, 1)));
839 }
840
841 #[test]
842 fn purge_dom_zero_leaves_child_dom_hits_intact() {
843 let mut hm = mouse_history(vec![hits(&[(0, &[1]), (4, &[2])])]);
844
845 hm.purge_dom(&dom(0));
846
847 assert_eq!(hm.current_hover_node(), None);
849 assert_eq!(hm.current_hover_node_full(), Some(dom_node(4, 2)));
850 assert_eq!(hm.frame_count(&InputPointId::Mouse), 1);
851 }
852
853 #[test]
854 fn purge_absent_or_extreme_dom_id_is_a_noop() {
855 let mut hm = mouse_history(vec![hits(&[(0, &[1])])]);
856 let before = hm.clone();
857
858 hm.purge_dom(&dom(9));
859 hm.purge_dom(&dom(usize::MAX));
860 assert_eq!(hm, before);
861
862 let mut empty = HoverManager::new();
864 empty.purge_dom(&dom(0));
865 assert_eq!(empty, HoverManager::new());
866 }
867
868 #[test]
869 fn purge_dom_twice_is_idempotent() {
870 let mut hm = mouse_history(vec![hits(&[(0, &[1]), (1, &[2])])]);
871
872 hm.purge_dom(&dom(1));
873 let once = hm.clone();
874 hm.purge_dom(&dom(1));
875
876 assert_eq!(hm, once);
877 }
878
879 fn sb_map(pairs: &[(usize, usize)]) -> BTreeMap<NodeId, NodeId> {
882 pairs
883 .iter()
884 .map(|(o, n)| (NodeId::new(*o), NodeId::new(*n)))
885 .collect()
886 }
887
888 #[test]
889 fn remap_scrollbar_hit_id_rewrites_every_variant_of_the_target_dom() {
890 let map = sb_map(&[(1, 42)]);
891 let d = dom(0);
892 let old = NodeId::new(1);
893 let new = NodeId::new(42);
894
895 assert_eq!(
896 remap_scrollbar_hit_id(&ScrollbarHitId::VerticalTrack(d, old), d, &map),
897 Some(ScrollbarHitId::VerticalTrack(d, new))
898 );
899 assert_eq!(
900 remap_scrollbar_hit_id(&ScrollbarHitId::VerticalThumb(d, old), d, &map),
901 Some(ScrollbarHitId::VerticalThumb(d, new))
902 );
903 assert_eq!(
904 remap_scrollbar_hit_id(&ScrollbarHitId::HorizontalTrack(d, old), d, &map),
905 Some(ScrollbarHitId::HorizontalTrack(d, new))
906 );
907 assert_eq!(
908 remap_scrollbar_hit_id(&ScrollbarHitId::HorizontalThumb(d, old), d, &map),
909 Some(ScrollbarHitId::HorizontalThumb(d, new))
910 );
911 }
912
913 #[test]
914 fn remap_scrollbar_hit_id_drops_unmounted_nodes() {
915 let map = sb_map(&[(1, 42)]);
916 let d = dom(0);
917 let unmounted = ScrollbarHitId::VerticalThumb(d, NodeId::new(2));
919
920 assert_eq!(remap_scrollbar_hit_id(&unmounted, d, &map), None);
921 let hit = ScrollbarHitId::VerticalThumb(d, NodeId::new(1));
923 assert_eq!(remap_scrollbar_hit_id(&hit, d, &BTreeMap::new()), None);
924 }
925
926 #[test]
927 fn remap_scrollbar_hit_id_passes_other_doms_through_untouched() {
928 let map = sb_map(&[(1, 42)]);
931 let other = ScrollbarHitId::HorizontalTrack(dom(1), NodeId::new(1));
932
933 assert_eq!(remap_scrollbar_hit_id(&other, dom(0), &map), Some(other));
934 assert_eq!(
936 remap_scrollbar_hit_id(&other, dom(0), &BTreeMap::new()),
937 Some(other)
938 );
939 }
940
941 #[test]
942 fn remap_scrollbar_hit_id_handles_extreme_ids() {
943 let big = usize::MAX - 1;
944 let map = sb_map(&[(big, 0)]);
945 let d = dom(usize::MAX);
946
947 assert_eq!(
948 remap_scrollbar_hit_id(&ScrollbarHitId::VerticalTrack(d, NodeId::new(big)), d, &map),
949 Some(ScrollbarHitId::VerticalTrack(d, NodeId::ZERO))
950 );
951 }
952
953 fn all_maps_hit(dom_inner: usize, node: usize) -> FullHitTest {
957 let mut full = FullHitTest::empty(None);
958 let mut ht = HitTest::empty();
959 ht.regular_hit_test_nodes
960 .insert(NodeId::new(node), hit_item(0));
961 ht.scroll_hit_test_nodes
962 .insert(NodeId::new(node), scroll_item());
963 ht.cursor_hit_test_nodes
964 .insert(NodeId::new(node), cursor_item());
965 ht.scrollbar_hit_test_nodes.insert(
966 ScrollbarHitId::VerticalThumb(dom(dom_inner), NodeId::new(node)),
967 scrollbar_item(),
968 );
969 full.hovered_nodes.insert(dom(dom_inner), ht);
970 full
971 }
972
973 #[test]
974 fn remap_node_ids_rewrites_all_four_hit_maps() {
975 let mut hm = mouse_history(vec![all_maps_hit(0, 3)]);
976
977 hm.remap_node_ids(dom(0), &NodeIdMap::from_pairs([(NodeId::new(3), NodeId::new(11))]));
978
979 let ht = &hm.get_current_mouse().expect("frame exists").hovered_nodes[&dom(0)];
980 assert_eq!(
981 ht.regular_hit_test_nodes.keys().copied().collect::<Vec<_>>(),
982 vec![NodeId::new(11)]
983 );
984 assert_eq!(
985 ht.scroll_hit_test_nodes.keys().copied().collect::<Vec<_>>(),
986 vec![NodeId::new(11)]
987 );
988 assert_eq!(
989 ht.cursor_hit_test_nodes.keys().copied().collect::<Vec<_>>(),
990 vec![NodeId::new(11)]
991 );
992 assert_eq!(
993 ht.scrollbar_hit_test_nodes.keys().copied().collect::<Vec<_>>(),
994 vec![ScrollbarHitId::VerticalThumb(dom(0), NodeId::new(11))]
995 );
996 assert_eq!(hm.current_hover_node(), Some(NodeId::new(11)));
997 }
998
999 #[test]
1000 fn remap_node_ids_with_an_empty_map_drops_every_hit_of_that_dom() {
1001 let mut hm = mouse_history(vec![all_maps_hit(0, 3)]);
1002
1003 hm.remap_node_ids(dom(0), &NodeIdMap::default());
1005
1006 let ht = &hm.get_current_mouse().expect("frame exists").hovered_nodes[&dom(0)];
1007 assert!(ht.regular_hit_test_nodes.is_empty());
1008 assert!(ht.scroll_hit_test_nodes.is_empty());
1009 assert!(ht.cursor_hit_test_nodes.is_empty());
1010 assert!(ht.scrollbar_hit_test_nodes.is_empty());
1011 assert_eq!(hm.current_hover_node(), None);
1012 assert!(hm
1014 .get_current_mouse()
1015 .expect("frame exists")
1016 .hovered_nodes
1017 .contains_key(&dom(0)));
1018 }
1019
1020 #[test]
1021 fn remap_node_ids_drops_unmounted_but_keeps_survivors() {
1022 let mut hm = mouse_history(vec![hits(&[(0, &[1, 4])])]);
1024
1025 hm.remap_node_ids(dom(0), &NodeIdMap::from_pairs([(NodeId::new(4), NodeId::ZERO)]));
1026
1027 let ht = &hm.get_current_mouse().expect("frame exists").hovered_nodes[&dom(0)];
1028 assert_eq!(
1029 ht.regular_hit_test_nodes.keys().copied().collect::<Vec<_>>(),
1030 vec![NodeId::ZERO]
1031 );
1032 assert_eq!(hm.current_hover_node(), Some(NodeId::ZERO));
1033 }
1034
1035 #[test]
1036 fn remap_node_ids_swap_does_not_lose_or_alias_entries() {
1037 let mut full = FullHitTest::empty(None);
1040 let mut ht = HitTest::empty();
1041 ht.regular_hit_test_nodes
1042 .insert(NodeId::new(1), hit_item(10));
1043 ht.regular_hit_test_nodes
1044 .insert(NodeId::new(2), hit_item(20));
1045 full.hovered_nodes.insert(dom(0), ht);
1046 let mut hm = mouse_history(vec![full]);
1047
1048 hm.remap_node_ids(
1049 dom(0),
1050 &NodeIdMap::from_pairs([
1051 (NodeId::new(1), NodeId::new(2)),
1052 (NodeId::new(2), NodeId::new(1)),
1053 ]),
1054 );
1055
1056 let ht = &hm.get_current_mouse().expect("frame exists").hovered_nodes[&dom(0)];
1057 assert_eq!(ht.regular_hit_test_nodes.len(), 2);
1058 assert_eq!(ht.regular_hit_test_nodes[&NodeId::new(2)].hit_depth, 10);
1059 assert_eq!(ht.regular_hit_test_nodes[&NodeId::new(1)].hit_depth, 20);
1060 }
1061
1062 #[test]
1063 fn remap_node_ids_can_change_which_node_is_deepest() {
1064 let mut hm = mouse_history(vec![hits(&[(0, &[3, 7])])]);
1067 assert_eq!(hm.current_hover_node(), Some(NodeId::new(7)));
1068
1069 hm.remap_node_ids(
1070 dom(0),
1071 &NodeIdMap::from_pairs([
1072 (NodeId::new(3), NodeId::new(9)),
1073 (NodeId::new(7), NodeId::new(2)),
1074 ]),
1075 );
1076
1077 assert_eq!(hm.current_hover_node(), Some(NodeId::new(9)));
1078 assert_eq!(hm.current_hover_node_full(), Some(dom_node(0, 9)));
1079 }
1080
1081 #[test]
1082 fn remap_node_ids_leaves_other_doms_alone() {
1083 let mut hm = mouse_history(vec![hits(&[(0, &[1]), (1, &[1])])]);
1084
1085 hm.remap_node_ids(dom(0), &NodeIdMap::from_pairs([(NodeId::new(1), NodeId::new(5))]));
1086
1087 let frame = hm.get_current_mouse().expect("frame exists");
1088 assert_eq!(
1089 frame.hovered_nodes[&dom(0)]
1090 .regular_hit_test_nodes
1091 .keys()
1092 .copied()
1093 .collect::<Vec<_>>(),
1094 vec![NodeId::new(5)],
1095 "DOM 0 is remapped"
1096 );
1097 assert_eq!(
1098 frame.hovered_nodes[&dom(1)]
1099 .regular_hit_test_nodes
1100 .keys()
1101 .copied()
1102 .collect::<Vec<_>>(),
1103 vec![NodeId::new(1)],
1104 "DOM 1 must be untouched by DOM 0's reconciliation"
1105 );
1106 }
1107
1108 #[test]
1109 fn remap_node_ids_keeps_foreign_dom_scrollbar_ids_stored_under_the_target_dom() {
1110 let mut full = FullHitTest::empty(None);
1113 let mut ht = HitTest::empty();
1114 ht.scrollbar_hit_test_nodes.insert(
1115 ScrollbarHitId::VerticalTrack(dom(1), NodeId::new(1)),
1116 scrollbar_item(),
1117 );
1118 ht.scrollbar_hit_test_nodes.insert(
1119 ScrollbarHitId::VerticalTrack(dom(0), NodeId::new(1)),
1120 scrollbar_item(),
1121 );
1122 full.hovered_nodes.insert(dom(0), ht);
1123 let mut hm = mouse_history(vec![full]);
1124
1125 hm.remap_node_ids(dom(0), &NodeIdMap::from_pairs([(NodeId::new(1), NodeId::new(8))]));
1126
1127 let keys: Vec<_> = hm.get_current_mouse().expect("frame exists").hovered_nodes[&dom(0)]
1128 .scrollbar_hit_test_nodes
1129 .keys()
1130 .copied()
1131 .collect();
1132 assert!(
1133 keys.contains(&ScrollbarHitId::VerticalTrack(dom(1), NodeId::new(1))),
1134 "foreign-DOM scrollbar id must survive unchanged, got {keys:?}"
1135 );
1136 assert!(
1137 keys.contains(&ScrollbarHitId::VerticalTrack(dom(0), NodeId::new(8))),
1138 "target-DOM scrollbar id must be rewritten, got {keys:?}"
1139 );
1140 assert_eq!(keys.len(), 2);
1141 }
1142
1143 #[test]
1144 fn remap_node_ids_applies_to_every_frame_and_every_input_point() {
1145 let mut hm = HoverManager::new();
1146 for id in [InputPointId::Mouse, InputPointId::Touch(1)] {
1147 hm.push_hit_test(id, hits(&[(0, &[2])]));
1148 hm.push_hit_test(id, hits(&[(0, &[2])]));
1149 }
1150
1151 hm.remap_node_ids(dom(0), &NodeIdMap::from_pairs([(NodeId::new(2), NodeId::new(6))]));
1152
1153 for id in [InputPointId::Mouse, InputPointId::Touch(1)] {
1154 for frame in hm.get_history(&id).expect("history exists") {
1155 assert_eq!(
1156 frame.hovered_nodes[&dom(0)]
1157 .regular_hit_test_nodes
1158 .keys()
1159 .copied()
1160 .collect::<Vec<_>>(),
1161 vec![NodeId::new(6)]
1162 );
1163 }
1164 }
1165 assert_eq!(hm.previous_hover_node(), Some(NodeId::new(6)));
1166 }
1167
1168 #[test]
1169 fn remap_node_ids_on_an_empty_manager_or_unknown_dom_does_not_panic() {
1170 let mut empty = HoverManager::new();
1171 empty.remap_node_ids(dom(usize::MAX), &NodeIdMap::default());
1172 assert_eq!(empty, HoverManager::new());
1173
1174 let mut hm = mouse_history(vec![hits(&[(0, &[1])])]);
1175 let before = hm.clone();
1176 hm.remap_node_ids(dom(3), &NodeIdMap::from_pairs([(NodeId::new(1), NodeId::new(2))]));
1178 assert_eq!(hm, before);
1179 }
1180
1181 #[test]
1182 fn remap_node_ids_identity_map_is_idempotent() {
1183 let mut hm = mouse_history(vec![all_maps_hit(0, 3)]);
1184 let before = hm.clone();
1185 let identity = NodeIdMap::from_pairs([(NodeId::new(3), NodeId::new(3))]);
1186
1187 hm.remap_node_ids(dom(0), &identity);
1188 assert_eq!(hm, before, "identity remap must not change anything");
1189
1190 hm.remap_node_ids(dom(0), &identity);
1191 assert_eq!(hm, before, "and applying it twice must not either");
1192 }
1193
1194 #[test]
1197 fn clone_is_equal_and_independent_of_the_original() {
1198 let mut hm = mouse_history(vec![hits(&[(0, &[1])])]);
1199 let snapshot = hm.clone();
1200 assert_eq!(hm, snapshot);
1201
1202 hm.push_hit_test(InputPointId::Mouse, hits(&[(0, &[2])]));
1203
1204 assert_ne!(hm, snapshot, "the clone must not observe later pushes");
1205 assert_eq!(snapshot.frame_count(&InputPointId::Mouse), 1);
1206 assert_eq!(hm.frame_count(&InputPointId::Mouse), 2);
1207 }
1208
1209 #[test]
1210 fn debug_counts_agrees_with_frame_count_and_active_points() {
1211 let mut hm = HoverManager::new();
1212 hm.push_hit_test(InputPointId::Mouse, hits(&[(0, &[1])]));
1213 hm.push_hit_test(InputPointId::Touch(7), hits(&[(0, &[1])]));
1214 hm.push_hit_test(InputPointId::Touch(7), hits(&[(0, &[2])]));
1215
1216 let (points, total) = hm.debug_counts();
1217 let active = hm.get_active_input_points();
1218 assert_eq!(points, active.len());
1219 assert_eq!(
1220 total,
1221 active.iter().map(|id| hm.frame_count(id)).sum::<usize>()
1222 );
1223 assert_eq!((points, total), (2, 3));
1224 }
1225}