use crate::event::KeyKind;
use crate::ui::interaction::{HitTestResult, InteractionCache, InteractionEntry, InteractionFlags};
use crate::widgets::WidgetArea;
use crate::{Event, MouseButton};
use std::collections::{HashMap, HashSet};
pub type OwnerId = usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RouteTarget {
Id(usize),
Owner(OwnerId),
}
pub struct UiScene {
cache: InteractionCache,
capture: Option<usize>,
focusables: Vec<usize>,
focusables_set: HashSet<usize>,
focus_under_cursor_on_click: bool,
tab_navigation_enabled: bool,
capture_on_left_click: bool,
capture_only_if_draggable: bool,
release_capture_on_left_release: bool,
id_to_owner: HashMap<usize, OwnerId>,
}
impl Default for UiScene {
fn default() -> Self {
Self::new()
}
}
impl UiScene {
pub fn new() -> Self {
Self {
cache: InteractionCache::new(),
capture: None,
focusables: Vec::new(),
focusables_set: HashSet::new(),
focus_under_cursor_on_click: true,
tab_navigation_enabled: true,
capture_on_left_click: true,
capture_only_if_draggable: true,
release_capture_on_left_release: true,
id_to_owner: HashMap::new(),
}
}
pub fn begin_frame(&mut self) {
self.cache.begin_frame();
self.focusables.clear();
self.focusables_set.clear();
self.id_to_owner.clear();
}
pub fn observe_event(&mut self, event: &Event) {
self.cache.observe_event(event);
}
pub fn cache(&self) -> &InteractionCache {
&self.cache
}
pub fn cache_mut(&mut self) -> &mut InteractionCache {
&mut self.cache
}
pub fn last_mouse_pos(&self) -> Option<(u16, u16)> {
self.cache.last_mouse_pos()
}
pub fn register_entry(&mut self, entry: InteractionEntry) {
if entry.flags.focusable {
self.track_focusable(entry.id);
}
self.cache.register_entry(entry);
}
pub fn register(&mut self, id: usize, area: WidgetArea) {
self.cache.register(id, area);
}
pub fn register_focusable(&mut self, id: usize, area: WidgetArea) {
self.track_focusable(id);
self.cache.register_focusable(id, area);
}
pub fn register_scrollable(&mut self, id: usize, area: WidgetArea) {
self.cache.register_scrollable(id, area);
}
pub fn register_draggable(&mut self, id: usize, area: WidgetArea) {
self.cache.register_draggable(id, area);
}
fn track_focusable(&mut self, id: usize) {
if self.focusables_set.insert(id) {
self.focusables.push(id);
}
}
pub fn focused(&self) -> Option<usize> {
self.cache.focused()
}
pub fn focus(&mut self, id: usize) {
self.cache.focus(id);
}
pub fn clear_focus(&mut self) {
self.cache.clear_focus();
}
pub fn capture(&self) -> Option<usize> {
self.capture
}
pub fn set_capture(&mut self, id: usize) {
self.capture = Some(id);
}
pub fn release_capture(&mut self) {
self.capture = None;
}
pub fn set_owner(&mut self, id: usize, owner: OwnerId) {
self.id_to_owner.insert(id, owner);
}
pub fn set_owner_for_ids(&mut self, owner: OwnerId, ids: &[usize]) {
for &id in ids {
self.set_owner(id, owner);
}
}
pub fn owner_of(&self, id: usize) -> Option<OwnerId> {
self.id_to_owner.get(&id).copied()
}
pub fn set_focus_under_cursor_on_click(&mut self, enabled: bool) {
self.focus_under_cursor_on_click = enabled;
}
pub fn set_tab_navigation_enabled(&mut self, enabled: bool) {
self.tab_navigation_enabled = enabled;
}
pub fn set_capture_on_left_click(&mut self, enabled: bool) {
self.capture_on_left_click = enabled;
}
pub fn set_capture_only_if_draggable(&mut self, enabled: bool) {
self.capture_only_if_draggable = enabled;
}
pub fn set_release_capture_on_left_release(&mut self, enabled: bool) {
self.release_capture_on_left_release = enabled;
}
pub fn handle_tab_navigation(&mut self, event: &Event) -> Option<usize> {
if !self.tab_navigation_enabled {
return None;
}
match event {
Event::KeyWithModifiers(k) => {
if matches!(k.key, KeyKind::Tab) {
if k.mods.shift {
self.focus_prev()
} else {
self.focus_next()
}
} else {
None
}
}
Event::Tab => self.focus_next(),
_ => None,
}
}
pub fn apply_policies(&mut self, event: &Event) -> PolicyEffects {
self.observe_event(event);
let focused_by_tab = self.handle_tab_navigation(event);
self.handle_focus_policy(event);
PolicyEffects { focused_by_tab }
}
pub fn focus_next(&mut self) -> Option<usize> {
let n = self.focusables.len();
if n == 0 {
return None;
}
let next = match self.focused() {
None => self.focusables[0],
Some(cur) => {
let mut idx = 0usize;
for (i, id) in self.focusables.iter().copied().enumerate() {
if id == cur {
idx = (i + 1) % n;
break;
}
}
self.focusables[idx]
}
};
self.focus(next);
Some(next)
}
pub fn focus_prev(&mut self) -> Option<usize> {
let n = self.focusables.len();
if n == 0 {
return None;
}
let prev = match self.focused() {
None => self.focusables[n - 1],
Some(cur) => {
let mut idx = n - 1;
for (i, id) in self.focusables.iter().copied().enumerate() {
if id == cur {
idx = if i == 0 { n - 1 } else { i - 1 };
break;
}
}
self.focusables[idx]
}
};
self.focus(prev);
Some(prev)
}
pub fn hit_test(&mut self, x: u16, y: u16) -> Option<HitTestResult> {
self.cache.hit_test(x, y)
}
pub fn hit_test_id(&mut self, x: u16, y: u16) -> Option<usize> {
self.cache.hit_test_id(x, y)
}
pub fn get(&self, id: usize) -> Option<InteractionEntry> {
self.cache.get(id)
}
pub fn handle_focus_policy(&mut self, event: &Event) {
match *event {
Event::MouseClick {
x,
y,
button: MouseButton::Left,
} => {
self.capture = None;
let Some(hit) = self.hit_test(x, y) else {
return;
};
if self.capture_on_left_click {
let can_capture = if self.capture_only_if_draggable {
hit.flags.draggable
} else {
true
};
if can_capture {
self.capture = Some(hit.id);
}
}
if self.focus_under_cursor_on_click && hit.flags.focusable {
self.focus(hit.id);
}
}
Event::MouseRelease {
button: MouseButton::Left,
..
} => {
if self.release_capture_on_left_release {
self.capture = None;
}
}
_ => {}
}
}
pub fn route_mouse_event(&mut self, event: &Event) -> Option<RouteTarget> {
if let Some(id) = self.capture {
return Some(RouteTarget::Id(id));
}
match *event {
Event::MouseClick { x, y, .. }
| Event::MouseDrag { x, y, .. }
| Event::MouseRelease { x, y, .. }
| Event::MouseMove { x, y } => self.hit_test_id(x, y).map(RouteTarget::Id),
_ => None,
}
}
pub fn route_mouse_event_to_owner(&mut self, event: &Event) -> Option<RouteTarget> {
match self.route_mouse_event(event)? {
RouteTarget::Id(id) => self
.owner_of(id)
.map(RouteTarget::Owner)
.or(Some(RouteTarget::Id(id))),
RouteTarget::Owner(owner) => Some(RouteTarget::Owner(owner)),
}
}
pub fn route_wheel_event(&mut self, event: &Event) -> Option<RouteTarget> {
match *event {
Event::MouseScroll { .. } | Event::MouseScrollHorizontal { .. } => {
if let Some((mx, my)) = self.last_mouse_pos() {
if let Some(hit) = self.hit_test(mx, my) {
if hit.flags.scrollable {
return Some(RouteTarget::Id(hit.id));
}
}
}
if let Some(focused) = self.focused() {
if let Some(entry) = self.get(focused) {
if entry.flags.scrollable {
return Some(RouteTarget::Id(focused));
}
}
}
None
}
_ => None,
}
}
pub fn route_wheel_event_to_owner(&mut self, event: &Event) -> Option<RouteTarget> {
match self.route_wheel_event(event)? {
RouteTarget::Id(id) => self
.owner_of(id)
.map(RouteTarget::Owner)
.or(Some(RouteTarget::Id(id))),
RouteTarget::Owner(owner) => Some(RouteTarget::Owner(owner)),
}
}
pub fn route_key_event(&self, event: &Event) -> Option<RouteTarget> {
if !is_key_event(event) {
return None;
}
self.focused().map(RouteTarget::Id)
}
pub fn flags(&self, id: usize) -> Option<InteractionFlags> {
self.get(id).map(|e| e.flags)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PolicyEffects {
pub focused_by_tab: Option<usize>,
}
fn is_key_event(event: &Event) -> bool {
matches!(
event,
Event::Character(_)
| Event::Paste(_)
| Event::KeyWithModifiers(_)
| Event::KeyUp
| Event::KeyDown
| Event::KeyLeft
| Event::KeyRight
| Event::Delete
| Event::Backspace
| Event::Tab
| Event::Enter
| Event::Escape
| Event::FunctionKey(_)
| Event::Keybind(_)
)
}