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}