1use std::collections::BTreeMap;
21
22use azul_core::{
23 dom::{DomId, DomNodeId, NodeId},
24 geom::{LogicalPosition, LogicalRect, LogicalSize},
25 hit_test::FullHitTest,
26 styled_dom::StyledDom,
27};
28
29use crate::solver3::{getters::{get_overflow_x, get_overflow_y}, layout_tree::LayoutNodeHot, PositionVec};
30use crate::window::DomLayoutResult;
31
32const CLIP_UNBOUNDED: f32 = 1.0e7;
37
38#[derive(Debug)]
48pub struct CpuHitTester {
49 node_rects: BTreeMap<DomId, Vec<HitTestEntry>>,
52}
53
54#[derive(Debug, Clone)]
56struct HitTestEntry {
57 node_id: NodeId,
59 rect: LogicalRect,
61 clip: Option<LogicalRect>,
63 pointer_events_none: bool,
65}
66
67impl Default for CpuHitTester {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl CpuHitTester {
74 #[must_use] pub const fn new() -> Self {
76 Self {
77 node_rects: BTreeMap::new(),
78 }
79 }
80
81 #[must_use] pub fn node_rects_total(&self) -> usize {
83 self.node_rects.values().map(Vec::len).sum()
84 }
85
86 pub fn rebuild_from_layout(
92 &mut self,
93 layout_results: &BTreeMap<DomId, DomLayoutResult>,
94 ) {
95 self.node_rects.clear();
96
97 let mut placements: BTreeMap<DomId, LogicalRect> = BTreeMap::new();
109 for _ in 0..4 {
110 let mut changed = false;
112 for (host_dom, lr) in layout_results {
113 let host_offset = if host_dom.inner == 0 {
114 Some(LogicalPosition::zero())
115 } else {
116 placements.get(host_dom).map(|r| r.origin)
117 };
118 let Some(host_offset) = host_offset else { continue };
119 for item in &lr.display_list.items {
120 if let crate::solver3::display_list::DisplayListItem::VirtualView {
121 child_dom_id,
122 bounds,
123 ..
124 } = item
125 {
126 let b = *bounds.inner();
127 let absolute = LogicalRect {
128 origin: LogicalPosition {
129 x: b.origin.x + host_offset.x,
130 y: b.origin.y + host_offset.y,
131 },
132 size: b.size,
133 };
134 if placements.get(child_dom_id) != Some(&absolute) {
135 placements.insert(*child_dom_id, absolute);
136 changed = true;
137 }
138 }
139 }
140 }
141 if !changed {
142 break;
143 }
144 }
145
146 for (dom_id, layout_result) in layout_results {
147 let mut entries = Vec::new();
148
149 let positions = &layout_result.calculated_positions;
150 let nodes = &layout_result.layout_tree.nodes;
151 let styled_dom = &layout_result.styled_dom;
152
153 let (offset, dom_clip) = placements.get(dom_id).map_or_else(|| (LogicalPosition::zero(), None), |b| (b.origin, Some(*b)));
155
156 for (idx, node) in nodes.iter().enumerate() {
158 let Some(node_id) = node.dom_node_id else {
160 continue; };
162
163 let pos = match positions.get(idx) {
165 Some(p) => *p,
166 None => continue,
167 };
168
169 let Some(size) = node.used_size else {
171 continue;
172 };
173
174 let rect = LogicalRect {
175 origin: LogicalPosition {
176 x: pos.x + offset.x,
177 y: pos.y + offset.y,
178 },
179 size,
180 };
181
182 let clip = compute_node_clip(styled_dom, nodes, positions, idx, offset, dom_clip);
187
188 entries.push(HitTestEntry {
189 node_id,
190 rect,
191 clip,
192 pointer_events_none: false,
196 });
197 }
198
199 self.node_rects.insert(*dom_id, entries);
200 }
201 }
202
203 #[must_use] pub fn hit_test(
207 &self,
208 position: LogicalPosition,
209 ) -> Vec<(DomId, NodeId)> {
210 let mut results = Vec::new();
211
212 for (dom_id, entries) in &self.node_rects {
213 for entry in entries.iter().rev() {
215 if entry.pointer_events_none {
216 continue;
217 }
218
219 if let Some(ref clip) = entry.clip {
221 if !point_in_rect(position, clip) {
222 continue;
223 }
224 }
225
226 if point_in_rect(position, &entry.rect) {
228 results.push((*dom_id, entry.node_id));
229 }
230 }
231 }
232
233 results
234 }
235}
236
237fn point_in_rect(point: LogicalPosition, rect: &LogicalRect) -> bool {
239 point.x >= rect.origin.x
240 && point.x < rect.origin.x + rect.size.width
241 && point.y >= rect.origin.y
242 && point.y < rect.origin.y + rect.size.height
243}
244
245#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines)] #[must_use]
260pub fn convert_cpu_hit_test_to_full(
261 hits: &[(DomId, NodeId)],
262 old_focus_node: Option<DomNodeId>,
263 layout_results: &BTreeMap<DomId, DomLayoutResult>,
264 cursor_position: LogicalPosition,
265) -> FullHitTest {
266 use azul_core::{
267 dom::OptionDomNodeId,
268 hit_test::{HitTest, HitTestItem, OverflowingScrollNode, ScrollHitTestItem},
269 };
270
271 let focused_node = old_focus_node.map_or(OptionDomNodeId::None, OptionDomNodeId::Some);
272
273 let mut hovered_nodes: BTreeMap<DomId, HitTest> = BTreeMap::new();
274
275 for (depth, (dom_id, node_id)) in hits.iter().enumerate() {
276 let point_relative = layout_results
281 .get(dom_id)
282 .and_then(|lr| {
283 lr.layout_tree
284 .dom_to_layout
285 .get(node_id)
286 .and_then(|indices| indices.first())
287 .and_then(|&idx| {
288 let node_pos = lr.calculated_positions.get(idx)?;
289 let node = lr.layout_tree.get(idx)?;
290 let bp = node.box_props.unpack();
291 let content_x = node_pos.x + bp.padding.left + bp.border.left;
292 let content_y = node_pos.y + bp.padding.top + bp.border.top;
293 Some(LogicalPosition::new(
294 cursor_position.x - content_x,
295 cursor_position.y - content_y,
296 ))
297 })
298 })
299 .unwrap_or_else(LogicalPosition::zero);
300
301 let hit_test = hovered_nodes.entry(*dom_id).or_insert_with(|| HitTest {
302 regular_hit_test_nodes: BTreeMap::new(),
303 scroll_hit_test_nodes: BTreeMap::new(),
304 scrollbar_hit_test_nodes: BTreeMap::new(),
305 cursor_hit_test_nodes: BTreeMap::new(),
306 });
307
308 hit_test.regular_hit_test_nodes.insert(
309 *node_id,
310 HitTestItem {
311 point_in_viewport: cursor_position,
312 point_relative_to_item: point_relative,
313 is_focusable: false,
314 is_virtual_view_hit: None,
315 hit_depth: depth as u32,
316 },
317 );
318 }
319
320 for (dom_id, lr) in layout_results {
327 for (&scroll_id, &node_id) in &lr.scroll_id_to_node_id {
328 let Some(&layout_idx) = lr
329 .layout_tree
330 .dom_to_layout
331 .get(&node_id)
332 .and_then(|indices| indices.first())
333 else {
334 continue;
335 };
336 let Some(layout_node) = lr.layout_tree.get(layout_idx) else {
337 continue;
338 };
339 let node_pos = lr
340 .calculated_positions
341 .get(layout_idx)
342 .copied()
343 .unwrap_or_default();
344 let node_size = layout_node.used_size.unwrap_or_default();
345 let inside = cursor_position.x >= node_pos.x
346 && cursor_position.x <= node_pos.x + node_size.width
347 && cursor_position.y >= node_pos.y
348 && cursor_position.y <= node_pos.y + node_size.height;
349 if !inside {
350 continue;
351 }
352 let parent_rect = LogicalRect::new(node_pos, node_size);
353 let child_rect = compute_scroll_child_rect(lr, layout_idx, parent_rect);
354
355 let scroll_node = OverflowingScrollNode {
356 parent_rect,
357 child_rect,
358 virtual_child_rect: child_rect,
359 parent_external_scroll_id: azul_core::hit_test::ExternalScrollId(
362 scroll_id,
363 azul_core::hit_test::PipelineId(dom_id.inner as u32, 0),
364 ),
365 parent_dom_hash: azul_core::dom::DomNodeHash {
366 inner: node_id.index() as u64,
367 },
368 scroll_tag_id: azul_core::dom::ScrollTagId {
369 inner: azul_core::dom::TagId {
370 inner: node_id.index() as u64,
371 },
372 },
373 };
374 hovered_nodes
375 .entry(*dom_id)
376 .or_insert_with(HitTest::empty)
377 .scroll_hit_test_nodes
378 .insert(
379 node_id,
380 ScrollHitTestItem {
381 point_in_viewport: cursor_position,
382 point_relative_to_item: LogicalPosition::new(
383 cursor_position.x - node_pos.x,
384 cursor_position.y - node_pos.y,
385 ),
386 scroll_node,
387 },
388 );
389 }
390 }
391
392 FullHitTest {
393 hovered_nodes,
394 focused_node,
395 }
396}
397
398#[must_use]
408pub fn compute_scroll_child_rect(
409 layout_result: &DomLayoutResult,
410 layout_idx: usize,
411 parent_rect: LogicalRect,
412) -> LogicalRect {
413 let content_size = layout_result.layout_tree.get_content_size(layout_idx);
414 LogicalRect::new(
415 parent_rect.origin,
416 LogicalSize::new(
417 content_size.width.max(parent_rect.size.width),
418 content_size.height.max(parent_rect.size.height),
419 ),
420 )
421}
422
423#[allow(clippy::similar_names)] fn compute_node_clip(
435 styled_dom: &StyledDom,
436 nodes: &[LayoutNodeHot],
437 positions: &PositionVec,
438 node_index: usize,
439 offset: LogicalPosition,
440 dom_clip: Option<LogicalRect>,
441) -> Option<LogicalRect> {
442 let (mut min_x, mut min_y, mut max_x, mut max_y) = (
444 f32::NEG_INFINITY,
445 f32::NEG_INFINITY,
446 f32::INFINITY,
447 f32::INFINITY,
448 );
449 let mut has_clip = false;
450 if let Some(dc) = dom_clip {
451 min_x = dc.min_x();
452 min_y = dc.min_y();
453 max_x = dc.max_x();
454 max_y = dc.max_y();
455 has_clip = true;
456 }
457
458 let styled_nodes = styled_dom.styled_nodes.as_container();
462 let mut cur = nodes.get(node_index).and_then(|n| n.parent);
463 let mut guard = 0usize;
464 while let Some(anc) = cur {
465 guard += 1;
466 if guard > nodes.len() {
467 break;
468 }
469 let Some(anc_node) = nodes.get(anc) else { break };
470 cur = anc_node.parent;
471
472 let Some(anc_dom_id) = anc_node.dom_node_id else {
473 continue;
474 };
475 let node_state = &styled_nodes[anc_dom_id].styled_node_state;
476 let clips_x = get_overflow_x(styled_dom, anc_dom_id, node_state).is_clipped();
477 let clips_y = get_overflow_y(styled_dom, anc_dom_id, node_state).is_clipped();
478 if !clips_x && !clips_y {
479 continue;
480 }
481 let (Some(pos), Some(size)) = (positions.get(anc), anc_node.used_size) else {
482 continue;
483 };
484 let (ax0, ay0) = (pos.x + offset.x, pos.y + offset.y);
485 if clips_x {
486 min_x = min_x.max(ax0);
487 max_x = max_x.min(ax0 + size.width);
488 has_clip = true;
489 }
490 if clips_y {
491 min_y = min_y.max(ay0);
492 max_y = max_y.min(ay0 + size.height);
493 has_clip = true;
494 }
495 }
496
497 if !has_clip {
498 return None;
499 }
500
501 if !min_x.is_finite() {
504 min_x = -CLIP_UNBOUNDED;
505 }
506 if !min_y.is_finite() {
507 min_y = -CLIP_UNBOUNDED;
508 }
509 if !max_x.is_finite() {
510 max_x = CLIP_UNBOUNDED;
511 }
512 if !max_y.is_finite() {
513 max_y = CLIP_UNBOUNDED;
514 }
515
516 Some(LogicalRect {
517 origin: LogicalPosition { x: min_x, y: min_y },
518 size: LogicalSize {
519 width: (max_x - min_x).max(0.0),
520 height: (max_y - min_y).max(0.0),
521 },
522 })
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 #[test]
530 fn test_cpu_hit_tester_empty() {
531 let tester = CpuHitTester::new();
532 let results = tester.hit_test(LogicalPosition { x: 100.0, y: 100.0 });
533 assert!(results.is_empty());
534 }
535
536 #[test]
537 fn test_point_in_rect() {
538 let rect = LogicalRect {
539 origin: LogicalPosition { x: 10.0, y: 10.0 },
540 size: LogicalSize {
541 width: 100.0,
542 height: 50.0,
543 },
544 };
545
546 assert!(point_in_rect(LogicalPosition { x: 50.0, y: 30.0 }, &rect));
548 assert!(point_in_rect(LogicalPosition { x: 10.0, y: 10.0 }, &rect));
550 assert!(!point_in_rect(LogicalPosition { x: 5.0, y: 5.0 }, &rect));
552 assert!(!point_in_rect(LogicalPosition { x: 200.0, y: 30.0 }, &rect));
553 }
554}
555
556#[cfg(test)]
557#[allow(clippy::float_cmp)] mod autotest_generated {
559 use std::collections::HashMap;
560
561 use azul_core::dom::{Dom, FormattingContext};
562
563 use super::*;
564 use crate::{
565 solver3::{
566 display_list::{DisplayList, DisplayListItem, WindowLogicalRect},
567 layout_tree::LayoutTree,
568 },
569 window::DomLayoutResult,
570 };
571
572 fn p(x: f32, y: f32) -> LogicalPosition {
577 LogicalPosition { x, y }
578 }
579
580 fn r(x: f32, y: f32, width: f32, height: f32) -> LogicalRect {
581 LogicalRect {
582 origin: p(x, y),
583 size: LogicalSize { width, height },
584 }
585 }
586
587 fn dom(inner: usize) -> DomId {
588 DomId { inner }
589 }
590
591 fn hot(
594 dom_node_id: Option<usize>,
595 size: Option<(f32, f32)>,
596 parent: Option<usize>,
597 ) -> LayoutNodeHot {
598 LayoutNodeHot {
599 box_props: Default::default(),
600 dom_node_id: dom_node_id.map(NodeId::new),
601 used_size: size.map(|(width, height)| LogicalSize { width, height }),
602 formatting_context: FormattingContext::default(),
603 parent,
604 }
605 }
606
607 fn styled(css_src: &str) -> StyledDom {
609 let css = azul_css::parser2::new_from_str(css_src).0;
610 let mut d = Dom::create_body().with_children(
611 vec![Dom::create_div()
612 .with_class("clip".to_string().into())
613 .with_children(vec![Dom::create_div()].into())]
614 .into(),
615 );
616 StyledDom::create(&mut d, css)
617 }
618
619 fn layout_result(
620 styled_dom: StyledDom,
621 nodes: Vec<LayoutNodeHot>,
622 calculated_positions: PositionVec,
623 items: Vec<DisplayListItem>,
624 ) -> DomLayoutResult {
625 DomLayoutResult {
626 styled_dom,
627 layout_tree: LayoutTree {
628 nodes,
629 warm: Vec::new(),
630 cold: Vec::new(),
631 root: 0,
632 dom_to_layout: BTreeMap::new(),
633 children_arena: Vec::new(),
634 children_offsets: Vec::new(),
635 subtree_needs_intrinsic: Vec::new(),
636 },
637 calculated_positions,
638 viewport: LogicalRect::zero(),
639 display_list: DisplayList {
640 items,
641 ..Default::default()
642 },
643 scroll_ids: HashMap::new(),
644 scroll_id_to_node_id: HashMap::new(),
645 }
646 }
647
648 fn virtual_view(child: usize, bounds: LogicalRect) -> DisplayListItem {
649 DisplayListItem::VirtualView {
650 child_dom_id: dom(child),
651 bounds: WindowLogicalRect::new(bounds.origin, bounds.size),
652 clip_rect: WindowLogicalRect::new(bounds.origin, bounds.size),
653 }
654 }
655
656 const HOSTILE_F32: [f32; 8] = [
658 0.0,
659 -0.0,
660 f32::NAN,
661 f32::INFINITY,
662 f32::NEG_INFINITY,
663 f32::MAX,
664 f32::MIN,
665 f32::MIN_POSITIVE,
666 ];
667
668 #[test]
673 fn point_in_rect_is_half_open_top_left_inclusive_bottom_right_exclusive() {
674 let rect = r(10.0, 10.0, 100.0, 50.0);
675
676 assert!(point_in_rect(p(10.0, 10.0), &rect), "top-left is inclusive");
677 assert!(point_in_rect(p(109.999, 59.999), &rect));
678 assert!(
679 !point_in_rect(p(110.0, 30.0), &rect),
680 "right edge is exclusive"
681 );
682 assert!(
683 !point_in_rect(p(50.0, 60.0), &rect),
684 "bottom edge is exclusive"
685 );
686 assert!(!point_in_rect(p(110.0, 60.0), &rect));
687 }
688
689 #[test]
690 fn point_in_rect_zero_sized_rect_contains_nothing_not_even_its_origin() {
691 let rect = r(0.0, 0.0, 0.0, 0.0);
692 assert!(!point_in_rect(p(0.0, 0.0), &rect));
693 assert!(!point_in_rect(p(-0.0, -0.0), &rect));
694
695 let elsewhere = r(7.0, 9.0, 0.0, 0.0);
696 assert!(!point_in_rect(p(7.0, 9.0), &elsewhere));
697 }
698
699 #[test]
700 fn point_in_rect_negative_size_rect_is_empty() {
701 let rect = r(100.0, 100.0, -50.0, -50.0);
705 for x in [50.0_f32, 75.0, 99.0, 100.0, 125.0] {
706 for y in [50.0_f32, 75.0, 99.0, 100.0, 125.0] {
707 assert!(!point_in_rect(p(x, y), &rect), "({x}, {y}) must not hit");
708 }
709 }
710 }
711
712 #[test]
713 fn point_in_rect_negative_zero_origin_still_contains_zero() {
714 let rect = r(-0.0, -0.0, 10.0, 10.0);
716 assert!(point_in_rect(p(0.0, 0.0), &rect));
717 assert!(point_in_rect(p(-0.0, -0.0), &rect));
718
719 let zero_origin = r(0.0, 0.0, 10.0, 10.0);
720 assert!(point_in_rect(p(-0.0, -0.0), &zero_origin));
721 }
722
723 #[test]
724 fn point_in_rect_nan_point_never_hits() {
725 let rect = r(-1000.0, -1000.0, 5000.0, 5000.0);
726 assert!(!point_in_rect(p(f32::NAN, 0.0), &rect));
727 assert!(!point_in_rect(p(0.0, f32::NAN), &rect));
728 assert!(!point_in_rect(p(f32::NAN, f32::NAN), &rect));
729 }
730
731 #[test]
732 fn point_in_rect_nan_rect_never_hits() {
733 for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
734 let nan_origin = r(bad, 0.0, 10.0, 10.0);
735 let nan_size = r(0.0, 0.0, bad, 10.0);
736 let _ = point_in_rect(p(5.0, 5.0), &nan_origin);
740 let _ = point_in_rect(p(5.0, 5.0), &nan_size);
741 }
742 assert!(!point_in_rect(p(5.0, 5.0), &r(f32::NAN, 0.0, 10.0, 10.0)));
743 assert!(!point_in_rect(p(5.0, 5.0), &r(0.0, 0.0, f32::NAN, 10.0)));
744 }
745
746 #[test]
747 fn point_in_rect_infinite_extent_is_empty_which_is_why_clip_unbounded_exists() {
748 let infinite = LogicalRect {
753 origin: p(f32::NEG_INFINITY, f32::NEG_INFINITY),
754 size: LogicalSize {
755 width: f32::INFINITY,
756 height: f32::INFINITY,
757 },
758 };
759 assert!(!point_in_rect(p(0.0, 0.0), &infinite));
760 assert!(!point_in_rect(p(-1.0e6, 1.0e6), &infinite));
761 }
762
763 #[test]
764 fn point_in_rect_clip_unbounded_extent_contains_every_realistic_coordinate() {
765 let unbounded = r(
768 -CLIP_UNBOUNDED,
769 -CLIP_UNBOUNDED,
770 2.0 * CLIP_UNBOUNDED,
771 2.0 * CLIP_UNBOUNDED,
772 );
773 for c in [0.0_f32, -0.0, 1.0, -1.0, 99_999.0, -99_999.0, 1.0e6, -1.0e6] {
774 assert!(point_in_rect(p(c, c), &unbounded), "{c} must be inside");
775 }
776 assert!(!point_in_rect(p(f32::MAX, 0.0), &unbounded));
778 }
779
780 #[test]
781 fn point_in_rect_saturates_at_f32_max_without_panicking() {
782 let huge = r(f32::MAX, f32::MAX, f32::MAX, f32::MAX);
785 assert!(point_in_rect(p(f32::MAX, f32::MAX), &huge));
786 assert!(!point_in_rect(p(0.0, 0.0), &huge));
787
788 let from_zero = r(0.0, 0.0, f32::MAX, f32::MAX);
789 assert!(point_in_rect(p(0.0, 0.0), &from_zero));
790 assert!(
791 !point_in_rect(p(f32::MAX, f32::MAX), &from_zero),
792 "the far edge stays exclusive even at f32::MAX"
793 );
794 }
795
796 #[test]
797 fn point_in_rect_never_panics_for_any_hostile_f32_combination() {
798 for &x in &HOSTILE_F32 {
799 for &y in &HOSTILE_F32 {
800 for &w in &HOSTILE_F32 {
801 let rect = r(x, y, w, w);
802 let _ = point_in_rect(p(y, x), &rect);
803 }
804 }
805 }
806 }
807
808 #[test]
813 fn new_hit_tester_is_empty_and_matches_default() {
814 let tester = CpuHitTester::new();
815 assert_eq!(tester.node_rects_total(), 0);
816 assert!(tester.hit_test(p(0.0, 0.0)).is_empty());
817
818 let defaulted = CpuHitTester::default();
819 assert_eq!(defaulted.node_rects_total(), tester.node_rects_total());
820 }
821
822 #[test]
823 fn node_rects_total_sums_entries_across_doms_and_skips_unlaid_nodes() {
824 let mut results = BTreeMap::new();
825 results.insert(
827 dom(0),
828 layout_result(
829 styled(""),
830 vec![
831 hot(Some(0), Some((10.0, 10.0)), None),
832 hot(Some(1), Some((10.0, 10.0)), None),
833 hot(None, Some((10.0, 10.0)), None), hot(Some(2), None, None), ],
836 vec![p(0.0, 0.0), p(0.0, 0.0), p(0.0, 0.0), p(0.0, 0.0)],
837 Vec::new(),
838 ),
839 );
840 results.insert(
842 dom(1),
843 layout_result(
844 styled(""),
845 vec![hot(Some(0), Some((10.0, 10.0)), None)],
846 vec![p(0.0, 0.0)],
847 Vec::new(),
848 ),
849 );
850
851 let mut tester = CpuHitTester::new();
852 tester.rebuild_from_layout(&results);
853 assert_eq!(tester.node_rects_total(), 3);
854 }
855
856 #[test]
857 fn node_rects_total_does_not_grow_when_the_same_layout_is_rebuilt() {
858 let mut results = BTreeMap::new();
860 results.insert(
861 dom(0),
862 layout_result(
863 styled(""),
864 vec![hot(Some(0), Some((10.0, 10.0)), None)],
865 vec![p(0.0, 0.0)],
866 Vec::new(),
867 ),
868 );
869
870 let mut tester = CpuHitTester::new();
871 for _ in 0..16 {
872 tester.rebuild_from_layout(&results);
873 assert_eq!(tester.node_rects_total(), 1);
874 }
875
876 tester.rebuild_from_layout(&BTreeMap::new());
877 assert_eq!(tester.node_rects_total(), 0);
878 assert!(tester.hit_test(p(1.0, 1.0)).is_empty());
879 }
880
881 #[test]
886 fn hit_test_on_empty_tester_never_panics_for_hostile_positions() {
887 let tester = CpuHitTester::new();
888 for &x in &HOSTILE_F32 {
889 for &y in &HOSTILE_F32 {
890 assert!(tester.hit_test(p(x, y)).is_empty());
891 }
892 }
893 }
894
895 #[test]
896 fn hit_test_with_hostile_positions_against_a_real_node_returns_no_spurious_hits() {
897 let mut results = BTreeMap::new();
898 results.insert(
899 dom(0),
900 layout_result(
901 styled(""),
902 vec![hot(Some(0), Some((100.0, 100.0)), None)],
903 vec![p(0.0, 0.0)],
904 Vec::new(),
905 ),
906 );
907 let mut tester = CpuHitTester::new();
908 tester.rebuild_from_layout(&results);
909
910 assert_eq!(tester.hit_test(p(50.0, 50.0)).len(), 1);
912
913 for pos in [
914 p(f32::NAN, f32::NAN),
915 p(f32::NAN, 50.0),
916 p(50.0, f32::NAN),
917 p(f32::INFINITY, f32::INFINITY),
918 p(f32::NEG_INFINITY, f32::NEG_INFINITY),
919 p(f32::MAX, f32::MAX),
920 p(f32::MIN, f32::MIN),
921 ] {
922 assert!(
923 tester.hit_test(pos).is_empty(),
924 "({}, {}) must not hit a 0,0,100x100 node",
925 pos.x,
926 pos.y
927 );
928 }
929
930 assert_eq!(tester.hit_test(p(0.0, 0.0)).len(), 1);
932 assert_eq!(tester.hit_test(p(-0.0, -0.0)).len(), 1);
933 assert!(tester.hit_test(p(100.0, 100.0)).is_empty());
935 assert_eq!(tester.hit_test(p(99.999, 99.999)).len(), 1);
936 }
937
938 #[test]
939 fn hit_test_returns_topmost_first() {
940 let mut results = BTreeMap::new();
943 results.insert(
944 dom(0),
945 layout_result(
946 styled(""),
947 vec![
948 hot(Some(1), Some((100.0, 100.0)), None),
949 hot(Some(2), Some((100.0, 100.0)), None),
950 ],
951 vec![p(0.0, 0.0), p(0.0, 0.0)],
952 Vec::new(),
953 ),
954 );
955 let mut tester = CpuHitTester::new();
956 tester.rebuild_from_layout(&results);
957
958 assert_eq!(
959 tester.hit_test(p(50.0, 50.0)),
960 vec![(dom(0), NodeId::new(2)), (dom(0), NodeId::new(1))]
961 );
962 }
963
964 #[test]
965 fn hit_test_skips_nodes_with_no_calculated_position() {
966 let mut results = BTreeMap::new();
969 results.insert(
970 dom(0),
971 layout_result(
972 styled(""),
973 vec![
974 hot(Some(0), Some((100.0, 100.0)), None),
975 hot(Some(1), Some((100.0, 100.0)), None),
976 hot(Some(2), Some((100.0, 100.0)), None),
977 ],
978 vec![p(0.0, 0.0)], Vec::new(),
980 ),
981 );
982 let mut tester = CpuHitTester::new();
983 tester.rebuild_from_layout(&results);
984
985 assert_eq!(tester.node_rects_total(), 1);
986 assert_eq!(tester.hit_test(p(50.0, 50.0)), vec![(dom(0), NodeId::ZERO)]);
987 }
988
989 #[test]
990 fn hit_test_respects_an_overflow_hidden_ancestor() {
991 let mut results = BTreeMap::new();
995 results.insert(
996 dom(0),
997 layout_result(
998 styled("div.clip { overflow: hidden; }"),
999 vec![
1000 hot(Some(0), Some((500.0, 500.0)), None),
1001 hot(Some(1), Some((100.0, 100.0)), Some(0)),
1002 hot(Some(2), Some((400.0, 400.0)), Some(1)),
1003 ],
1004 vec![p(0.0, 0.0), p(0.0, 0.0), p(0.0, 0.0)],
1005 Vec::new(),
1006 ),
1007 );
1008 let mut tester = CpuHitTester::new();
1009 tester.rebuild_from_layout(&results);
1010
1011 assert_eq!(
1012 tester.hit_test(p(50.0, 50.0)),
1013 vec![
1014 (dom(0), NodeId::new(2)),
1015 (dom(0), NodeId::new(1)),
1016 (dom(0), NodeId::new(0)),
1017 ],
1018 "inside the clip: all three nodes are hit, topmost first"
1019 );
1020 assert_eq!(
1021 tester.hit_test(p(200.0, 200.0)),
1022 vec![(dom(0), NodeId::new(0))],
1023 "outside the clip: the clipped-out child must not eat the event"
1024 );
1025 }
1026
1027 #[test]
1032 fn rebuild_from_layout_with_no_doms_is_a_no_op() {
1033 let mut tester = CpuHitTester::new();
1034 tester.rebuild_from_layout(&BTreeMap::new());
1035 assert_eq!(tester.node_rects_total(), 0);
1036 assert!(tester.hit_test(p(0.0, 0.0)).is_empty());
1037 }
1038
1039 #[test]
1040 fn rebuild_translates_and_clips_virtual_view_child_doms() {
1041 let mut results = BTreeMap::new();
1046 results.insert(
1047 dom(0),
1048 layout_result(
1049 styled(""),
1050 Vec::new(),
1051 Vec::new(),
1052 vec![virtual_view(1, r(100.0, 100.0, 50.0, 50.0))],
1053 ),
1054 );
1055 results.insert(
1056 dom(1),
1057 layout_result(
1058 styled(""),
1059 vec![hot(Some(1), Some((200.0, 200.0)), None)],
1060 vec![p(0.0, 0.0)],
1061 Vec::new(),
1062 ),
1063 );
1064
1065 let mut tester = CpuHitTester::new();
1066 tester.rebuild_from_layout(&results);
1067
1068 assert!(
1069 tester.hit_test(p(10.0, 10.0)).is_empty(),
1070 "the child's local (10,10) is not its window position"
1071 );
1072 assert_eq!(
1073 tester.hit_test(p(120.0, 120.0)),
1074 vec![(dom(1), NodeId::new(1))],
1075 "translated into the host's VirtualView bounds"
1076 );
1077 assert!(
1078 tester.hit_test(p(180.0, 180.0)).is_empty(),
1079 "inside the child's 200x200 rect but outside the 50x50 composite clip"
1080 );
1081 }
1082
1083 #[test]
1084 fn rebuild_accumulates_offsets_through_nested_virtual_views() {
1085 let mut results = BTreeMap::new();
1088 results.insert(
1089 dom(0),
1090 layout_result(
1091 styled(""),
1092 Vec::new(),
1093 Vec::new(),
1094 vec![virtual_view(1, r(10.0, 10.0, 200.0, 200.0))],
1095 ),
1096 );
1097 results.insert(
1098 dom(1),
1099 layout_result(
1100 styled(""),
1101 Vec::new(),
1102 Vec::new(),
1103 vec![virtual_view(2, r(5.0, 5.0, 100.0, 100.0))],
1104 ),
1105 );
1106 results.insert(
1107 dom(2),
1108 layout_result(
1109 styled(""),
1110 vec![hot(Some(1), Some((20.0, 20.0)), None)],
1111 vec![p(0.0, 0.0)],
1112 Vec::new(),
1113 ),
1114 );
1115
1116 let mut tester = CpuHitTester::new();
1117 tester.rebuild_from_layout(&results);
1118
1119 assert_eq!(
1120 tester.hit_test(p(16.0, 16.0)),
1121 vec![(dom(2), NodeId::new(1))]
1122 );
1123 assert!(
1124 tester.hit_test(p(14.0, 14.0)).is_empty(),
1125 "(14,14) is before the doubly-offset origin (15,15)"
1126 );
1127 assert!(tester.hit_test(p(36.0, 36.0)).is_empty());
1128 }
1129
1130 #[test]
1131 fn rebuild_ignores_virtual_views_pointing_at_a_missing_child_dom() {
1132 let mut results = BTreeMap::new();
1133 results.insert(
1134 dom(0),
1135 layout_result(
1136 styled(""),
1137 vec![hot(Some(0), Some((10.0, 10.0)), None)],
1138 vec![p(0.0, 0.0)],
1139 vec![virtual_view(42, r(0.0, 0.0, 10.0, 10.0))],
1140 ),
1141 );
1142
1143 let mut tester = CpuHitTester::new();
1144 tester.rebuild_from_layout(&results);
1145
1146 assert_eq!(tester.node_rects_total(), 1);
1147 assert_eq!(tester.hit_test(p(5.0, 5.0)), vec![(dom(0), NodeId::ZERO)]);
1148 }
1149
1150 #[test]
1151 fn rebuild_terminates_on_a_cyclic_virtual_view_graph() {
1152 let mut results = BTreeMap::new();
1156 results.insert(
1157 dom(1),
1158 layout_result(
1159 styled(""),
1160 vec![hot(Some(1), Some((10.0, 10.0)), None)],
1161 vec![p(0.0, 0.0)],
1162 vec![virtual_view(2, r(1.0, 1.0, 10.0, 10.0))],
1163 ),
1164 );
1165 results.insert(
1166 dom(2),
1167 layout_result(
1168 styled(""),
1169 vec![hot(Some(1), Some((10.0, 10.0)), None)],
1170 vec![p(0.0, 0.0)],
1171 vec![virtual_view(1, r(2.0, 2.0, 10.0, 10.0))],
1172 ),
1173 );
1174
1175 let mut tester = CpuHitTester::new();
1176 tester.rebuild_from_layout(&results);
1177 assert_eq!(tester.node_rects_total(), 2);
1178 }
1179
1180 #[test]
1181 fn rebuild_handles_a_virtual_view_with_hostile_bounds() {
1182 for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX] {
1185 let mut results = BTreeMap::new();
1186 results.insert(
1187 dom(0),
1188 layout_result(
1189 styled(""),
1190 Vec::new(),
1191 Vec::new(),
1192 vec![virtual_view(1, r(bad, bad, bad, bad))],
1193 ),
1194 );
1195 results.insert(
1196 dom(1),
1197 layout_result(
1198 styled(""),
1199 vec![hot(Some(1), Some((20.0, 20.0)), None)],
1200 vec![p(0.0, 0.0)],
1201 Vec::new(),
1202 ),
1203 );
1204
1205 let mut tester = CpuHitTester::new();
1206 tester.rebuild_from_layout(&results);
1207 assert_eq!(tester.node_rects_total(), 1);
1208 let _ = tester.hit_test(p(10.0, 10.0));
1210 let _ = tester.hit_test(p(f32::NAN, 0.0));
1211 }
1212 }
1213
1214 #[test]
1219 fn compute_node_clip_without_ancestors_or_dom_clip_is_unclipped() {
1220 let styled_dom = styled("");
1221 let nodes = vec![hot(Some(0), Some((10.0, 10.0)), None)];
1222 let positions: PositionVec = vec![p(0.0, 0.0)];
1223
1224 assert_eq!(
1225 compute_node_clip(&styled_dom, &nodes, &positions, 0, p(0.0, 0.0), None),
1226 None
1227 );
1228 }
1229
1230 #[test]
1231 fn compute_node_clip_out_of_bounds_node_index_does_not_panic() {
1232 let styled_dom = styled("");
1233 let nodes: Vec<LayoutNodeHot> = Vec::new();
1234 let positions: PositionVec = Vec::new();
1235
1236 for idx in [0_usize, 1, 999, usize::MAX] {
1237 assert_eq!(
1238 compute_node_clip(&styled_dom, &nodes, &positions, idx, p(0.0, 0.0), None),
1239 None
1240 );
1241 let clip = compute_node_clip(
1243 &styled_dom,
1244 &nodes,
1245 &positions,
1246 idx,
1247 p(0.0, 0.0),
1248 Some(r(1.0, 2.0, 3.0, 4.0)),
1249 );
1250 assert_eq!(clip, Some(r(1.0, 2.0, 3.0, 4.0)));
1251 }
1252 }
1253
1254 #[test]
1255 fn compute_node_clip_round_trips_a_dom_clip_when_no_ancestor_clips() {
1256 let styled_dom = styled("");
1260 let nodes = vec![hot(Some(0), Some((10.0, 10.0)), None)];
1261 let positions: PositionVec = vec![p(0.0, 0.0)];
1262 let dom_clip = r(100.0, 200.0, 50.0, 25.0);
1263
1264 let clip = compute_node_clip(
1265 &styled_dom,
1266 &nodes,
1267 &positions,
1268 0,
1269 p(100.0, 200.0),
1270 Some(dom_clip),
1271 )
1272 .expect("dom_clip must survive");
1273
1274 assert_eq!(clip.origin.x, dom_clip.origin.x);
1275 assert_eq!(clip.origin.y, dom_clip.origin.y);
1276 assert_eq!(clip.size.width, dom_clip.size.width);
1277 assert_eq!(clip.size.height, dom_clip.size.height);
1278 }
1279
1280 #[test]
1281 fn compute_node_clip_never_lets_nan_escape_into_the_clip_rect() {
1282 let styled_dom = styled("");
1283 let nodes = vec![hot(Some(0), Some((10.0, 10.0)), None)];
1284 let positions: PositionVec = vec![p(0.0, 0.0)];
1285
1286 for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
1287 for dom_clip in [
1288 r(bad, 0.0, 10.0, 10.0),
1289 r(0.0, bad, 10.0, 10.0),
1290 r(0.0, 0.0, bad, 10.0),
1291 r(0.0, 0.0, 10.0, bad),
1292 r(bad, bad, bad, bad),
1293 ] {
1294 let clip = compute_node_clip(
1295 &styled_dom,
1296 &nodes,
1297 &positions,
1298 0,
1299 p(0.0, 0.0),
1300 Some(dom_clip),
1301 )
1302 .expect("a dom_clip always yields a clip");
1303
1304 assert!(
1305 clip.origin.x.is_finite()
1306 && clip.origin.y.is_finite()
1307 && clip.size.width.is_finite()
1308 && clip.size.height.is_finite(),
1309 "clip {clip:?} from dom_clip {dom_clip:?} must stay finite"
1310 );
1311 assert!(clip.size.width >= 0.0 && clip.size.height >= 0.0);
1312 assert!(
1313 clip.max_x().is_finite() && clip.max_y().is_finite(),
1314 "origin + size must not overflow to inf/NaN"
1315 );
1316 let _ = point_in_rect(p(0.0, 0.0), &clip);
1319 }
1320 }
1321 }
1322
1323 #[test]
1324 fn compute_node_clip_clamps_an_infinite_dom_clip_to_clip_unbounded() {
1325 let styled_dom = styled("");
1326 let nodes = vec![hot(Some(0), Some((10.0, 10.0)), None)];
1327 let positions: PositionVec = vec![p(0.0, 0.0)];
1328
1329 let clip = compute_node_clip(
1330 &styled_dom,
1331 &nodes,
1332 &positions,
1333 0,
1334 p(0.0, 0.0),
1335 Some(LogicalRect {
1336 origin: p(0.0, 0.0),
1337 size: LogicalSize {
1338 width: f32::INFINITY,
1339 height: f32::INFINITY,
1340 },
1341 }),
1342 )
1343 .expect("a dom_clip always yields a clip");
1344
1345 assert_eq!(clip.origin.x, 0.0);
1346 assert_eq!(clip.origin.y, 0.0);
1347 assert_eq!(clip.size.width, CLIP_UNBOUNDED);
1348 assert_eq!(clip.size.height, CLIP_UNBOUNDED);
1349 assert!(point_in_rect(p(1.0e6, 1.0e6), &clip));
1350 }
1351
1352 #[test]
1353 fn compute_node_clip_saturates_a_negative_sized_dom_clip_to_zero_not_negative() {
1354 let styled_dom = styled("");
1355 let nodes = vec![hot(Some(0), Some((10.0, 10.0)), None)];
1356 let positions: PositionVec = vec![p(0.0, 0.0)];
1357
1358 let clip = compute_node_clip(
1359 &styled_dom,
1360 &nodes,
1361 &positions,
1362 0,
1363 p(0.0, 0.0),
1364 Some(r(100.0, 100.0, -50.0, -50.0)),
1365 )
1366 .expect("a dom_clip always yields a clip");
1367
1368 assert_eq!(clip.size.width, 0.0);
1369 assert_eq!(clip.size.height, 0.0);
1370 assert!(!point_in_rect(p(100.0, 100.0), &clip));
1371 assert!(!point_in_rect(p(75.0, 75.0), &clip));
1372 }
1373
1374 #[test]
1375 fn compute_node_clip_intersects_a_clipping_ancestor_with_the_dom_clip() {
1376 let styled_dom = styled("div.clip { overflow: hidden; }");
1379 let nodes = vec![
1380 hot(Some(0), Some((500.0, 500.0)), None),
1381 hot(Some(1), Some((100.0, 50.0)), Some(0)),
1382 hot(Some(2), Some((400.0, 400.0)), Some(1)),
1383 ];
1384 let positions: PositionVec = vec![p(0.0, 0.0), p(10.0, 10.0), p(10.0, 10.0)];
1385
1386 let clip = compute_node_clip(
1387 &styled_dom,
1388 &nodes,
1389 &positions,
1390 2,
1391 p(0.0, 0.0),
1392 Some(r(0.0, 0.0, 60.0, 60.0)),
1393 )
1394 .expect("an overflow:hidden ancestor must clip");
1395
1396 assert_eq!(clip.origin.x, 10.0);
1397 assert_eq!(clip.origin.y, 10.0);
1398 assert_eq!(clip.size.width, 50.0);
1399 assert_eq!(clip.size.height, 50.0);
1400 }
1401
1402 #[test]
1403 fn compute_node_clip_applies_the_offset_to_the_ancestor_box() {
1404 let styled_dom = styled("div.clip { overflow: hidden; }");
1405 let nodes = vec![
1406 hot(Some(0), Some((500.0, 500.0)), None),
1407 hot(Some(1), Some((100.0, 50.0)), Some(0)),
1408 hot(Some(2), Some((400.0, 400.0)), Some(1)),
1409 ];
1410 let positions: PositionVec = vec![p(0.0, 0.0), p(10.0, 10.0), p(10.0, 10.0)];
1411
1412 let clip = compute_node_clip(&styled_dom, &nodes, &positions, 2, p(1000.0, 2000.0), None)
1413 .expect("an overflow:hidden ancestor must clip");
1414
1415 assert_eq!(clip.origin.x, 1010.0);
1416 assert_eq!(clip.origin.y, 2010.0);
1417 assert_eq!(clip.size.width, 100.0);
1418 assert_eq!(clip.size.height, 50.0);
1419 }
1420
1421 #[test]
1422 fn compute_node_clip_leaves_the_unclipped_axis_unbounded() {
1423 let styled_dom = styled("div.clip { overflow-x: hidden; }");
1426 let nodes = vec![
1427 hot(Some(0), Some((500.0, 500.0)), None),
1428 hot(Some(1), Some((100.0, 50.0)), Some(0)),
1429 hot(Some(2), Some((400.0, 400.0)), Some(1)),
1430 ];
1431 let positions: PositionVec = vec![p(0.0, 0.0), p(10.0, 10.0), p(10.0, 10.0)];
1432
1433 let clip = compute_node_clip(&styled_dom, &nodes, &positions, 2, p(0.0, 0.0), None)
1434 .expect("overflow-x: hidden must clip the x axis");
1435
1436 assert_eq!(clip.origin.x, 10.0);
1437 assert_eq!(clip.size.width, 100.0);
1438 assert_eq!(clip.origin.y, -CLIP_UNBOUNDED);
1439 assert_eq!(clip.size.height, 2.0 * CLIP_UNBOUNDED);
1440 assert!(clip.max_y().is_finite());
1441
1442 assert!(point_in_rect(p(50.0, 900_000.0), &clip));
1445 assert!(!point_in_rect(p(500.0, 20.0), &clip));
1446 }
1447
1448 #[test]
1449 fn compute_node_clip_skips_a_clipping_ancestor_that_was_never_laid_out() {
1450 let styled_dom = styled("div.clip { overflow: hidden; }");
1453 let nodes = vec![
1454 hot(Some(0), Some((500.0, 500.0)), None),
1455 hot(Some(1), None, Some(0)), hot(Some(2), Some((400.0, 400.0)), Some(1)),
1457 ];
1458 let positions: PositionVec = vec![p(0.0, 0.0), p(10.0, 10.0), p(10.0, 10.0)];
1459
1460 assert_eq!(
1461 compute_node_clip(&styled_dom, &nodes, &positions, 2, p(0.0, 0.0), None),
1462 None
1463 );
1464 }
1465
1466 #[test]
1467 fn compute_node_clip_terminates_on_a_parent_cycle() {
1468 let styled_dom = styled("");
1472 let nodes = vec![
1473 hot(None, Some((10.0, 10.0)), Some(1)),
1474 hot(None, Some((10.0, 10.0)), Some(0)),
1475 ];
1476 let positions: PositionVec = vec![p(0.0, 0.0), p(0.0, 0.0)];
1477
1478 assert_eq!(
1479 compute_node_clip(&styled_dom, &nodes, &positions, 0, p(0.0, 0.0), None),
1480 None
1481 );
1482 assert_eq!(
1484 compute_node_clip(
1485 &styled_dom,
1486 &nodes,
1487 &positions,
1488 1,
1489 p(0.0, 0.0),
1490 Some(r(0.0, 0.0, 5.0, 5.0))
1491 ),
1492 Some(r(0.0, 0.0, 5.0, 5.0))
1493 );
1494 }
1495
1496 #[test]
1497 fn compute_node_clip_terminates_on_a_self_parent_cycle() {
1498 let styled_dom = styled("");
1499 let nodes = vec![hot(None, Some((10.0, 10.0)), Some(0))];
1500 let positions: PositionVec = vec![p(0.0, 0.0)];
1501
1502 assert_eq!(
1503 compute_node_clip(&styled_dom, &nodes, &positions, 0, p(0.0, 0.0), None),
1504 None
1505 );
1506 }
1507
1508 #[test]
1509 fn compute_node_clip_tolerates_a_parent_index_past_the_end_of_the_node_slice() {
1510 let styled_dom = styled("");
1511 let nodes = vec![hot(Some(0), Some((10.0, 10.0)), Some(usize::MAX))];
1512 let positions: PositionVec = vec![p(0.0, 0.0)];
1513
1514 assert_eq!(
1515 compute_node_clip(&styled_dom, &nodes, &positions, 0, p(0.0, 0.0), None),
1516 None
1517 );
1518 }
1519}