Files
oak-gpui/crates/gpui/src/tab_stop.rs
T
Jason Lee caa4b529e4 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
2025-07-20 16:38:54 -07:00

158 lines
4.4 KiB
Rust

use crate::{FocusHandle, FocusId};
/// Represents a collection of tab handles.
///
/// Used to manage the `Tab` event to switch between focus handles.
#[derive(Default)]
pub(crate) struct TabHandles {
handles: Vec<FocusHandle>,
}
impl TabHandles {
pub(crate) fn insert(&mut self, focus_handle: &FocusHandle) {
if !focus_handle.tab_stop {
return;
}
let focus_handle = focus_handle.clone();
// Insert handle with same tab_index last
if let Some(ix) = self
.handles
.iter()
.position(|tab| tab.tab_index > focus_handle.tab_index)
{
self.handles.insert(ix, focus_handle);
} else {
self.handles.push(focus_handle);
}
}
pub(crate) fn clear(&mut self) {
self.handles.clear();
}
fn current_index(&self, focused_id: Option<&FocusId>) -> usize {
self.handles
.iter()
.position(|h| Some(&h.id) == focused_id)
.unwrap_or_default()
}
pub(crate) fn next(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
let ix = self.current_index(focused_id);
let mut next_ix = ix + 1;
if next_ix + 1 > self.handles.len() {
next_ix = 0;
}
if let Some(next_handle) = self.handles.get(next_ix) {
Some(next_handle.clone())
} else {
None
}
}
pub(crate) fn prev(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
let ix = self.current_index(focused_id);
let prev_ix;
if ix == 0 {
prev_ix = self.handles.len().saturating_sub(1);
} else {
prev_ix = ix.saturating_sub(1);
}
if let Some(prev_handle) = self.handles.get(prev_ix) {
Some(prev_handle.clone())
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use crate::{FocusHandle, FocusMap, TabHandles};
use std::sync::Arc;
#[test]
fn test_tab_handles() {
let focus_map = Arc::new(FocusMap::default());
let mut tab = TabHandles::default();
let focus_handles = vec![
FocusHandle::new(&focus_map).tab_stop(true).tab_index(0),
FocusHandle::new(&focus_map).tab_stop(true).tab_index(1),
FocusHandle::new(&focus_map).tab_stop(true).tab_index(1),
FocusHandle::new(&focus_map),
FocusHandle::new(&focus_map).tab_index(2),
FocusHandle::new(&focus_map).tab_stop(true).tab_index(0),
FocusHandle::new(&focus_map).tab_stop(true).tab_index(2),
];
for handle in focus_handles.iter() {
tab.insert(&handle);
}
assert_eq!(
tab.handles
.iter()
.map(|handle| handle.id)
.collect::<Vec<_>>(),
vec![
focus_handles[0].id,
focus_handles[5].id,
focus_handles[1].id,
focus_handles[2].id,
focus_handles[6].id,
]
);
// next
assert_eq!(tab.next(None), Some(tab.handles[1].clone()));
assert_eq!(
tab.next(Some(&tab.handles[0].id)),
Some(tab.handles[1].clone())
);
assert_eq!(
tab.next(Some(&tab.handles[1].id)),
Some(tab.handles[2].clone())
);
assert_eq!(
tab.next(Some(&tab.handles[2].id)),
Some(tab.handles[3].clone())
);
assert_eq!(
tab.next(Some(&tab.handles[3].id)),
Some(tab.handles[4].clone())
);
assert_eq!(
tab.next(Some(&tab.handles[4].id)),
Some(tab.handles[0].clone())
);
// prev
assert_eq!(tab.prev(None), Some(tab.handles[4].clone()));
assert_eq!(
tab.prev(Some(&tab.handles[0].id)),
Some(tab.handles[4].clone())
);
assert_eq!(
tab.prev(Some(&tab.handles[1].id)),
Some(tab.handles[0].clone())
);
assert_eq!(
tab.prev(Some(&tab.handles[2].id)),
Some(tab.handles[1].clone())
);
assert_eq!(
tab.prev(Some(&tab.handles[3].id)),
Some(tab.handles[2].clone())
);
assert_eq!(
tab.prev(Some(&tab.handles[4].id)),
Some(tab.handles[3].clone())
);
}
}