1use std::collections::BTreeMap;
21
22use azul_core::{
23 dom::{DomId, NodeId},
24 geom::{LogicalPosition, LogicalRect, LogicalSize},
25 styled_dom::StyledDom,
26};
27
28use crate::solver3::{getters::{get_overflow_x, get_overflow_y}, layout_tree::LayoutNodeHot, PositionVec};
29
30const CLIP_UNBOUNDED: f32 = 1.0e7;
35
36#[derive(Debug)]
46pub struct CpuHitTester {
47 node_rects: BTreeMap<DomId, Vec<HitTestEntry>>,
50}
51
52#[derive(Debug, Clone)]
54struct HitTestEntry {
55 node_id: NodeId,
57 rect: LogicalRect,
59 clip: Option<LogicalRect>,
61 pointer_events_none: bool,
63}
64
65impl Default for CpuHitTester {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl CpuHitTester {
72 #[must_use] pub const fn new() -> Self {
74 Self {
75 node_rects: BTreeMap::new(),
76 }
77 }
78
79 #[must_use] pub fn node_rects_total(&self) -> usize {
81 self.node_rects.values().map(Vec::len).sum()
82 }
83
84 pub fn rebuild_from_layout(
90 &mut self,
91 layout_results: &BTreeMap<DomId, crate::window::DomLayoutResult>,
92 ) {
93 self.node_rects.clear();
94
95 let mut placements: BTreeMap<DomId, LogicalRect> = BTreeMap::new();
107 for _ in 0..4 {
108 let mut changed = false;
110 for (host_dom, lr) in layout_results {
111 let host_offset = if host_dom.inner == 0 {
112 Some(LogicalPosition::zero())
113 } else {
114 placements.get(host_dom).map(|r| r.origin)
115 };
116 let Some(host_offset) = host_offset else { continue };
117 for item in &lr.display_list.items {
118 if let crate::solver3::display_list::DisplayListItem::VirtualView {
119 child_dom_id,
120 bounds,
121 ..
122 } = item
123 {
124 let b = *bounds.inner();
125 let absolute = LogicalRect {
126 origin: LogicalPosition {
127 x: b.origin.x + host_offset.x,
128 y: b.origin.y + host_offset.y,
129 },
130 size: b.size,
131 };
132 if placements.get(child_dom_id) != Some(&absolute) {
133 placements.insert(*child_dom_id, absolute);
134 changed = true;
135 }
136 }
137 }
138 }
139 if !changed {
140 break;
141 }
142 }
143
144 for (dom_id, layout_result) in layout_results {
145 let mut entries = Vec::new();
146
147 let positions = &layout_result.calculated_positions;
148 let nodes = &layout_result.layout_tree.nodes;
149 let styled_dom = &layout_result.styled_dom;
150
151 let (offset, dom_clip) = placements.get(dom_id).map_or_else(|| (LogicalPosition::zero(), None), |b| (b.origin, Some(*b)));
153
154 for (idx, node) in nodes.iter().enumerate() {
156 let Some(node_id) = node.dom_node_id else {
158 continue; };
160
161 let pos = match positions.get(idx) {
163 Some(p) => *p,
164 None => continue,
165 };
166
167 let Some(size) = node.used_size else {
169 continue;
170 };
171
172 let rect = LogicalRect {
173 origin: LogicalPosition {
174 x: pos.x + offset.x,
175 y: pos.y + offset.y,
176 },
177 size,
178 };
179
180 let clip = compute_node_clip(styled_dom, nodes, positions, idx, offset, dom_clip);
185
186 entries.push(HitTestEntry {
187 node_id,
188 rect,
189 clip,
190 pointer_events_none: false,
194 });
195 }
196
197 self.node_rects.insert(*dom_id, entries);
198 }
199 }
200
201 #[must_use] pub fn hit_test(
205 &self,
206 position: LogicalPosition,
207 ) -> Vec<(DomId, NodeId)> {
208 let mut results = Vec::new();
209
210 for (dom_id, entries) in &self.node_rects {
211 for entry in entries.iter().rev() {
213 if entry.pointer_events_none {
214 continue;
215 }
216
217 if let Some(ref clip) = entry.clip {
219 if !point_in_rect(position, clip) {
220 continue;
221 }
222 }
223
224 if point_in_rect(position, &entry.rect) {
226 results.push((*dom_id, entry.node_id));
227 }
228 }
229 }
230
231 results
232 }
233}
234
235fn point_in_rect(point: LogicalPosition, rect: &LogicalRect) -> bool {
237 point.x >= rect.origin.x
238 && point.x < rect.origin.x + rect.size.width
239 && point.y >= rect.origin.y
240 && point.y < rect.origin.y + rect.size.height
241}
242
243#[allow(clippy::similar_names)] fn compute_node_clip(
255 styled_dom: &StyledDom,
256 nodes: &[LayoutNodeHot],
257 positions: &PositionVec,
258 node_index: usize,
259 offset: LogicalPosition,
260 dom_clip: Option<LogicalRect>,
261) -> Option<LogicalRect> {
262 let (mut min_x, mut min_y, mut max_x, mut max_y) = (
264 f32::NEG_INFINITY,
265 f32::NEG_INFINITY,
266 f32::INFINITY,
267 f32::INFINITY,
268 );
269 let mut has_clip = false;
270 if let Some(dc) = dom_clip {
271 min_x = dc.min_x();
272 min_y = dc.min_y();
273 max_x = dc.max_x();
274 max_y = dc.max_y();
275 has_clip = true;
276 }
277
278 let styled_nodes = styled_dom.styled_nodes.as_container();
282 let mut cur = nodes.get(node_index).and_then(|n| n.parent);
283 let mut guard = 0usize;
284 while let Some(anc) = cur {
285 guard += 1;
286 if guard > nodes.len() {
287 break;
288 }
289 let Some(anc_node) = nodes.get(anc) else { break };
290 cur = anc_node.parent;
291
292 let Some(anc_dom_id) = anc_node.dom_node_id else {
293 continue;
294 };
295 let node_state = &styled_nodes[anc_dom_id].styled_node_state;
296 let clips_x = get_overflow_x(styled_dom, anc_dom_id, node_state).is_clipped();
297 let clips_y = get_overflow_y(styled_dom, anc_dom_id, node_state).is_clipped();
298 if !clips_x && !clips_y {
299 continue;
300 }
301 let (Some(pos), Some(size)) = (positions.get(anc), anc_node.used_size) else {
302 continue;
303 };
304 let (ax0, ay0) = (pos.x + offset.x, pos.y + offset.y);
305 if clips_x {
306 min_x = min_x.max(ax0);
307 max_x = max_x.min(ax0 + size.width);
308 has_clip = true;
309 }
310 if clips_y {
311 min_y = min_y.max(ay0);
312 max_y = max_y.min(ay0 + size.height);
313 has_clip = true;
314 }
315 }
316
317 if !has_clip {
318 return None;
319 }
320
321 if !min_x.is_finite() {
324 min_x = -CLIP_UNBOUNDED;
325 }
326 if !min_y.is_finite() {
327 min_y = -CLIP_UNBOUNDED;
328 }
329 if !max_x.is_finite() {
330 max_x = CLIP_UNBOUNDED;
331 }
332 if !max_y.is_finite() {
333 max_y = CLIP_UNBOUNDED;
334 }
335
336 Some(LogicalRect {
337 origin: LogicalPosition { x: min_x, y: min_y },
338 size: LogicalSize {
339 width: (max_x - min_x).max(0.0),
340 height: (max_y - min_y).max(0.0),
341 },
342 })
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn test_cpu_hit_tester_empty() {
351 let tester = CpuHitTester::new();
352 let results = tester.hit_test(LogicalPosition { x: 100.0, y: 100.0 });
353 assert!(results.is_empty());
354 }
355
356 #[test]
357 fn test_point_in_rect() {
358 let rect = LogicalRect {
359 origin: LogicalPosition { x: 10.0, y: 10.0 },
360 size: LogicalSize {
361 width: 100.0,
362 height: 50.0,
363 },
364 };
365
366 assert!(point_in_rect(LogicalPosition { x: 50.0, y: 30.0 }, &rect));
368 assert!(point_in_rect(LogicalPosition { x: 10.0, y: 10.0 }, &rect));
370 assert!(!point_in_rect(LogicalPosition { x: 5.0, y: 5.0 }, &rect));
372 assert!(!point_in_rect(LogicalPosition { x: 200.0, y: 30.0 }, &rect));
373 }
374}
375
376#[cfg(test)]
377#[allow(clippy::float_cmp)] mod autotest_generated {
379 use std::collections::HashMap;
380
381 use azul_core::dom::{Dom, FormattingContext};
382
383 use super::*;
384 use crate::{
385 solver3::{
386 display_list::{DisplayList, DisplayListItem, WindowLogicalRect},
387 layout_tree::LayoutTree,
388 },
389 window::DomLayoutResult,
390 };
391
392 fn p(x: f32, y: f32) -> LogicalPosition {
397 LogicalPosition { x, y }
398 }
399
400 fn r(x: f32, y: f32, width: f32, height: f32) -> LogicalRect {
401 LogicalRect {
402 origin: p(x, y),
403 size: LogicalSize { width, height },
404 }
405 }
406
407 fn dom(inner: usize) -> DomId {
408 DomId { inner }
409 }
410
411 fn hot(
414 dom_node_id: Option<usize>,
415 size: Option<(f32, f32)>,
416 parent: Option<usize>,
417 ) -> LayoutNodeHot {
418 LayoutNodeHot {
419 box_props: Default::default(),
420 dom_node_id: dom_node_id.map(NodeId::new),
421 used_size: size.map(|(width, height)| LogicalSize { width, height }),
422 formatting_context: FormattingContext::default(),
423 parent,
424 }
425 }
426
427 fn styled(css_src: &str) -> StyledDom {
429 let css = azul_css::parser2::new_from_str(css_src).0;
430 let mut d = Dom::create_body().with_children(
431 vec![Dom::create_div()
432 .with_class("clip".to_string().into())
433 .with_children(vec![Dom::create_div()].into())]
434 .into(),
435 );
436 StyledDom::create(&mut d, css)
437 }
438
439 fn layout_result(
440 styled_dom: StyledDom,
441 nodes: Vec<LayoutNodeHot>,
442 calculated_positions: PositionVec,
443 items: Vec<DisplayListItem>,
444 ) -> DomLayoutResult {
445 DomLayoutResult {
446 styled_dom,
447 layout_tree: LayoutTree {
448 nodes,
449 warm: Vec::new(),
450 cold: Vec::new(),
451 root: 0,
452 dom_to_layout: BTreeMap::new(),
453 children_arena: Vec::new(),
454 children_offsets: Vec::new(),
455 subtree_needs_intrinsic: Vec::new(),
456 },
457 calculated_positions,
458 viewport: LogicalRect::zero(),
459 display_list: DisplayList {
460 items,
461 ..Default::default()
462 },
463 scroll_ids: HashMap::new(),
464 scroll_id_to_node_id: HashMap::new(),
465 }
466 }
467
468 fn virtual_view(child: usize, bounds: LogicalRect) -> DisplayListItem {
469 DisplayListItem::VirtualView {
470 child_dom_id: dom(child),
471 bounds: WindowLogicalRect::new(bounds.origin, bounds.size),
472 clip_rect: WindowLogicalRect::new(bounds.origin, bounds.size),
473 }
474 }
475
476 const HOSTILE_F32: [f32; 8] = [
478 0.0,
479 -0.0,
480 f32::NAN,
481 f32::INFINITY,
482 f32::NEG_INFINITY,
483 f32::MAX,
484 f32::MIN,
485 f32::MIN_POSITIVE,
486 ];
487
488 #[test]
493 fn point_in_rect_is_half_open_top_left_inclusive_bottom_right_exclusive() {
494 let rect = r(10.0, 10.0, 100.0, 50.0);
495
496 assert!(point_in_rect(p(10.0, 10.0), &rect), "top-left is inclusive");
497 assert!(point_in_rect(p(109.999, 59.999), &rect));
498 assert!(
499 !point_in_rect(p(110.0, 30.0), &rect),
500 "right edge is exclusive"
501 );
502 assert!(
503 !point_in_rect(p(50.0, 60.0), &rect),
504 "bottom edge is exclusive"
505 );
506 assert!(!point_in_rect(p(110.0, 60.0), &rect));
507 }
508
509 #[test]
510 fn point_in_rect_zero_sized_rect_contains_nothing_not_even_its_origin() {
511 let rect = r(0.0, 0.0, 0.0, 0.0);
512 assert!(!point_in_rect(p(0.0, 0.0), &rect));
513 assert!(!point_in_rect(p(-0.0, -0.0), &rect));
514
515 let elsewhere = r(7.0, 9.0, 0.0, 0.0);
516 assert!(!point_in_rect(p(7.0, 9.0), &elsewhere));
517 }
518
519 #[test]
520 fn point_in_rect_negative_size_rect_is_empty() {
521 let rect = r(100.0, 100.0, -50.0, -50.0);
525 for x in [50.0_f32, 75.0, 99.0, 100.0, 125.0] {
526 for y in [50.0_f32, 75.0, 99.0, 100.0, 125.0] {
527 assert!(!point_in_rect(p(x, y), &rect), "({x}, {y}) must not hit");
528 }
529 }
530 }
531
532 #[test]
533 fn point_in_rect_negative_zero_origin_still_contains_zero() {
534 let rect = r(-0.0, -0.0, 10.0, 10.0);
536 assert!(point_in_rect(p(0.0, 0.0), &rect));
537 assert!(point_in_rect(p(-0.0, -0.0), &rect));
538
539 let zero_origin = r(0.0, 0.0, 10.0, 10.0);
540 assert!(point_in_rect(p(-0.0, -0.0), &zero_origin));
541 }
542
543 #[test]
544 fn point_in_rect_nan_point_never_hits() {
545 let rect = r(-1000.0, -1000.0, 5000.0, 5000.0);
546 assert!(!point_in_rect(p(f32::NAN, 0.0), &rect));
547 assert!(!point_in_rect(p(0.0, f32::NAN), &rect));
548 assert!(!point_in_rect(p(f32::NAN, f32::NAN), &rect));
549 }
550
551 #[test]
552 fn point_in_rect_nan_rect_never_hits() {
553 for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
554 let nan_origin = r(bad, 0.0, 10.0, 10.0);
555 let nan_size = r(0.0, 0.0, bad, 10.0);
556 let _ = point_in_rect(p(5.0, 5.0), &nan_origin);
560 let _ = point_in_rect(p(5.0, 5.0), &nan_size);
561 }
562 assert!(!point_in_rect(p(5.0, 5.0), &r(f32::NAN, 0.0, 10.0, 10.0)));
563 assert!(!point_in_rect(p(5.0, 5.0), &r(0.0, 0.0, f32::NAN, 10.0)));
564 }
565
566 #[test]
567 fn point_in_rect_infinite_extent_is_empty_which_is_why_clip_unbounded_exists() {
568 let infinite = LogicalRect {
573 origin: p(f32::NEG_INFINITY, f32::NEG_INFINITY),
574 size: LogicalSize {
575 width: f32::INFINITY,
576 height: f32::INFINITY,
577 },
578 };
579 assert!(!point_in_rect(p(0.0, 0.0), &infinite));
580 assert!(!point_in_rect(p(-1.0e6, 1.0e6), &infinite));
581 }
582
583 #[test]
584 fn point_in_rect_clip_unbounded_extent_contains_every_realistic_coordinate() {
585 let unbounded = r(
588 -CLIP_UNBOUNDED,
589 -CLIP_UNBOUNDED,
590 2.0 * CLIP_UNBOUNDED,
591 2.0 * CLIP_UNBOUNDED,
592 );
593 for c in [0.0_f32, -0.0, 1.0, -1.0, 99_999.0, -99_999.0, 1.0e6, -1.0e6] {
594 assert!(point_in_rect(p(c, c), &unbounded), "{c} must be inside");
595 }
596 assert!(!point_in_rect(p(f32::MAX, 0.0), &unbounded));
598 }
599
600 #[test]
601 fn point_in_rect_saturates_at_f32_max_without_panicking() {
602 let huge = r(f32::MAX, f32::MAX, f32::MAX, f32::MAX);
605 assert!(point_in_rect(p(f32::MAX, f32::MAX), &huge));
606 assert!(!point_in_rect(p(0.0, 0.0), &huge));
607
608 let from_zero = r(0.0, 0.0, f32::MAX, f32::MAX);
609 assert!(point_in_rect(p(0.0, 0.0), &from_zero));
610 assert!(
611 !point_in_rect(p(f32::MAX, f32::MAX), &from_zero),
612 "the far edge stays exclusive even at f32::MAX"
613 );
614 }
615
616 #[test]
617 fn point_in_rect_never_panics_for_any_hostile_f32_combination() {
618 for &x in &HOSTILE_F32 {
619 for &y in &HOSTILE_F32 {
620 for &w in &HOSTILE_F32 {
621 let rect = r(x, y, w, w);
622 let _ = point_in_rect(p(y, x), &rect);
623 }
624 }
625 }
626 }
627
628 #[test]
633 fn new_hit_tester_is_empty_and_matches_default() {
634 let tester = CpuHitTester::new();
635 assert_eq!(tester.node_rects_total(), 0);
636 assert!(tester.hit_test(p(0.0, 0.0)).is_empty());
637
638 let defaulted = CpuHitTester::default();
639 assert_eq!(defaulted.node_rects_total(), tester.node_rects_total());
640 }
641
642 #[test]
643 fn node_rects_total_sums_entries_across_doms_and_skips_unlaid_nodes() {
644 let mut results = BTreeMap::new();
645 results.insert(
647 dom(0),
648 layout_result(
649 styled(""),
650 vec![
651 hot(Some(0), Some((10.0, 10.0)), None),
652 hot(Some(1), Some((10.0, 10.0)), None),
653 hot(None, Some((10.0, 10.0)), None), hot(Some(2), None, None), ],
656 vec![p(0.0, 0.0), p(0.0, 0.0), p(0.0, 0.0), p(0.0, 0.0)],
657 Vec::new(),
658 ),
659 );
660 results.insert(
662 dom(1),
663 layout_result(
664 styled(""),
665 vec![hot(Some(0), Some((10.0, 10.0)), None)],
666 vec![p(0.0, 0.0)],
667 Vec::new(),
668 ),
669 );
670
671 let mut tester = CpuHitTester::new();
672 tester.rebuild_from_layout(&results);
673 assert_eq!(tester.node_rects_total(), 3);
674 }
675
676 #[test]
677 fn node_rects_total_does_not_grow_when_the_same_layout_is_rebuilt() {
678 let mut results = BTreeMap::new();
680 results.insert(
681 dom(0),
682 layout_result(
683 styled(""),
684 vec![hot(Some(0), Some((10.0, 10.0)), None)],
685 vec![p(0.0, 0.0)],
686 Vec::new(),
687 ),
688 );
689
690 let mut tester = CpuHitTester::new();
691 for _ in 0..16 {
692 tester.rebuild_from_layout(&results);
693 assert_eq!(tester.node_rects_total(), 1);
694 }
695
696 tester.rebuild_from_layout(&BTreeMap::new());
697 assert_eq!(tester.node_rects_total(), 0);
698 assert!(tester.hit_test(p(1.0, 1.0)).is_empty());
699 }
700
701 #[test]
706 fn hit_test_on_empty_tester_never_panics_for_hostile_positions() {
707 let tester = CpuHitTester::new();
708 for &x in &HOSTILE_F32 {
709 for &y in &HOSTILE_F32 {
710 assert!(tester.hit_test(p(x, y)).is_empty());
711 }
712 }
713 }
714
715 #[test]
716 fn hit_test_with_hostile_positions_against_a_real_node_returns_no_spurious_hits() {
717 let mut results = BTreeMap::new();
718 results.insert(
719 dom(0),
720 layout_result(
721 styled(""),
722 vec![hot(Some(0), Some((100.0, 100.0)), None)],
723 vec![p(0.0, 0.0)],
724 Vec::new(),
725 ),
726 );
727 let mut tester = CpuHitTester::new();
728 tester.rebuild_from_layout(&results);
729
730 assert_eq!(tester.hit_test(p(50.0, 50.0)).len(), 1);
732
733 for pos in [
734 p(f32::NAN, f32::NAN),
735 p(f32::NAN, 50.0),
736 p(50.0, f32::NAN),
737 p(f32::INFINITY, f32::INFINITY),
738 p(f32::NEG_INFINITY, f32::NEG_INFINITY),
739 p(f32::MAX, f32::MAX),
740 p(f32::MIN, f32::MIN),
741 ] {
742 assert!(
743 tester.hit_test(pos).is_empty(),
744 "({}, {}) must not hit a 0,0,100x100 node",
745 pos.x,
746 pos.y
747 );
748 }
749
750 assert_eq!(tester.hit_test(p(0.0, 0.0)).len(), 1);
752 assert_eq!(tester.hit_test(p(-0.0, -0.0)).len(), 1);
753 assert!(tester.hit_test(p(100.0, 100.0)).is_empty());
755 assert_eq!(tester.hit_test(p(99.999, 99.999)).len(), 1);
756 }
757
758 #[test]
759 fn hit_test_returns_topmost_first() {
760 let mut results = BTreeMap::new();
763 results.insert(
764 dom(0),
765 layout_result(
766 styled(""),
767 vec![
768 hot(Some(1), Some((100.0, 100.0)), None),
769 hot(Some(2), Some((100.0, 100.0)), None),
770 ],
771 vec![p(0.0, 0.0), p(0.0, 0.0)],
772 Vec::new(),
773 ),
774 );
775 let mut tester = CpuHitTester::new();
776 tester.rebuild_from_layout(&results);
777
778 assert_eq!(
779 tester.hit_test(p(50.0, 50.0)),
780 vec![(dom(0), NodeId::new(2)), (dom(0), NodeId::new(1))]
781 );
782 }
783
784 #[test]
785 fn hit_test_skips_nodes_with_no_calculated_position() {
786 let mut results = BTreeMap::new();
789 results.insert(
790 dom(0),
791 layout_result(
792 styled(""),
793 vec![
794 hot(Some(0), Some((100.0, 100.0)), None),
795 hot(Some(1), Some((100.0, 100.0)), None),
796 hot(Some(2), Some((100.0, 100.0)), None),
797 ],
798 vec![p(0.0, 0.0)], Vec::new(),
800 ),
801 );
802 let mut tester = CpuHitTester::new();
803 tester.rebuild_from_layout(&results);
804
805 assert_eq!(tester.node_rects_total(), 1);
806 assert_eq!(tester.hit_test(p(50.0, 50.0)), vec![(dom(0), NodeId::ZERO)]);
807 }
808
809 #[test]
810 fn hit_test_respects_an_overflow_hidden_ancestor() {
811 let mut results = BTreeMap::new();
815 results.insert(
816 dom(0),
817 layout_result(
818 styled("div.clip { overflow: hidden; }"),
819 vec![
820 hot(Some(0), Some((500.0, 500.0)), None),
821 hot(Some(1), Some((100.0, 100.0)), Some(0)),
822 hot(Some(2), Some((400.0, 400.0)), Some(1)),
823 ],
824 vec![p(0.0, 0.0), p(0.0, 0.0), p(0.0, 0.0)],
825 Vec::new(),
826 ),
827 );
828 let mut tester = CpuHitTester::new();
829 tester.rebuild_from_layout(&results);
830
831 assert_eq!(
832 tester.hit_test(p(50.0, 50.0)),
833 vec![
834 (dom(0), NodeId::new(2)),
835 (dom(0), NodeId::new(1)),
836 (dom(0), NodeId::new(0)),
837 ],
838 "inside the clip: all three nodes are hit, topmost first"
839 );
840 assert_eq!(
841 tester.hit_test(p(200.0, 200.0)),
842 vec![(dom(0), NodeId::new(0))],
843 "outside the clip: the clipped-out child must not eat the event"
844 );
845 }
846
847 #[test]
852 fn rebuild_from_layout_with_no_doms_is_a_no_op() {
853 let mut tester = CpuHitTester::new();
854 tester.rebuild_from_layout(&BTreeMap::new());
855 assert_eq!(tester.node_rects_total(), 0);
856 assert!(tester.hit_test(p(0.0, 0.0)).is_empty());
857 }
858
859 #[test]
860 fn rebuild_translates_and_clips_virtual_view_child_doms() {
861 let mut results = BTreeMap::new();
866 results.insert(
867 dom(0),
868 layout_result(
869 styled(""),
870 Vec::new(),
871 Vec::new(),
872 vec![virtual_view(1, r(100.0, 100.0, 50.0, 50.0))],
873 ),
874 );
875 results.insert(
876 dom(1),
877 layout_result(
878 styled(""),
879 vec![hot(Some(1), Some((200.0, 200.0)), None)],
880 vec![p(0.0, 0.0)],
881 Vec::new(),
882 ),
883 );
884
885 let mut tester = CpuHitTester::new();
886 tester.rebuild_from_layout(&results);
887
888 assert!(
889 tester.hit_test(p(10.0, 10.0)).is_empty(),
890 "the child's local (10,10) is not its window position"
891 );
892 assert_eq!(
893 tester.hit_test(p(120.0, 120.0)),
894 vec![(dom(1), NodeId::new(1))],
895 "translated into the host's VirtualView bounds"
896 );
897 assert!(
898 tester.hit_test(p(180.0, 180.0)).is_empty(),
899 "inside the child's 200x200 rect but outside the 50x50 composite clip"
900 );
901 }
902
903 #[test]
904 fn rebuild_accumulates_offsets_through_nested_virtual_views() {
905 let mut results = BTreeMap::new();
908 results.insert(
909 dom(0),
910 layout_result(
911 styled(""),
912 Vec::new(),
913 Vec::new(),
914 vec![virtual_view(1, r(10.0, 10.0, 200.0, 200.0))],
915 ),
916 );
917 results.insert(
918 dom(1),
919 layout_result(
920 styled(""),
921 Vec::new(),
922 Vec::new(),
923 vec![virtual_view(2, r(5.0, 5.0, 100.0, 100.0))],
924 ),
925 );
926 results.insert(
927 dom(2),
928 layout_result(
929 styled(""),
930 vec![hot(Some(1), Some((20.0, 20.0)), None)],
931 vec![p(0.0, 0.0)],
932 Vec::new(),
933 ),
934 );
935
936 let mut tester = CpuHitTester::new();
937 tester.rebuild_from_layout(&results);
938
939 assert_eq!(
940 tester.hit_test(p(16.0, 16.0)),
941 vec![(dom(2), NodeId::new(1))]
942 );
943 assert!(
944 tester.hit_test(p(14.0, 14.0)).is_empty(),
945 "(14,14) is before the doubly-offset origin (15,15)"
946 );
947 assert!(tester.hit_test(p(36.0, 36.0)).is_empty());
948 }
949
950 #[test]
951 fn rebuild_ignores_virtual_views_pointing_at_a_missing_child_dom() {
952 let mut results = BTreeMap::new();
953 results.insert(
954 dom(0),
955 layout_result(
956 styled(""),
957 vec![hot(Some(0), Some((10.0, 10.0)), None)],
958 vec![p(0.0, 0.0)],
959 vec![virtual_view(42, r(0.0, 0.0, 10.0, 10.0))],
960 ),
961 );
962
963 let mut tester = CpuHitTester::new();
964 tester.rebuild_from_layout(&results);
965
966 assert_eq!(tester.node_rects_total(), 1);
967 assert_eq!(tester.hit_test(p(5.0, 5.0)), vec![(dom(0), NodeId::ZERO)]);
968 }
969
970 #[test]
971 fn rebuild_terminates_on_a_cyclic_virtual_view_graph() {
972 let mut results = BTreeMap::new();
976 results.insert(
977 dom(1),
978 layout_result(
979 styled(""),
980 vec![hot(Some(1), Some((10.0, 10.0)), None)],
981 vec![p(0.0, 0.0)],
982 vec![virtual_view(2, r(1.0, 1.0, 10.0, 10.0))],
983 ),
984 );
985 results.insert(
986 dom(2),
987 layout_result(
988 styled(""),
989 vec![hot(Some(1), Some((10.0, 10.0)), None)],
990 vec![p(0.0, 0.0)],
991 vec![virtual_view(1, r(2.0, 2.0, 10.0, 10.0))],
992 ),
993 );
994
995 let mut tester = CpuHitTester::new();
996 tester.rebuild_from_layout(&results);
997 assert_eq!(tester.node_rects_total(), 2);
998 }
999
1000 #[test]
1001 fn rebuild_handles_a_virtual_view_with_hostile_bounds() {
1002 for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX] {
1005 let mut results = BTreeMap::new();
1006 results.insert(
1007 dom(0),
1008 layout_result(
1009 styled(""),
1010 Vec::new(),
1011 Vec::new(),
1012 vec![virtual_view(1, r(bad, bad, bad, bad))],
1013 ),
1014 );
1015 results.insert(
1016 dom(1),
1017 layout_result(
1018 styled(""),
1019 vec![hot(Some(1), Some((20.0, 20.0)), None)],
1020 vec![p(0.0, 0.0)],
1021 Vec::new(),
1022 ),
1023 );
1024
1025 let mut tester = CpuHitTester::new();
1026 tester.rebuild_from_layout(&results);
1027 assert_eq!(tester.node_rects_total(), 1);
1028 let _ = tester.hit_test(p(10.0, 10.0));
1030 let _ = tester.hit_test(p(f32::NAN, 0.0));
1031 }
1032 }
1033
1034 #[test]
1039 fn compute_node_clip_without_ancestors_or_dom_clip_is_unclipped() {
1040 let styled_dom = styled("");
1041 let nodes = vec![hot(Some(0), Some((10.0, 10.0)), None)];
1042 let positions: PositionVec = vec![p(0.0, 0.0)];
1043
1044 assert_eq!(
1045 compute_node_clip(&styled_dom, &nodes, &positions, 0, p(0.0, 0.0), None),
1046 None
1047 );
1048 }
1049
1050 #[test]
1051 fn compute_node_clip_out_of_bounds_node_index_does_not_panic() {
1052 let styled_dom = styled("");
1053 let nodes: Vec<LayoutNodeHot> = Vec::new();
1054 let positions: PositionVec = Vec::new();
1055
1056 for idx in [0_usize, 1, 999, usize::MAX] {
1057 assert_eq!(
1058 compute_node_clip(&styled_dom, &nodes, &positions, idx, p(0.0, 0.0), None),
1059 None
1060 );
1061 let clip = compute_node_clip(
1063 &styled_dom,
1064 &nodes,
1065 &positions,
1066 idx,
1067 p(0.0, 0.0),
1068 Some(r(1.0, 2.0, 3.0, 4.0)),
1069 );
1070 assert_eq!(clip, Some(r(1.0, 2.0, 3.0, 4.0)));
1071 }
1072 }
1073
1074 #[test]
1075 fn compute_node_clip_round_trips_a_dom_clip_when_no_ancestor_clips() {
1076 let styled_dom = styled("");
1080 let nodes = vec![hot(Some(0), Some((10.0, 10.0)), None)];
1081 let positions: PositionVec = vec![p(0.0, 0.0)];
1082 let dom_clip = r(100.0, 200.0, 50.0, 25.0);
1083
1084 let clip = compute_node_clip(
1085 &styled_dom,
1086 &nodes,
1087 &positions,
1088 0,
1089 p(100.0, 200.0),
1090 Some(dom_clip),
1091 )
1092 .expect("dom_clip must survive");
1093
1094 assert_eq!(clip.origin.x, dom_clip.origin.x);
1095 assert_eq!(clip.origin.y, dom_clip.origin.y);
1096 assert_eq!(clip.size.width, dom_clip.size.width);
1097 assert_eq!(clip.size.height, dom_clip.size.height);
1098 }
1099
1100 #[test]
1101 fn compute_node_clip_never_lets_nan_escape_into_the_clip_rect() {
1102 let styled_dom = styled("");
1103 let nodes = vec![hot(Some(0), Some((10.0, 10.0)), None)];
1104 let positions: PositionVec = vec![p(0.0, 0.0)];
1105
1106 for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
1107 for dom_clip in [
1108 r(bad, 0.0, 10.0, 10.0),
1109 r(0.0, bad, 10.0, 10.0),
1110 r(0.0, 0.0, bad, 10.0),
1111 r(0.0, 0.0, 10.0, bad),
1112 r(bad, bad, bad, bad),
1113 ] {
1114 let clip = compute_node_clip(
1115 &styled_dom,
1116 &nodes,
1117 &positions,
1118 0,
1119 p(0.0, 0.0),
1120 Some(dom_clip),
1121 )
1122 .expect("a dom_clip always yields a clip");
1123
1124 assert!(
1125 clip.origin.x.is_finite()
1126 && clip.origin.y.is_finite()
1127 && clip.size.width.is_finite()
1128 && clip.size.height.is_finite(),
1129 "clip {clip:?} from dom_clip {dom_clip:?} must stay finite"
1130 );
1131 assert!(clip.size.width >= 0.0 && clip.size.height >= 0.0);
1132 assert!(
1133 clip.max_x().is_finite() && clip.max_y().is_finite(),
1134 "origin + size must not overflow to inf/NaN"
1135 );
1136 let _ = point_in_rect(p(0.0, 0.0), &clip);
1139 }
1140 }
1141 }
1142
1143 #[test]
1144 fn compute_node_clip_clamps_an_infinite_dom_clip_to_clip_unbounded() {
1145 let styled_dom = styled("");
1146 let nodes = vec![hot(Some(0), Some((10.0, 10.0)), None)];
1147 let positions: PositionVec = vec![p(0.0, 0.0)];
1148
1149 let clip = compute_node_clip(
1150 &styled_dom,
1151 &nodes,
1152 &positions,
1153 0,
1154 p(0.0, 0.0),
1155 Some(LogicalRect {
1156 origin: p(0.0, 0.0),
1157 size: LogicalSize {
1158 width: f32::INFINITY,
1159 height: f32::INFINITY,
1160 },
1161 }),
1162 )
1163 .expect("a dom_clip always yields a clip");
1164
1165 assert_eq!(clip.origin.x, 0.0);
1166 assert_eq!(clip.origin.y, 0.0);
1167 assert_eq!(clip.size.width, CLIP_UNBOUNDED);
1168 assert_eq!(clip.size.height, CLIP_UNBOUNDED);
1169 assert!(point_in_rect(p(1.0e6, 1.0e6), &clip));
1170 }
1171
1172 #[test]
1173 fn compute_node_clip_saturates_a_negative_sized_dom_clip_to_zero_not_negative() {
1174 let styled_dom = styled("");
1175 let nodes = vec![hot(Some(0), Some((10.0, 10.0)), None)];
1176 let positions: PositionVec = vec![p(0.0, 0.0)];
1177
1178 let clip = compute_node_clip(
1179 &styled_dom,
1180 &nodes,
1181 &positions,
1182 0,
1183 p(0.0, 0.0),
1184 Some(r(100.0, 100.0, -50.0, -50.0)),
1185 )
1186 .expect("a dom_clip always yields a clip");
1187
1188 assert_eq!(clip.size.width, 0.0);
1189 assert_eq!(clip.size.height, 0.0);
1190 assert!(!point_in_rect(p(100.0, 100.0), &clip));
1191 assert!(!point_in_rect(p(75.0, 75.0), &clip));
1192 }
1193
1194 #[test]
1195 fn compute_node_clip_intersects_a_clipping_ancestor_with_the_dom_clip() {
1196 let styled_dom = styled("div.clip { overflow: hidden; }");
1199 let nodes = vec![
1200 hot(Some(0), Some((500.0, 500.0)), None),
1201 hot(Some(1), Some((100.0, 50.0)), Some(0)),
1202 hot(Some(2), Some((400.0, 400.0)), Some(1)),
1203 ];
1204 let positions: PositionVec = vec![p(0.0, 0.0), p(10.0, 10.0), p(10.0, 10.0)];
1205
1206 let clip = compute_node_clip(
1207 &styled_dom,
1208 &nodes,
1209 &positions,
1210 2,
1211 p(0.0, 0.0),
1212 Some(r(0.0, 0.0, 60.0, 60.0)),
1213 )
1214 .expect("an overflow:hidden ancestor must clip");
1215
1216 assert_eq!(clip.origin.x, 10.0);
1217 assert_eq!(clip.origin.y, 10.0);
1218 assert_eq!(clip.size.width, 50.0);
1219 assert_eq!(clip.size.height, 50.0);
1220 }
1221
1222 #[test]
1223 fn compute_node_clip_applies_the_offset_to_the_ancestor_box() {
1224 let styled_dom = styled("div.clip { overflow: hidden; }");
1225 let nodes = vec![
1226 hot(Some(0), Some((500.0, 500.0)), None),
1227 hot(Some(1), Some((100.0, 50.0)), Some(0)),
1228 hot(Some(2), Some((400.0, 400.0)), Some(1)),
1229 ];
1230 let positions: PositionVec = vec![p(0.0, 0.0), p(10.0, 10.0), p(10.0, 10.0)];
1231
1232 let clip = compute_node_clip(&styled_dom, &nodes, &positions, 2, p(1000.0, 2000.0), None)
1233 .expect("an overflow:hidden ancestor must clip");
1234
1235 assert_eq!(clip.origin.x, 1010.0);
1236 assert_eq!(clip.origin.y, 2010.0);
1237 assert_eq!(clip.size.width, 100.0);
1238 assert_eq!(clip.size.height, 50.0);
1239 }
1240
1241 #[test]
1242 fn compute_node_clip_leaves_the_unclipped_axis_unbounded() {
1243 let styled_dom = styled("div.clip { overflow-x: hidden; }");
1246 let nodes = vec![
1247 hot(Some(0), Some((500.0, 500.0)), None),
1248 hot(Some(1), Some((100.0, 50.0)), Some(0)),
1249 hot(Some(2), Some((400.0, 400.0)), Some(1)),
1250 ];
1251 let positions: PositionVec = vec![p(0.0, 0.0), p(10.0, 10.0), p(10.0, 10.0)];
1252
1253 let clip = compute_node_clip(&styled_dom, &nodes, &positions, 2, p(0.0, 0.0), None)
1254 .expect("overflow-x: hidden must clip the x axis");
1255
1256 assert_eq!(clip.origin.x, 10.0);
1257 assert_eq!(clip.size.width, 100.0);
1258 assert_eq!(clip.origin.y, -CLIP_UNBOUNDED);
1259 assert_eq!(clip.size.height, 2.0 * CLIP_UNBOUNDED);
1260 assert!(clip.max_y().is_finite());
1261
1262 assert!(point_in_rect(p(50.0, 900_000.0), &clip));
1265 assert!(!point_in_rect(p(500.0, 20.0), &clip));
1266 }
1267
1268 #[test]
1269 fn compute_node_clip_skips_a_clipping_ancestor_that_was_never_laid_out() {
1270 let styled_dom = styled("div.clip { overflow: hidden; }");
1273 let nodes = vec![
1274 hot(Some(0), Some((500.0, 500.0)), None),
1275 hot(Some(1), None, Some(0)), hot(Some(2), Some((400.0, 400.0)), Some(1)),
1277 ];
1278 let positions: PositionVec = vec![p(0.0, 0.0), p(10.0, 10.0), p(10.0, 10.0)];
1279
1280 assert_eq!(
1281 compute_node_clip(&styled_dom, &nodes, &positions, 2, p(0.0, 0.0), None),
1282 None
1283 );
1284 }
1285
1286 #[test]
1287 fn compute_node_clip_terminates_on_a_parent_cycle() {
1288 let styled_dom = styled("");
1292 let nodes = vec![
1293 hot(None, Some((10.0, 10.0)), Some(1)),
1294 hot(None, Some((10.0, 10.0)), Some(0)),
1295 ];
1296 let positions: PositionVec = vec![p(0.0, 0.0), p(0.0, 0.0)];
1297
1298 assert_eq!(
1299 compute_node_clip(&styled_dom, &nodes, &positions, 0, p(0.0, 0.0), None),
1300 None
1301 );
1302 assert_eq!(
1304 compute_node_clip(
1305 &styled_dom,
1306 &nodes,
1307 &positions,
1308 1,
1309 p(0.0, 0.0),
1310 Some(r(0.0, 0.0, 5.0, 5.0))
1311 ),
1312 Some(r(0.0, 0.0, 5.0, 5.0))
1313 );
1314 }
1315
1316 #[test]
1317 fn compute_node_clip_terminates_on_a_self_parent_cycle() {
1318 let styled_dom = styled("");
1319 let nodes = vec![hot(None, Some((10.0, 10.0)), Some(0))];
1320 let positions: PositionVec = vec![p(0.0, 0.0)];
1321
1322 assert_eq!(
1323 compute_node_clip(&styled_dom, &nodes, &positions, 0, p(0.0, 0.0), None),
1324 None
1325 );
1326 }
1327
1328 #[test]
1329 fn compute_node_clip_tolerates_a_parent_index_past_the_end_of_the_node_slice() {
1330 let styled_dom = styled("");
1331 let nodes = vec![hot(Some(0), Some((10.0, 10.0)), Some(usize::MAX))];
1332 let positions: PositionVec = vec![p(0.0, 0.0)];
1333
1334 assert_eq!(
1335 compute_node_clip(&styled_dom, &nodes, &positions, 0, p(0.0, 0.0), None),
1336 None
1337 );
1338 }
1339}