Skip to main content

saorsa_core/
focus.rs

1//! Focus management for widget navigation.
2
3/// Unique identifier for a focusable widget.
4pub type WidgetId = u64;
5
6/// Whether a widget currently has focus.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum FocusState {
9    /// The widget has focus.
10    Focused,
11    /// The widget does not have focus.
12    Unfocused,
13}
14
15/// Manages focus among a set of widgets.
16///
17/// Supports Tab / Shift-Tab navigation with wraparound.
18#[derive(Clone, Debug)]
19pub struct FocusManager {
20    /// Ordered list of focusable widget IDs.
21    order: Vec<WidgetId>,
22    /// Index of the currently focused widget, or None if nothing is focused.
23    current: Option<usize>,
24}
25
26impl FocusManager {
27    /// Create a new focus manager with no widgets.
28    pub fn new() -> Self {
29        Self {
30            order: Vec::new(),
31            current: None,
32        }
33    }
34
35    /// Register a widget as focusable. Order of registration determines tab order.
36    pub fn register(&mut self, id: WidgetId) {
37        if !self.order.contains(&id) {
38            self.order.push(id);
39            // Auto-focus the first widget
40            if self.current.is_none() {
41                self.current = Some(0);
42            }
43        }
44    }
45
46    /// Unregister a widget.
47    pub fn unregister(&mut self, id: WidgetId) {
48        if let Some(pos) = self.order.iter().position(|&w| w == id) {
49            self.order.remove(pos);
50            if self.order.is_empty() {
51                self.current = None;
52            } else if let Some(current) = self.current {
53                if current >= self.order.len() {
54                    self.current = Some(self.order.len() - 1);
55                } else if current > pos {
56                    self.current = Some(current - 1);
57                }
58            }
59        }
60    }
61
62    /// Get the currently focused widget ID.
63    pub fn focused(&self) -> Option<WidgetId> {
64        self.current.and_then(|i| self.order.get(i).copied())
65    }
66
67    /// Check if a specific widget has focus.
68    pub fn focus_state(&self, id: WidgetId) -> FocusState {
69        if self.focused() == Some(id) {
70            FocusState::Focused
71        } else {
72            FocusState::Unfocused
73        }
74    }
75
76    /// Move focus to the next widget (Tab).
77    pub fn focus_next(&mut self) {
78        if self.order.is_empty() {
79            return;
80        }
81        match self.current {
82            Some(i) => {
83                self.current = Some((i + 1) % self.order.len());
84            }
85            None => {
86                self.current = Some(0);
87            }
88        }
89    }
90
91    /// Move focus to the previous widget (Shift-Tab).
92    pub fn focus_previous(&mut self) {
93        if self.order.is_empty() {
94            return;
95        }
96        match self.current {
97            Some(i) => {
98                if i == 0 {
99                    self.current = Some(self.order.len() - 1);
100                } else {
101                    self.current = Some(i - 1);
102                }
103            }
104            None => {
105                self.current = Some(self.order.len() - 1);
106            }
107        }
108    }
109
110    /// Set focus directly to a specific widget.
111    pub fn set_focus(&mut self, id: WidgetId) {
112        if let Some(pos) = self.order.iter().position(|&w| w == id) {
113            self.current = Some(pos);
114        }
115    }
116
117    /// Get the number of registered focusable widgets.
118    pub fn count(&self) -> usize {
119        self.order.len()
120    }
121}
122
123impl Default for FocusManager {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn empty_focus_manager() {
135        let fm = FocusManager::new();
136        assert!(fm.focused().is_none());
137        assert_eq!(fm.count(), 0);
138    }
139
140    #[test]
141    fn register_auto_focuses_first() {
142        let mut fm = FocusManager::new();
143        fm.register(1);
144        assert_eq!(fm.focused(), Some(1));
145    }
146
147    #[test]
148    fn focus_next_cycles() {
149        let mut fm = FocusManager::new();
150        fm.register(1);
151        fm.register(2);
152        fm.register(3);
153
154        assert_eq!(fm.focused(), Some(1));
155        fm.focus_next();
156        assert_eq!(fm.focused(), Some(2));
157        fm.focus_next();
158        assert_eq!(fm.focused(), Some(3));
159        fm.focus_next();
160        assert_eq!(fm.focused(), Some(1)); // wraps around
161    }
162
163    #[test]
164    fn focus_previous_cycles() {
165        let mut fm = FocusManager::new();
166        fm.register(1);
167        fm.register(2);
168        fm.register(3);
169
170        assert_eq!(fm.focused(), Some(1));
171        fm.focus_previous();
172        assert_eq!(fm.focused(), Some(3)); // wraps to end
173        fm.focus_previous();
174        assert_eq!(fm.focused(), Some(2));
175    }
176
177    #[test]
178    fn focus_state_query() {
179        let mut fm = FocusManager::new();
180        fm.register(1);
181        fm.register(2);
182
183        assert_eq!(fm.focus_state(1), FocusState::Focused);
184        assert_eq!(fm.focus_state(2), FocusState::Unfocused);
185    }
186
187    #[test]
188    fn set_focus_directly() {
189        let mut fm = FocusManager::new();
190        fm.register(10);
191        fm.register(20);
192        fm.register(30);
193
194        fm.set_focus(30);
195        assert_eq!(fm.focused(), Some(30));
196    }
197
198    #[test]
199    fn unregister_adjusts_focus() {
200        let mut fm = FocusManager::new();
201        fm.register(1);
202        fm.register(2);
203        fm.register(3);
204        fm.set_focus(2);
205
206        fm.unregister(2);
207        // Focus should stay at same index (now pointing to 3)
208        assert_eq!(fm.count(), 2);
209        assert!(fm.focused().is_some());
210    }
211
212    #[test]
213    fn unregister_last_clears_focus() {
214        let mut fm = FocusManager::new();
215        fm.register(1);
216        fm.unregister(1);
217        assert!(fm.focused().is_none());
218    }
219
220    #[test]
221    fn duplicate_register_ignored() {
222        let mut fm = FocusManager::new();
223        fm.register(1);
224        fm.register(1);
225        assert_eq!(fm.count(), 1);
226    }
227
228    #[test]
229    fn focus_next_on_empty_is_noop() {
230        let mut fm = FocusManager::new();
231        fm.focus_next(); // Should not crash
232        assert!(fm.focused().is_none());
233    }
234}