Skip to main content

retroglyph_widgets/interact/
hit.rs

1//! [`HitTester`]: resolve a pointer position to the topmost widget id
2//! occupying it.
3
4use retroglyph_core::{Pos, Rect};
5
6/// A per-frame registry of `(Rect, Id)` pairs, queried by pointer position
7/// to find the topmost widget under a point.
8///
9/// Standalone and headless -- no [`Backend`](retroglyph_core::Backend)
10/// dependency, so it's usable (and unit-testable) without a
11/// [`Terminal`](retroglyph_core::Terminal) or any drawing at all, e.g. for
12/// hand-rolled hit-testing outside of [`Interaction`](crate::Interaction).
13///
14/// Registrations are draw-ordered: a later [`push`](Self::push) means drawn
15/// (and therefore visually on top) later, so [`topmost_at`](Self::topmost_at)
16/// scans back-to-front and returns the *last* match. This mirrors the
17/// painter's algorithm every widget in this crate already draws with.
18#[derive(Debug, Clone)]
19pub struct HitTester<Id> {
20    hits: Vec<(Rect, Id)>,
21}
22
23impl<Id> HitTester<Id> {
24    /// An empty registry.
25    #[must_use]
26    pub const fn new() -> Self {
27        Self { hits: Vec::new() }
28    }
29
30    /// Register `id` as occupying `rect`, on top of everything registered
31    /// so far this pass.
32    pub fn push(&mut self, rect: Rect, id: Id) {
33        self.hits.push((rect, id));
34    }
35
36    /// Discard all registrations, e.g. at the start of a new frame's draw
37    /// pass.
38    pub fn clear(&mut self) {
39        self.hits.clear();
40    }
41
42    /// Number of rects currently registered.
43    #[must_use]
44    pub const fn len(&self) -> usize {
45        self.hits.len()
46    }
47
48    /// `true` if nothing has been registered.
49    #[must_use]
50    pub const fn is_empty(&self) -> bool {
51        self.hits.is_empty()
52    }
53}
54
55impl<Id: Copy> HitTester<Id> {
56    /// The id of the topmost (most recently [`push`](Self::push)ed)
57    /// registration whose rect contains `pos`, if any.
58    #[must_use]
59    pub fn topmost_at(&self, pos: Pos) -> Option<Id> {
60        self.hits
61            .iter()
62            .rev()
63            .find(|(rect, _)| rect.contains_pos(pos))
64            .map(|&(_, id)| id)
65    }
66}
67
68// Not `#[derive(Default)]`: that would add an unnecessary `Id: Default`
69// bound to the generated impl, even though an empty `Vec<(Rect, Id)>` never
70// needs one.
71impl<Id> Default for HitTester<Id> {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn topmost_at_prefers_the_most_recently_pushed_overlap() {
83        let mut hits = HitTester::new();
84        hits.push(Rect::new(0, 0, 10, 10), "back");
85        hits.push(Rect::new(5, 5, 10, 10), "front");
86
87        assert_eq!(hits.topmost_at(Pos::new(6, 6)), Some("front")); // overlap
88        assert_eq!(hits.topmost_at(Pos::new(1, 1)), Some("back")); // back only
89        assert_eq!(hits.topmost_at(Pos::new(20, 20)), None); // neither
90    }
91
92    #[test]
93    fn clear_empties_the_registry() {
94        let mut hits = HitTester::new();
95        hits.push(Rect::new(0, 0, 5, 5), 1);
96        assert!(!hits.is_empty());
97        hits.clear();
98        assert!(hits.is_empty());
99        assert_eq!(hits.len(), 0);
100        assert_eq!(hits.topmost_at(Pos::new(0, 0)), None);
101    }
102
103    #[test]
104    fn default_is_empty() {
105        let hits: HitTester<()> = HitTester::default();
106        assert!(hits.is_empty());
107    }
108}