spec_ai/spec_ai_tui/widget/
focus.rs1use crate::spec_ai_tui::geometry::Rect;
4use std::collections::HashMap;
5use std::sync::atomic::{AtomicU32, Ordering};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub struct FocusId(u32);
10
11impl FocusId {
12 pub fn new() -> Self {
14 static COUNTER: AtomicU32 = AtomicU32::new(0);
15 Self(COUNTER.fetch_add(1, Ordering::Relaxed))
16 }
17
18 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum FocusDirection {
33 Next,
35 Previous,
37 Up,
39 Down,
41 Left,
43 Right,
45}
46
47#[derive(Debug, Default)]
49pub struct FocusManager {
50 current: Option<FocusId>,
52 focus_order: Vec<FocusId>,
54 positions: HashMap<FocusId, Rect>,
56}
57
58impl FocusManager {
59 pub fn new() -> Self {
61 Self::default()
62 }
63
64 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 if self.current.is_none() {
73 self.current = Some(id);
74 }
75 }
76
77 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 pub fn clear(&mut self) {
89 self.focus_order.clear();
90 self.positions.clear();
91 self.current = None;
92 }
93
94 pub fn current(&self) -> Option<FocusId> {
96 self.current
97 }
98
99 pub fn is_focused(&self, id: FocusId) -> bool {
101 self.current == Some(id)
102 }
103
104 pub fn focus(&mut self, id: FocusId) {
106 if self.focus_order.contains(&id) {
107 self.current = Some(id);
108 }
109 }
110
111 pub fn blur(&mut self) {
113 self.current = None;
114 }
115
116 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 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 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 fn focus_spatial(&mut self, direction: FocusDirection) -> Option<FocusId> {
171 let current_pos = self.current.and_then(|id| self.positions.get(&id))?;
172
173 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 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 })
195 .map(|&(&id, _)| id);
196
197 if let Some(id) = nearest {
198 self.current = Some(id);
199 }
200
201 self.current
202 }
203
204 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)); 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)); }
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)); }
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)); assert_eq!(fm.count(), 1);
280 }
281}