ailloli_ui_runtime/input/hit_test.rs
1//! Rectangle hit-testing primitives for retained elements.
2
3use ailloli_ui_core::{ElementId, Point, Rect};
4
5/// Stateless rectangle hit-testing helper.
6///
7/// Rectangles and points are expected in the same logical-pixel coordinate
8/// space. The engine performs no allocation and does not validate geometry.
9///
10/// # Examples
11///
12/// ```
13/// use ailloli_ui_core::{ElementId, Point, Rect};
14/// use ailloli_ui_runtime::input::HitTestEngine;
15/// let engine = HitTestEngine;
16/// let rects = [(ElementId(1), Rect::new(0.0, 0.0, 10.0, 10.0))];
17/// assert_eq!(engine.hit_test(&rects, Point::new(5.0, 5.0), None), Some(ElementId(1)));
18/// ```
19#[derive(Debug, Default, Clone)]
20pub struct HitTestEngine;
21
22/// Provides the operations defined for HitTestEngine.
23impl HitTestEngine {
24 /// Returns the last rectangle containing `pos`, subject to an optional clip.
25 ///
26 /// Reverse iteration makes later entries topmost. If `clip` is `Some` and
27 /// does not contain the point, no candidate is examined and `None` is
28 /// returned. Edge inclusion and non-finite behavior follow [`Rect::contains`].
29 ///
30 /// # Examples
31 ///
32 /// ```
33 /// use ailloli_ui_core::{ElementId, Point, Rect};
34 /// use ailloli_ui_runtime::input::HitTestEngine;
35 /// let rects = [
36 /// (ElementId(1), Rect::new(0.0, 0.0, 10.0, 10.0)),
37 /// (ElementId(2), Rect::new(0.0, 0.0, 10.0, 10.0)),
38 /// ];
39 /// assert_eq!(HitTestEngine.hit_test(&rects, Point::new(2.0, 2.0), None), Some(ElementId(2)));
40 /// ```
41 pub fn hit_test(
42 &self,
43 rects: &[(ElementId, Rect)],
44 pos: Point,
45 clip: Option<Rect>,
46 ) -> Option<ElementId> {
47 if let Some(c) = clip {
48 if !c.contains(pos.x, pos.y) {
49 return None;
50 }
51 }
52 rects
53 .iter()
54 .rev()
55 .find(|(_, r)| r.contains(pos.x, pos.y))
56 .map(|(id, _)| *id)
57 }
58
59 /// Hit-tests overlay rects first, then base (modal / chrome above content).
60 ///
61 /// Each slice is internally topmost-last. The same optional clip is applied
62 /// to both strata, and an overlay hit prevents examining base rectangles.
63 ///
64 /// # Examples
65 ///
66 /// ```
67 /// use ailloli_ui_core::{ElementId, Point, Rect};
68 /// use ailloli_ui_runtime::input::HitTestEngine;
69 /// let base = [(ElementId(1), Rect::new(0.0, 0.0, 10.0, 10.0))];
70 /// let overlay = [(ElementId(2), Rect::new(0.0, 0.0, 4.0, 4.0))];
71 /// assert_eq!(HitTestEngine.hit_test_overlay_first(
72 /// &overlay, &base, Point::new(2.0, 2.0), None,
73 /// ), Some(ElementId(2)));
74 /// ```
75 pub fn hit_test_overlay_first(
76 &self,
77 overlay: &[(ElementId, Rect)],
78 base: &[(ElementId, Rect)],
79 pos: Point,
80 clip: Option<Rect>,
81 ) -> Option<ElementId> {
82 self.hit_test(overlay, pos, clip)
83 .or_else(|| self.hit_test(base, pos, clip))
84 }
85}
86
87#[cfg(test)]
88/// Tests implementation details.
89mod tests {
90 use super::*;
91
92 #[test]
93 /// Implements the hit_test_respects_clip helper used by this module.
94 fn hit_test_respects_clip() {
95 let engine = HitTestEngine;
96 let rects = vec![(ElementId(1), Rect::new(0.0, 0.0, 10.0, 10.0))];
97 let clip = Some(Rect::new(0.0, 0.0, 5.0, 5.0));
98 assert_eq!(engine.hit_test(&rects, Point::new(7.0, 2.0), clip), None);
99 }
100
101 #[test]
102 /// Verifies that overlay first prefers top layer.
103 fn overlay_first_prefers_top_layer() {
104 let engine = HitTestEngine;
105 let base = vec![(ElementId(1), Rect::new(0.0, 0.0, 10.0, 10.0))];
106 let overlay = vec![(ElementId(2), Rect::new(2.0, 2.0, 4.0, 4.0))];
107 let p = Point::new(4.0, 4.0);
108 assert_eq!(
109 engine.hit_test_overlay_first(&overlay, &base, p, None),
110 Some(ElementId(2))
111 );
112 }
113}