oo-ide 0.0.4

∞ is a terminal IDE focused on low distraction, high usability.
Documentation
use super::focusable::WidgetId;

/// Manages a linear focus ring of widget IDs.
///
/// Views use this to track which widget has focus and to cycle through
/// the focus order with Tab / Shift+Tab.
#[derive(Debug, Clone)]
pub struct FocusRing {
    order: Vec<WidgetId>,
    current: usize,
}

impl FocusRing {
    pub fn new(order: Vec<WidgetId>) -> Self {
        assert!(!order.is_empty(), "FocusRing must have at least one entry");
        Self { order, current: 0 }
    }

    /// The currently focused widget's ID.
    pub fn current(&self) -> WidgetId {
        self.order[self.current]
    }

    /// Advance focus to the next widget (wraps around).
    pub fn focus_next(&mut self) {
        self.current = (self.current + 1) % self.order.len();
    }

    /// Move focus to the previous widget (wraps around).
    pub fn focus_prev(&mut self) {
        self.current = if self.current == 0 {
            self.order.len() - 1
        } else {
            self.current - 1
        };
    }

    /// Set focus to the widget with the given ID.
    /// No-op if the ID is not in the ring.
    pub fn set_focus(&mut self, id: WidgetId) {
        if let Some(pos) = self.order.iter().position(|&w| w == id) {
            self.current = pos;
        }
    }

    /// Returns `true` if the given widget currently has focus.
    pub fn is_focused(&self, id: WidgetId) -> bool {
        self.order[self.current] == id
    }

    /// Returns `true` if the ring contains no widgets.
    pub fn is_empty(&self) -> bool {
        self.order.is_empty()
    }

    /// The number of widgets in the ring.
    pub fn len(&self) -> usize {
        self.order.len()
    }

    /// The ordered list of widget IDs.
    pub fn order(&self) -> &[WidgetId] {
        &self.order
    }
}

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

    #[test]
    fn test_new_starts_at_first() {
        let ring = FocusRing::new(vec!["a", "b", "c"]);
        assert_eq!(ring.current(), "a");
    }

    #[test]
    fn test_focus_next_cycles() {
        let mut ring = FocusRing::new(vec!["a", "b", "c"]);
        ring.focus_next();
        assert_eq!(ring.current(), "b");
        ring.focus_next();
        assert_eq!(ring.current(), "c");
        ring.focus_next();
        assert_eq!(ring.current(), "a");
    }

    #[test]
    fn test_focus_prev_cycles() {
        let mut ring = FocusRing::new(vec!["a", "b", "c"]);
        ring.focus_prev();
        assert_eq!(ring.current(), "c");
        ring.focus_prev();
        assert_eq!(ring.current(), "b");
        ring.focus_prev();
        assert_eq!(ring.current(), "a");
    }

    #[test]
    fn test_set_focus() {
        let mut ring = FocusRing::new(vec!["a", "b", "c"]);
        ring.set_focus("c");
        assert_eq!(ring.current(), "c");
    }

    #[test]
    fn test_set_focus_unknown_id_is_noop() {
        let mut ring = FocusRing::new(vec!["a", "b", "c"]);
        ring.set_focus("z");
        assert_eq!(ring.current(), "a");
    }

    #[test]
    fn test_is_focused() {
        let ring = FocusRing::new(vec!["a", "b", "c"]);
        assert!(ring.is_focused("a"));
        assert!(!ring.is_focused("b"));
    }

    #[test]
    fn test_single_element_ring() {
        let mut ring = FocusRing::new(vec!["only"]);
        assert_eq!(ring.current(), "only");
        ring.focus_next();
        assert_eq!(ring.current(), "only");
        ring.focus_prev();
        assert_eq!(ring.current(), "only");
    }

    #[test]
    fn test_len() {
        let ring = FocusRing::new(vec!["a", "b"]);
        assert_eq!(ring.len(), 2);
    }

    #[test]
    fn test_order() {
        let ring = FocusRing::new(vec!["x", "y", "z"]);
        assert_eq!(ring.order(), &["x", "y", "z"]);
    }

    #[test]
    fn test_next_then_prev_returns_to_start() {
        let mut ring = FocusRing::new(vec!["a", "b", "c"]);
        ring.focus_next();
        ring.focus_next();
        ring.focus_prev();
        ring.focus_prev();
        assert_eq!(ring.current(), "a");
    }

    #[test]
    #[should_panic(expected = "FocusRing must have at least one entry")]
    fn test_empty_ring_panics() {
        FocusRing::new(vec![]);
    }
}