use std::collections::HashMap;
pub type ComponentId = String;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FocusDirection {
Next,
Previous,
Up,
Down,
Left,
Right,
}
#[derive(Debug, Clone)]
pub struct FocusableInfo {
pub id: ComponentId,
pub focusable: bool,
pub tab_index: i32,
pub group: Option<String>,
}
impl FocusableInfo {
pub fn new(id: impl Into<ComponentId>) -> Self {
Self {
id: id.into(),
focusable: true,
tab_index: 0,
group: None,
}
}
pub fn with_tab_index(mut self, index: i32) -> Self {
self.tab_index = index;
self
}
pub fn with_group(mut self, group: impl Into<String>) -> Self {
self.group = Some(group.into());
self
}
pub fn with_focusable(mut self, focusable: bool) -> Self {
self.focusable = focusable;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct FocusManager {
focused_id: Option<ComponentId>,
focus_order: Vec<FocusableInfo>,
id_to_index: HashMap<ComponentId, usize>,
focus_ring_visible: bool,
wrap_around: bool,
}
impl FocusManager {
pub fn new() -> Self {
Self {
focused_id: None,
focus_order: Vec::new(),
id_to_index: HashMap::new(),
focus_ring_visible: true,
wrap_around: true,
}
}
pub fn register(&mut self, id: impl Into<ComponentId>) {
self.register_with_info(FocusableInfo::new(id));
}
pub fn register_with_info(&mut self, info: FocusableInfo) {
if self.id_to_index.contains_key(&info.id) {
if let Some(&idx) = self.id_to_index.get(&info.id) {
self.focus_order[idx] = info;
}
} else {
let idx = self.focus_order.len();
self.id_to_index.insert(info.id.clone(), idx);
self.focus_order.push(info);
}
self.sort_by_tab_index();
}
pub fn unregister(&mut self, id: &str) {
if let Some(&idx) = self.id_to_index.get(id) {
if self.focused_id.as_deref() == Some(id) {
self.focused_id = None;
}
self.focus_order.remove(idx);
self.id_to_index.remove(id);
self.id_to_index.clear();
for (i, info) in self.focus_order.iter().enumerate() {
self.id_to_index.insert(info.id.clone(), i);
}
}
}
fn sort_by_tab_index(&mut self) {
self.focus_order.sort_by_key(|info| info.tab_index);
self.id_to_index.clear();
for (i, info) in self.focus_order.iter().enumerate() {
self.id_to_index.insert(info.id.clone(), i);
}
}
pub fn focus(&mut self, id: impl Into<ComponentId>) -> bool {
let id = id.into();
if let Some(&idx) = self.id_to_index.get(&id) {
if self.focus_order[idx].focusable {
self.focused_id = Some(id);
return true;
}
}
false
}
pub fn blur(&mut self) {
self.focused_id = None;
}
pub fn focus_next(&mut self) -> bool {
self.move_focus(FocusDirection::Next)
}
pub fn focus_prev(&mut self) -> bool {
self.move_focus(FocusDirection::Previous)
}
pub fn move_focus(&mut self, direction: FocusDirection) -> bool {
let focusable: Vec<_> = self
.focus_order
.iter()
.enumerate()
.filter(|(_, info)| info.focusable)
.collect();
if focusable.is_empty() {
return false;
}
let current_idx = self
.focused_id
.as_ref()
.and_then(|id| self.id_to_index.get(id))
.and_then(|&idx| focusable.iter().position(|(i, _)| *i == idx));
let new_idx = self.next_focus_index(current_idx, focusable.len(), direction);
if let Some(idx) = new_idx {
let (_, info) = &focusable[idx];
self.focused_id = Some(info.id.clone());
true
} else {
false
}
}
fn next_focus_index(
&self,
current: Option<usize>,
len: usize,
direction: FocusDirection,
) -> Option<usize> {
let forward = matches!(
direction,
FocusDirection::Next | FocusDirection::Down | FocusDirection::Right
);
match current {
None if forward => Some(0),
None => Some(len - 1),
Some(idx) if forward && idx + 1 < len => Some(idx + 1),
Some(idx) if !forward && idx > 0 => Some(idx - 1),
Some(_) if self.wrap_around && forward => Some(0),
Some(_) if self.wrap_around => Some(len - 1),
Some(_) => None,
}
}
pub fn is_focused(&self, id: &str) -> bool {
self.focused_id.as_deref() == Some(id)
}
pub fn focused(&self) -> Option<&str> {
self.focused_id.as_deref()
}
pub fn is_focus_ring_visible(&self) -> bool {
self.focus_ring_visible
}
pub fn set_focus_ring_visible(&mut self, visible: bool) {
self.focus_ring_visible = visible;
}
pub fn set_wrap_around(&mut self, wrap: bool) {
self.wrap_around = wrap;
}
pub fn count(&self) -> usize {
self.focus_order
.iter()
.filter(|info| info.focusable)
.count()
}
pub fn is_registered(&self, id: &str) -> bool {
self.id_to_index.contains_key(id)
}
pub fn focus_order(&self) -> impl Iterator<Item = &str> {
self.focus_order
.iter()
.filter(|info| info.focusable)
.map(|info| info.id.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_focus() {
let mut fm = FocusManager::new();
fm.register("a");
fm.register("b");
fm.register("c");
assert_eq!(fm.focused(), None);
fm.focus("b");
assert!(fm.is_focused("b"));
assert!(!fm.is_focused("a"));
}
#[test]
fn test_focus_next_prev() {
let mut fm = FocusManager::new();
fm.register("a");
fm.register("b");
fm.register("c");
fm.focus("a");
fm.focus_next();
assert!(fm.is_focused("b"));
fm.focus_next();
assert!(fm.is_focused("c"));
fm.focus_next(); assert!(fm.is_focused("a"));
fm.focus_prev(); assert!(fm.is_focused("c"));
}
#[test]
fn test_tab_index_ordering() {
let mut fm = FocusManager::new();
fm.register_with_info(FocusableInfo::new("c").with_tab_index(3));
fm.register_with_info(FocusableInfo::new("a").with_tab_index(1));
fm.register_with_info(FocusableInfo::new("b").with_tab_index(2));
fm.focus_next(); assert!(fm.is_focused("a"));
fm.focus_next(); assert!(fm.is_focused("b"));
fm.focus_next(); assert!(fm.is_focused("c"));
}
#[test]
fn test_unfocusable_components() {
let mut fm = FocusManager::new();
fm.register_with_info(FocusableInfo::new("a"));
fm.register_with_info(FocusableInfo::new("b").with_focusable(false));
fm.register_with_info(FocusableInfo::new("c"));
fm.focus("a");
fm.focus_next();
assert!(fm.is_focused("c"));
}
#[test]
fn test_unregister() {
let mut fm = FocusManager::new();
fm.register("a");
fm.register("b");
fm.register("c");
fm.focus("b");
assert!(fm.is_focused("b"));
fm.unregister("b");
assert_eq!(fm.focused(), None);
assert!(!fm.is_registered("b"));
assert_eq!(fm.count(), 2);
}
#[test]
fn test_no_wrap() {
let mut fm = FocusManager::new();
fm.set_wrap_around(false);
fm.register("a");
fm.register("b");
fm.focus("b");
let moved = fm.focus_next();
assert!(!moved); assert!(fm.is_focused("b"));
fm.focus("a");
let moved = fm.focus_prev();
assert!(!moved); assert!(fm.is_focused("a"));
}
}