a3s-tui 0.1.14

TEA (The Elm Architecture) framework for terminal user interfaces
Documentation
//! Focus management for navigating between interactive components.

/// Unique identifier for a focusable element.
pub type FocusId = u32;

/// Manages focus state across multiple focusable elements.
pub struct FocusManager {
    focusable: Vec<FocusId>,
    current: Option<usize>,
}

impl FocusManager {
    pub fn new() -> Self {
        Self {
            focusable: Vec::new(),
            current: None,
        }
    }

    pub fn register(&mut self, id: FocusId) {
        if !self.focusable.contains(&id) {
            self.focusable.push(id);
            if self.current.is_none() {
                self.current = Some(0);
            }
        }
    }

    pub fn unregister(&mut self, id: FocusId) {
        if let Some(pos) = self.focusable.iter().position(|&x| x == id) {
            let current = self.normalized_current();
            self.focusable.remove(pos);
            if self.focusable.is_empty() {
                self.current = None;
            } else if let Some(cur) = current {
                if pos < cur {
                    self.current = Some(cur - 1);
                } else if cur >= self.focusable.len() {
                    self.current = Some(self.focusable.len() - 1);
                }
            }
        }
    }

    pub fn focus_next(&mut self) {
        if self.focusable.is_empty() {
            return;
        }
        self.current = Some(match self.current {
            Some(_) => {
                let current = self.normalized_current().unwrap_or(0);
                if current + 1 == self.focusable.len() {
                    0
                } else {
                    current + 1
                }
            }
            None => 0,
        });
    }

    pub fn focus_prev(&mut self) {
        if self.focusable.is_empty() {
            return;
        }
        self.current = Some(match self.current {
            Some(_) => self
                .normalized_current()
                .and_then(|idx| idx.checked_sub(1))
                .unwrap_or_else(|| self.focusable.len() - 1),
            None => 0,
        });
    }

    pub fn focus(&mut self, id: FocusId) {
        if let Some(pos) = self.focusable.iter().position(|&x| x == id) {
            self.current = Some(pos);
        }
    }

    pub fn is_focused(&self, id: FocusId) -> bool {
        match self.normalized_current() {
            Some(idx) => self.focusable.get(idx) == Some(&id),
            None => false,
        }
    }

    pub fn current(&self) -> Option<FocusId> {
        self.normalized_current()
            .and_then(|idx| self.focusable.get(idx).copied())
    }

    pub fn clear(&mut self) {
        self.focusable.clear();
        self.current = None;
    }

    fn normalized_current(&self) -> Option<usize> {
        self.current
            .map(|idx| idx.min(self.focusable.len().saturating_sub(1)))
    }
}

impl Default for FocusManager {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn register_sets_initial_focus() {
        let mut fm = FocusManager::new();
        fm.register(1);
        assert_eq!(fm.current(), Some(1));
        assert!(fm.is_focused(1));
    }

    #[test]
    fn focus_next_cycles() {
        let mut fm = FocusManager::new();
        fm.register(1);
        fm.register(2);
        fm.register(3);
        assert_eq!(fm.current(), Some(1));
        fm.focus_next();
        assert_eq!(fm.current(), Some(2));
        fm.focus_next();
        assert_eq!(fm.current(), Some(3));
        fm.focus_next();
        assert_eq!(fm.current(), Some(1));
    }

    #[test]
    fn focus_prev_cycles() {
        let mut fm = FocusManager::new();
        fm.register(1);
        fm.register(2);
        fm.register(3);
        fm.focus_prev();
        assert_eq!(fm.current(), Some(3));
        fm.focus_prev();
        assert_eq!(fm.current(), Some(2));
    }

    #[test]
    fn unregister_adjusts_focus() {
        let mut fm = FocusManager::new();
        fm.register(1);
        fm.register(2);
        fm.register(3);
        fm.focus_next();
        fm.focus_next();
        assert_eq!(fm.current(), Some(3));
        fm.unregister(3);
        assert_eq!(fm.current(), Some(2));
    }

    #[test]
    fn unregister_before_current_preserves_focused_id() {
        let mut fm = FocusManager::new();
        fm.register(1);
        fm.register(2);
        fm.register(3);
        fm.focus(2);

        fm.unregister(1);

        assert_eq!(fm.current(), Some(2));
        assert!(fm.is_focused(2));
    }

    #[test]
    fn unregister_all_clears_focus() {
        let mut fm = FocusManager::new();
        fm.register(1);
        fm.unregister(1);
        assert_eq!(fm.current(), None);
    }

    #[test]
    fn focus_specific_id() {
        let mut fm = FocusManager::new();
        fm.register(10);
        fm.register(20);
        fm.register(30);
        fm.focus(20);
        assert!(fm.is_focused(20));
        assert!(!fm.is_focused(10));
    }

    #[test]
    fn clear_removes_all() {
        let mut fm = FocusManager::new();
        fm.register(1);
        fm.register(2);
        fm.clear();
        assert_eq!(fm.current(), None);
    }

    #[test]
    fn focus_next_on_empty_is_noop() {
        let mut fm = FocusManager::new();
        fm.focus_next();
        assert_eq!(fm.current(), None);
    }

    #[test]
    fn stale_current_is_normalized_for_queries() {
        let mut fm = FocusManager::new();
        fm.register(1);
        fm.register(2);
        fm.register(3);
        fm.current = Some(usize::MAX);

        assert_eq!(fm.current(), Some(3));
        assert!(fm.is_focused(3));
    }

    #[test]
    fn focus_next_normalizes_stale_current() {
        let mut fm = FocusManager::new();
        fm.register(1);
        fm.register(2);
        fm.register(3);
        fm.current = Some(usize::MAX);

        fm.focus_next();

        assert_eq!(fm.current(), Some(1));
    }

    #[test]
    fn focus_prev_normalizes_stale_current() {
        let mut fm = FocusManager::new();
        fm.register(1);
        fm.register(2);
        fm.register(3);
        fm.current = Some(usize::MAX);

        fm.focus_prev();

        assert_eq!(fm.current(), Some(2));
    }

    #[test]
    fn unregister_normalizes_stale_current() {
        let mut fm = FocusManager::new();
        fm.register(1);
        fm.register(2);
        fm.register(3);
        fm.current = Some(usize::MAX);

        fm.unregister(1);

        assert_eq!(fm.current(), Some(3));
    }
}