Skip to main content

spec_ai/spec_ai_tui/widget/
focus.rs

1//! Focus management for interactive widgets
2
3use crate::spec_ai_tui::geometry::Rect;
4use std::collections::HashMap;
5use std::sync::atomic::{AtomicU32, Ordering};
6
7/// Unique identifier for a focusable widget
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub struct FocusId(u32);
10
11impl FocusId {
12    /// Generate a new unique focus ID
13    pub fn new() -> Self {
14        static COUNTER: AtomicU32 = AtomicU32::new(0);
15        Self(COUNTER.fetch_add(1, Ordering::Relaxed))
16    }
17
18    /// Get the numeric value
19    pub fn value(&self) -> u32 {
20        self.0
21    }
22}
23
24impl Default for FocusId {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30/// Focus navigation direction
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum FocusDirection {
33    /// Move to next widget in tab order
34    Next,
35    /// Move to previous widget in tab order
36    Previous,
37    /// Move up spatially
38    Up,
39    /// Move down spatially
40    Down,
41    /// Move left spatially
42    Left,
43    /// Move right spatially
44    Right,
45}
46
47/// Manages focus state across widgets
48#[derive(Debug, Default)]
49pub struct FocusManager {
50    /// Currently focused widget
51    current: Option<FocusId>,
52    /// Ordered list of focusable widgets (tab order)
53    focus_order: Vec<FocusId>,
54    /// Spatial positions for directional navigation
55    positions: HashMap<FocusId, Rect>,
56}
57
58impl FocusManager {
59    /// Create a new focus manager
60    pub fn new() -> Self {
61        Self::default()
62    }
63
64    /// Register a focusable widget
65    pub fn register(&mut self, id: FocusId, position: Rect) {
66        if !self.focus_order.contains(&id) {
67            self.focus_order.push(id);
68        }
69        self.positions.insert(id, position);
70
71        // Auto-focus first widget
72        if self.current.is_none() {
73            self.current = Some(id);
74        }
75    }
76
77    /// Unregister a widget
78    pub fn unregister(&mut self, id: FocusId) {
79        self.focus_order.retain(|&i| i != id);
80        self.positions.remove(&id);
81
82        if self.current == Some(id) {
83            self.current = self.focus_order.first().copied();
84        }
85    }
86
87    /// Clear all registered widgets
88    pub fn clear(&mut self) {
89        self.focus_order.clear();
90        self.positions.clear();
91        self.current = None;
92    }
93
94    /// Get the currently focused widget
95    pub fn current(&self) -> Option<FocusId> {
96        self.current
97    }
98
99    /// Check if a widget is focused
100    pub fn is_focused(&self, id: FocusId) -> bool {
101        self.current == Some(id)
102    }
103
104    /// Set focus to a specific widget
105    pub fn focus(&mut self, id: FocusId) {
106        if self.focus_order.contains(&id) {
107            self.current = Some(id);
108        }
109    }
110
111    /// Clear focus (no widget focused)
112    pub fn blur(&mut self) {
113        self.current = None;
114    }
115
116    /// Navigate focus in the given direction
117    pub fn navigate(&mut self, direction: FocusDirection) -> Option<FocusId> {
118        match direction {
119            FocusDirection::Next => self.focus_next(),
120            FocusDirection::Previous => self.focus_previous(),
121            FocusDirection::Up
122            | FocusDirection::Down
123            | FocusDirection::Left
124            | FocusDirection::Right => self.focus_spatial(direction),
125        }
126    }
127
128    /// Move focus to the next widget in tab order
129    pub fn focus_next(&mut self) -> Option<FocusId> {
130        if self.focus_order.is_empty() {
131            return None;
132        }
133
134        let next = match self.current {
135            Some(id) => {
136                let idx = self.focus_order.iter().position(|&i| i == id).unwrap_or(0);
137                self.focus_order[(idx + 1) % self.focus_order.len()]
138            }
139            None => self.focus_order[0],
140        };
141
142        self.current = Some(next);
143        self.current
144    }
145
146    /// Move focus to the previous widget in tab order
147    pub fn focus_previous(&mut self) -> Option<FocusId> {
148        if self.focus_order.is_empty() {
149            return None;
150        }
151
152        let prev = match self.current {
153            Some(id) => {
154                let idx = self.focus_order.iter().position(|&i| i == id).unwrap_or(0);
155                let prev_idx = if idx == 0 {
156                    self.focus_order.len() - 1
157                } else {
158                    idx - 1
159                };
160                self.focus_order[prev_idx]
161            }
162            None => *self.focus_order.last().unwrap(),
163        };
164
165        self.current = Some(prev);
166        self.current
167    }
168
169    /// Move focus spatially in a direction
170    fn focus_spatial(&mut self, direction: FocusDirection) -> Option<FocusId> {
171        let current_pos = self.current.and_then(|id| self.positions.get(&id))?;
172
173        // Find candidates in the given direction
174        let candidates: Vec<_> = self
175            .positions
176            .iter()
177            .filter(|&(&id, _)| Some(id) != self.current)
178            .filter(|(_, rect)| match direction {
179                FocusDirection::Up => rect.bottom() <= current_pos.top(),
180                FocusDirection::Down => rect.top() >= current_pos.bottom(),
181                FocusDirection::Left => rect.right() <= current_pos.left(),
182                FocusDirection::Right => rect.left() >= current_pos.right(),
183                _ => false,
184            })
185            .collect();
186
187        // Find the nearest candidate
188        let nearest = candidates
189            .iter()
190            .min_by_key(|(_, rect)| {
191                let dx = (rect.x as i32 - current_pos.x as i32).abs();
192                let dy = (rect.y as i32 - current_pos.y as i32).abs();
193                dx + dy // Manhattan distance
194            })
195            .map(|&(&id, _)| id);
196
197        if let Some(id) = nearest {
198            self.current = Some(id);
199        }
200
201        self.current
202    }
203
204    /// Get the number of focusable widgets
205    pub fn count(&self) -> usize {
206        self.focus_order.len()
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_focus_id_unique() {
216        let id1 = FocusId::new();
217        let id2 = FocusId::new();
218        assert_ne!(id1, id2);
219    }
220
221    #[test]
222    fn test_focus_manager_register() {
223        let mut fm = FocusManager::new();
224        let id = FocusId::new();
225
226        fm.register(id, Rect::new(0, 0, 10, 10));
227
228        assert_eq!(fm.current(), Some(id)); // Auto-focused
229        assert_eq!(fm.count(), 1);
230    }
231
232    #[test]
233    fn test_focus_next() {
234        let mut fm = FocusManager::new();
235        let id1 = FocusId::new();
236        let id2 = FocusId::new();
237        let id3 = FocusId::new();
238
239        fm.register(id1, Rect::new(0, 0, 10, 10));
240        fm.register(id2, Rect::new(0, 10, 10, 10));
241        fm.register(id3, Rect::new(0, 20, 10, 10));
242
243        assert_eq!(fm.current(), Some(id1));
244        fm.focus_next();
245        assert_eq!(fm.current(), Some(id2));
246        fm.focus_next();
247        assert_eq!(fm.current(), Some(id3));
248        fm.focus_next();
249        assert_eq!(fm.current(), Some(id1)); // Wraps around
250    }
251
252    #[test]
253    fn test_focus_previous() {
254        let mut fm = FocusManager::new();
255        let id1 = FocusId::new();
256        let id2 = FocusId::new();
257
258        fm.register(id1, Rect::new(0, 0, 10, 10));
259        fm.register(id2, Rect::new(0, 10, 10, 10));
260
261        assert_eq!(fm.current(), Some(id1));
262        fm.focus_previous();
263        assert_eq!(fm.current(), Some(id2)); // Wraps around
264    }
265
266    #[test]
267    fn test_focus_unregister() {
268        let mut fm = FocusManager::new();
269        let id1 = FocusId::new();
270        let id2 = FocusId::new();
271
272        fm.register(id1, Rect::new(0, 0, 10, 10));
273        fm.register(id2, Rect::new(0, 10, 10, 10));
274
275        fm.focus(id2);
276        fm.unregister(id2);
277
278        assert_eq!(fm.current(), Some(id1)); // Falls back to first
279        assert_eq!(fm.count(), 1);
280    }
281}