use crate::layout::Rect;
use std::collections::HashSet;
pub type WidgetId = u64;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
#[derive(Clone, Debug)]
struct FocusableWidget {
id: WidgetId,
position: Option<(u16, u16)>,
bounds: Option<Rect>,
}
pub struct FocusManager {
widgets: Vec<FocusableWidget>,
current: Option<usize>,
trap: Option<WidgetId>,
trapped_ids: Vec<WidgetId>,
saved_focus: Option<WidgetId>,
trap_stack: Vec<TrapState>,
}
#[derive(Clone, Debug)]
struct TrapState {
container_id: Option<WidgetId>,
trapped_ids: Vec<WidgetId>,
previous_focus: Option<WidgetId>,
}
impl FocusManager {
pub fn new() -> Self {
Self {
widgets: Vec::new(),
current: None,
trap: None,
trapped_ids: Vec::new(),
saved_focus: None,
trap_stack: Vec::new(),
}
}
fn focusable_ids(&self) -> std::borrow::Cow<'_, [WidgetId]> {
if self.trap.is_some() && !self.trapped_ids.is_empty() {
std::borrow::Cow::Borrowed(&self.trapped_ids)
} else {
std::borrow::Cow::Owned(self.widgets.iter().map(|w| w.id).collect())
}
}
pub fn register(&mut self, id: WidgetId) {
if !self.widgets.iter().any(|w| w.id == id) {
self.widgets.push(FocusableWidget {
id,
position: None,
bounds: None,
});
}
}
pub fn register_with_position(&mut self, id: WidgetId, x: u16, y: u16) {
if let Some(widget) = self.widgets.iter_mut().find(|w| w.id == id) {
widget.position = Some((x, y));
} else {
self.widgets.push(FocusableWidget {
id,
position: Some((x, y)),
bounds: None,
});
}
}
pub fn register_with_bounds(&mut self, id: WidgetId, bounds: Rect) {
let center_x = bounds.x + bounds.width / 2;
let center_y = bounds.y + bounds.height / 2;
if let Some(widget) = self.widgets.iter_mut().find(|w| w.id == id) {
widget.position = Some((center_x, center_y));
widget.bounds = Some(bounds);
} else {
self.widgets.push(FocusableWidget {
id,
position: Some((center_x, center_y)),
bounds: Some(bounds),
});
}
}
pub fn unregister(&mut self, id: WidgetId) {
if let Some(pos) = self.widgets.iter().position(|w| w.id == id) {
self.widgets.remove(pos);
if let Some(current) = self.current {
if self.widgets.is_empty() {
self.current = None;
} else if pos < current {
self.current = Some(current.saturating_sub(1));
} else if pos == current {
if current >= self.widgets.len() {
self.current = Some(self.widgets.len().saturating_sub(1));
}
}
}
self.trapped_ids.retain(|&i| i != id);
}
}
pub fn current(&self) -> Option<WidgetId> {
self.current
.and_then(|idx| self.widgets.get(idx).map(|w| w.id))
}
pub fn next(&mut self) {
let ids = self.focusable_ids();
if ids.is_empty() {
return;
}
let current_id = self.current();
let current_idx = current_id.and_then(|id| ids.iter().position(|&i| i == id));
let next_id = match current_idx {
Some(idx) => ids[(idx + 1) % ids.len()],
None => ids[0],
};
self.focus(next_id);
}
pub fn prev(&mut self) {
let ids = self.focusable_ids();
if ids.is_empty() {
return;
}
let current_id = self.current();
let current_idx = current_id.and_then(|id| ids.iter().position(|&i| i == id));
let prev_id = match current_idx {
Some(0) => ids[ids.len() - 1],
Some(idx) => ids[idx - 1],
None => ids[ids.len() - 1],
};
self.focus(prev_id);
}
pub fn focus(&mut self, id: WidgetId) {
if let Some(idx) = self.widgets.iter().position(|w| w.id == id) {
self.current = Some(idx);
}
}
pub fn is_focused(&self, id: WidgetId) -> bool {
self.current() == Some(id)
}
pub fn blur(&mut self) {
self.current = None;
}
pub fn move_focus(&mut self, direction: Direction) -> bool {
let current_idx = match self.current {
Some(idx) => idx,
None => return false,
};
let current_pos = match self.widgets.get(current_idx).and_then(|w| w.position) {
Some(pos) => pos,
None => return false, };
let ids = self.focusable_ids();
let focusable_set: HashSet<WidgetId> = ids.iter().copied().collect();
let current_id = self.widgets[current_idx].id;
let candidates: Vec<_> = self
.widgets
.iter()
.filter(|w| focusable_set.contains(&w.id)) .filter(|w| w.id != current_id)
.filter_map(|w| w.position.map(|p| (w.id, p)))
.filter(|(_, pos)| match direction {
Direction::Up => pos.1 < current_pos.1,
Direction::Down => pos.1 > current_pos.1,
Direction::Left => pos.0 < current_pos.0,
Direction::Right => pos.0 > current_pos.0,
})
.collect();
if candidates.is_empty() {
return false;
}
let closest = candidates.into_iter().min_by_key(|(_, pos)| {
let dx = (pos.0 as i32 - current_pos.0 as i32).abs();
let dy = (pos.1 as i32 - current_pos.1 as i32).abs();
match direction {
Direction::Up | Direction::Down => dy * 2 + dx,
Direction::Left | Direction::Right => dx * 2 + dy,
}
});
if let Some((id, _)) = closest {
self.focus(id);
true
} else {
false
}
}
pub fn trap_focus(&mut self, container_id: WidgetId) {
self.saved_focus = self.current();
self.trap = Some(container_id);
self.trapped_ids.clear();
}
pub fn trap_focus_with_initial(&mut self, container_id: WidgetId, initial_focus: WidgetId) {
self.trap_focus(container_id);
self.focus(initial_focus);
}
pub fn add_to_trap(&mut self, id: WidgetId) {
if self.trap.is_some() && !self.trapped_ids.contains(&id) {
self.trapped_ids.push(id);
}
}
pub fn release_trap(&mut self) {
self.trap = None;
self.trapped_ids.clear();
}
pub fn release_trap_and_restore(&mut self) {
let saved = self.saved_focus.take();
self.release_trap();
if let Some(id) = saved {
self.focus(id);
}
}
pub fn is_trapped(&self) -> bool {
self.trap.is_some()
}
pub fn trap_container(&self) -> Option<WidgetId> {
self.trap
}
pub fn saved_focus(&self) -> Option<WidgetId> {
self.saved_focus
}
pub fn push_trap(&mut self, container_id: WidgetId, children: &[WidgetId]) {
let state = TrapState {
container_id: self.trap,
trapped_ids: self.trapped_ids.clone(),
previous_focus: self.current(),
};
self.trap_stack.push(state);
self.trap = Some(container_id);
self.trapped_ids = children.to_vec();
if let Some(&first) = children.first() {
self.focus(first);
}
}
pub fn pop_trap(&mut self) -> bool {
if let Some(state) = self.trap_stack.pop() {
self.trap = state.container_id;
self.trapped_ids = state.trapped_ids;
if let Some(id) = state.previous_focus {
self.focus(id);
}
true
} else {
self.release_trap_and_restore();
false
}
}
pub fn trap_depth(&self) -> usize {
if self.trap.is_some() {
self.trap_stack.len()
} else {
0
}
}
}
#[derive(Clone, Debug)]
pub struct FocusTrapConfig {
pub restore_on_release: bool,
pub initial_focus: Option<WidgetId>,
pub loop_focus: bool,
}
impl Default for FocusTrapConfig {
fn default() -> Self {
Self {
restore_on_release: true,
initial_focus: None,
loop_focus: true,
}
}
}
#[derive(Clone, Debug)]
pub struct FocusTrap {
container_id: WidgetId,
children: Vec<WidgetId>,
config: FocusTrapConfig,
active: bool,
}
impl FocusTrap {
pub fn new(container_id: WidgetId) -> Self {
Self {
container_id,
children: Vec::new(),
config: FocusTrapConfig::default(),
active: false,
}
}
pub fn with_children(mut self, children: &[WidgetId]) -> Self {
self.children = children.to_vec();
self
}
pub fn add_child(mut self, id: WidgetId) -> Self {
if !self.children.contains(&id) {
self.children.push(id);
}
self
}
pub fn initial_focus(mut self, id: WidgetId) -> Self {
self.config.initial_focus = Some(id);
self
}
pub fn restore_focus_on_release(mut self, restore: bool) -> Self {
self.config.restore_on_release = restore;
self
}
pub fn loop_focus(mut self, loop_focus: bool) -> Self {
self.config.loop_focus = loop_focus;
self
}
pub fn container_id(&self) -> WidgetId {
self.container_id
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn activate(&mut self, fm: &mut FocusManager) {
if self.active {
return;
}
fm.push_trap(self.container_id, &self.children);
if let Some(initial) = self.config.initial_focus {
fm.focus(initial);
} else if let Some(&first) = self.children.first() {
fm.focus(first);
}
self.active = true;
}
pub fn deactivate(&mut self, fm: &mut FocusManager) {
if !self.active {
return;
}
fm.pop_trap();
self.active = false;
}
}
impl Default for FocusManager {
fn default() -> Self {
Self::new()
}
}