use crate::geometry::{Point, Rect}; use std::hash::{Hash, Hasher}; /// Stable across frames, resizing and text changes. Use semantic keys, never row indices. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct WidgetId(u64); impl WidgetId { pub fn new(key: impl Hash) -> Self { let mut hash = std::collections::hash_map::DefaultHasher::new(); key.hash(&mut hash); Self(hash.finish()) } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Axis { Horizontal, Vertical, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Role { Button, Tab, Toggle, TextField, Row, Swatch, Splitter(Axis), Scrollbar(Axis), } impl Role { fn focusable(self) -> bool { !matches!(self, Self::Splitter(_) | Self::Scrollbar(_) | Self::Row) } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Cursor { Default, Pointer, Text, ResizeHorizontal, ResizeVertical, Grab, Disabled, } #[derive(Clone, Debug)] pub struct Target { pub id: WidgetId, pub rect: Rect, pub role: Role, pub enabled: bool, pub action: Option, } #[derive(Clone, Copy, Debug)] pub struct VisualState { pub hovered: bool, pub pressed: bool, pub focused: bool, pub selected: bool, pub enabled: bool, } /// Retained input state for an immediate-mode frame. Actions fire on release inside /// the same enabled control, so dragging away cancels a click. #[derive(Clone, Debug, Default)] pub struct Interaction { pointer: Option, hovered: Option, pointer_active: Option, key_active: Option, focused: Option, keyboard_focus: bool, } impl Interaction { pub fn hit<'a, A>(&self, targets: &'a [Target]) -> Option<&'a Target> { let p = self.pointer?; targets.iter().rev().find(|t| t.rect.contains(p)) } pub fn pointer_moved(&mut self, p: Point, targets: &[Target]) { self.pointer = Some(p); self.hovered = self.hit(targets).map(|t| t.id); } pub fn pointer_left(&mut self) { self.pointer = None; self.hovered = None; } pub fn press(&mut self, targets: &[Target]) -> Option> { self.key_active = None; self.keyboard_focus = false; let target = self.hit(targets).filter(|t| t.enabled).cloned(); self.pointer_active = target.as_ref().map(|t| t.id); self.focused = target.as_ref().filter(|t| t.role.focusable()).map(|t| t.id); target } pub fn release(&mut self, targets: &[Target]) -> Option { let active = self.pointer_active.take()?; self.hit(targets) .filter(|t| t.enabled && t.id == active && t.role != Role::TextField) .and_then(|t| t.action.clone()) } pub fn cancel(&mut self) { self.pointer_active = None; self.key_active = None; } pub fn blur(&mut self) { self.cancel(); self.focused = None; self.keyboard_focus = false; self.pointer_left(); } pub fn reconcile(&mut self, targets: &[Target]) -> bool { let before = ( self.hovered, self.pointer_active, self.focused, self.key_active, ); let exists = |id| targets.iter().any(|t| t.id == id && t.enabled); self.focused = self.focused.filter(|id| exists(*id)); self.pointer_active = self.pointer_active.filter(|id| exists(*id)); self.key_active = self.key_active.filter(|id| exists(*id)); self.hovered = self.hit(targets).map(|t| t.id); before != ( self.hovered, self.pointer_active, self.focused, self.key_active, ) } pub fn focused<'a, A>(&self, targets: &'a [Target]) -> Option<&'a Target> { targets .iter() .find(|t| Some(t.id) == self.focused && t.enabled) } pub fn focus_next(&mut self, targets: &[Target], back: bool) -> Option> { self.cancel(); self.keyboard_focus = true; let candidates = targets .iter() .filter(|t| t.enabled && t.role.focusable()) .collect::>(); if candidates.is_empty() { self.focused = None; return None; } let current = candidates.iter().position(|t| Some(t.id) == self.focused); let index = match current { Some(i) if back => (i + candidates.len() - 1) % candidates.len(), Some(i) => (i + 1) % candidates.len(), None if back => candidates.len() - 1, None => 0, }; let target = candidates[index]; self.focused = Some(target.id); Some(target.clone()) } pub fn key_press(&mut self, targets: &[Target]) { self.key_active = self .focused(targets) .filter(|t| t.role != Role::TextField) .map(|t| t.id); } pub fn key_release(&mut self, targets: &[Target]) -> Option { let active = self.key_active.take()?; self.focused(targets) .filter(|t| t.id == active) .and_then(|t| t.action.clone()) } pub fn visual(&self, id: WidgetId, selected: bool, enabled: bool) -> VisualState { let hovered = enabled && self.hovered == Some(id); VisualState { hovered, pressed: enabled && (self.key_active == Some(id) || (hovered && self.pointer_active == Some(id))), focused: enabled && self.keyboard_focus && self.focused == Some(id), selected, enabled, } } pub fn captured(&self, id: WidgetId) -> bool { self.pointer_active == Some(id) } pub fn cursor(&self, targets: &[Target]) -> Cursor { let target = self .pointer_active .and_then(|id| { targets.iter().find(|t| { t.id == id && matches!(t.role, Role::Splitter(_) | Role::Scrollbar(_)) }) }) .or_else(|| self.hit(targets)); match target { Some(t) if !t.enabled => Cursor::Disabled, Some(t) => match t.role { Role::TextField => Cursor::Text, Role::Splitter(Axis::Horizontal) => Cursor::ResizeHorizontal, Role::Splitter(Axis::Vertical) => Cursor::ResizeVertical, Role::Scrollbar(_) => Cursor::Grab, _ => Cursor::Pointer, }, None => Cursor::Default, } } }