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

@ -12,10 +12,11 @@ use crate::{
PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad,
Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge,
SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size,
StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle,
TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions,
WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, transparent_black,
StrikethroughStyle, Style, SubscriberSet, Subscription, TabHandles, TaffyLayoutEngine, Task,
TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations,
WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size,
transparent_black,
};
use anyhow::{Context as _, Result, anyhow};
use collections::{FxHashMap, FxHashSet};
@ -222,7 +223,12 @@ impl ArenaClearNeeded {
}
}
pub(crate) type FocusMap = RwLock<SlotMap<FocusId, AtomicUsize>>;
pub(crate) type FocusMap = RwLock<SlotMap<FocusId, FocusRef>>;
pub(crate) struct FocusRef {
pub(crate) ref_count: AtomicUsize,
pub(crate) tab_index: isize,
pub(crate) tab_stop: bool,
}
impl FocusId {
/// Obtains whether the element associated with this handle is currently focused.
@ -258,6 +264,10 @@ impl FocusId {
pub struct FocusHandle {
pub(crate) id: FocusId,
handles: Arc<FocusMap>,
/// The index of this element in the tab order.
pub tab_index: isize,
/// Whether this element can be focused by tab navigation.
pub tab_stop: bool,
}
impl std::fmt::Debug for FocusHandle {
@ -268,25 +278,54 @@ impl std::fmt::Debug for FocusHandle {
impl FocusHandle {
pub(crate) fn new(handles: &Arc<FocusMap>) -> Self {
let id = handles.write().insert(AtomicUsize::new(1));
let id = handles.write().insert(FocusRef {
ref_count: AtomicUsize::new(1),
tab_index: 0,
tab_stop: false,
});
Self {
id,
tab_index: 0,
tab_stop: false,
handles: handles.clone(),
}
}
pub(crate) fn for_id(id: FocusId, handles: &Arc<FocusMap>) -> Option<Self> {
let lock = handles.read();
let ref_count = lock.get(id)?;
if atomic_incr_if_not_zero(ref_count) == 0 {
let focus = lock.get(id)?;
if atomic_incr_if_not_zero(&focus.ref_count) == 0 {
return None;
}
Some(Self {
id,
tab_index: focus.tab_index,
tab_stop: focus.tab_stop,
handles: handles.clone(),
})
}
/// Sets the tab index of the element associated with this handle.
pub fn tab_index(mut self, index: isize) -> Self {
self.tab_index = index;
if let Some(focus) = self.handles.write().get_mut(self.id) {
focus.tab_index = index;
}
self
}
/// Sets whether the element associated with this handle is a tab stop.
///
/// When `false`, the element will not be included in the tab order.
pub fn tab_stop(mut self, tab_stop: bool) -> Self {
self.tab_stop = tab_stop;
if let Some(focus) = self.handles.write().get_mut(self.id) {
focus.tab_stop = tab_stop;
}
self
}
/// Converts this focus handle into a weak variant, which does not prevent it from being released.
pub fn downgrade(&self) -> WeakFocusHandle {
WeakFocusHandle {
@ -354,6 +393,7 @@ impl Drop for FocusHandle {
.read()
.get(self.id)
.unwrap()
.ref_count
.fetch_sub(1, SeqCst);
}
}
@ -642,6 +682,7 @@ pub(crate) struct Frame {
pub(crate) next_inspector_instance_ids: FxHashMap<Rc<crate::InspectorElementPath>, usize>,
#[cfg(any(feature = "inspector", debug_assertions))]
pub(crate) inspector_hitboxes: FxHashMap<HitboxId, crate::InspectorElementId>,
pub(crate) tab_handles: TabHandles,
}
#[derive(Clone, Default)]
@ -689,6 +730,7 @@ impl Frame {
#[cfg(any(feature = "inspector", debug_assertions))]
inspector_hitboxes: FxHashMap::default(),
tab_handles: TabHandles::default(),
}
}
@ -704,6 +746,7 @@ impl Frame {
self.hitboxes.clear();
self.window_control_hitboxes.clear();
self.deferred_draws.clear();
self.tab_handles.clear();
self.focus = None;
#[cfg(any(feature = "inspector", debug_assertions))]
@ -1289,6 +1332,28 @@ impl Window {
self.focus_enabled = false;
}
/// Move focus to next tab stop.
pub fn focus_next(&mut self) {
if !self.focus_enabled {
return;
}
if let Some(handle) = self.rendered_frame.tab_handles.next(self.focus.as_ref()) {
self.focus(&handle)
}
}
/// Move focus to previous tab stop.
pub fn focus_prev(&mut self) {
if !self.focus_enabled {
return;
}
if let Some(handle) = self.rendered_frame.tab_handles.prev(self.focus.as_ref()) {
self.focus(&handle)
}
}
/// Accessor for the text system.
pub fn text_system(&self) -> &Arc<WindowTextSystem> {
&self.text_system