ftui_widgets/modal/
focus_integration.rs1#![forbid(unsafe_code)]
2
3use std::sync::atomic::{AtomicU32, Ordering};
49
50use ftui_core::event::Event;
51use ftui_core::geometry::Rect;
52use ftui_render::frame::Frame;
53
54use crate::focus::{FocusId, FocusManager};
55use crate::modal::{ModalId, ModalResult, ModalStack, StackModal};
56
57static FOCUS_GROUP_COUNTER: AtomicU32 = AtomicU32::new(1_000_000);
59
60fn next_focus_group_id() -> u32 {
62 FOCUS_GROUP_COUNTER.fetch_add(1, Ordering::Relaxed)
63}
64
65pub struct FocusAwareModalStack {
77 stack: ModalStack,
78 focus_manager: FocusManager,
79}
80
81impl Default for FocusAwareModalStack {
82 fn default() -> Self {
83 Self::new()
84 }
85}
86
87impl FocusAwareModalStack {
88 pub fn new() -> Self {
90 Self {
91 stack: ModalStack::new(),
92 focus_manager: FocusManager::new(),
93 }
94 }
95
96 pub fn with_focus_manager(focus_manager: FocusManager) -> Self {
101 Self {
102 stack: ModalStack::new(),
103 focus_manager,
104 }
105 }
106
107 pub fn push(&mut self, modal: Box<dyn StackModal>) -> ModalId {
113 self.stack.push(modal)
114 }
115
116 pub fn push_with_trap(
127 &mut self,
128 modal: Box<dyn StackModal>,
129 focusable_ids: Vec<FocusId>,
130 ) -> ModalId {
131 let group_id = next_focus_group_id();
132
133 self.focus_manager.create_group(group_id, focusable_ids);
135 self.focus_manager.push_trap(group_id);
136
137 self.stack.push_with_focus(modal, Some(group_id))
139 }
140
141 pub fn pop(&mut self) -> Option<ModalResult> {
146 let result = self.stack.pop()?;
147 if result.focus_group_id.is_some() {
148 self.focus_manager.pop_trap();
149 }
150 Some(result)
151 }
152
153 pub fn pop_id(&mut self, id: ModalId) -> Option<ModalResult> {
163 let is_top = self.stack.top_id() == Some(id);
165
166 let result = self.stack.pop_id(id)?;
167
168 if is_top && result.focus_group_id.is_some() {
171 self.focus_manager.pop_trap();
172 }
173
174 Some(result)
175 }
176
177 pub fn pop_all(&mut self) -> Vec<ModalResult> {
179 let results = self.stack.pop_all();
180 for result in &results {
181 if result.focus_group_id.is_some() {
182 self.focus_manager.pop_trap();
183 }
184 }
185 results
186 }
187
188 pub fn handle_event(&mut self, event: &Event) -> Option<ModalResult> {
193 let result = self.stack.handle_event(event)?;
194 if result.focus_group_id.is_some() {
195 self.focus_manager.pop_trap();
196 }
197 Some(result)
198 }
199
200 pub fn render(&self, frame: &mut Frame, screen: Rect) {
202 self.stack.render(frame, screen);
203 }
204
205 #[inline]
209 pub fn is_empty(&self) -> bool {
210 self.stack.is_empty()
211 }
212
213 #[inline]
215 pub fn depth(&self) -> usize {
216 self.stack.depth()
217 }
218
219 #[inline]
221 pub fn is_focus_trapped(&self) -> bool {
222 self.focus_manager.is_trapped()
223 }
224
225 pub fn stack(&self) -> &ModalStack {
227 &self.stack
228 }
229
230 pub fn stack_mut(&mut self) -> &mut ModalStack {
234 &mut self.stack
235 }
236
237 pub fn focus_manager(&self) -> &FocusManager {
239 &self.focus_manager
240 }
241
242 pub fn focus_manager_mut(&mut self) -> &mut FocusManager {
244 &mut self.focus_manager
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use crate::Widget;
252 use crate::focus::FocusNode;
253 use crate::modal::WidgetModalEntry;
254 use ftui_core::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
255 use ftui_core::geometry::Rect;
256
257 #[derive(Debug, Clone)]
258 struct StubWidget;
259
260 impl Widget for StubWidget {
261 fn render(&self, _area: Rect, _frame: &mut Frame) {}
262 }
263
264 fn make_focus_node(id: FocusId) -> FocusNode {
265 FocusNode::new(id, Rect::new(0, 0, 10, 3)).with_tab_index(id as i32)
266 }
267
268 #[test]
269 fn push_with_trap_creates_focus_trap() {
270 let mut modals = FocusAwareModalStack::new();
271
272 modals
274 .focus_manager_mut()
275 .graph_mut()
276 .insert(make_focus_node(1));
277 modals
278 .focus_manager_mut()
279 .graph_mut()
280 .insert(make_focus_node(2));
281 modals
282 .focus_manager_mut()
283 .graph_mut()
284 .insert(make_focus_node(3));
285
286 modals.focus_manager_mut().focus(3);
288 assert_eq!(modals.focus_manager().current(), Some(3));
289
290 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1, 2]);
292
293 assert!(modals.is_focus_trapped());
295 assert_eq!(modals.focus_manager().current(), Some(1));
296 }
297
298 #[test]
299 fn pop_restores_focus() {
300 let mut modals = FocusAwareModalStack::new();
301
302 modals
304 .focus_manager_mut()
305 .graph_mut()
306 .insert(make_focus_node(1));
307 modals
308 .focus_manager_mut()
309 .graph_mut()
310 .insert(make_focus_node(2));
311 modals
312 .focus_manager_mut()
313 .graph_mut()
314 .insert(make_focus_node(3));
315
316 modals.focus_manager_mut().focus(3);
318
319 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1, 2]);
321 assert_eq!(modals.focus_manager().current(), Some(1));
322
323 modals.pop();
325 assert!(!modals.is_focus_trapped());
326 assert_eq!(modals.focus_manager().current(), Some(3));
327 }
328
329 #[test]
330 fn nested_modals_restore_correctly() {
331 let mut modals = FocusAwareModalStack::new();
332
333 for id in 1..=6 {
335 modals
336 .focus_manager_mut()
337 .graph_mut()
338 .insert(make_focus_node(id));
339 }
340
341 modals.focus_manager_mut().focus(1);
343
344 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
346 assert_eq!(modals.focus_manager().current(), Some(2));
347
348 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5, 6]);
350 assert_eq!(modals.focus_manager().current(), Some(4));
351
352 modals.pop();
354 assert_eq!(modals.focus_manager().current(), Some(2));
355
356 modals.pop();
358 assert_eq!(modals.focus_manager().current(), Some(1));
359 assert!(!modals.is_focus_trapped());
360 }
361
362 #[test]
363 fn handle_event_escape_restores_focus() {
364 let mut modals = FocusAwareModalStack::new();
365
366 modals
368 .focus_manager_mut()
369 .graph_mut()
370 .insert(make_focus_node(1));
371 modals
372 .focus_manager_mut()
373 .graph_mut()
374 .insert(make_focus_node(2));
375
376 modals.focus_manager_mut().focus(2);
378
379 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
381 assert_eq!(modals.focus_manager().current(), Some(1));
382
383 let escape = Event::Key(KeyEvent {
385 code: KeyCode::Escape,
386 modifiers: Modifiers::empty(),
387 kind: KeyEventKind::Press,
388 });
389
390 let result = modals.handle_event(&escape);
391 assert!(result.is_some());
392 assert_eq!(modals.focus_manager().current(), Some(2));
393 }
394
395 #[test]
396 fn push_without_trap_no_focus_change() {
397 let mut modals = FocusAwareModalStack::new();
398
399 modals
401 .focus_manager_mut()
402 .graph_mut()
403 .insert(make_focus_node(1));
404 modals
405 .focus_manager_mut()
406 .graph_mut()
407 .insert(make_focus_node(2));
408
409 modals.focus_manager_mut().focus(2);
411
412 modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
414
415 assert!(!modals.is_focus_trapped());
417 assert_eq!(modals.focus_manager().current(), Some(2));
418 }
419
420 #[test]
421 fn pop_all_restores_all_focus() {
422 let mut modals = FocusAwareModalStack::new();
423
424 for id in 1..=4 {
426 modals
427 .focus_manager_mut()
428 .graph_mut()
429 .insert(make_focus_node(id));
430 }
431
432 modals.focus_manager_mut().focus(1);
434
435 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
437 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
438 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
439
440 assert_eq!(modals.depth(), 3);
441 assert_eq!(modals.focus_manager().current(), Some(4));
442
443 let results = modals.pop_all();
445 assert_eq!(results.len(), 3);
446 assert!(modals.is_empty());
447 assert!(!modals.is_focus_trapped());
448 assert_eq!(modals.focus_manager().current(), Some(1));
449 }
450
451 #[test]
452 fn tab_navigation_trapped_in_modal() {
453 let mut modals = FocusAwareModalStack::new();
454
455 for id in 1..=5 {
457 modals
458 .focus_manager_mut()
459 .graph_mut()
460 .insert(make_focus_node(id));
461 }
462
463 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
465
466 assert_eq!(modals.focus_manager().current(), Some(2));
468
469 modals.focus_manager_mut().focus_next();
471 assert_eq!(modals.focus_manager().current(), Some(3));
472
473 modals.focus_manager_mut().focus_next();
475 assert_eq!(modals.focus_manager().current(), Some(2));
476
477 assert!(modals.focus_manager_mut().focus(5).is_none());
479 assert_eq!(modals.focus_manager().current(), Some(2));
480 }
481
482 #[test]
483 fn empty_focus_group_no_panic() {
484 let mut modals = FocusAwareModalStack::new();
485
486 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![]);
488
489 assert!(modals.is_focus_trapped());
491
492 modals.pop();
494 assert!(!modals.is_focus_trapped());
495 }
496
497 #[test]
498 fn pop_id_non_top_modal_does_not_corrupt_focus() {
499 let mut modals = FocusAwareModalStack::new();
500
501 for id in 1..=6 {
503 modals
504 .focus_manager_mut()
505 .graph_mut()
506 .insert(make_focus_node(id));
507 }
508
509 modals.focus_manager_mut().focus(1);
511
512 let id1 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
515 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
516 let _id3 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
517
518 assert_eq!(modals.focus_manager().current(), Some(4));
520
521 modals.pop_id(id1);
525
526 assert!(modals.is_focus_trapped());
528 assert_eq!(modals.focus_manager().current(), Some(4));
530 assert_eq!(modals.depth(), 2);
531
532 modals.pop(); assert_eq!(modals.focus_manager().current(), Some(3));
535
536 modals.pop(); assert_eq!(modals.focus_manager().current(), Some(2));
539
540 assert!(modals.is_empty());
542 assert!(modals.is_focus_trapped());
545 }
546
547 #[test]
548 fn pop_id_top_modal_restores_focus_correctly() {
549 let mut modals = FocusAwareModalStack::new();
550
551 for id in 1..=4 {
553 modals
554 .focus_manager_mut()
555 .graph_mut()
556 .insert(make_focus_node(id));
557 }
558
559 modals.focus_manager_mut().focus(1);
561
562 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
564 let id2 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
565
566 assert_eq!(modals.focus_manager().current(), Some(3));
567
568 modals.pop_id(id2);
570
571 assert_eq!(modals.focus_manager().current(), Some(2));
573 assert!(modals.is_focus_trapped()); modals.pop();
577 assert_eq!(modals.focus_manager().current(), Some(1));
578 assert!(!modals.is_focus_trapped());
579 }
580}