Cargo.lock 🔗
@@ -7087,6 +7087,7 @@ dependencies = [
"parking_lot",
"pathfinder_geometry",
"postage",
+ "pretty_assertions",
"profiling",
"rand 0.9.1",
"raw-window-handle",
Ben Kunkle , Kate , Mikayla , Anthony , and Mikayla Maki created
Closes #ISSUE
Co-Authored-By: Mikayla <mikayla@zed.dev>
Co-Authored-By: Anthony <anthony@zed.dev>
Co-Authored-By: Kate <kate@zed.dev>
Release Notes:
- N/A *or* Added/Fixed/Improved ...
---------
Co-authored-by: Kate <kate@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Cargo.lock | 1
crates/gpui/Cargo.toml | 3
crates/gpui/src/elements/div.rs | 112 ++++--
crates/gpui/src/tab_stop.rs | 611 ++++++++++++++++++++++++++++++----
crates/gpui/src/window.rs | 35 +
5 files changed, 629 insertions(+), 133 deletions(-)
@@ -7087,6 +7087,7 @@ dependencies = [
"parking_lot",
"pathfinder_geometry",
"postage",
+ "pretty_assertions",
"profiling",
"rand 0.9.1",
"raw-window-handle",
@@ -230,9 +230,10 @@ collections = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
http_client = { workspace = true, features = ["test-support"] }
lyon = { version = "1.0", features = ["extra"] }
+pretty_assertions.workspace = true
rand.workspace = true
-unicode-segmentation.workspace = true
reqwest_client = { workspace = true, features = ["test-support"] }
+unicode-segmentation.workspace = true
util = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "windows")'.build-dependencies]
@@ -618,13 +618,32 @@ pub trait InteractiveElement: Sized {
self
}
- /// Set index of the tab stop order.
+ /// Designate this element as a tab stop, equivalent to `tab_index(0)`.
+ /// This should be the primary mechanism for tab navigation within the application.
+ fn tab_stop(mut self) -> Self {
+ self.tab_index(0)
+ }
+
+ /// Set index of the tab stop order. This should only be used in conjunction with `tab_group`
+ /// in order to not interfere with the tab index of other elements.
fn tab_index(mut self, index: isize) -> Self {
self.interactivity().focusable = true;
self.interactivity().tab_index = Some(index);
self
}
+ /// Designate this div as a "tab group". Tab groups have their own location in the tab-index order,
+ /// but for children of the tab group, the tab index is reset to 0. This can be useful for swapping
+ /// the order of tab stops within the group, without having to renumber all the tab stops in the whole
+ /// application.
+ fn tab_group(mut self) -> Self {
+ self.interactivity().tab_group = true;
+ if self.interactivity().tab_index.is_none() {
+ self.interactivity().tab_index = Some(0);
+ }
+ 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
@@ -1481,6 +1500,7 @@ pub struct Interactivity {
pub(crate) window_control: Option<WindowControlArea>,
pub(crate) hitbox_behavior: HitboxBehavior,
pub(crate) tab_index: Option<isize>,
+ pub(crate) tab_group: bool,
#[cfg(any(feature = "inspector", debug_assertions))]
pub(crate) source_location: Option<&'static core::panic::Location<'static>>,
@@ -1766,8 +1786,12 @@ impl Interactivity {
return ((), element_state);
}
+ let mut tab_group = None;
+ if self.tab_group {
+ tab_group = self.tab_index;
+ }
if let Some(focus_handle) = &self.tracked_focus_handle {
- window.next_frame.tab_handles.insert(focus_handle);
+ window.next_frame.tab_stops.insert(focus_handle);
}
window.with_element_opacity(style.opacity, |window| {
@@ -1776,55 +1800,59 @@ impl Interactivity {
window.with_content_mask(
style.overflow_mask(bounds, window.rem_size()),
|window| {
- if let Some(hitbox) = hitbox {
- #[cfg(debug_assertions)]
- self.paint_debug_info(
- global_id, hitbox, &style, window, cx,
- );
-
- if let Some(drag) = cx.active_drag.as_ref() {
- if let Some(mouse_cursor) = drag.cursor_style {
- window.set_window_cursor_style(mouse_cursor);
+ window.with_tab_group(tab_group, |window| {
+ if let Some(hitbox) = hitbox {
+ #[cfg(debug_assertions)]
+ self.paint_debug_info(
+ global_id, hitbox, &style, window, cx,
+ );
+
+ if let Some(drag) = cx.active_drag.as_ref() {
+ if let Some(mouse_cursor) = drag.cursor_style {
+ window.set_window_cursor_style(mouse_cursor);
+ }
+ } else {
+ if let Some(mouse_cursor) = style.mouse_cursor {
+ window.set_cursor_style(mouse_cursor, hitbox);
+ }
}
- } else {
- if let Some(mouse_cursor) = style.mouse_cursor {
- window.set_cursor_style(mouse_cursor, hitbox);
+
+ if let Some(group) = self.group.clone() {
+ GroupHitboxes::push(group, hitbox.id, cx);
}
- }
- if let Some(group) = self.group.clone() {
- GroupHitboxes::push(group, hitbox.id, cx);
- }
+ if let Some(area) = self.window_control {
+ window.insert_window_control_hitbox(
+ area,
+ hitbox.clone(),
+ );
+ }
- if let Some(area) = self.window_control {
- window
- .insert_window_control_hitbox(area, hitbox.clone());
+ self.paint_mouse_listeners(
+ hitbox,
+ element_state.as_mut(),
+ window,
+ cx,
+ );
+ self.paint_scroll_listener(hitbox, &style, window, cx);
}
- self.paint_mouse_listeners(
- hitbox,
- element_state.as_mut(),
- window,
- cx,
- );
- self.paint_scroll_listener(hitbox, &style, window, cx);
- }
+ self.paint_keyboard_listeners(window, cx);
+ f(&style, window, cx);
- self.paint_keyboard_listeners(window, cx);
- f(&style, window, cx);
+ if let Some(_hitbox) = hitbox {
+ #[cfg(any(feature = "inspector", debug_assertions))]
+ window.insert_inspector_hitbox(
+ _hitbox.id,
+ _inspector_id,
+ cx,
+ );
- if let Some(_hitbox) = hitbox {
- #[cfg(any(feature = "inspector", debug_assertions))]
- window.insert_inspector_hitbox(
- _hitbox.id,
- _inspector_id,
- cx,
- );
-
- if let Some(group) = self.group.as_ref() {
- GroupHitboxes::pop(group, cx);
+ if let Some(group) = self.group.as_ref() {
+ GroupHitboxes::pop(group, cx);
+ }
}
- }
+ })
},
);
});
@@ -1,74 +1,320 @@
+use std::fmt::Debug;
+
+use ::sum_tree::SumTree;
+use collections::FxHashMap;
+use sum_tree::Bias;
+
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 {
- pub(crate) handles: Vec<FocusHandle>,
+/// Represents a collection of focus handles using the tab-index APIs.
+#[derive(Debug)]
+pub(crate) struct TabStopMap {
+ current_path: TabStopPath,
+ pub(crate) insertion_history: Vec<TabStopOperation>,
+ by_id: FxHashMap<FocusId, TabStopNode>,
+ order: SumTree<TabStopNode>,
}
-impl TabHandles {
- pub(crate) fn insert(&mut self, focus_handle: &FocusHandle) {
- if !focus_handle.tab_stop {
- return;
+#[derive(Debug, Clone)]
+pub enum TabStopOperation {
+ Insert(FocusHandle),
+ Group(TabIndex),
+ GroupEnd,
+}
+
+impl TabStopOperation {
+ fn focus_handle(&self) -> Option<&FocusHandle> {
+ match self {
+ TabStopOperation::Insert(focus_handle) => Some(focus_handle),
+ _ => None,
}
+ }
+}
+
+type TabIndex = isize;
+
+#[derive(Debug, Default, PartialEq, Eq, Clone, Ord, PartialOrd)]
+struct TabStopPath(smallvec::SmallVec<[TabIndex; 6]>);
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+struct TabStopNode {
+ /// Path to access the node in the tree
+ /// The final node in the list is a leaf node corresponding to an actual focus handle,
+ /// all other nodes are group nodes
+ path: TabStopPath,
+ /// index into the backing array of nodes. Corresponds to insertion order
+ node_insertion_index: usize,
- let focus_handle = focus_handle.clone();
+ /// Whether this node is a tab stop
+ tab_stop: bool,
+}
- // Insert handle with same tab_index last
- if let Some(ix) = self
- .handles
- .iter()
- .position(|tab| tab.tab_index > focus_handle.tab_index)
+impl Ord for TabStopNode {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ self.path
+ .cmp(&other.path)
+ .then(self.node_insertion_index.cmp(&other.node_insertion_index))
+ }
+}
+
+impl PartialOrd for TabStopNode {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ Some(self.cmp(&other))
+ }
+}
+
+impl Default for TabStopMap {
+ fn default() -> Self {
+ Self {
+ current_path: TabStopPath::default(),
+ insertion_history: Vec::new(),
+ by_id: FxHashMap::default(),
+ order: SumTree::new(()),
+ }
+ }
+}
+
+impl TabStopMap {
+ pub fn insert(&mut self, focus_handle: &FocusHandle) {
+ self.insertion_history
+ .push(TabStopOperation::Insert(focus_handle.clone()));
+ let mut path = self.current_path.clone();
+ path.0.push(focus_handle.tab_index);
+ let order = TabStopNode {
+ node_insertion_index: self.insertion_history.len() - 1,
+ tab_stop: focus_handle.tab_stop,
+ path,
+ };
+ self.by_id.insert(focus_handle.id, order.clone());
+ self.order.insert_or_replace(order, ());
+ }
+
+ pub fn begin_group(&mut self, tab_index: isize) {
+ self.insertion_history
+ .push(TabStopOperation::Group(tab_index));
+ self.current_path.0.push(tab_index);
+ }
+
+ pub fn end_group(&mut self) {
+ self.insertion_history.push(TabStopOperation::GroupEnd);
+ self.current_path.0.pop();
+ }
+
+ pub fn clear(&mut self) {
+ *self = Self::default();
+ self.current_path.0.clear();
+ self.insertion_history.clear();
+ self.by_id.clear();
+ self.order = SumTree::new(());
+ }
+
+ pub fn next(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
+ let Some(focused_id) = focused_id else {
+ let first = self.order.first()?;
+ if first.tab_stop {
+ return self.focus_handle_for_order(first);
+ } else {
+ return self
+ .next_inner(first)
+ .and_then(|order| self.focus_handle_for_order(order));
+ }
+ };
+
+ let node = self.tab_node_for_focus_id(focused_id)?;
+ let item = self.next_inner(node);
+
+ if let Some(item) = item {
+ self.focus_handle_for_order(&item)
+ } else {
+ self.next(None)
+ }
+ }
+
+ fn next_inner(&self, node: &TabStopNode) -> Option<&TabStopNode> {
+ let mut cursor = self.order.cursor::<TabStopNode>(());
+ cursor.seek(&node, Bias::Left);
+ cursor.next();
+ while let Some(item) = cursor.item()
+ && !item.tab_stop
{
- self.handles.insert(ix, focus_handle);
+ cursor.next();
+ }
+
+ cursor.item()
+ }
+
+ pub fn prev(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
+ let Some(focused_id) = focused_id else {
+ let last = self.order.last()?;
+ if last.tab_stop {
+ return self.focus_handle_for_order(last);
+ } else {
+ return self
+ .prev_inner(last)
+ .and_then(|order| self.focus_handle_for_order(order));
+ }
+ };
+
+ let node = self.tab_node_for_focus_id(focused_id)?;
+ let item = self.prev_inner(node);
+
+ if let Some(item) = item {
+ self.focus_handle_for_order(&item)
} else {
- self.handles.push(focus_handle);
+ self.prev(None)
}
}
- pub(crate) fn clear(&mut self) {
- self.handles.clear();
+ fn prev_inner(&self, node: &TabStopNode) -> Option<&TabStopNode> {
+ let mut cursor = self.order.cursor::<TabStopNode>(());
+ cursor.seek(&node, Bias::Left);
+ cursor.prev();
+ while let Some(item) = cursor.item()
+ && !item.tab_stop
+ {
+ cursor.prev();
+ }
+
+ cursor.item()
}
- fn current_index(&self, focused_id: Option<&FocusId>) -> Option<usize> {
- self.handles.iter().position(|h| Some(&h.id) == focused_id)
+ pub fn replay(&mut self, nodes: &[TabStopOperation]) {
+ for node in nodes {
+ match node {
+ TabStopOperation::Insert(focus_handle) => self.insert(focus_handle),
+ TabStopOperation::Group(tab_index) => self.begin_group(*tab_index),
+ TabStopOperation::GroupEnd => self.end_group(),
+ }
+ }
}
- pub(crate) fn next(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
- let next_ix = self
- .current_index(focused_id)
- .and_then(|ix| {
- let next_ix = ix + 1;
- (next_ix < self.handles.len()).then_some(next_ix)
- })
- .unwrap_or_default();
+ pub fn paint_index(&self) -> usize {
+ self.insertion_history.len()
+ }
- self.handles.get(next_ix).cloned()
+ fn focus_handle_for_order(&self, order: &TabStopNode) -> Option<FocusHandle> {
+ let handle = self.insertion_history[order.node_insertion_index].focus_handle();
+ debug_assert!(
+ handle.is_some(),
+ "The order node did not correspond to an element, this is a GPUI bug"
+ );
+ handle.cloned()
}
- pub(crate) fn prev(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
- let ix = self.current_index(focused_id).unwrap_or_default();
- let prev_ix = if ix == 0 {
- self.handles.len().saturating_sub(1)
- } else {
- ix.saturating_sub(1)
+ fn tab_node_for_focus_id(&self, focused_id: &FocusId) -> Option<&TabStopNode> {
+ let Some(order) = self.by_id.get(focused_id) else {
+ return None;
};
+ Some(order)
+ }
+}
+
+mod sum_tree_impl {
+ use sum_tree::SeekTarget;
+
+ use crate::tab_stop::{TabStopNode, TabStopPath};
+
+ #[derive(Clone, Debug)]
+ pub struct TabStopOrderNodeSummary {
+ max_index: usize,
+ max_path: TabStopPath,
+ pub tab_stops: usize,
+ }
+
+ pub type TabStopCount = usize;
+
+ impl sum_tree::ContextLessSummary for TabStopOrderNodeSummary {
+ fn zero() -> Self {
+ TabStopOrderNodeSummary {
+ max_index: 0,
+ max_path: TabStopPath::default(),
+ tab_stops: 0,
+ }
+ }
+
+ fn add_summary(&mut self, summary: &Self) {
+ self.max_index = summary.max_index;
+ self.max_path = summary.max_path.clone();
+ self.tab_stops += summary.tab_stops;
+ }
+ }
+
+ impl sum_tree::KeyedItem for TabStopNode {
+ type Key = Self;
+
+ fn key(&self) -> Self::Key {
+ self.clone()
+ }
+ }
+
+ impl sum_tree::Item for TabStopNode {
+ type Summary = TabStopOrderNodeSummary;
+
+ fn summary(&self, _cx: <Self::Summary as sum_tree::Summary>::Context<'_>) -> Self::Summary {
+ TabStopOrderNodeSummary {
+ max_index: self.node_insertion_index,
+ max_path: self.path.clone(),
+ tab_stops: if self.tab_stop { 1 } else { 0 },
+ }
+ }
+ }
+
+ impl<'a> sum_tree::Dimension<'a, TabStopOrderNodeSummary> for TabStopCount {
+ fn zero(_: <TabStopOrderNodeSummary as sum_tree::Summary>::Context<'_>) -> Self {
+ 0
+ }
+
+ fn add_summary(
+ &mut self,
+ summary: &'a TabStopOrderNodeSummary,
+ _: <TabStopOrderNodeSummary as sum_tree::Summary>::Context<'_>,
+ ) {
+ *self += summary.tab_stops;
+ }
+ }
+
+ impl<'a> sum_tree::Dimension<'a, TabStopOrderNodeSummary> for TabStopNode {
+ fn zero(_: <TabStopOrderNodeSummary as sum_tree::Summary>::Context<'_>) -> Self {
+ TabStopNode::default()
+ }
- self.handles.get(prev_ix).cloned()
+ fn add_summary(
+ &mut self,
+ summary: &'a TabStopOrderNodeSummary,
+ _: <TabStopOrderNodeSummary as sum_tree::Summary>::Context<'_>,
+ ) {
+ self.node_insertion_index = summary.max_index;
+ self.path = summary.max_path.clone();
+ }
+ }
+
+ impl<'a, 'b> SeekTarget<'a, TabStopOrderNodeSummary, TabStopNode> for &'b TabStopNode {
+ fn cmp(
+ &self,
+ cursor_location: &TabStopNode,
+ _: <TabStopOrderNodeSummary as sum_tree::Summary>::Context<'_>,
+ ) -> std::cmp::Ordering {
+ Iterator::cmp(self.path.0.iter(), cursor_location.path.0.iter()).then(
+ <usize as Ord>::cmp(
+ &self.node_insertion_index,
+ &cursor_location.node_insertion_index,
+ ),
+ )
+ }
}
}
#[cfg(test)]
mod tests {
- use crate::{FocusHandle, FocusMap, TabHandles};
+ use itertools::Itertools as _;
+
+ use crate::{FocusHandle, FocusId, FocusMap, TabStopMap};
use std::sync::Arc;
#[test]
fn test_tab_handles() {
let focus_map = Arc::new(FocusMap::default());
- let mut tab = TabHandles::default();
+ let mut tab_index_map = TabStopMap::default();
let focus_handles = vec![
FocusHandle::new(&focus_map).tab_stop(true).tab_index(0),
@@ -81,72 +327,281 @@ mod tests {
];
for handle in focus_handles.iter() {
- tab.insert(handle);
+ tab_index_map.insert(handle);
}
+ let expected = [
+ focus_handles[0].clone(),
+ focus_handles[5].clone(),
+ focus_handles[1].clone(),
+ focus_handles[2].clone(),
+ focus_handles[6].clone(),
+ ];
+
+ let mut prev = None;
+ let mut found = vec![];
+ for _ in 0..expected.len() {
+ let handle = tab_index_map.next(prev.as_ref()).unwrap();
+ prev = Some(handle.id);
+ found.push(handle.id);
+ }
+
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,
- ]
+ found,
+ expected.iter().map(|handle| handle.id).collect::<Vec<_>>()
);
// Select first tab index if no handle is currently focused.
- assert_eq!(tab.next(None), Some(tab.handles[0].clone()));
+ assert_eq!(tab_index_map.next(None), Some(expected[0].clone()));
// Select last tab index if no handle is currently focused.
- assert_eq!(
- tab.prev(None),
- Some(tab.handles[tab.handles.len() - 1].clone())
- );
+ assert_eq!(tab_index_map.prev(None), expected.last().cloned(),);
assert_eq!(
- tab.next(Some(&tab.handles[0].id)),
- Some(tab.handles[1].clone())
+ tab_index_map.next(Some(&expected[0].id)),
+ Some(expected[1].clone())
);
assert_eq!(
- tab.next(Some(&tab.handles[1].id)),
- Some(tab.handles[2].clone())
+ tab_index_map.next(Some(&expected[1].id)),
+ Some(expected[2].clone())
);
assert_eq!(
- tab.next(Some(&tab.handles[2].id)),
- Some(tab.handles[3].clone())
+ tab_index_map.next(Some(&expected[2].id)),
+ Some(expected[3].clone())
);
assert_eq!(
- tab.next(Some(&tab.handles[3].id)),
- Some(tab.handles[4].clone())
+ tab_index_map.next(Some(&expected[3].id)),
+ Some(expected[4].clone())
);
assert_eq!(
- tab.next(Some(&tab.handles[4].id)),
- Some(tab.handles[0].clone())
+ tab_index_map.next(Some(&expected[4].id)),
+ Some(expected[0].clone())
);
// prev
- assert_eq!(tab.prev(None), Some(tab.handles[4].clone()));
+ assert_eq!(tab_index_map.prev(None), Some(expected[4].clone()));
assert_eq!(
- tab.prev(Some(&tab.handles[0].id)),
- Some(tab.handles[4].clone())
+ tab_index_map.prev(Some(&expected[0].id)),
+ Some(expected[4].clone())
);
assert_eq!(
- tab.prev(Some(&tab.handles[1].id)),
- Some(tab.handles[0].clone())
+ tab_index_map.prev(Some(&expected[1].id)),
+ Some(expected[0].clone())
);
assert_eq!(
- tab.prev(Some(&tab.handles[2].id)),
- Some(tab.handles[1].clone())
+ tab_index_map.prev(Some(&expected[2].id)),
+ Some(expected[1].clone())
);
assert_eq!(
- tab.prev(Some(&tab.handles[3].id)),
- Some(tab.handles[2].clone())
+ tab_index_map.prev(Some(&expected[3].id)),
+ Some(expected[2].clone())
);
assert_eq!(
- tab.prev(Some(&tab.handles[4].id)),
- Some(tab.handles[3].clone())
+ tab_index_map.prev(Some(&expected[4].id)),
+ Some(expected[3].clone())
);
}
+
+ #[test]
+ fn test_tab_non_stop_filtering() {
+ let focus_map = Arc::new(FocusMap::default());
+ let mut tab_index_map = TabStopMap::default();
+
+ // Check that we can query next from a non-stop tab
+ let tab_non_stop_1 = FocusHandle::new(&focus_map).tab_stop(false).tab_index(1);
+ let tab_stop_2 = FocusHandle::new(&focus_map).tab_stop(true).tab_index(2);
+ tab_index_map.insert(&tab_non_stop_1);
+ tab_index_map.insert(&tab_stop_2);
+ let result = tab_index_map.next(Some(&tab_non_stop_1.id)).unwrap();
+ assert_eq!(result.id, tab_stop_2.id);
+
+ // Check that we skip over non-stop tabs
+ let tab_stop_0 = FocusHandle::new(&focus_map).tab_stop(true).tab_index(0);
+ let tab_non_stop_0 = FocusHandle::new(&focus_map).tab_stop(false).tab_index(0);
+ tab_index_map.insert(&tab_stop_0);
+ tab_index_map.insert(&tab_non_stop_0);
+ let result = tab_index_map.next(Some(&tab_stop_0.id)).unwrap();
+ assert_eq!(result.id, tab_stop_2.id);
+ }
+
+ #[must_use]
+ struct TabStopMapTest {
+ tab_map: TabStopMap,
+ focus_map: Arc<FocusMap>,
+ expected: Vec<(usize, FocusId)>,
+ }
+
+ impl TabStopMapTest {
+ #[must_use]
+ fn new() -> Self {
+ Self {
+ tab_map: TabStopMap::default(),
+ focus_map: Arc::new(FocusMap::default()),
+ expected: Vec::default(),
+ }
+ }
+
+ #[must_use]
+ fn tab_non_stop(mut self, index: isize) -> Self {
+ let handle = FocusHandle::new(&self.focus_map)
+ .tab_stop(false)
+ .tab_index(index);
+ self.tab_map.insert(&handle);
+ self
+ }
+
+ #[must_use]
+ fn tab_stop(mut self, index: isize, expected: usize) -> Self {
+ let handle = FocusHandle::new(&self.focus_map)
+ .tab_stop(true)
+ .tab_index(index);
+ self.tab_map.insert(&handle);
+ self.expected.push((expected, handle.id));
+ self.expected.sort_by_key(|(expected, _)| *expected);
+ self
+ }
+
+ #[must_use]
+ fn tab_group(mut self, tab_index: isize, children: impl FnOnce(Self) -> Self) -> Self {
+ self.tab_map.begin_group(tab_index);
+ self = children(self);
+ self.tab_map.end_group();
+ self
+ }
+
+ fn traverse_tab_map(
+ &self,
+ traverse: impl Fn(&TabStopMap, Option<&FocusId>) -> Option<FocusHandle>,
+ ) -> Vec<FocusId> {
+ let mut last_focus_id = None;
+ let mut found = vec![];
+ for _ in 0..self.expected.len() {
+ let handle = traverse(&self.tab_map, last_focus_id.as_ref()).unwrap();
+ last_focus_id = Some(handle.id);
+ found.push(handle.id);
+ }
+ found
+ }
+
+ fn assert(self) {
+ let mut expected = self.expected.iter().map(|(_, id)| *id).collect_vec();
+
+ // Check next order
+ let forward_found = self.traverse_tab_map(|tab_map, prev| tab_map.next(prev));
+ assert_eq!(forward_found, expected);
+
+ // Test overflow. Last to first
+ assert_eq!(
+ self.tab_map
+ .next(forward_found.last())
+ .map(|handle| handle.id),
+ expected.first().cloned()
+ );
+
+ // Check previous order
+ let reversed_found = self.traverse_tab_map(|tab_map, prev| tab_map.prev(prev));
+ expected.reverse();
+ assert_eq!(reversed_found, expected);
+
+ // Test overflow. First to last
+ assert_eq!(
+ self.tab_map
+ .prev(reversed_found.last())
+ .map(|handle| handle.id),
+ expected.first().cloned(),
+ );
+ }
+ }
+
+ #[test]
+ fn test_with_disabled_tab_stop() {
+ TabStopMapTest::new()
+ .tab_stop(0, 0)
+ .tab_non_stop(1)
+ .tab_stop(2, 1)
+ .tab_stop(3, 2)
+ .assert();
+ }
+
+ #[test]
+ fn test_with_multiple_disabled_tab_stops() {
+ TabStopMapTest::new()
+ .tab_non_stop(0)
+ .tab_stop(1, 0)
+ .tab_non_stop(3)
+ .tab_stop(3, 1)
+ .tab_non_stop(4)
+ .assert();
+ }
+
+ #[test]
+ fn test_tab_group_functionality() {
+ TabStopMapTest::new()
+ .tab_stop(0, 0)
+ .tab_stop(0, 1)
+ .tab_group(2, |t| t.tab_stop(0, 2).tab_stop(1, 3))
+ .tab_stop(3, 4)
+ .tab_stop(4, 5)
+ .assert()
+ }
+
+ #[test]
+ fn test_sibling_groups() {
+ TabStopMapTest::new()
+ .tab_stop(0, 0)
+ .tab_stop(1, 1)
+ .tab_group(2, |test| test.tab_stop(0, 2).tab_stop(1, 3))
+ .tab_stop(3, 4)
+ .tab_stop(4, 5)
+ .tab_group(6, |test| test.tab_stop(0, 6).tab_stop(1, 7))
+ .tab_stop(7, 8)
+ .tab_stop(8, 9)
+ .assert();
+ }
+
+ #[test]
+ fn test_nested_group() {
+ TabStopMapTest::new()
+ .tab_stop(0, 0)
+ .tab_stop(1, 1)
+ .tab_group(2, |t| {
+ t.tab_group(0, |t| t.tab_stop(0, 2).tab_stop(1, 3))
+ .tab_stop(1, 4)
+ })
+ .tab_stop(3, 5)
+ .tab_stop(4, 6)
+ .assert();
+ }
+
+ #[test]
+ fn test_sibling_nested_groups() {
+ TabStopMapTest::new()
+ .tab_stop(0, 0)
+ .tab_stop(1, 1)
+ .tab_group(2, |builder| {
+ builder
+ .tab_stop(0, 2)
+ .tab_stop(2, 5)
+ .tab_group(1, |builder| builder.tab_stop(0, 3).tab_stop(1, 4))
+ .tab_group(3, |builder| builder.tab_stop(0, 6).tab_stop(1, 7))
+ })
+ .tab_stop(3, 8)
+ .tab_stop(4, 9)
+ .assert();
+ }
+
+ #[test]
+ fn test_sibling_nested_groups_out_of_order() {
+ TabStopMapTest::new()
+ .tab_stop(9, 9)
+ .tab_stop(8, 8)
+ .tab_group(7, |builder| {
+ builder
+ .tab_stop(0, 2)
+ .tab_stop(2, 5)
+ .tab_group(3, |builder| builder.tab_stop(1, 7).tab_stop(0, 6))
+ .tab_group(1, |builder| builder.tab_stop(0, 3).tab_stop(1, 4))
+ })
+ .tab_stop(3, 0)
+ .tab_stop(4, 1)
+ .assert();
+ }
}
@@ -13,7 +13,7 @@ use crate::{
Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge,
SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ScaledPixels, Scene, Shadow,
SharedString, Size, StrikethroughStyle, Style, SubscriberSet, Subscription, SystemWindowTab,
- SystemWindowTabController, TabHandles, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
+ SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
point, prelude::*, px, rems, size, transparent_black,
@@ -684,7 +684,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,
+ pub(crate) tab_stops: TabStopMap,
}
#[derive(Clone, Default)]
@@ -733,7 +733,7 @@ impl Frame {
#[cfg(any(feature = "inspector", debug_assertions))]
inspector_hitboxes: FxHashMap::default(),
- tab_handles: TabHandles::default(),
+ tab_stops: TabStopMap::default(),
}
}
@@ -749,7 +749,7 @@ impl Frame {
self.hitboxes.clear();
self.window_control_hitboxes.clear();
self.deferred_draws.clear();
- self.tab_handles.clear();
+ self.tab_stops.clear();
self.focus = None;
#[cfg(any(feature = "inspector", debug_assertions))]
@@ -1415,7 +1415,7 @@ impl Window {
return;
}
- if let Some(handle) = self.rendered_frame.tab_handles.next(self.focus.as_ref()) {
+ if let Some(handle) = self.rendered_frame.tab_stops.next(self.focus.as_ref()) {
self.focus(&handle)
}
}
@@ -1426,7 +1426,7 @@ impl Window {
return;
}
- if let Some(handle) = self.rendered_frame.tab_handles.prev(self.focus.as_ref()) {
+ if let Some(handle) = self.rendered_frame.tab_stops.prev(self.focus.as_ref()) {
self.focus(&handle)
}
}
@@ -2285,7 +2285,7 @@ impl Window {
input_handlers_index: self.next_frame.input_handlers.len(),
cursor_styles_index: self.next_frame.cursor_styles.len(),
accessed_element_states_index: self.next_frame.accessed_element_states.len(),
- tab_handle_index: self.next_frame.tab_handles.handles.len(),
+ tab_handle_index: self.next_frame.tab_stops.paint_index(),
line_layout_index: self.text_system.layout_index(),
}
}
@@ -2315,11 +2315,9 @@ impl Window {
.iter()
.map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)),
);
- self.next_frame.tab_handles.handles.extend(
- self.rendered_frame.tab_handles.handles
- [range.start.tab_handle_index..range.end.tab_handle_index]
- .iter()
- .cloned(),
+ self.next_frame.tab_stops.replay(
+ &self.rendered_frame.tab_stops.insertion_history
+ [range.start.tab_handle_index..range.end.tab_handle_index],
);
self.text_system
@@ -2734,6 +2732,19 @@ impl Window {
}
}
+ /// Executes the given closure within the context of a tab group.
+ #[inline]
+ pub fn with_tab_group<R>(&mut self, index: Option<isize>, f: impl FnOnce(&mut Self) -> R) -> R {
+ if let Some(index) = index {
+ self.next_frame.tab_stops.begin_group(index);
+ let result = f(self);
+ self.next_frame.tab_stops.end_group();
+ result
+ } else {
+ f(self)
+ }
+ }
+
/// Defers the drawing of the given element, scheduling it to be painted on top of the currently-drawn tree
/// at a later time. The `priority` parameter determines the drawing order relative to other deferred elements,
/// with higher values being drawn on top.