use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SemanticRole {
Region,
Navigation,
List,
ListItem,
Search,
Status,
Alert,
Dialog,
Toolbar,
Grid,
}
impl std::fmt::Display for SemanticRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Region => write!(f, "region"),
Self::Navigation => write!(f, "navigation"),
Self::List => write!(f, "list"),
Self::ListItem => write!(f, "listitem"),
Self::Search => write!(f, "search"),
Self::Status => write!(f, "status"),
Self::Alert => write!(f, "alert"),
Self::Dialog => write!(f, "dialog"),
Self::Toolbar => write!(f, "toolbar"),
Self::Grid => write!(f, "grid"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FocusDirection {
Next,
Previous,
Up,
Down,
Left,
Right,
}
pub struct FocusManager {
widgets: Vec<String>,
focused: usize,
}
impl FocusManager {
#[must_use]
pub const fn new() -> Self {
Self {
widgets: Vec::new(),
focused: 0,
}
}
pub fn register(&mut self, id: impl Into<String>) {
self.widgets.push(id.into());
}
#[must_use]
pub fn focused(&self) -> Option<&str> {
self.widgets.get(self.focused).map(String::as_str)
}
#[must_use]
pub fn is_focused(&self, id: &str) -> bool {
self.focused() == Some(id)
}
pub const fn move_focus(&mut self, direction: FocusDirection) {
if self.widgets.is_empty() {
return;
}
match direction {
FocusDirection::Next | FocusDirection::Down | FocusDirection::Right => {
self.focused = (self.focused + 1) % self.widgets.len();
}
FocusDirection::Previous | FocusDirection::Up | FocusDirection::Left => {
self.focused = if self.focused == 0 {
self.widgets.len() - 1
} else {
self.focused - 1
};
}
}
}
pub fn focus(&mut self, id: &str) -> bool {
if let Some(pos) = self.widgets.iter().position(|w| w == id) {
self.focused = pos;
true
} else {
false
}
}
#[must_use]
pub const fn len(&self) -> usize {
self.widgets.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.widgets.is_empty()
}
pub fn clear(&mut self) {
self.widgets.clear();
self.focused = 0;
}
}
impl Default for FocusManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn focus_manager_empty() {
let fm = FocusManager::new();
assert!(fm.is_empty());
assert!(fm.focused().is_none());
}
#[test]
fn focus_manager_register_and_focus() {
let mut fm = FocusManager::new();
fm.register("search");
fm.register("results");
fm.register("details");
assert_eq!(fm.len(), 3);
assert_eq!(fm.focused(), Some("search"));
assert!(fm.is_focused("search"));
}
#[test]
fn focus_manager_next_wraps() {
let mut fm = FocusManager::new();
fm.register("a");
fm.register("b");
fm.register("c");
fm.move_focus(FocusDirection::Next);
assert_eq!(fm.focused(), Some("b"));
fm.move_focus(FocusDirection::Next);
assert_eq!(fm.focused(), Some("c"));
fm.move_focus(FocusDirection::Next);
assert_eq!(fm.focused(), Some("a")); }
#[test]
fn focus_manager_prev_wraps() {
let mut fm = FocusManager::new();
fm.register("a");
fm.register("b");
fm.move_focus(FocusDirection::Previous);
assert_eq!(fm.focused(), Some("b")); }
#[test]
fn focus_manager_focus_by_id() {
let mut fm = FocusManager::new();
fm.register("x");
fm.register("y");
fm.register("z");
assert!(fm.focus("z"));
assert_eq!(fm.focused(), Some("z"));
assert!(!fm.focus("nonexistent"));
}
#[test]
fn focus_manager_clear() {
let mut fm = FocusManager::new();
fm.register("a");
fm.register("b");
fm.clear();
assert!(fm.is_empty());
assert!(fm.focused().is_none());
}
#[test]
fn semantic_role_display() {
assert_eq!(SemanticRole::Region.to_string(), "region");
assert_eq!(SemanticRole::Navigation.to_string(), "navigation");
assert_eq!(SemanticRole::Grid.to_string(), "grid");
}
#[test]
fn semantic_role_serde_roundtrip() {
for role in [
SemanticRole::Region,
SemanticRole::List,
SemanticRole::Search,
SemanticRole::Dialog,
] {
let json = serde_json::to_string(&role).unwrap();
let decoded: SemanticRole = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, role);
}
}
#[test]
fn focus_direction_aliases() {
let mut fm = FocusManager::new();
fm.register("a");
fm.register("b");
fm.move_focus(FocusDirection::Down);
assert_eq!(fm.focused(), Some("b"));
fm.move_focus(FocusDirection::Up);
assert_eq!(fm.focused(), Some("a"));
}
}