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:
parent
137081f050
commit
caa4b529e4
6 changed files with 387 additions and 16 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue