use alloc::vec::Vec;
use retroglyph_core::grid::{Pos, Rect};
#[derive(Debug, Clone)]
pub struct HitTester<Id> {
hits: Vec<Entry<Id>>,
}
#[derive(Debug, Clone)]
enum Entry<Id> {
Hit(Rect, Id),
Barrier(Rect),
}
impl<Id> HitTester<Id> {
#[must_use]
pub const fn new() -> Self {
Self { hits: Vec::new() }
}
pub fn push(&mut self, rect: Rect, id: Id) {
self.hits.push(Entry::Hit(rect, id));
}
pub fn push_barrier(&mut self, rect: Rect) {
self.hits.push(Entry::Barrier(rect));
}
pub fn clear(&mut self) {
self.hits.clear();
}
#[must_use]
pub const fn len(&self) -> usize {
self.hits.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.hits.is_empty()
}
}
impl<Id: Copy> HitTester<Id> {
#[must_use]
pub fn topmost_at(&self, pos: Pos) -> Option<Id> {
for entry in self.hits.iter().rev() {
match entry {
Entry::Hit(rect, id) if rect.contains_pos(pos) => return Some(*id),
Entry::Barrier(rect) if rect.contains_pos(pos) => return None,
Entry::Hit(_, _) | Entry::Barrier(_) => {}
}
}
None
}
}
impl<Id> Default for HitTester<Id> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn topmost_at_prefers_the_most_recently_pushed_overlap() {
let mut hits = HitTester::new();
hits.push(Rect::new(0, 0, 10, 10), "back");
hits.push(Rect::new(5, 5, 10, 10), "front");
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); }
#[test]
fn clear_empties_the_registry() {
let mut hits = HitTester::new();
hits.push(Rect::new(0, 0, 5, 5), 1);
assert!(!hits.is_empty());
hits.clear();
assert!(hits.is_empty());
assert_eq!(hits.len(), 0);
assert_eq!(hits.topmost_at(Pos::new(0, 0)), None);
}
#[test]
fn default_is_empty() {
let hits: HitTester<()> = HitTester::default();
assert!(hits.is_empty());
}
#[test]
fn a_barrier_hides_hits_registered_before_it_inside_its_own_rect() {
let mut hits = HitTester::new();
hits.push(Rect::new(0, 0, 10, 10), "behind");
hits.push_barrier(Rect::new(2, 2, 4, 4));
assert_eq!(hits.topmost_at(Pos::new(3, 3)), None); assert_eq!(hits.topmost_at(Pos::new(0, 0)), Some("behind")); }
#[test]
fn a_hit_registered_after_a_barrier_still_wins_inside_it() {
let mut hits = HitTester::new();
hits.push(Rect::new(0, 0, 10, 10), "behind");
hits.push_barrier(Rect::new(2, 2, 4, 4));
hits.push(Rect::new(3, 3, 1, 1), "in front");
assert_eq!(hits.topmost_at(Pos::new(3, 3)), Some("in front"));
}
#[test]
fn a_barrier_does_not_affect_positions_outside_its_own_rect() {
let mut hits = HitTester::new();
hits.push(Rect::new(0, 0, 10, 10), "back");
hits.push_barrier(Rect::new(2, 2, 2, 2));
hits.push(Rect::new(5, 5, 3, 3), "front");
assert_eq!(hits.topmost_at(Pos::new(6, 6)), Some("front"));
assert_eq!(hits.topmost_at(Pos::new(0, 0)), Some("back"));
}
#[test]
fn an_older_barrier_does_not_shadow_a_newer_ones_uncovered_region() {
let mut hits = HitTester::new();
hits.push(Rect::new(0, 0, 10, 10), "back");
hits.push_barrier(Rect::new(0, 0, 10, 10));
hits.push(Rect::new(2, 2, 2, 2), "modal widget");
assert_eq!(hits.topmost_at(Pos::new(2, 2)), Some("modal widget"));
assert_eq!(hits.topmost_at(Pos::new(8, 8)), None);
}
}