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:
co-authored by
Mikayla
Anthony
Kate
Mikayla Maki
parent
b2b90b003d
commit
50bb8a4ae6
Generated
+1
@@ -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,
|
||||
);
|
||||
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
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user