use retroglyph_core::{Event, KeyCode};
#[derive(Debug, Clone)]
pub struct FocusRing<Id> {
current: Option<Id>,
order: Vec<Id>,
pending: Vec<Id>,
}
impl<Id> FocusRing<Id> {
#[must_use]
pub const fn new() -> Self {
Self {
current: None,
order: Vec::new(),
pending: Vec::new(),
}
}
pub fn begin_frame(&mut self) {
self.order = core::mem::take(&mut self.pending);
}
pub fn clear(&mut self) {
self.current = None;
}
}
impl<Id: Copy + PartialEq> FocusRing<Id> {
pub fn register(&mut self, id: Id) {
self.pending.push(id);
}
#[must_use]
pub const fn focused(&self) -> Option<Id> {
self.current
}
#[must_use]
pub fn is_focused(&self, id: Id) -> bool {
self.current == Some(id)
}
pub const fn request(&mut self, id: Id) {
self.current = Some(id);
}
pub fn advance(&mut self) {
self.current = Self::step(&self.order, self.current, 1);
}
pub fn retreat(&mut self) {
self.current = Self::step(&self.order, self.current, -1);
}
pub fn handle_event(&mut self, event: &Event) {
let Event::Key(key) = event else {
return;
};
if !key.is_down() {
return;
}
match key.code {
KeyCode::Tab => self.advance(),
KeyCode::BackTab => self.retreat(),
_ => {}
}
}
fn step(order: &[Id], current: Option<Id>, delta: i32) -> Option<Id> {
if order.is_empty() {
return None;
}
let Ok(len) = i32::try_from(order.len()) else {
return current; };
let index = current.and_then(|id| order.iter().position(|&o| o == id));
let base = index.map_or(if delta > 0 { -1 } else { 0 }, |i| {
i32::try_from(i).unwrap_or(0)
});
let next = (base + delta).rem_euclid(len);
usize::try_from(next)
.ok()
.and_then(|i| order.get(i))
.copied()
}
}
impl<Id> Default for FocusRing<Id> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use retroglyph_core::{KeyEvent, KeyModifiers};
use super::*;
fn ring_of(ids: &[&'static str]) -> FocusRing<&'static str> {
let mut ring = FocusRing::new();
for &id in ids {
ring.register(id);
}
ring.begin_frame();
ring
}
#[test]
fn advance_from_nothing_focuses_the_first() {
let mut ring = ring_of(&["a", "b", "c"]);
ring.advance();
assert_eq!(ring.focused(), Some("a"));
}
#[test]
fn retreat_from_nothing_focuses_the_last() {
let mut ring = ring_of(&["a", "b", "c"]);
ring.retreat();
assert_eq!(ring.focused(), Some("c"));
}
#[test]
fn advance_wraps_past_the_end() {
let mut ring = ring_of(&["a", "b"]);
ring.request("b");
ring.advance();
assert_eq!(ring.focused(), Some("a"));
}
#[test]
fn retreat_wraps_past_the_start() {
let mut ring = ring_of(&["a", "b"]);
ring.request("a");
ring.retreat();
assert_eq!(ring.focused(), Some("b"));
}
#[test]
fn stale_focus_not_in_order_is_treated_as_unfocused() {
let mut ring = ring_of(&["a", "b"]);
ring.request("gone"); ring.advance();
assert_eq!(ring.focused(), Some("a"));
}
#[test]
fn empty_order_is_a_no_op() {
let mut ring: FocusRing<&str> = FocusRing::new();
ring.begin_frame();
ring.advance();
assert_eq!(ring.focused(), None);
}
#[test]
fn tab_and_backtab_cycle_focus() {
let mut ring = ring_of(&["a", "b"]);
ring.handle_event(&Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)));
assert_eq!(ring.focused(), Some("a"));
ring.handle_event(&Event::Key(KeyEvent::new(
KeyCode::BackTab,
KeyModifiers::NONE,
)));
assert_eq!(ring.focused(), Some("b")); }
#[test]
fn clear_drops_focus() {
let mut ring = ring_of(&["a"]);
ring.request("a");
ring.clear();
assert_eq!(ring.focused(), None);
}
}