use crate::types::ElementId;
#[derive(Copy, Clone, Debug)]
pub struct FocusableEntry {
pub id: ElementId,
pub tab_index: i32,
pub focus_ring: bool,
}
pub struct FocusRegistry {
entries: Vec<FocusableEntry>,
focused: Option<ElementId>,
dirty: bool,
sorted_indices: Vec<usize>,
}
impl FocusRegistry {
pub fn new() -> Self {
Self {
entries: Vec::new(),
focused: None,
dirty: false,
sorted_indices: Vec::new(),
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.sorted_indices.clear();
self.dirty = false;
}
pub fn register(&mut self, entry: FocusableEntry) {
self.entries.push(entry);
self.dirty = true;
}
pub fn set_focus(&mut self, id: ElementId) -> bool {
if self.entries.iter().any(|e| e.id == id) {
self.focused = Some(id);
true
} else {
false
}
}
pub fn clear_focus(&mut self) {
self.focused = None;
}
pub fn focused(&self) -> Option<ElementId> {
self.focused
}
pub fn is_focusable(&self, id: ElementId) -> bool {
self.entries.iter().any(|e| e.id == id)
}
pub fn is_tab_reachable(&self, id: ElementId) -> bool {
self.entries.iter().any(|e| e.id == id && e.tab_index >= 0)
}
pub fn entry(&self, id: ElementId) -> Option<&FocusableEntry> {
self.entries.iter().find(|e| e.id == id)
}
pub fn shift_forward(&mut self) -> Option<ElementId> {
self.shift(ShiftDir::Forward)
}
pub fn shift_backward(&mut self) -> Option<ElementId> {
self.shift(ShiftDir::Backward)
}
pub fn prune_missing(&mut self) {
if let Some(id) = self.focused
&& !self.is_focusable(id)
{
self.focused = None;
}
}
fn ensure_sorted(&mut self) {
if !self.dirty {
return;
}
self.sorted_indices.clear();
self.sorted_indices.extend(0..self.entries.len());
self.sorted_indices
.sort_by_key(|&i| self.entries[i].tab_index);
self.dirty = false;
}
fn shift(&mut self, dir: ShiftDir) -> Option<ElementId> {
self.ensure_sorted();
let tab_reachable: Vec<usize> = self
.sorted_indices
.iter()
.copied()
.filter(|&i| self.entries[i].tab_index >= 0)
.collect();
if tab_reachable.is_empty() {
return None;
}
let cur_pos = self
.focused
.and_then(|id| tab_reachable.iter().position(|&i| self.entries[i].id == id));
let next_idx = match (cur_pos, dir) {
(None, ShiftDir::Forward) => tab_reachable[0],
(None, ShiftDir::Backward) => *tab_reachable.last().unwrap(),
(Some(p), ShiftDir::Forward) => tab_reachable[(p + 1) % tab_reachable.len()],
(Some(p), ShiftDir::Backward) => {
tab_reachable[(p + tab_reachable.len() - 1) % tab_reachable.len()]
}
};
Some(self.entries[next_idx].id)
}
}
impl Default for FocusRegistry {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for FocusRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FocusRegistry")
.field("entries", &self.entries)
.field("focused", &self.focused)
.field("dirty", &self.dirty)
.finish()
}
}
#[derive(Copy, Clone, Debug)]
enum ShiftDir {
Forward,
Backward,
}
#[cfg(test)]
mod tests {
use super::*;
fn id(n: u64) -> ElementId {
ElementId::from_raw(n)
}
fn entry(n: u64, tab_index: i32) -> FocusableEntry {
FocusableEntry {
id: id(n),
tab_index,
focus_ring: true,
}
}
#[test]
fn empty_registry_has_no_focus_and_no_shift() {
let mut r = FocusRegistry::new();
assert_eq!(r.focused(), None);
assert_eq!(r.shift_forward(), None);
assert_eq!(r.shift_backward(), None);
assert!(!r.is_focusable(id(1)));
}
#[test]
fn single_entry_set_focus() {
let mut r = FocusRegistry::new();
r.register(entry(1, 0));
assert!(r.set_focus(id(1)));
assert_eq!(r.focused(), Some(id(1)));
}
#[test]
fn set_focus_missing_returns_false() {
let mut r = FocusRegistry::new();
r.register(entry(1, 0));
assert!(!r.set_focus(id(99)));
assert_eq!(r.focused(), None);
}
#[test]
fn clear_focus_sets_none() {
let mut r = FocusRegistry::new();
r.register(entry(1, 0));
r.set_focus(id(1));
r.clear_focus();
assert_eq!(r.focused(), None);
}
}