use std::collections::HashMap;
use std::time::{Duration, Instant};
use crate::Event;
use crate::widgets::WidgetArea;
#[derive(Debug, Clone)]
pub struct IdAllocator {
next: InteractionId,
}
impl Default for IdAllocator {
fn default() -> Self {
Self::new()
}
}
impl IdAllocator {
pub const fn new() -> Self {
Self { next: 1 }
}
pub const fn with_start(start: InteractionId) -> Self {
Self { next: start }
}
pub fn alloc(&mut self) -> InteractionId {
let id = self.next;
self.next = self
.next
.checked_add(1)
.expect("InteractionId overflow in IdAllocator");
id
}
pub fn peek(&self) -> InteractionId {
self.next
}
pub fn reset(&mut self, start: InteractionId) {
self.next = start;
}
}
pub type InteractionId = usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct InteractionFlags {
pub focusable: bool,
pub scrollable: bool,
pub draggable: bool,
}
impl InteractionFlags {
pub const fn new(focusable: bool, scrollable: bool, draggable: bool) -> Self {
Self {
focusable,
scrollable,
draggable,
}
}
pub const fn none() -> Self {
Self::new(false, false, false)
}
pub const fn focusable() -> Self {
Self::new(true, false, false)
}
pub const fn scrollable() -> Self {
Self::new(false, true, false)
}
pub const fn draggable() -> Self {
Self::new(false, false, true)
}
pub const fn union(self, other: Self) -> Self {
Self::new(
self.focusable || other.focusable,
self.scrollable || other.scrollable,
self.draggable || other.draggable,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InteractionEntry {
pub id: InteractionId,
pub area: WidgetArea,
pub z_index: i16,
pub flags: InteractionFlags,
}
impl InteractionEntry {
pub const fn new(id: InteractionId, area: WidgetArea) -> Self {
Self {
id,
area,
z_index: 0,
flags: InteractionFlags::none(),
}
}
pub const fn with_z_index(mut self, z_index: i16) -> Self {
self.z_index = z_index;
self
}
pub const fn with_flags(mut self, flags: InteractionFlags) -> Self {
self.flags = flags;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HitTestResult {
pub id: InteractionId,
pub area: WidgetArea,
pub flags: InteractionFlags,
pub z_index: i16,
}
#[derive(Debug, Clone)]
pub struct AutoHide {
reveal_for: Duration,
proximity_margin: u16,
last_activity: Option<Instant>,
}
impl Default for AutoHide {
fn default() -> Self {
Self::new(Duration::from_millis(900), 2)
}
}
impl AutoHide {
pub fn new(reveal_for: Duration, proximity_margin: u16) -> Self {
Self {
reveal_for,
proximity_margin,
last_activity: None,
}
}
pub fn mark_activity(&mut self) {
self.last_activity = Some(Instant::now());
}
pub fn mark_activity_from_event(&mut self, event: &Event) -> bool {
match event {
Event::MouseScroll { .. } | Event::MouseScrollHorizontal { .. } => {
self.mark_activity();
true
}
_ => false,
}
}
pub fn is_recently_active(&self) -> bool {
match self.last_activity {
None => false,
Some(t) => t.elapsed() <= self.reveal_for,
}
}
pub fn set_reveal_for(&mut self, reveal_for: Duration) {
self.reveal_for = reveal_for;
}
pub fn set_proximity_margin(&mut self, proximity_margin: u16) {
self.proximity_margin = proximity_margin;
}
pub fn is_point_near_area(&self, x: u16, y: u16, area: WidgetArea) -> bool {
let expanded = expand_area(area, self.proximity_margin);
expanded.contains_point(x, y)
}
pub fn should_show(
&self,
area: WidgetArea,
mouse_pos: Option<(u16, u16)>,
is_dragging: bool,
) -> bool {
if is_dragging {
return true;
}
if self.is_recently_active() {
return true;
}
if let Some((mx, my)) = mouse_pos {
return self.is_point_near_area(mx, my, area);
}
false
}
}
#[derive(Debug, Default)]
pub struct InteractionCache {
entries: Vec<InteractionEntry>,
latest_by_id: HashMap<InteractionId, InteractionEntry>,
focused: Option<InteractionId>,
last_hovered: Option<InteractionId>,
last_mouse_pos: Option<(u16, u16)>,
}
impl InteractionCache {
pub fn new() -> Self {
Self::default()
}
pub fn begin_frame(&mut self) {
self.entries.clear();
self.latest_by_id.clear();
self.last_hovered = None;
}
pub fn observe_event(&mut self, event: &Event) {
if let Event::MouseMove { x, y } = *event {
self.last_mouse_pos = Some((x, y));
}
}
pub fn last_mouse_pos(&self) -> Option<(u16, u16)> {
self.last_mouse_pos
}
pub fn register(&mut self, id: InteractionId, area: WidgetArea) {
self.register_entry(InteractionEntry::new(id, area));
}
pub fn register_entry(&mut self, entry: InteractionEntry) {
let entry = if let Some(previous) = self.latest_by_id.get(&entry.id) {
entry.with_flags(previous.flags.union(entry.flags))
} else {
entry
};
self.entries.push(entry);
self.latest_by_id.insert(entry.id, entry);
}
pub fn register_focusable(&mut self, id: InteractionId, area: WidgetArea) {
self.register_entry(
InteractionEntry::new(id, area).with_flags(InteractionFlags::focusable()),
);
}
pub fn register_scrollable(&mut self, id: InteractionId, area: WidgetArea) {
self.register_entry(
InteractionEntry::new(id, area).with_flags(InteractionFlags::scrollable()),
);
}
pub fn register_draggable(&mut self, id: InteractionId, area: WidgetArea) {
self.register_entry(
InteractionEntry::new(id, area).with_flags(InteractionFlags::draggable()),
);
}
pub fn get(&self, id: InteractionId) -> Option<InteractionEntry> {
self.latest_by_id.get(&id).copied()
}
pub fn focused(&self) -> Option<InteractionId> {
self.focused
}
pub fn focus(&mut self, id: InteractionId) {
self.focused = Some(id);
}
pub fn clear_focus(&mut self) {
self.focused = None;
}
pub fn last_hovered(&self) -> Option<InteractionId> {
self.last_hovered
}
pub fn hit_test(&mut self, x: u16, y: u16) -> Option<HitTestResult> {
let mut best: Option<(usize, InteractionEntry)> = None;
for (i, e) in self.entries.iter().copied().enumerate() {
if !e.area.contains_point(x, y) {
continue;
}
best = match best {
None => Some((i, e)),
Some((best_i, best_e)) => {
if e.z_index > best_e.z_index {
Some((i, e))
} else if e.z_index == best_e.z_index && i > best_i {
Some((i, e))
} else {
Some((best_i, best_e))
}
}
};
}
let result = best.map(|(_, e)| HitTestResult {
id: e.id,
area: e.area,
flags: e.flags,
z_index: e.z_index,
});
self.last_hovered = result.map(|r| r.id);
result
}
pub fn hit_test_id(&mut self, x: u16, y: u16) -> Option<InteractionId> {
self.hit_test(x, y).map(|r| r.id)
}
pub fn focus_under_cursor(&mut self, x: u16, y: u16) -> Option<InteractionId> {
let hit = self.hit_test(x, y)?;
if hit.flags.focusable {
self.focus(hit.id);
Some(hit.id)
} else {
None
}
}
}
pub fn expand_area(area: WidgetArea, margin: u16) -> WidgetArea {
let x = area.x.saturating_sub(margin);
let y = area.y.saturating_sub(margin);
let width = area.width.saturating_add(margin.saturating_mul(2));
let height = area.height.saturating_add(margin.saturating_mul(2));
WidgetArea::new(x, y, width, height)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hit_test_prefers_higher_z() {
let mut cache = InteractionCache::new();
cache.begin_frame();
let a = WidgetArea::new(0, 0, 10, 10);
let b = WidgetArea::new(0, 0, 10, 10);
cache.register_entry(InteractionEntry::new(1, a).with_z_index(0));
cache.register_entry(InteractionEntry::new(2, b).with_z_index(10));
let hit = cache.hit_test(5, 5).unwrap();
assert_eq!(hit.id, 2);
}
#[test]
fn hit_test_last_registered_wins_on_tie() {
let mut cache = InteractionCache::new();
cache.begin_frame();
let a = WidgetArea::new(0, 0, 10, 10);
let b = WidgetArea::new(0, 0, 10, 10);
cache.register_entry(InteractionEntry::new(1, a).with_z_index(0));
cache.register_entry(InteractionEntry::new(2, b).with_z_index(0));
let hit = cache.hit_test(5, 5).unwrap();
assert_eq!(hit.id, 2);
}
#[test]
fn focus_under_cursor_only_focuses_focusable() {
let mut cache = InteractionCache::new();
cache.begin_frame();
cache.register_entry(InteractionEntry::new(1, WidgetArea::new(0, 0, 5, 5)));
cache.register_focusable(2, WidgetArea::new(0, 0, 5, 5));
assert_eq!(cache.focused(), None);
let focused = cache.focus_under_cursor(1, 1);
assert_eq!(focused, Some(2));
assert_eq!(cache.focused(), Some(2));
}
}