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}