205 lines
6.6 KiB
Rust
205 lines
6.6 KiB
Rust
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<A> {
|
|
pub id: WidgetId,
|
|
pub rect: Rect,
|
|
pub role: Role,
|
|
pub enabled: bool,
|
|
pub action: Option<A>,
|
|
}
|
|
#[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<Point>,
|
|
hovered: Option<WidgetId>,
|
|
pointer_active: Option<WidgetId>,
|
|
key_active: Option<WidgetId>,
|
|
focused: Option<WidgetId>,
|
|
keyboard_focus: bool,
|
|
}
|
|
impl Interaction {
|
|
pub fn hit<'a, A>(&self, targets: &'a [Target<A>]) -> Option<&'a Target<A>> {
|
|
let p = self.pointer?;
|
|
targets.iter().rev().find(|t| t.rect.contains(p))
|
|
}
|
|
pub fn pointer_moved<A>(&mut self, p: Point, targets: &[Target<A>]) {
|
|
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<A: Clone>(&mut self, targets: &[Target<A>]) -> Option<Target<A>> {
|
|
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<A: Clone>(&mut self, targets: &[Target<A>]) -> Option<A> {
|
|
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<A>(&mut self, targets: &[Target<A>]) -> 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<A>]) -> Option<&'a Target<A>> {
|
|
targets
|
|
.iter()
|
|
.find(|t| Some(t.id) == self.focused && t.enabled)
|
|
}
|
|
pub fn focus_next<A: Clone>(&mut self, targets: &[Target<A>], back: bool) -> Option<Target<A>> {
|
|
self.cancel();
|
|
self.keyboard_focus = true;
|
|
let candidates = targets
|
|
.iter()
|
|
.filter(|t| t.enabled && t.role.focusable())
|
|
.collect::<Vec<_>>();
|
|
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<A>(&mut self, targets: &[Target<A>]) {
|
|
self.key_active = self
|
|
.focused(targets)
|
|
.filter(|t| t.role != Role::TextField)
|
|
.map(|t| t.id);
|
|
}
|
|
pub fn key_release<A: Clone>(&mut self, targets: &[Target<A>]) -> Option<A> {
|
|
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<A>(&self, targets: &[Target<A>]) -> 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,
|
|
}
|
|
}
|
|
}
|