1use alloc::vec::Vec;
5
6use retroglyph_core::grid::{Pos, Rect};
7
8#[derive(Debug, Clone)]
26pub struct HitTester<Id> {
27 hits: Vec<Entry<Id>>,
28}
29
30#[derive(Debug, Clone)]
31enum Entry<Id> {
32 Hit(Rect, Id),
33 Barrier(Rect),
34}
35
36impl<Id> HitTester<Id> {
37 #[must_use]
39 pub const fn new() -> Self {
40 Self { hits: Vec::new() }
41 }
42
43 pub fn push(&mut self, rect: Rect, id: Id) {
46 self.hits.push(Entry::Hit(rect, id));
47 }
48
49 pub fn push_barrier(&mut self, rect: Rect) {
52 self.hits.push(Entry::Barrier(rect));
53 }
54
55 pub fn clear(&mut self) {
58 self.hits.clear();
59 }
60
61 #[must_use]
63 pub const fn len(&self) -> usize {
64 self.hits.len()
65 }
66
67 #[must_use]
69 pub const fn is_empty(&self) -> bool {
70 self.hits.is_empty()
71 }
72}
73
74impl<Id: Copy> HitTester<Id> {
75 #[must_use]
82 pub fn topmost_at(&self, pos: Pos) -> Option<Id> {
83 for entry in self.hits.iter().rev() {
84 match entry {
85 Entry::Hit(rect, id) if rect.contains_pos(pos) => return Some(*id),
86 Entry::Barrier(rect) if rect.contains_pos(pos) => return None,
87 Entry::Hit(_, _) | Entry::Barrier(_) => {}
88 }
89 }
90 None
91 }
92}
93
94impl<Id> Default for HitTester<Id> {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn topmost_at_prefers_the_most_recently_pushed_overlap() {
109 let mut hits = HitTester::new();
110 hits.push(Rect::new(0, 0, 10, 10), "back");
111 hits.push(Rect::new(5, 5, 10, 10), "front");
112
113 assert_eq!(hits.topmost_at(Pos::new(6, 6)), Some("front")); assert_eq!(hits.topmost_at(Pos::new(1, 1)), Some("back")); assert_eq!(hits.topmost_at(Pos::new(20, 20)), None); }
117
118 #[test]
119 fn clear_empties_the_registry() {
120 let mut hits = HitTester::new();
121 hits.push(Rect::new(0, 0, 5, 5), 1);
122 assert!(!hits.is_empty());
123 hits.clear();
124 assert!(hits.is_empty());
125 assert_eq!(hits.len(), 0);
126 assert_eq!(hits.topmost_at(Pos::new(0, 0)), None);
127 }
128
129 #[test]
130 fn default_is_empty() {
131 let hits: HitTester<()> = HitTester::default();
132 assert!(hits.is_empty());
133 }
134
135 #[test]
136 fn a_barrier_hides_hits_registered_before_it_inside_its_own_rect() {
137 let mut hits = HitTester::new();
138 hits.push(Rect::new(0, 0, 10, 10), "behind");
139 hits.push_barrier(Rect::new(2, 2, 4, 4));
140
141 assert_eq!(hits.topmost_at(Pos::new(3, 3)), None); assert_eq!(hits.topmost_at(Pos::new(0, 0)), Some("behind")); }
144
145 #[test]
146 fn a_hit_registered_after_a_barrier_still_wins_inside_it() {
147 let mut hits = HitTester::new();
148 hits.push(Rect::new(0, 0, 10, 10), "behind");
149 hits.push_barrier(Rect::new(2, 2, 4, 4));
150 hits.push(Rect::new(3, 3, 1, 1), "in front");
151
152 assert_eq!(hits.topmost_at(Pos::new(3, 3)), Some("in front"));
153 }
154
155 #[test]
156 fn a_barrier_does_not_affect_positions_outside_its_own_rect() {
157 let mut hits = HitTester::new();
158 hits.push(Rect::new(0, 0, 10, 10), "back");
159 hits.push_barrier(Rect::new(2, 2, 2, 2));
160 hits.push(Rect::new(5, 5, 3, 3), "front");
161
162 assert_eq!(hits.topmost_at(Pos::new(6, 6)), Some("front"));
164 assert_eq!(hits.topmost_at(Pos::new(0, 0)), Some("back"));
165 }
166
167 #[test]
168 fn an_older_barrier_does_not_shadow_a_newer_ones_uncovered_region() {
169 let mut hits = HitTester::new();
170 hits.push(Rect::new(0, 0, 10, 10), "back");
171 hits.push_barrier(Rect::new(0, 0, 10, 10));
172 hits.push(Rect::new(2, 2, 2, 2), "modal widget");
173
174 assert_eq!(hits.topmost_at(Pos::new(2, 2)), Some("modal widget"));
177 assert_eq!(hits.topmost_at(Pos::new(8, 8)), None);
180 }
181}