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 #[must_use] pub fn hover_node_full_for(
243 &self,
244 input_id: &InputPointId,
245 ) -> Option<azul_core::dom::DomNodeId> {
246 deepest_node_across_doms(self.get_current(input_id)?)
247 }
248}
249
250impl crate::managers::NodeIdRemap for HoverManager {
251 fn remap_node_ids(&mut self, dom_id: azul_core::dom::DomId, map: &crate::managers::NodeIdMap) {
257 let node_id_map = map.as_btree_map();
258 for history in self.hover_histories.values_mut() {
259 for hit_test in history.iter_mut() {
260 if let Some(ht) = hit_test.hovered_nodes.get_mut(&dom_id) {
261 crate::managers::remap_keys(&mut ht.regular_hit_test_nodes, map);
262 crate::managers::remap_keys(&mut ht.scroll_hit_test_nodes, map);
263 crate::managers::remap_keys(&mut ht.cursor_hit_test_nodes, map);
264
265 let old_sb: Vec<_> = ht.scrollbar_hit_test_nodes.keys().copied().collect();
267 let mut new_sb = BTreeMap::new();
268 for old_key in old_sb {
269 let Some(new_key) = remap_scrollbar_hit_id(&old_key, dom_id, node_id_map)
270 else {
271 ht.scrollbar_hit_test_nodes.remove(&old_key);
273 continue;
274 };
275 if let Some(item) = ht.scrollbar_hit_test_nodes.remove(&old_key) {
276 new_sb.insert(new_key, item);
277 }
278 }
279 ht.scrollbar_hit_test_nodes = new_sb;
280 }
281 }
282 }
283 }
284}
285
286impl Default for HoverManager {
287 fn default() -> Self {
288 Self::new()
289 }
290}
291
292fn remap_scrollbar_hit_id(
296 id: &azul_core::hit_test::ScrollbarHitId,
297 dom_id: azul_core::dom::DomId,
298 node_id_map: &BTreeMap<azul_core::id::NodeId, azul_core::id::NodeId>,
299) -> Option<azul_core::hit_test::ScrollbarHitId> {
300 use azul_core::hit_test::ScrollbarHitId;
301 Some(match id {
302 ScrollbarHitId::VerticalTrack(d, n) if *d == dom_id => {
303 ScrollbarHitId::VerticalTrack(*d, *node_id_map.get(n)?)
304 }
305 ScrollbarHitId::VerticalThumb(d, n) if *d == dom_id => {
306 ScrollbarHitId::VerticalThumb(*d, *node_id_map.get(n)?)
307 }
308 ScrollbarHitId::HorizontalTrack(d, n) if *d == dom_id => {
309 ScrollbarHitId::HorizontalTrack(*d, *node_id_map.get(n)?)
310 }
311 ScrollbarHitId::HorizontalThumb(d, n) if *d == dom_id => {
312 ScrollbarHitId::HorizontalThumb(*d, *node_id_map.get(n)?)
313 }
314 other => *other,
315 })
316}
317
318#[cfg(test)]
319mod autotest_generated {
320 use azul_core::{
321 dom::{DomId, DomNodeId, ScrollbarOrientation},
322 geom::LogicalPosition,
323 hit_test::{
324 CursorHitTestItem, CursorType, HitTest, HitTestItem, OverflowingScrollNode,
325 ScrollHitTestItem, ScrollbarHitId, ScrollbarHitTestItem,
326 },
327 id::NodeId,
328 styled_dom::NodeHierarchyItemId,
329 };
330
331 use super::*;
332 use crate::managers::{NodeIdMap, NodeIdRemap};
333
334 fn hit_item(depth: u32) -> HitTestItem {
337 HitTestItem {
338 point_in_viewport: LogicalPosition::zero(),
339 point_relative_to_item: LogicalPosition::zero(),
340 is_focusable: false,
341 is_virtual_view_hit: None,
342 hit_depth: depth,
343 }
344 }
345
346 fn scroll_item() -> ScrollHitTestItem {
347 ScrollHitTestItem {
348 point_in_viewport: LogicalPosition::zero(),
349 point_relative_to_item: LogicalPosition::zero(),
350 scroll_node: OverflowingScrollNode::default(),
351 }
352 }
353
354 fn cursor_item() -> CursorHitTestItem {
355 CursorHitTestItem {
356 cursor_type: CursorType::Text,
357 hit_depth: 0,
358 point_in_viewport: LogicalPosition::zero(),
359 }
360 }
361
362 fn scrollbar_item() -> ScrollbarHitTestItem {
363 ScrollbarHitTestItem {
364 point_in_viewport: LogicalPosition::zero(),
365 point_relative_to_item: LogicalPosition::zero(),
366 orientation: ScrollbarOrientation::Vertical,
367 }
368 }
369
370 fn dom(inner: usize) -> DomId {
371 DomId { inner }
372 }
373
374 fn hits(entries: &[(usize, &[usize])]) -> FullHitTest {
377 let mut full = FullHitTest::empty(None);
378 for (dom_inner, nodes) in entries {
379 let ht = full
380 .hovered_nodes
381 .entry(dom(*dom_inner))
382 .or_insert_with(HitTest::empty);
383 for n in *nodes {
384 ht.regular_hit_test_nodes
385 .insert(NodeId::new(*n), hit_item(0));
386 }
387 }
388 full
389 }
390
391 fn dom_node(dom_inner: usize, node: usize) -> DomNodeId {
393 DomNodeId {
394 dom: dom(dom_inner),
395 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node))),
396 }
397 }
398
399 fn mouse_history(frames: Vec<FullHitTest>) -> HoverManager {
402 let mut hm = HoverManager::new();
403 for f in frames {
404 hm.push_hit_test(InputPointId::Mouse, f);
405 }
406 hm
407 }
408
409 #[test]
412 fn deepest_node_across_doms_empty_returns_none() {
413 assert_eq!(deepest_node_across_doms(&FullHitTest::empty(None)), None);
414 }
415
416 #[test]
417 fn deepest_node_across_doms_uses_nodeid_order_not_insertion_order() {
418 let ht = hits(&[(0, &[2, 9, 7])]);
420 assert_eq!(deepest_node_across_doms(&ht), Some(dom_node(0, 9)));
421 }
422
423 #[test]
424 fn deepest_node_across_doms_prefers_highest_dom_even_if_its_node_is_shallower() {
425 let ht = hits(&[(0, &[99]), (3, &[1])]);
427 assert_eq!(deepest_node_across_doms(&ht), Some(dom_node(3, 1)));
428 }
429
430 #[test]
431 fn deepest_node_across_doms_skips_dom_with_no_regular_hits() {
432 let mut ht = hits(&[(0, &[4])]);
435 let mut empty_regular = HitTest::empty();
436 empty_regular
437 .scroll_hit_test_nodes
438 .insert(NodeId::new(1), scroll_item());
439 empty_regular
440 .cursor_hit_test_nodes
441 .insert(NodeId::new(1), cursor_item());
442 empty_regular.scrollbar_hit_test_nodes.insert(
443 ScrollbarHitId::VerticalThumb(dom(5), NodeId::new(1)),
444 scrollbar_item(),
445 );
446 ht.hovered_nodes.insert(dom(5), empty_regular);
447
448 assert_eq!(deepest_node_across_doms(&ht), Some(dom_node(0, 4)));
449 }
450
451 #[test]
452 fn deepest_node_across_doms_all_doms_empty_returns_none() {
453 let mut ht = FullHitTest::empty(None);
454 ht.hovered_nodes.insert(dom(0), HitTest::empty());
455 ht.hovered_nodes.insert(dom(usize::MAX), HitTest::empty());
456 assert_eq!(deepest_node_across_doms(&ht), None);
457 }
458
459 #[test]
460 fn deepest_node_across_doms_extreme_ids_survive_the_nodeid_encoding() {
461 let max_node = usize::MAX - 1;
464 let ht = hits(&[(usize::MAX, &[0, max_node])]);
465 let got = deepest_node_across_doms(&ht).expect("a hit exists");
466 assert_eq!(got.dom, dom(usize::MAX));
467 assert_eq!(got.node.into_crate_internal(), Some(NodeId::new(max_node)));
469 }
470
471 #[test]
474 fn new_manager_is_empty_and_every_getter_is_none_or_zero() {
475 let hm = HoverManager::new();
476 let mouse = InputPointId::Mouse;
477
478 assert_eq!(hm.debug_counts(), (0, 0));
479 assert!(hm.get_active_input_points().is_empty());
480 assert!(hm.get_current(&mouse).is_none());
481 assert!(hm.get_current_mouse().is_none());
482 assert!(hm.get_history(&mouse).is_none());
483 assert_eq!(hm.frame_count(&mouse), 0);
484 assert!(!hm.has_sufficient_history_for_gestures(&mouse));
485 assert!(!hm.any_has_sufficient_history_for_gestures());
486 assert!(hm.current_hover_node().is_none());
487 assert!(hm.previous_hover_node().is_none());
488 assert!(hm.current_hover_node_full().is_none());
489 assert!(hm.previous_hover_node_full().is_none());
490 assert!(hm.get_frame(&mouse, 0).is_none());
492 assert!(hm.get_frame(&mouse, usize::MAX).is_none());
493 }
494
495 #[test]
496 fn default_matches_new() {
497 assert_eq!(HoverManager::default(), HoverManager::new());
498 }
499
500 #[test]
503 fn push_hit_test_index_zero_is_the_newest_frame() {
504 let hm = mouse_history(vec![hits(&[(0, &[1])]), hits(&[(0, &[2])])]);
505
506 assert_eq!(hm.frame_count(&InputPointId::Mouse), 2);
507 assert_eq!(hm.get_frame(&InputPointId::Mouse, 0), Some(&hits(&[(0, &[2])])));
508 assert_eq!(hm.get_frame(&InputPointId::Mouse, 1), Some(&hits(&[(0, &[1])])));
509 assert_eq!(hm.get_current_mouse(), Some(&hits(&[(0, &[2])])));
510 }
511
512 #[test]
513 fn push_hit_test_ring_buffer_never_exceeds_max_hover_history() {
514 let mut hm = HoverManager::new();
515 for i in 0..1000_usize {
516 hm.push_hit_test(InputPointId::Mouse, hits(&[(0, &[i])]));
517 }
518
519 assert_eq!(hm.frame_count(&InputPointId::Mouse), MAX_HOVER_HISTORY);
520 assert_eq!(hm.debug_counts(), (1, MAX_HOVER_HISTORY));
521 for ago in 0..MAX_HOVER_HISTORY {
523 assert_eq!(
524 hm.get_frame(&InputPointId::Mouse, ago),
525 Some(&hits(&[(0, &[999 - ago])])),
526 "frame {ago} frames ago"
527 );
528 }
529 assert!(hm.get_frame(&InputPointId::Mouse, MAX_HOVER_HISTORY).is_none());
531 }
532
533 #[test]
534 fn get_frame_out_of_range_index_returns_none_without_overflow() {
535 let hm = mouse_history(vec![hits(&[(0, &[1])])]);
536 let mouse = InputPointId::Mouse;
537
538 assert!(hm.get_frame(&mouse, 0).is_some());
539 assert!(hm.get_frame(&mouse, 1).is_none());
540 assert!(hm.get_frame(&mouse, usize::MAX).is_none());
541 assert!(hm.get_frame(&mouse, usize::MAX / 2).is_none());
542 assert!(hm.get_frame(&InputPointId::Touch(u64::MAX), usize::MAX).is_none());
544 }
545
546 #[test]
547 fn touch_ids_at_u64_boundaries_are_distinct_histories() {
548 let mut hm = HoverManager::new();
549 hm.push_hit_test(InputPointId::Touch(u64::MIN), hits(&[(0, &[1])]));
550 hm.push_hit_test(InputPointId::Touch(u64::MAX), hits(&[(0, &[2])]));
551 hm.push_hit_test(InputPointId::Mouse, hits(&[(0, &[3])]));
552
553 assert_eq!(hm.debug_counts(), (3, 3));
554 assert_eq!(
555 hm.get_current(&InputPointId::Touch(u64::MIN)),
556 Some(&hits(&[(0, &[1])]))
557 );
558 assert_eq!(
559 hm.get_current(&InputPointId::Touch(u64::MAX)),
560 Some(&hits(&[(0, &[2])]))
561 );
562 assert_eq!(hm.get_current_mouse(), Some(&hits(&[(0, &[3])])));
563 assert_eq!(
565 hm.get_active_input_points(),
566 vec![
567 InputPointId::Mouse,
568 InputPointId::Touch(0),
569 InputPointId::Touch(u64::MAX),
570 ]
571 );
572 }
573
574 #[test]
575 fn debug_counts_stays_bounded_under_a_flood_of_points_and_frames() {
576 let mut hm = HoverManager::new();
577 for point in 0..100_u64 {
578 for frame in 0..50_usize {
579 hm.push_hit_test(InputPointId::Touch(point), hits(&[(0, &[frame])]));
580 }
581 }
582 assert_eq!(hm.debug_counts(), (100, 100 * MAX_HOVER_HISTORY));
584 }
585
586 #[test]
587 fn push_hit_test_stores_the_value_verbatim_including_focused_node() {
588 let focused = dom_node(0, 7);
589 let mut ht = FullHitTest::empty(Some(focused));
590 ht.hovered_nodes.insert(dom(0), HitTest::empty());
591
592 let mut hm = HoverManager::new();
593 hm.push_hit_test(InputPointId::Mouse, ht.clone());
594
595 assert_eq!(hm.get_current_mouse(), Some(&ht));
596 assert_eq!(
597 hm.get_current_mouse().map(|h| h.focused_node),
598 Some(Some(focused).into())
599 );
600 assert!(hm.current_hover_node().is_none());
602 assert!(hm.current_hover_node_full().is_none());
603 }
604
605 #[test]
606 fn get_history_returns_all_frames_newest_first() {
607 let hm = mouse_history(vec![
608 hits(&[(0, &[1])]),
609 hits(&[(0, &[2])]),
610 hits(&[(0, &[3])]),
611 ]);
612 let history = hm.get_history(&InputPointId::Mouse).expect("history exists");
613
614 assert_eq!(history.len(), 3);
615 assert_eq!(history[0], hits(&[(0, &[3])]));
616 assert_eq!(history[2], hits(&[(0, &[1])]));
617 assert!(hm.get_history(&InputPointId::Touch(0)).is_none());
618 }
619
620 #[test]
623 fn remove_absent_input_point_is_a_noop() {
624 let mut hm = mouse_history(vec![hits(&[(0, &[1])])]);
625 let before = hm.clone();
626
627 hm.remove_input_point(&InputPointId::Touch(0));
628 hm.remove_input_point(&InputPointId::Touch(u64::MAX));
629
630 assert_eq!(hm, before);
631 }
632
633 #[test]
634 fn remove_input_point_only_drops_the_target_point() {
635 let mut hm = HoverManager::new();
636 hm.push_hit_test(InputPointId::Mouse, hits(&[(0, &[1])]));
637 hm.push_hit_test(InputPointId::Touch(3), hits(&[(0, &[2])]));
638
639 hm.remove_input_point(&InputPointId::Touch(3));
640
641 assert_eq!(hm.debug_counts(), (1, 1));
642 assert_eq!(hm.get_active_input_points(), vec![InputPointId::Mouse]);
643 assert!(hm.get_current(&InputPointId::Touch(3)).is_none());
644 assert_eq!(hm.frame_count(&InputPointId::Touch(3)), 0);
645 assert!(hm.get_current_mouse().is_some());
646 }
647
648 #[test]
649 fn remove_then_push_restarts_the_history_from_scratch() {
650 let mut hm = mouse_history(vec![hits(&[(0, &[1])]), hits(&[(0, &[2])])]);
651 assert!(hm.has_sufficient_history_for_gestures(&InputPointId::Mouse));
652
653 hm.remove_input_point(&InputPointId::Mouse);
654 hm.push_hit_test(InputPointId::Mouse, hits(&[(0, &[3])]));
655
656 assert_eq!(hm.frame_count(&InputPointId::Mouse), 1);
657 assert!(!hm.has_sufficient_history_for_gestures(&InputPointId::Mouse));
658 assert!(hm.previous_hover_node().is_none());
659 }
660
661 #[test]
662 fn clear_drops_every_point() {
663 let mut hm = HoverManager::new();
664 hm.push_hit_test(InputPointId::Mouse, hits(&[(0, &[1])]));
665 hm.push_hit_test(InputPointId::Touch(9), hits(&[(0, &[2])]));
666
667 hm.clear();
668
669 assert_eq!(hm, HoverManager::new());
670 assert_eq!(hm.debug_counts(), (0, 0));
671 assert!(!hm.any_has_sufficient_history_for_gestures());
672 hm.clear();
674 assert_eq!(hm.debug_counts(), (0, 0));
675 }
676
677 #[test]
678 fn clear_input_point_empties_history_but_keeps_the_point_registered() {
679 let mut hm = mouse_history(vec![hits(&[(0, &[1])]), hits(&[(0, &[2])])]);
680
681 hm.clear_input_point(&InputPointId::Mouse);
682
683 assert_eq!(hm.debug_counts(), (1, 0));
685 assert_eq!(hm.get_active_input_points(), vec![InputPointId::Mouse]);
686 assert_eq!(hm.frame_count(&InputPointId::Mouse), 0);
687 assert!(hm.get_current_mouse().is_none());
688 assert!(hm.get_history(&InputPointId::Mouse).is_some());
689 assert!(!hm.has_sufficient_history_for_gestures(&InputPointId::Mouse));
690 assert!(!hm.any_has_sufficient_history_for_gestures());
691 assert!(hm.current_hover_node().is_none());
692 assert!(hm.previous_hover_node().is_none());
693 }
694
695 #[test]
696 fn clear_input_point_on_an_absent_point_is_a_noop() {
697 let mut hm = mouse_history(vec![hits(&[(0, &[1])])]);
698 let before = hm.clone();
699
700 hm.clear_input_point(&InputPointId::Touch(u64::MAX));
701
702 assert_eq!(hm, before);
703 }
704
705 #[test]
708 fn has_sufficient_history_needs_at_least_two_frames() {
709 let mouse = InputPointId::Mouse;
710 let mut hm = HoverManager::new();
711 assert!(!hm.has_sufficient_history_for_gestures(&mouse));
712
713 hm.push_hit_test(mouse, hits(&[(0, &[1])]));
714 assert!(!hm.has_sufficient_history_for_gestures(&mouse), "1 frame is not enough");
715
716 hm.push_hit_test(mouse, hits(&[(0, &[2])]));
717 assert!(hm.has_sufficient_history_for_gestures(&mouse), "2 frames is the threshold");
718
719 for i in 0..10 {
720 hm.push_hit_test(mouse, hits(&[(0, &[i])]));
721 }
722 assert!(hm.has_sufficient_history_for_gestures(&mouse), "stays true when saturated");
723 }
724
725 #[test]
726 fn any_has_sufficient_history_is_an_or_across_points() {
727 let mut hm = HoverManager::new();
728 for id in [
730 InputPointId::Mouse,
731 InputPointId::Touch(0),
732 InputPointId::Touch(u64::MAX),
733 ] {
734 hm.push_hit_test(id, hits(&[(0, &[1])]));
735 }
736 assert!(!hm.any_has_sufficient_history_for_gestures());
737
738 hm.push_hit_test(InputPointId::Touch(u64::MAX), hits(&[(0, &[2])]));
740 assert!(hm.any_has_sufficient_history_for_gestures());
741
742 hm.clear_input_point(&InputPointId::Touch(u64::MAX));
744 assert!(!hm.any_has_sufficient_history_for_gestures());
745 }
746
747 #[test]
750 fn current_hover_node_returns_the_deepest_node_of_dom_zero() {
751 let hm = mouse_history(vec![hits(&[(0, &[3, 8, 5])])]);
752
753 assert_eq!(hm.current_hover_node(), Some(NodeId::new(8)));
754 assert_eq!(hm.current_hover_node_full(), Some(dom_node(0, 8)));
756 }
757
758 #[test]
759 fn current_hover_node_ignores_non_zero_doms_but_full_does_not() {
760 let hm = mouse_history(vec![hits(&[(2, &[4])])]);
762
763 assert_eq!(hm.current_hover_node(), None);
764 assert_eq!(hm.current_hover_node_full(), Some(dom_node(2, 4)));
765 }
766
767 #[test]
768 fn current_hover_node_full_prefers_the_front_most_child_dom() {
769 let hm = mouse_history(vec![hits(&[(0, &[9]), (1, &[2])])]);
770
771 assert_eq!(hm.current_hover_node(), Some(NodeId::new(9)));
773 assert_eq!(hm.current_hover_node_full(), Some(dom_node(1, 2)));
775 }
776
777 #[test]
778 fn previous_hover_node_is_none_until_a_second_frame_exists() {
779 let hm = mouse_history(vec![hits(&[(0, &[1])])]);
780
781 assert_eq!(hm.current_hover_node(), Some(NodeId::new(1)));
782 assert_eq!(hm.previous_hover_node(), None);
783 assert_eq!(hm.previous_hover_node_full(), None);
784 }
785
786 #[test]
787 fn previous_hover_node_reads_frame_one_not_the_oldest_frame() {
788 let hm = mouse_history((0..6).map(|i| hits(&[(0, &[i])])).collect());
790
791 assert_eq!(hm.current_hover_node(), Some(NodeId::new(5)));
792 assert_eq!(hm.previous_hover_node(), Some(NodeId::new(4)));
793 assert_eq!(hm.previous_hover_node_full(), Some(dom_node(0, 4)));
794 }
795
796 #[test]
797 fn previous_hover_node_full_sees_child_doms_of_the_previous_frame() {
798 let hm = mouse_history(vec![hits(&[(0, &[1]), (7, &[3])]), hits(&[(0, &[2])])]);
799
800 assert_eq!(hm.previous_hover_node(), Some(NodeId::new(1)));
801 assert_eq!(hm.previous_hover_node_full(), Some(dom_node(7, 3)));
802 assert_eq!(hm.current_hover_node_full(), Some(dom_node(0, 2)));
803 }
804
805 #[test]
806 fn hover_node_getters_are_none_when_the_frame_hit_nothing() {
807 let hm = mouse_history(vec![FullHitTest::empty(None), FullHitTest::empty(None)]);
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 }
814
815 #[test]
816 fn hover_node_getters_ignore_touch_history_entirely() {
817 let mut hm = HoverManager::new();
818 hm.push_hit_test(InputPointId::Touch(1), hits(&[(0, &[5])]));
819 hm.push_hit_test(InputPointId::Touch(1), hits(&[(0, &[6])]));
820
821 assert!(hm.current_hover_node().is_none());
822 assert!(hm.previous_hover_node().is_none());
823 assert!(hm.current_hover_node_full().is_none());
824 assert!(hm.previous_hover_node_full().is_none());
825 assert!(hm.any_has_sufficient_history_for_gestures());
826 }
827
828 #[test]
831 fn purge_dom_removes_that_dom_from_every_frame_of_every_point() {
832 let mut hm = HoverManager::new();
833 for id in [InputPointId::Mouse, InputPointId::Touch(2)] {
834 hm.push_hit_test(id, hits(&[(0, &[1]), (1, &[2])]));
835 hm.push_hit_test(id, hits(&[(0, &[3]), (1, &[4])]));
836 }
837
838 hm.purge_dom(&dom(1));
839
840 assert_eq!(hm.debug_counts(), (2, 4));
842 for id in [InputPointId::Mouse, InputPointId::Touch(2)] {
843 let history = hm.get_history(&id).expect("history exists");
844 for frame in history {
845 assert!(!frame.hovered_nodes.contains_key(&dom(1)));
846 assert!(frame.hovered_nodes.contains_key(&dom(0)));
847 }
848 }
849 assert_eq!(hm.current_hover_node_full(), Some(dom_node(0, 3)));
850 assert_eq!(hm.previous_hover_node_full(), Some(dom_node(0, 1)));
851 }
852
853 #[test]
854 fn purge_dom_zero_leaves_child_dom_hits_intact() {
855 let mut hm = mouse_history(vec![hits(&[(0, &[1]), (4, &[2])])]);
856
857 hm.purge_dom(&dom(0));
858
859 assert_eq!(hm.current_hover_node(), None);
861 assert_eq!(hm.current_hover_node_full(), Some(dom_node(4, 2)));
862 assert_eq!(hm.frame_count(&InputPointId::Mouse), 1);
863 }
864
865 #[test]
866 fn purge_absent_or_extreme_dom_id_is_a_noop() {
867 let mut hm = mouse_history(vec![hits(&[(0, &[1])])]);
868 let before = hm.clone();
869
870 hm.purge_dom(&dom(9));
871 hm.purge_dom(&dom(usize::MAX));
872 assert_eq!(hm, before);
873
874 let mut empty = HoverManager::new();
876 empty.purge_dom(&dom(0));
877 assert_eq!(empty, HoverManager::new());
878 }
879
880 #[test]
881 fn purge_dom_twice_is_idempotent() {
882 let mut hm = mouse_history(vec![hits(&[(0, &[1]), (1, &[2])])]);
883
884 hm.purge_dom(&dom(1));
885 let once = hm.clone();
886 hm.purge_dom(&dom(1));
887
888 assert_eq!(hm, once);
889 }
890
891 fn sb_map(pairs: &[(usize, usize)]) -> BTreeMap<NodeId, NodeId> {
894 pairs
895 .iter()
896 .map(|(o, n)| (NodeId::new(*o), NodeId::new(*n)))
897 .collect()
898 }
899
900 #[test]
901 fn remap_scrollbar_hit_id_rewrites_every_variant_of_the_target_dom() {
902 let map = sb_map(&[(1, 42)]);
903 let d = dom(0);
904 let old = NodeId::new(1);
905 let new = NodeId::new(42);
906
907 assert_eq!(
908 remap_scrollbar_hit_id(&ScrollbarHitId::VerticalTrack(d, old), d, &map),
909 Some(ScrollbarHitId::VerticalTrack(d, new))
910 );
911 assert_eq!(
912 remap_scrollbar_hit_id(&ScrollbarHitId::VerticalThumb(d, old), d, &map),
913 Some(ScrollbarHitId::VerticalThumb(d, new))
914 );
915 assert_eq!(
916 remap_scrollbar_hit_id(&ScrollbarHitId::HorizontalTrack(d, old), d, &map),
917 Some(ScrollbarHitId::HorizontalTrack(d, new))
918 );
919 assert_eq!(
920 remap_scrollbar_hit_id(&ScrollbarHitId::HorizontalThumb(d, old), d, &map),
921 Some(ScrollbarHitId::HorizontalThumb(d, new))
922 );
923 }
924
925 #[test]
926 fn remap_scrollbar_hit_id_drops_unmounted_nodes() {
927 let map = sb_map(&[(1, 42)]);
928 let d = dom(0);
929 let unmounted = ScrollbarHitId::VerticalThumb(d, NodeId::new(2));
931
932 assert_eq!(remap_scrollbar_hit_id(&unmounted, d, &map), None);
933 let hit = ScrollbarHitId::VerticalThumb(d, NodeId::new(1));
935 assert_eq!(remap_scrollbar_hit_id(&hit, d, &BTreeMap::new()), None);
936 }
937
938 #[test]
939 fn remap_scrollbar_hit_id_passes_other_doms_through_untouched() {
940 let map = sb_map(&[(1, 42)]);
943 let other = ScrollbarHitId::HorizontalTrack(dom(1), NodeId::new(1));
944
945 assert_eq!(remap_scrollbar_hit_id(&other, dom(0), &map), Some(other));
946 assert_eq!(
948 remap_scrollbar_hit_id(&other, dom(0), &BTreeMap::new()),
949 Some(other)
950 );
951 }
952
953 #[test]
954 fn remap_scrollbar_hit_id_handles_extreme_ids() {
955 let big = usize::MAX - 1;
956 let map = sb_map(&[(big, 0)]);
957 let d = dom(usize::MAX);
958
959 assert_eq!(
960 remap_scrollbar_hit_id(&ScrollbarHitId::VerticalTrack(d, NodeId::new(big)), d, &map),
961 Some(ScrollbarHitId::VerticalTrack(d, NodeId::ZERO))
962 );
963 }
964
965 fn all_maps_hit(dom_inner: usize, node: usize) -> FullHitTest {
969 let mut full = FullHitTest::empty(None);
970 let mut ht = HitTest::empty();
971 ht.regular_hit_test_nodes
972 .insert(NodeId::new(node), hit_item(0));
973 ht.scroll_hit_test_nodes
974 .insert(NodeId::new(node), scroll_item());
975 ht.cursor_hit_test_nodes
976 .insert(NodeId::new(node), cursor_item());
977 ht.scrollbar_hit_test_nodes.insert(
978 ScrollbarHitId::VerticalThumb(dom(dom_inner), NodeId::new(node)),
979 scrollbar_item(),
980 );
981 full.hovered_nodes.insert(dom(dom_inner), ht);
982 full
983 }
984
985 #[test]
986 fn remap_node_ids_rewrites_all_four_hit_maps() {
987 let mut hm = mouse_history(vec![all_maps_hit(0, 3)]);
988
989 hm.remap_node_ids(dom(0), &NodeIdMap::from_pairs([(NodeId::new(3), NodeId::new(11))]));
990
991 let ht = &hm.get_current_mouse().expect("frame exists").hovered_nodes[&dom(0)];
992 assert_eq!(
993 ht.regular_hit_test_nodes.keys().copied().collect::<Vec<_>>(),
994 vec![NodeId::new(11)]
995 );
996 assert_eq!(
997 ht.scroll_hit_test_nodes.keys().copied().collect::<Vec<_>>(),
998 vec![NodeId::new(11)]
999 );
1000 assert_eq!(
1001 ht.cursor_hit_test_nodes.keys().copied().collect::<Vec<_>>(),
1002 vec![NodeId::new(11)]
1003 );
1004 assert_eq!(
1005 ht.scrollbar_hit_test_nodes.keys().copied().collect::<Vec<_>>(),
1006 vec![ScrollbarHitId::VerticalThumb(dom(0), NodeId::new(11))]
1007 );
1008 assert_eq!(hm.current_hover_node(), Some(NodeId::new(11)));
1009 }
1010
1011 #[test]
1012 fn remap_node_ids_with_an_empty_map_drops_every_hit_of_that_dom() {
1013 let mut hm = mouse_history(vec![all_maps_hit(0, 3)]);
1014
1015 hm.remap_node_ids(dom(0), &NodeIdMap::default());
1017
1018 let ht = &hm.get_current_mouse().expect("frame exists").hovered_nodes[&dom(0)];
1019 assert!(ht.regular_hit_test_nodes.is_empty());
1020 assert!(ht.scroll_hit_test_nodes.is_empty());
1021 assert!(ht.cursor_hit_test_nodes.is_empty());
1022 assert!(ht.scrollbar_hit_test_nodes.is_empty());
1023 assert_eq!(hm.current_hover_node(), None);
1024 assert!(hm
1026 .get_current_mouse()
1027 .expect("frame exists")
1028 .hovered_nodes
1029 .contains_key(&dom(0)));
1030 }
1031
1032 #[test]
1033 fn remap_node_ids_drops_unmounted_but_keeps_survivors() {
1034 let mut hm = mouse_history(vec![hits(&[(0, &[1, 4])])]);
1036
1037 hm.remap_node_ids(dom(0), &NodeIdMap::from_pairs([(NodeId::new(4), NodeId::ZERO)]));
1038
1039 let ht = &hm.get_current_mouse().expect("frame exists").hovered_nodes[&dom(0)];
1040 assert_eq!(
1041 ht.regular_hit_test_nodes.keys().copied().collect::<Vec<_>>(),
1042 vec![NodeId::ZERO]
1043 );
1044 assert_eq!(hm.current_hover_node(), Some(NodeId::ZERO));
1045 }
1046
1047 #[test]
1048 fn remap_node_ids_swap_does_not_lose_or_alias_entries() {
1049 let mut full = FullHitTest::empty(None);
1052 let mut ht = HitTest::empty();
1053 ht.regular_hit_test_nodes
1054 .insert(NodeId::new(1), hit_item(10));
1055 ht.regular_hit_test_nodes
1056 .insert(NodeId::new(2), hit_item(20));
1057 full.hovered_nodes.insert(dom(0), ht);
1058 let mut hm = mouse_history(vec![full]);
1059
1060 hm.remap_node_ids(
1061 dom(0),
1062 &NodeIdMap::from_pairs([
1063 (NodeId::new(1), NodeId::new(2)),
1064 (NodeId::new(2), NodeId::new(1)),
1065 ]),
1066 );
1067
1068 let ht = &hm.get_current_mouse().expect("frame exists").hovered_nodes[&dom(0)];
1069 assert_eq!(ht.regular_hit_test_nodes.len(), 2);
1070 assert_eq!(ht.regular_hit_test_nodes[&NodeId::new(2)].hit_depth, 10);
1071 assert_eq!(ht.regular_hit_test_nodes[&NodeId::new(1)].hit_depth, 20);
1072 }
1073
1074 #[test]
1075 fn remap_node_ids_can_change_which_node_is_deepest() {
1076 let mut hm = mouse_history(vec![hits(&[(0, &[3, 7])])]);
1079 assert_eq!(hm.current_hover_node(), Some(NodeId::new(7)));
1080
1081 hm.remap_node_ids(
1082 dom(0),
1083 &NodeIdMap::from_pairs([
1084 (NodeId::new(3), NodeId::new(9)),
1085 (NodeId::new(7), NodeId::new(2)),
1086 ]),
1087 );
1088
1089 assert_eq!(hm.current_hover_node(), Some(NodeId::new(9)));
1090 assert_eq!(hm.current_hover_node_full(), Some(dom_node(0, 9)));
1091 }
1092
1093 #[test]
1094 fn remap_node_ids_leaves_other_doms_alone() {
1095 let mut hm = mouse_history(vec![hits(&[(0, &[1]), (1, &[1])])]);
1096
1097 hm.remap_node_ids(dom(0), &NodeIdMap::from_pairs([(NodeId::new(1), NodeId::new(5))]));
1098
1099 let frame = hm.get_current_mouse().expect("frame exists");
1100 assert_eq!(
1101 frame.hovered_nodes[&dom(0)]
1102 .regular_hit_test_nodes
1103 .keys()
1104 .copied()
1105 .collect::<Vec<_>>(),
1106 vec![NodeId::new(5)],
1107 "DOM 0 is remapped"
1108 );
1109 assert_eq!(
1110 frame.hovered_nodes[&dom(1)]
1111 .regular_hit_test_nodes
1112 .keys()
1113 .copied()
1114 .collect::<Vec<_>>(),
1115 vec![NodeId::new(1)],
1116 "DOM 1 must be untouched by DOM 0's reconciliation"
1117 );
1118 }
1119
1120 #[test]
1121 fn remap_node_ids_keeps_foreign_dom_scrollbar_ids_stored_under_the_target_dom() {
1122 let mut full = FullHitTest::empty(None);
1125 let mut ht = HitTest::empty();
1126 ht.scrollbar_hit_test_nodes.insert(
1127 ScrollbarHitId::VerticalTrack(dom(1), NodeId::new(1)),
1128 scrollbar_item(),
1129 );
1130 ht.scrollbar_hit_test_nodes.insert(
1131 ScrollbarHitId::VerticalTrack(dom(0), NodeId::new(1)),
1132 scrollbar_item(),
1133 );
1134 full.hovered_nodes.insert(dom(0), ht);
1135 let mut hm = mouse_history(vec![full]);
1136
1137 hm.remap_node_ids(dom(0), &NodeIdMap::from_pairs([(NodeId::new(1), NodeId::new(8))]));
1138
1139 let keys: Vec<_> = hm.get_current_mouse().expect("frame exists").hovered_nodes[&dom(0)]
1140 .scrollbar_hit_test_nodes
1141 .keys()
1142 .copied()
1143 .collect();
1144 assert!(
1145 keys.contains(&ScrollbarHitId::VerticalTrack(dom(1), NodeId::new(1))),
1146 "foreign-DOM scrollbar id must survive unchanged, got {keys:?}"
1147 );
1148 assert!(
1149 keys.contains(&ScrollbarHitId::VerticalTrack(dom(0), NodeId::new(8))),
1150 "target-DOM scrollbar id must be rewritten, got {keys:?}"
1151 );
1152 assert_eq!(keys.len(), 2);
1153 }
1154
1155 #[test]
1156 fn remap_node_ids_applies_to_every_frame_and_every_input_point() {
1157 let mut hm = HoverManager::new();
1158 for id in [InputPointId::Mouse, InputPointId::Touch(1)] {
1159 hm.push_hit_test(id, hits(&[(0, &[2])]));
1160 hm.push_hit_test(id, hits(&[(0, &[2])]));
1161 }
1162
1163 hm.remap_node_ids(dom(0), &NodeIdMap::from_pairs([(NodeId::new(2), NodeId::new(6))]));
1164
1165 for id in [InputPointId::Mouse, InputPointId::Touch(1)] {
1166 for frame in hm.get_history(&id).expect("history exists") {
1167 assert_eq!(
1168 frame.hovered_nodes[&dom(0)]
1169 .regular_hit_test_nodes
1170 .keys()
1171 .copied()
1172 .collect::<Vec<_>>(),
1173 vec![NodeId::new(6)]
1174 );
1175 }
1176 }
1177 assert_eq!(hm.previous_hover_node(), Some(NodeId::new(6)));
1178 }
1179
1180 #[test]
1181 fn remap_node_ids_on_an_empty_manager_or_unknown_dom_does_not_panic() {
1182 let mut empty = HoverManager::new();
1183 empty.remap_node_ids(dom(usize::MAX), &NodeIdMap::default());
1184 assert_eq!(empty, HoverManager::new());
1185
1186 let mut hm = mouse_history(vec![hits(&[(0, &[1])])]);
1187 let before = hm.clone();
1188 hm.remap_node_ids(dom(3), &NodeIdMap::from_pairs([(NodeId::new(1), NodeId::new(2))]));
1190 assert_eq!(hm, before);
1191 }
1192
1193 #[test]
1194 fn remap_node_ids_identity_map_is_idempotent() {
1195 let mut hm = mouse_history(vec![all_maps_hit(0, 3)]);
1196 let before = hm.clone();
1197 let identity = NodeIdMap::from_pairs([(NodeId::new(3), NodeId::new(3))]);
1198
1199 hm.remap_node_ids(dom(0), &identity);
1200 assert_eq!(hm, before, "identity remap must not change anything");
1201
1202 hm.remap_node_ids(dom(0), &identity);
1203 assert_eq!(hm, before, "and applying it twice must not either");
1204 }
1205
1206 #[test]
1209 fn clone_is_equal_and_independent_of_the_original() {
1210 let mut hm = mouse_history(vec![hits(&[(0, &[1])])]);
1211 let snapshot = hm.clone();
1212 assert_eq!(hm, snapshot);
1213
1214 hm.push_hit_test(InputPointId::Mouse, hits(&[(0, &[2])]));
1215
1216 assert_ne!(hm, snapshot, "the clone must not observe later pushes");
1217 assert_eq!(snapshot.frame_count(&InputPointId::Mouse), 1);
1218 assert_eq!(hm.frame_count(&InputPointId::Mouse), 2);
1219 }
1220
1221 #[test]
1222 fn debug_counts_agrees_with_frame_count_and_active_points() {
1223 let mut hm = HoverManager::new();
1224 hm.push_hit_test(InputPointId::Mouse, hits(&[(0, &[1])]));
1225 hm.push_hit_test(InputPointId::Touch(7), hits(&[(0, &[1])]));
1226 hm.push_hit_test(InputPointId::Touch(7), hits(&[(0, &[2])]));
1227
1228 let (points, total) = hm.debug_counts();
1229 let active = hm.get_active_input_points();
1230 assert_eq!(points, active.len());
1231 assert_eq!(
1232 total,
1233 active.iter().map(|id| hm.frame_count(id)).sum::<usize>()
1234 );
1235 assert_eq!((points, total), (2, 3));
1236 }
1237}