gpui: Add tab focus support (#33008)

Release Notes:

- N/A

With a `tab_index` and `tab_stop` option to `FocusHandle` to us can
switch focus by `Tab`, `Shift-Tab`.

The `tab_index` is from
[WinUI](https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.control.tabindex?view=winrt-26100)
and [HTML
tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/tabindex),
only the `tab_stop` is enabled that can be added into the `tab_handles`
list.

- Added `window.focus_next()` and `window.focus_previous()` method to
switch focus.
- Added `tab_index` to `InteractiveElement`.

```bash
cargo run -p gpui --example tab_stop
```


https://github.com/user-attachments/assets/ac4e3e49-8359-436c-9a6e-badba2225211
This commit is contained in:
Jason Lee 2025-07-21 07:38:54 +08:00 committed by GitHub
parent 137081f050
commit caa4b529e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 387 additions and 16 deletions

View file

@ -0,0 +1,130 @@
use gpui::{
App, Application, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString,
Stateful, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
};
actions!(example, [Tab, TabPrev]);
struct Example {
items: Vec<FocusHandle>,
message: SharedString,
}
impl Example {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let items = vec![
cx.focus_handle().tab_index(1).tab_stop(true),
cx.focus_handle().tab_index(2).tab_stop(true),
cx.focus_handle().tab_index(3).tab_stop(true),
cx.focus_handle(),
cx.focus_handle().tab_index(2).tab_stop(true),
];
window.focus(items.first().unwrap());
Self {
items,
message: SharedString::from("Press `Tab`, `Shift-Tab` to switch focus."),
}
}
fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
window.focus_next();
self.message = SharedString::from("You have pressed `Tab`.");
}
fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context<Self>) {
window.focus_prev();
self.message = SharedString::from("You have pressed `Shift-Tab`.");
}
}
impl Render for Example {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn button(id: impl Into<ElementId>) -> Stateful<Div> {
div()
.id(id)
.h_10()
.flex_1()
.flex()
.justify_center()
.items_center()
.border_1()
.border_color(gpui::black())
.bg(gpui::black())
.text_color(gpui::white())
.focus(|this| this.border_color(gpui::blue()))
.shadow_sm()
}
div()
.id("app")
.on_action(cx.listener(Self::on_tab))
.on_action(cx.listener(Self::on_tab_prev))
.size_full()
.flex()
.flex_col()
.p_4()
.gap_3()
.bg(gpui::white())
.text_color(gpui::black())
.child(self.message.clone())
.children(
self.items
.clone()
.into_iter()
.enumerate()
.map(|(ix, item_handle)| {
div()
.id(("item", ix))
.track_focus(&item_handle)
.h_10()
.w_full()
.flex()
.justify_center()
.items_center()
.border_1()
.border_color(gpui::black())
.when(
item_handle.tab_stop && item_handle.is_focused(window),
|this| this.border_color(gpui::blue()),
)
.map(|this| match item_handle.tab_stop {
true => this
.hover(|this| this.bg(gpui::black().opacity(0.1)))
.child(format!("tab_index: {}", item_handle.tab_index)),
false => this.opacity(0.4).child("tab_stop: false"),
})
}),
)
.child(
div()
.flex()
.flex_row()
.gap_3()
.items_center()
.child(button("el1").tab_index(4).child("Button 1"))
.child(button("el2").tab_index(5).child("Button 2")),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.bind_keys([
KeyBinding::new("tab", Tab, None),
KeyBinding::new("shift-tab", TabPrev, None),
]);
let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| cx.new(|cx| Example::new(window, cx)),
)
.unwrap();
cx.activate(true);
});
}