1#![forbid(unsafe_code)]
2
3use std::collections::HashMap;
6
7use ftui_core::event::KeyCode;
8
9use super::spatial;
10use super::{FocusGraph, FocusId, NavDirection};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum FocusEvent {
15 FocusGained { id: FocusId },
16 FocusLost { id: FocusId },
17 FocusMoved { from: FocusId, to: FocusId },
18}
19
20#[derive(Debug, Clone)]
22pub struct FocusGroup {
23 pub id: u32,
24 pub members: Vec<FocusId>,
25 pub wrap: bool,
26 pub exit_key: Option<KeyCode>,
27}
28
29impl FocusGroup {
30 #[must_use]
31 pub fn new(id: u32, members: Vec<FocusId>) -> Self {
32 Self {
33 id,
34 members,
35 wrap: true,
36 exit_key: None,
37 }
38 }
39
40 #[must_use]
41 pub fn with_wrap(mut self, wrap: bool) -> Self {
42 self.wrap = wrap;
43 self
44 }
45
46 #[must_use]
47 pub fn with_exit_key(mut self, key: KeyCode) -> Self {
48 self.exit_key = Some(key);
49 self
50 }
51
52 fn contains(&self, id: FocusId) -> bool {
53 self.members.contains(&id)
54 }
55}
56
57#[derive(Debug, Clone, Copy)]
59pub struct FocusTrap {
60 pub group_id: u32,
61 pub return_focus: Option<FocusId>,
62}
63
64#[derive(Debug, Default)]
66pub struct FocusManager {
67 graph: FocusGraph,
68 current: Option<FocusId>,
69 history: Vec<FocusId>,
70 trap_stack: Vec<FocusTrap>,
71 groups: HashMap<u32, FocusGroup>,
72 last_event: Option<FocusEvent>,
73}
74
75impl FocusManager {
76 #[must_use]
78 pub fn new() -> Self {
79 Self::default()
80 }
81
82 #[must_use]
84 pub fn graph(&self) -> &FocusGraph {
85 &self.graph
86 }
87
88 pub fn graph_mut(&mut self) -> &mut FocusGraph {
90 &mut self.graph
91 }
92
93 #[must_use]
95 pub fn current(&self) -> Option<FocusId> {
96 self.current
97 }
98
99 #[must_use]
101 pub fn is_focused(&self, id: FocusId) -> bool {
102 self.current == Some(id)
103 }
104
105 pub fn focus(&mut self, id: FocusId) -> Option<FocusId> {
107 if !self.can_focus(id) || !self.allowed_by_trap(id) {
108 return None;
109 }
110 let prev = self.current;
111 if prev == Some(id) {
112 return prev;
113 }
114 self.set_focus(id);
115 prev
116 }
117
118 pub fn blur(&mut self) -> Option<FocusId> {
120 let prev = self.current.take();
121 if let Some(id) = prev {
122 self.last_event = Some(FocusEvent::FocusLost { id });
123 }
124 prev
125 }
126
127 pub fn navigate(&mut self, dir: NavDirection) -> bool {
129 match dir {
130 NavDirection::Next => self.focus_next(),
131 NavDirection::Prev => self.focus_prev(),
132 _ => {
133 let Some(current) = self.current else {
134 return false;
135 };
136 let target = self
138 .graph
139 .navigate(current, dir)
140 .or_else(|| spatial::spatial_navigate(&self.graph, current, dir));
141 let Some(target) = target else {
142 return false;
143 };
144 if !self.allowed_by_trap(target) {
145 return false;
146 }
147 self.set_focus(target)
148 }
149 }
150 }
151
152 pub fn focus_next(&mut self) -> bool {
154 self.move_in_tab_order(true)
155 }
156
157 pub fn focus_prev(&mut self) -> bool {
159 self.move_in_tab_order(false)
160 }
161
162 pub fn focus_first(&mut self) -> bool {
164 let order = self.active_tab_order();
165 let Some(first) = order.first().copied() else {
166 return false;
167 };
168 self.set_focus(first)
169 }
170
171 pub fn focus_last(&mut self) -> bool {
173 let order = self.active_tab_order();
174 let Some(last) = order.last().copied() else {
175 return false;
176 };
177 self.set_focus(last)
178 }
179
180 pub fn focus_back(&mut self) -> bool {
182 while let Some(id) = self.history.pop() {
183 if self.can_focus(id) && self.allowed_by_trap(id) {
184 let prev = self.current;
187 self.current = Some(id);
188 self.last_event = Some(match prev {
189 Some(from) => FocusEvent::FocusMoved { from, to: id },
190 None => FocusEvent::FocusGained { id },
191 });
192 return true;
193 }
194 }
195 false
196 }
197
198 pub fn clear_history(&mut self) {
200 self.history.clear();
201 }
202
203 pub fn push_trap(&mut self, group_id: u32) {
205 let return_focus = self.current;
206 self.trap_stack.push(FocusTrap {
207 group_id,
208 return_focus,
209 });
210
211 if !self.is_current_in_group(group_id) {
212 self.focus_first_in_group(group_id);
213 }
214 }
215
216 pub fn pop_trap(&mut self) -> bool {
218 let Some(trap) = self.trap_stack.pop() else {
219 return false;
220 };
221
222 if let Some(id) = trap.return_focus
223 && self.can_focus(id)
224 && self.allowed_by_trap(id)
225 {
226 return self.set_focus(id);
227 }
228
229 if let Some(active) = self.active_trap_group() {
230 return self.focus_first_in_group(active);
231 }
232
233 self.focus_first()
234 }
235
236 #[must_use]
238 pub fn is_trapped(&self) -> bool {
239 !self.trap_stack.is_empty()
240 }
241
242 pub fn create_group(&mut self, id: u32, members: Vec<FocusId>) {
244 let members = self.filter_focusable(members);
245 self.groups.insert(id, FocusGroup::new(id, members));
246 }
247
248 pub fn add_to_group(&mut self, group_id: u32, widget_id: FocusId) {
250 if !self.can_focus(widget_id) {
251 return;
252 }
253 let group = self
254 .groups
255 .entry(group_id)
256 .or_insert_with(|| FocusGroup::new(group_id, Vec::new()));
257 if !group.contains(widget_id) {
258 group.members.push(widget_id);
259 }
260 }
261
262 pub fn remove_from_group(&mut self, group_id: u32, widget_id: FocusId) {
264 let Some(group) = self.groups.get_mut(&group_id) else {
265 return;
266 };
267 group.members.retain(|id| *id != widget_id);
268 }
269
270 #[must_use]
272 pub fn focus_event(&self) -> Option<&FocusEvent> {
273 self.last_event.as_ref()
274 }
275
276 pub fn take_focus_event(&mut self) -> Option<FocusEvent> {
278 self.last_event.take()
279 }
280
281 fn set_focus(&mut self, id: FocusId) -> bool {
282 if !self.can_focus(id) || !self.allowed_by_trap(id) {
283 return false;
284 }
285 if self.current == Some(id) {
286 return false;
287 }
288
289 let prev = self.current;
290 if let Some(prev_id) = prev {
291 if Some(prev_id) != self.history.last().copied() {
292 self.history.push(prev_id);
293 }
294 self.last_event = Some(FocusEvent::FocusMoved {
295 from: prev_id,
296 to: id,
297 });
298 } else {
299 self.last_event = Some(FocusEvent::FocusGained { id });
300 }
301
302 self.current = Some(id);
303 true
304 }
305
306 fn can_focus(&self, id: FocusId) -> bool {
307 self.graph.get(id).map(|n| n.is_focusable).unwrap_or(false)
308 }
309
310 fn active_trap_group(&self) -> Option<u32> {
311 self.trap_stack.last().map(|t| t.group_id)
312 }
313
314 fn allowed_by_trap(&self, id: FocusId) -> bool {
315 let Some(group_id) = self.active_trap_group() else {
316 return true;
317 };
318 self.groups
319 .get(&group_id)
320 .map(|g| g.contains(id))
321 .unwrap_or(false)
322 }
323
324 fn is_current_in_group(&self, group_id: u32) -> bool {
325 let Some(current) = self.current else {
326 return false;
327 };
328 self.groups
329 .get(&group_id)
330 .map(|g| g.contains(current))
331 .unwrap_or(false)
332 }
333
334 fn active_tab_order(&self) -> Vec<FocusId> {
335 if let Some(group_id) = self.active_trap_group() {
336 return self.group_tab_order(group_id);
337 }
338 self.graph.tab_order()
339 }
340
341 fn group_tab_order(&self, group_id: u32) -> Vec<FocusId> {
342 let Some(group) = self.groups.get(&group_id) else {
343 return Vec::new();
344 };
345 let order = self.graph.tab_order();
346 order.into_iter().filter(|id| group.contains(*id)).collect()
347 }
348
349 fn focus_first_in_group(&mut self, group_id: u32) -> bool {
350 let order = self.group_tab_order(group_id);
351 let Some(first) = order.first().copied() else {
352 return false;
353 };
354 self.set_focus(first)
355 }
356
357 fn move_in_tab_order(&mut self, forward: bool) -> bool {
358 let order = self.active_tab_order();
359 if order.is_empty() {
360 return false;
361 }
362
363 let wrap = self
364 .active_trap_group()
365 .and_then(|id| self.groups.get(&id).map(|g| g.wrap))
366 .unwrap_or(true);
367
368 let next = match self.current {
369 None => order[0],
370 Some(current) => {
371 let pos = order.iter().position(|id| *id == current);
372 match pos {
373 None => order[0],
374 Some(idx) if forward => {
375 if idx + 1 < order.len() {
376 order[idx + 1]
377 } else if wrap {
378 order[0]
379 } else {
380 return false;
381 }
382 }
383 Some(idx) => {
384 if idx > 0 {
385 order[idx - 1]
386 } else if wrap {
387 *order.last().unwrap()
388 } else {
389 return false;
390 }
391 }
392 }
393 }
394 };
395
396 self.set_focus(next)
397 }
398
399 fn filter_focusable(&self, ids: Vec<FocusId>) -> Vec<FocusId> {
400 let mut out = Vec::new();
401 for id in ids {
402 if self.can_focus(id) && !out.contains(&id) {
403 out.push(id);
404 }
405 }
406 out
407 }
408}
409
410#[cfg(test)]
415mod tests {
416 use super::*;
417 use crate::focus::FocusNode;
418 use ftui_core::geometry::Rect;
419
420 fn node(id: FocusId, tab: i32) -> FocusNode {
421 FocusNode::new(id, Rect::new(0, 0, 1, 1)).with_tab_index(tab)
422 }
423
424 #[test]
425 fn focus_basic() {
426 let mut fm = FocusManager::new();
427 fm.graph_mut().insert(node(1, 0));
428 fm.graph_mut().insert(node(2, 1));
429
430 assert!(fm.focus(1).is_none());
431 assert_eq!(fm.current(), Some(1));
432
433 assert_eq!(fm.focus(2), Some(1));
434 assert_eq!(fm.current(), Some(2));
435
436 assert_eq!(fm.blur(), Some(2));
437 assert_eq!(fm.current(), None);
438 }
439
440 #[test]
441 fn focus_history_back() {
442 let mut fm = FocusManager::new();
443 fm.graph_mut().insert(node(1, 0));
444 fm.graph_mut().insert(node(2, 1));
445 fm.graph_mut().insert(node(3, 2));
446
447 fm.focus(1);
448 fm.focus(2);
449 fm.focus(3);
450
451 assert!(fm.focus_back());
452 assert_eq!(fm.current(), Some(2));
453
454 assert!(fm.focus_back());
455 assert_eq!(fm.current(), Some(1));
456 }
457
458 #[test]
459 fn focus_next_prev() {
460 let mut fm = FocusManager::new();
461 fm.graph_mut().insert(node(1, 0));
462 fm.graph_mut().insert(node(2, 1));
463 fm.graph_mut().insert(node(3, 2));
464
465 assert!(fm.focus_next());
466 assert_eq!(fm.current(), Some(1));
467
468 assert!(fm.focus_next());
469 assert_eq!(fm.current(), Some(2));
470
471 assert!(fm.focus_prev());
472 assert_eq!(fm.current(), Some(1));
473 }
474
475 #[test]
476 fn focus_trap_push_pop() {
477 let mut fm = FocusManager::new();
478 fm.graph_mut().insert(node(1, 0));
479 fm.graph_mut().insert(node(2, 1));
480 fm.graph_mut().insert(node(3, 2));
481
482 fm.focus(3);
483 fm.create_group(7, vec![1, 2]);
484
485 fm.push_trap(7);
486 assert!(fm.is_trapped());
487 assert_eq!(fm.current(), Some(1));
488
489 fm.pop_trap();
490 assert!(!fm.is_trapped());
491 assert_eq!(fm.current(), Some(3));
492 }
493
494 #[test]
495 fn focus_group_wrap_respected() {
496 let mut fm = FocusManager::new();
497 fm.graph_mut().insert(node(1, 0));
498 fm.graph_mut().insert(node(2, 1));
499 fm.create_group(9, vec![1, 2]);
500 fm.groups.get_mut(&9).unwrap().wrap = false;
501
502 fm.push_trap(9);
503 fm.focus(2);
504 assert!(!fm.focus_next());
505 assert_eq!(fm.current(), Some(2));
506 }
507
508 #[test]
509 fn focus_event_generation() {
510 let mut fm = FocusManager::new();
511 fm.graph_mut().insert(node(1, 0));
512 fm.graph_mut().insert(node(2, 1));
513
514 fm.focus(1);
515 assert_eq!(
516 fm.take_focus_event(),
517 Some(FocusEvent::FocusGained { id: 1 })
518 );
519
520 fm.focus(2);
521 assert_eq!(
522 fm.take_focus_event(),
523 Some(FocusEvent::FocusMoved { from: 1, to: 2 })
524 );
525
526 fm.blur();
527 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 2 }));
528 }
529
530 #[test]
531 fn trap_prevents_focus_outside_group() {
532 let mut fm = FocusManager::new();
533 fm.graph_mut().insert(node(1, 0));
534 fm.graph_mut().insert(node(2, 1));
535 fm.graph_mut().insert(node(3, 2));
536 fm.create_group(5, vec![1, 2]);
537
538 fm.push_trap(5);
539 assert_eq!(fm.current(), Some(1));
540
541 assert!(fm.focus(3).is_none());
543 assert_ne!(fm.current(), Some(3));
544 }
545
546 fn spatial_node(id: FocusId, x: u16, y: u16, w: u16, h: u16, tab: i32) -> FocusNode {
549 FocusNode::new(id, Rect::new(x, y, w, h)).with_tab_index(tab)
550 }
551
552 #[test]
553 fn navigate_spatial_fallback() {
554 let mut fm = FocusManager::new();
555 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
557 fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
558
559 fm.focus(1);
560 assert!(fm.navigate(NavDirection::Right));
561 assert_eq!(fm.current(), Some(2));
562
563 assert!(fm.navigate(NavDirection::Left));
564 assert_eq!(fm.current(), Some(1));
565 }
566
567 #[test]
568 fn navigate_explicit_edge_overrides_spatial() {
569 let mut fm = FocusManager::new();
570 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
571 fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1)); fm.graph_mut().insert(spatial_node(3, 40, 0, 10, 3, 2)); fm.graph_mut().connect(1, NavDirection::Right, 3);
576
577 fm.focus(1);
578 assert!(fm.navigate(NavDirection::Right));
579 assert_eq!(fm.current(), Some(3));
580 }
581
582 #[test]
583 fn navigate_spatial_respects_trap() {
584 let mut fm = FocusManager::new();
585 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
586 fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
587 fm.graph_mut().insert(spatial_node(3, 40, 0, 10, 3, 2));
588
589 fm.create_group(1, vec![1, 2]);
591 fm.focus(2);
592 fm.push_trap(1);
593
594 assert!(!fm.navigate(NavDirection::Right));
596 assert_eq!(fm.current(), Some(2));
597 }
598
599 #[test]
600 fn navigate_spatial_grid_round_trip() {
601 let mut fm = FocusManager::new();
602 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
604 fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
605 fm.graph_mut().insert(spatial_node(3, 0, 6, 10, 3, 2));
606 fm.graph_mut().insert(spatial_node(4, 20, 6, 10, 3, 3));
607
608 fm.focus(1);
609
610 assert!(fm.navigate(NavDirection::Right));
612 assert_eq!(fm.current(), Some(2));
613
614 assert!(fm.navigate(NavDirection::Down));
615 assert_eq!(fm.current(), Some(4));
616
617 assert!(fm.navigate(NavDirection::Left));
618 assert_eq!(fm.current(), Some(3));
619
620 assert!(fm.navigate(NavDirection::Up));
621 assert_eq!(fm.current(), Some(1));
622 }
623
624 #[test]
625 fn navigate_spatial_no_candidate() {
626 let mut fm = FocusManager::new();
627 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
628 fm.focus(1);
629
630 assert!(!fm.navigate(NavDirection::Right));
632 assert!(!fm.navigate(NavDirection::Up));
633 assert_eq!(fm.current(), Some(1));
634 }
635}