Skip to main content

a3s_tui/
focus.rs

1//! Focus management for navigating between interactive components.
2
3/// Unique identifier for a focusable element.
4pub type FocusId = u32;
5
6/// Manages focus state across multiple focusable elements.
7pub struct FocusManager {
8    focusable: Vec<FocusId>,
9    current: Option<usize>,
10}
11
12impl FocusManager {
13    pub fn new() -> Self {
14        Self {
15            focusable: Vec::new(),
16            current: None,
17        }
18    }
19
20    pub fn register(&mut self, id: FocusId) {
21        if !self.focusable.contains(&id) {
22            self.focusable.push(id);
23            if self.current.is_none() {
24                self.current = Some(0);
25            }
26        }
27    }
28
29    pub fn unregister(&mut self, id: FocusId) {
30        if let Some(pos) = self.focusable.iter().position(|&x| x == id) {
31            let current = self.normalized_current();
32            self.focusable.remove(pos);
33            if self.focusable.is_empty() {
34                self.current = None;
35            } else if let Some(cur) = current {
36                if pos < cur {
37                    self.current = Some(cur - 1);
38                } else if cur >= self.focusable.len() {
39                    self.current = Some(self.focusable.len() - 1);
40                }
41            }
42        }
43    }
44
45    pub fn focus_next(&mut self) {
46        if self.focusable.is_empty() {
47            return;
48        }
49        self.current = Some(match self.current {
50            Some(_) => {
51                let current = self.normalized_current().unwrap_or(0);
52                if current + 1 == self.focusable.len() {
53                    0
54                } else {
55                    current + 1
56                }
57            }
58            None => 0,
59        });
60    }
61
62    pub fn focus_prev(&mut self) {
63        if self.focusable.is_empty() {
64            return;
65        }
66        self.current = Some(match self.current {
67            Some(_) => self
68                .normalized_current()
69                .and_then(|idx| idx.checked_sub(1))
70                .unwrap_or_else(|| self.focusable.len() - 1),
71            None => 0,
72        });
73    }
74
75    pub fn focus(&mut self, id: FocusId) {
76        if let Some(pos) = self.focusable.iter().position(|&x| x == id) {
77            self.current = Some(pos);
78        }
79    }
80
81    pub fn is_focused(&self, id: FocusId) -> bool {
82        match self.normalized_current() {
83            Some(idx) => self.focusable.get(idx) == Some(&id),
84            None => false,
85        }
86    }
87
88    pub fn current(&self) -> Option<FocusId> {
89        self.normalized_current()
90            .and_then(|idx| self.focusable.get(idx).copied())
91    }
92
93    pub fn clear(&mut self) {
94        self.focusable.clear();
95        self.current = None;
96    }
97
98    fn normalized_current(&self) -> Option<usize> {
99        self.current
100            .map(|idx| idx.min(self.focusable.len().saturating_sub(1)))
101    }
102}
103
104impl Default for FocusManager {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn register_sets_initial_focus() {
116        let mut fm = FocusManager::new();
117        fm.register(1);
118        assert_eq!(fm.current(), Some(1));
119        assert!(fm.is_focused(1));
120    }
121
122    #[test]
123    fn focus_next_cycles() {
124        let mut fm = FocusManager::new();
125        fm.register(1);
126        fm.register(2);
127        fm.register(3);
128        assert_eq!(fm.current(), Some(1));
129        fm.focus_next();
130        assert_eq!(fm.current(), Some(2));
131        fm.focus_next();
132        assert_eq!(fm.current(), Some(3));
133        fm.focus_next();
134        assert_eq!(fm.current(), Some(1));
135    }
136
137    #[test]
138    fn focus_prev_cycles() {
139        let mut fm = FocusManager::new();
140        fm.register(1);
141        fm.register(2);
142        fm.register(3);
143        fm.focus_prev();
144        assert_eq!(fm.current(), Some(3));
145        fm.focus_prev();
146        assert_eq!(fm.current(), Some(2));
147    }
148
149    #[test]
150    fn unregister_adjusts_focus() {
151        let mut fm = FocusManager::new();
152        fm.register(1);
153        fm.register(2);
154        fm.register(3);
155        fm.focus_next();
156        fm.focus_next();
157        assert_eq!(fm.current(), Some(3));
158        fm.unregister(3);
159        assert_eq!(fm.current(), Some(2));
160    }
161
162    #[test]
163    fn unregister_before_current_preserves_focused_id() {
164        let mut fm = FocusManager::new();
165        fm.register(1);
166        fm.register(2);
167        fm.register(3);
168        fm.focus(2);
169
170        fm.unregister(1);
171
172        assert_eq!(fm.current(), Some(2));
173        assert!(fm.is_focused(2));
174    }
175
176    #[test]
177    fn unregister_all_clears_focus() {
178        let mut fm = FocusManager::new();
179        fm.register(1);
180        fm.unregister(1);
181        assert_eq!(fm.current(), None);
182    }
183
184    #[test]
185    fn focus_specific_id() {
186        let mut fm = FocusManager::new();
187        fm.register(10);
188        fm.register(20);
189        fm.register(30);
190        fm.focus(20);
191        assert!(fm.is_focused(20));
192        assert!(!fm.is_focused(10));
193    }
194
195    #[test]
196    fn clear_removes_all() {
197        let mut fm = FocusManager::new();
198        fm.register(1);
199        fm.register(2);
200        fm.clear();
201        assert_eq!(fm.current(), None);
202    }
203
204    #[test]
205    fn focus_next_on_empty_is_noop() {
206        let mut fm = FocusManager::new();
207        fm.focus_next();
208        assert_eq!(fm.current(), None);
209    }
210
211    #[test]
212    fn stale_current_is_normalized_for_queries() {
213        let mut fm = FocusManager::new();
214        fm.register(1);
215        fm.register(2);
216        fm.register(3);
217        fm.current = Some(usize::MAX);
218
219        assert_eq!(fm.current(), Some(3));
220        assert!(fm.is_focused(3));
221    }
222
223    #[test]
224    fn focus_next_normalizes_stale_current() {
225        let mut fm = FocusManager::new();
226        fm.register(1);
227        fm.register(2);
228        fm.register(3);
229        fm.current = Some(usize::MAX);
230
231        fm.focus_next();
232
233        assert_eq!(fm.current(), Some(1));
234    }
235
236    #[test]
237    fn focus_prev_normalizes_stale_current() {
238        let mut fm = FocusManager::new();
239        fm.register(1);
240        fm.register(2);
241        fm.register(3);
242        fm.current = Some(usize::MAX);
243
244        fm.focus_prev();
245
246        assert_eq!(fm.current(), Some(2));
247    }
248
249    #[test]
250    fn unregister_normalizes_stale_current() {
251        let mut fm = FocusManager::new();
252        fm.register(1);
253        fm.register(2);
254        fm.register(3);
255        fm.current = Some(usize::MAX);
256
257        fm.unregister(1);
258
259        assert_eq!(fm.current(), Some(3));
260    }
261}