Skip to main content

gpui_macos/
focus.rs

1//! Keyboard traversal for a composed desktop region.
2use gpui_kit::{App, Div, ElementId, KeyBinding, Stateful, Window, actions, div, prelude::*};
3
4actions!(macos_focus, [Next, Previous]);
5
6pub(crate) fn init(cx: &mut App) {
7    cx.bind_keys([
8        KeyBinding::new(
9            if cfg!(target_os = "macos") {
10                "cmd-c"
11            } else {
12                "ctrl-c"
13            },
14            gpui_kit::base::input::Copy,
15            Some("macOSFocusScope"),
16        ),
17        KeyBinding::new("tab", Next, Some("macOSFocusScope")),
18        KeyBinding::new("shift-tab", Previous, Some("macOSFocusScope")),
19    ]);
20}
21
22/// Wrap a window or form in this scope to connect Tab and Shift+Tab to GPUI's
23/// tab order. Nested editors may consume Tab for indentation; single-line
24/// fields propagate it here. Popup-specific handlers take priority.
25pub fn focus_scope(id: impl Into<ElementId>) -> Stateful<Div> {
26    div()
27        .id(id)
28        .tab_group()
29        .key_context("macOSFocusScope")
30        .on_action(|_: &gpui_kit::base::input::Copy, window: &mut Window, cx| {
31            let selected = gpui_kit::base::TextSelection::selected_text(window, cx);
32            if selected.is_empty() {
33                cx.propagate();
34            } else {
35                cx.write_to_clipboard(gpui_kit::ClipboardItem::new_string(selected));
36            }
37        })
38        .on_action(|_: &Next, window: &mut Window, cx| traverse(window, cx, false))
39        .on_action(|_: &Previous, window: &mut Window, cx| traverse(window, cx, true))
40        .on_action(
41            |_: &gpui_kit::base::input::IndentInline, window: &mut Window, cx| {
42                traverse(window, cx, false)
43            },
44        )
45        .on_action(
46            |_: &gpui_kit::base::input::OutdentInline, window: &mut Window, cx| {
47                traverse(window, cx, true)
48            },
49        )
50}
51
52// GPUI's window tab order includes background controls. Respect the modal
53// boundary registered by gpui-base before settling on the next tab stop.
54fn traverse(window: &mut Window, cx: &mut App, backwards: bool) {
55    let trap = gpui_kit::base::active_focus_trap(window, cx);
56    let mut visited = Vec::new();
57    loop {
58        if backwards {
59            window.focus_prev(cx);
60        } else {
61            window.focus_next(cx);
62        }
63        let Some(trap) = &trap else {
64            return;
65        };
66        if trap.contains_focused(window, cx) {
67            return;
68        }
69        let focused = window.focused(cx);
70        // A full lap without re-entering the trap means every remaining stop is
71        // outside it; park on the trap itself rather than leaving the dialog.
72        if focused.is_none() || visited.contains(&focused) {
73            trap.focus(window, cx);
74            return;
75        }
76        visited.push(focused);
77    }
78}