diff --git a/Cargo.lock b/Cargo.lock index 8d78c1809b52ff20bf059a619efa98c053abe178..14f2952b78fafbd947b7c6732339ac378ab9275e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7087,6 +7087,7 @@ dependencies = [ "parking_lot", "pathfinder_geometry", "postage", + "pretty_assertions", "profiling", "rand 0.9.1", "raw-window-handle", diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 2919fecabf050a011109b2abfe69394a0ead2e67..4544b561c33f4507e8264e4eed46711425ce2a04 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -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] diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index bd2eed33973eb520305ab7b185f41569ef778dbf..9e747d864b06391f7c72721918b1c35a2eb7ce62 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -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(mut self, key_context: C) -> Self @@ -1481,6 +1500,7 @@ pub struct Interactivity { pub(crate) window_control: Option, pub(crate) hitbox_behavior: HitboxBehavior, pub(crate) tab_index: Option, + 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); + } } - } + }) }, ); }); diff --git a/crates/gpui/src/tab_stop.rs b/crates/gpui/src/tab_stop.rs index c4d2fda6e9a9c3e0adfb2d02cf5c372869d42751..ea69bd11304fc14dec3f0ce9d9eea78abfdb218e 100644 --- a/crates/gpui/src/tab_stop.rs +++ b/crates/gpui/src/tab_stop.rs @@ -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, +/// 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, + by_id: FxHashMap, + order: SumTree, } -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 { + 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 { + 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::(()); + 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 { + 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::(()); + 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 { - 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 { - 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 { + 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 { - 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: ::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(_: ::Context<'_>) -> Self { + 0 + } + + fn add_summary( + &mut self, + summary: &'a TabStopOrderNodeSummary, + _: ::Context<'_>, + ) { + *self += summary.tab_stops; + } + } + + impl<'a> sum_tree::Dimension<'a, TabStopOrderNodeSummary> for TabStopNode { + fn zero(_: ::Context<'_>) -> Self { + TabStopNode::default() + } - self.handles.get(prev_ix).cloned() + fn add_summary( + &mut self, + summary: &'a TabStopOrderNodeSummary, + _: ::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, + _: ::Context<'_>, + ) -> std::cmp::Ordering { + Iterator::cmp(self.path.0.iter(), cursor_location.path.0.iter()).then( + ::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![ - 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::>() ); // 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, + 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, + ) -> Vec { + 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(); + } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index d9bf27dca1253fa0a5286148ea64a03c3a5bac37..19faa1135ff29f5c8cbd473e86469748b2c94595 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -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, usize>, #[cfg(any(feature = "inspector", debug_assertions))] pub(crate) inspector_hitboxes: FxHashMap, - 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(&mut self, index: Option, 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.