gpui: Add tab group (#38531)

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>
This commit is contained in:
Ben Kunkle
2025-09-25 14:41:29 +00:00
committed by GitHub
co-authored by Mikayla Anthony Kate Mikayla Maki
parent b2b90b003d
commit 50bb8a4ae6
5 changed files with 639 additions and 143 deletions
Generated
+1
View File
@@ -7087,6 +7087,7 @@ dependencies = [
"parking_lot",
"pathfinder_geometry",
"postage",
"pretty_assertions",
"profiling",
"rand 0.9.1",
"raw-window-handle",
+2 -1
View File
@@ -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]
+75 -47
View File
@@ -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,
);
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);
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(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_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(group) = self.group.as_ref() {
GroupHitboxes::pop(group, 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());
}
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);
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);
}
}
})
},
);
});
+538 -83
View File
@@ -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,
}
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);
impl TabStopOperation {
fn focus_handle(&self) -> Option<&FocusHandle> {
match self {
TabStopOperation::Insert(focus_handle) => Some(focus_handle),
_ => None,
}
}
}
pub(crate) fn clear(&mut self) {
self.handles.clear();
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,
/// Whether this node is a tab stop
tab_stop: bool,
}
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, ());
}
fn current_index(&self, focused_id: Option<&FocusId>) -> Option<usize> {
self.handles.iter().position(|h| Some(&h.id) == focused_id)
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(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();
self.handles.get(next_ix).cloned()
pub fn end_group(&mut self) {
self.insertion_history.push(TabStopOperation::GroupEnd);
self.current_path.0.pop();
}
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)
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));
}
};
self.handles.get(prev_ix).cloned()
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
{
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.prev(None)
}
}
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()
}
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 fn paint_index(&self) -> usize {
self.insertion_history.len()
}
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()
}
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()
}
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();
}
}
+23 -12
View File
@@ -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.