Skip to main content

gpui_kit/overlay/
focus.rs

1//! Keeping keyboard focus inside an open overlay, and giving it back on close.
2//!
3//! GPUI's tab order is window-wide. A modal that used it would let Tab walk
4//! out of the dialog and into the content behind the scrim, which the user
5//! cannot see or click, so an overlay tracks its own stops instead.
6
7use gpui::{App, FocusHandle, Window};
8
9/// The focus stops of one open overlay, in tab order.
10///
11/// Content re-registers its stops on every render, because the set changes
12/// with the content itself.
13#[derive(Debug, Clone, Default)]
14pub struct FocusTrap {
15    stops: Vec<FocusHandle>,
16    restore: Option<FocusHandle>,
17    engaged: bool,
18}
19
20impl FocusTrap {
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    pub fn is_engaged(&self) -> bool {
26        self.engaged
27    }
28
29    pub fn stops(&self) -> &[FocusHandle] {
30        &self.stops
31    }
32
33    /// Records what was focused before the overlay opened, so the same element
34    /// can be focused again when it closes.
35    pub fn engage(&mut self, window: &Window, cx: &App) {
36        if self.engaged {
37            return;
38        }
39        self.restore = window.focused(cx);
40        self.engaged = true;
41    }
42
43    /// Restores the previously focused element. Nothing is focused when the
44    /// element that had focus is gone, rather than guessing at a replacement.
45    pub fn release(&mut self, window: &mut Window, cx: &mut App) {
46        if let Some(handle) = self.restore.take() {
47            handle.focus(window, cx);
48        }
49        self.stops.clear();
50        self.engaged = false;
51    }
52
53    /// Clears the registered stops before a render republishes them.
54    pub fn begin_frame(&mut self) {
55        self.stops.clear();
56    }
57
58    pub fn register(&mut self, handle: FocusHandle) {
59        if !self.stops.iter().any(|stop| stop == &handle) {
60            self.stops.push(handle);
61        }
62    }
63
64    /// Focuses the first stop, which is what opening an overlay should do.
65    pub fn focus_first(&self, window: &mut Window, cx: &mut App) {
66        if let Some(handle) = self.stops.first() {
67            handle.focus(window, cx);
68        }
69    }
70
71    pub fn focus_next(&self, window: &mut Window, cx: &mut App) {
72        self.step(1, window, cx);
73    }
74
75    pub fn focus_prev(&self, window: &mut Window, cx: &mut App) {
76        self.step(-1, window, cx);
77    }
78
79    /// True while focus is inside the overlay.
80    pub fn contains_focus(&self, window: &Window, cx: &App) -> bool {
81        window
82            .focused(cx)
83            .is_some_and(|focused| self.stops.contains(&focused))
84    }
85
86    fn step(&self, delta: isize, window: &mut Window, cx: &mut App) {
87        if self.stops.is_empty() {
88            return;
89        }
90        let current = window
91            .focused(cx)
92            .and_then(|focused| self.stops.iter().position(|stop| stop == &focused));
93        let next = match current {
94            // Focus entering from outside starts at the appropriate end rather
95            // than jumping to the middle of the overlay.
96            None if delta >= 0 => 0,
97            None => self.stops.len() - 1,
98            Some(index) => (index as isize + delta).rem_euclid(self.stops.len() as isize) as usize,
99        };
100        self.stops[next].focus(window, cx);
101    }
102}