Detailed changes
@@ -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);
+ });
+}
@@ -954,8 +954,8 @@ impl App {
self.focus_handles
.clone()
.write()
- .retain(|handle_id, count| {
- if count.load(SeqCst) == 0 {
+ .retain(|handle_id, focus| {
+ if focus.ref_count.load(SeqCst) == 0 {
for window_handle in self.windows() {
window_handle
.update(self, |_, window, _| {
@@ -619,6 +619,13 @@ pub trait InteractiveElement: Sized {
self
}
+ /// Set index of the tab stop order.
+ fn tab_index(mut self, index: isize) -> Self {
+ self.interactivity().focusable = true;
+ self.interactivity().tab_index = Some(index);
+ self
+ }
+
/// Set the keymap context for this element. This will be used to determine
/// which action to dispatch from the keymap.
fn key_context<C, E>(mut self, key_context: C) -> Self
@@ -1462,6 +1469,7 @@ pub struct Interactivity {
pub(crate) tooltip_builder: Option<TooltipBuilder>,
pub(crate) window_control: Option<WindowControlArea>,
pub(crate) hitbox_behavior: HitboxBehavior,
+ pub(crate) tab_index: Option<isize>,
#[cfg(any(feature = "inspector", debug_assertions))]
pub(crate) source_location: Option<&'static core::panic::Location<'static>>,
@@ -1521,12 +1529,17 @@ impl Interactivity {
// as frames contain an element with this id.
if self.focusable && self.tracked_focus_handle.is_none() {
if let Some(element_state) = element_state.as_mut() {
- self.tracked_focus_handle = Some(
- element_state
- .focus_handle
- .get_or_insert_with(|| cx.focus_handle())
- .clone(),
- );
+ let mut handle = element_state
+ .focus_handle
+ .get_or_insert_with(|| cx.focus_handle())
+ .clone()
+ .tab_stop(false);
+
+ if let Some(index) = self.tab_index {
+ handle = handle.tab_index(index).tab_stop(true);
+ }
+
+ self.tracked_focus_handle = Some(handle);
}
}
@@ -1729,6 +1742,10 @@ impl Interactivity {
return ((), element_state);
}
+ if let Some(focus_handle) = &self.tracked_focus_handle {
+ window.next_frame.tab_handles.insert(focus_handle);
+ }
+
window.with_element_opacity(style.opacity, |window| {
style.paint(bounds, window, cx, |window: &mut Window, cx: &mut App| {
window.with_text_style(style.text_style().cloned(), |window| {
@@ -95,6 +95,7 @@ mod style;
mod styled;
mod subscription;
mod svg_renderer;
+mod tab_stop;
mod taffy;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
@@ -151,6 +152,7 @@ pub use style::*;
pub use styled::*;
pub use subscription::*;
use svg_renderer::*;
+pub(crate) use tab_stop::*;
pub use taffy::{AvailableSpace, LayoutId};
#[cfg(any(test, feature = "test-support"))]
pub use test::*;
@@ -0,0 +1,157 @@
+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())
+ );
+ }
+}
@@ -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