chore: audit components
This commit is contained in:
+68
-62
@@ -15,7 +15,9 @@ type ChangeHandler = Rc<dyn Fn(Option<usize>, &mut Window, &mut App)>;
|
||||
type ItemsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<ListItem>>;
|
||||
|
||||
/// One row of a [`List`].
|
||||
#[must_use]
|
||||
pub struct ListItem {
|
||||
key: ElementId,
|
||||
label: SharedString,
|
||||
icon: Option<Icon>,
|
||||
detail: Option<SharedString>,
|
||||
@@ -23,12 +25,14 @@ pub struct ListItem {
|
||||
}
|
||||
|
||||
impl ListItem {
|
||||
/// Creates a row with this label.
|
||||
pub fn new<L>(label: L) -> Self
|
||||
/// Creates a row shown as `label`, identified by `key`.
|
||||
pub fn new<K, L>(key: K, label: L) -> Self
|
||||
where
|
||||
K: Into<ElementId>,
|
||||
L: Into<SharedString>,
|
||||
{
|
||||
Self {
|
||||
key: key.into(),
|
||||
label: label.into(),
|
||||
icon: None,
|
||||
detail: None,
|
||||
@@ -36,7 +40,7 @@ impl ListItem {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows an icon leading the label, a role or any custom [`IconNamed`].
|
||||
/// Shows an icon leading the label, a role or any custom [`gpui_native_icons::IconNamed`].
|
||||
pub fn icon<I>(mut self, icon: I) -> Self
|
||||
where
|
||||
I: Into<Icon>,
|
||||
@@ -63,6 +67,7 @@ impl ListItem {
|
||||
|
||||
/// A single-selection list.
|
||||
#[derive(IntoElement)]
|
||||
#[must_use]
|
||||
pub struct List {
|
||||
id: ElementId,
|
||||
selected: Option<usize>,
|
||||
@@ -101,7 +106,7 @@ impl List {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the rows, built fresh each render.
|
||||
/// Sets the rows, the closure runs during render and must not mutate entities or notify.
|
||||
pub fn items<F>(mut self, items: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) -> Vec<ListItem> + 'static,
|
||||
@@ -146,11 +151,10 @@ impl RenderOnce for List {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, item)| {
|
||||
render_row(
|
||||
item.render_row(
|
||||
cx,
|
||||
index,
|
||||
items.len(),
|
||||
item,
|
||||
self.selected == Some(index),
|
||||
&self.on_change,
|
||||
&focus,
|
||||
@@ -187,65 +191,67 @@ impl RenderOnce for List {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_row(
|
||||
cx: &App,
|
||||
index: usize,
|
||||
count: usize,
|
||||
item: &ListItem,
|
||||
selected: bool,
|
||||
on_change: &Option<ChangeHandler>,
|
||||
focus: &FocusHandle,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let label_color = selectable_label_color(cx, selected, item.disabled);
|
||||
let disabled = item.disabled;
|
||||
impl ListItem {
|
||||
fn render_row(
|
||||
&self,
|
||||
cx: &App,
|
||||
index: usize,
|
||||
count: usize,
|
||||
selected: bool,
|
||||
on_change: &Option<ChangeHandler>,
|
||||
focus: &FocusHandle,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let label_color = selectable_label_color(cx, selected, self.disabled);
|
||||
let disabled = self.disabled;
|
||||
|
||||
div()
|
||||
.id(("list-item", index))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(theme.radius.sm)
|
||||
.role(Role::ListItem)
|
||||
.aria_selected(selected)
|
||||
.aria_position_in_set(index + 1)
|
||||
.aria_size_of_set(count)
|
||||
.map(|row| paint_selectable_row(cx, row, selected, disabled))
|
||||
.when(!disabled, |row| {
|
||||
row.cursor_pointer().on_click({
|
||||
let on_change = on_change.clone();
|
||||
let focus = focus.clone();
|
||||
move |_, window, cx| {
|
||||
window.focus(&focus, cx);
|
||||
if let Some(on_change) = &on_change {
|
||||
on_change(Some(index), window, cx);
|
||||
div()
|
||||
.id((self.key.clone(), "list-item"))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(theme.radius.sm)
|
||||
.role(Role::ListItem)
|
||||
.aria_selected(selected)
|
||||
.aria_position_in_set(index + 1)
|
||||
.aria_size_of_set(count)
|
||||
.map(|row| paint_selectable_row(cx, row, selected, disabled))
|
||||
.when(!disabled, |row| {
|
||||
row.cursor_pointer().on_click({
|
||||
let on_change = on_change.clone();
|
||||
let focus = focus.clone();
|
||||
move |_, window, cx| {
|
||||
window.focus(&focus, cx);
|
||||
if let Some(on_change) = &on_change {
|
||||
on_change(Some(index), window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.when_some(item.icon.clone(), |row, icon| {
|
||||
row.child(icon.size(theme.sizes.medium.icon_size).color(label_color))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.text_color(label_color)
|
||||
.child(item.label.clone()),
|
||||
)
|
||||
.when_some(item.detail.clone(), |row, detail| {
|
||||
row.child(
|
||||
.when_some(self.icon.clone(), |row, icon| {
|
||||
row.child(icon.size(theme.sizes.medium.icon_size).color(label_color))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_sm()
|
||||
.text_color(if selected {
|
||||
theme.colors.primary_foreground
|
||||
} else {
|
||||
theme.colors.muted_foreground
|
||||
})
|
||||
.child(detail),
|
||||
.flex_1()
|
||||
.text_color(label_color)
|
||||
.child(self.label.clone()),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
.when_some(self.detail.clone(), |row, detail| {
|
||||
row.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_sm()
|
||||
.text_color(if selected {
|
||||
theme.colors.primary_foreground
|
||||
} else {
|
||||
theme.colors.muted_foreground
|
||||
})
|
||||
.child(detail),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
+100
-85
@@ -18,11 +18,13 @@ type ChangeHandler = Rc<dyn Fn(Option<usize>, &mut Window, &mut App)>;
|
||||
type EntriesFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<SidebarEntry>>;
|
||||
|
||||
/// One entry of a [`Sidebar`], a section header or a selectable row.
|
||||
#[must_use]
|
||||
pub enum SidebarEntry {
|
||||
/// A non-selectable section label.
|
||||
Header { label: SharedString },
|
||||
/// A selectable row.
|
||||
Row {
|
||||
key: ElementId,
|
||||
label: SharedString,
|
||||
detail: Option<SharedString>,
|
||||
prefix: Option<AnyElement>,
|
||||
@@ -42,12 +44,14 @@ impl SidebarEntry {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a selectable row.
|
||||
pub fn row<L>(label: L) -> Self
|
||||
/// Creates a selectable row shown as `label`, identified by `key`.
|
||||
pub fn row<K, L>(key: K, label: L) -> Self
|
||||
where
|
||||
K: Into<ElementId>,
|
||||
L: Into<SharedString>,
|
||||
{
|
||||
Self::Row {
|
||||
key: key.into(),
|
||||
label: label.into(),
|
||||
detail: None,
|
||||
prefix: None,
|
||||
@@ -107,6 +111,7 @@ impl SidebarEntry {
|
||||
|
||||
/// A sidebar.
|
||||
#[derive(IntoElement)]
|
||||
#[must_use]
|
||||
pub struct Sidebar {
|
||||
id: ElementId,
|
||||
selected: Option<usize>,
|
||||
@@ -158,7 +163,7 @@ impl Sidebar {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the entries, built fresh each render.
|
||||
/// Sets the entries, the closure runs during render and must not mutate entities or notify.
|
||||
pub fn entries<F>(mut self, entries: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) -> Vec<SidebarEntry> + 'static,
|
||||
@@ -222,17 +227,19 @@ impl RenderOnce for Sidebar {
|
||||
.enumerate()
|
||||
.map(|(index, entry)| {
|
||||
let is_row = matches!(entry, SidebarEntry::Row { .. });
|
||||
|
||||
if is_row {
|
||||
row_position += 1;
|
||||
}
|
||||
|
||||
let (position_in_set, size_of_set) = if is_row {
|
||||
(row_position, row_count)
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
render_entry(
|
||||
|
||||
entry.render_entry(
|
||||
cx,
|
||||
entry,
|
||||
index,
|
||||
position_in_set,
|
||||
size_of_set,
|
||||
@@ -282,91 +289,99 @@ impl RenderOnce for Sidebar {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_entry(
|
||||
cx: &App,
|
||||
entry: SidebarEntry,
|
||||
index: usize,
|
||||
position_in_set: usize,
|
||||
size_of_set: usize,
|
||||
selected: bool,
|
||||
material: bool,
|
||||
on_change: &Option<ChangeHandler>,
|
||||
focus: &FocusHandle,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
impl SidebarEntry {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_entry(
|
||||
self,
|
||||
cx: &App,
|
||||
index: usize,
|
||||
position_in_set: usize,
|
||||
size_of_set: usize,
|
||||
selected: bool,
|
||||
material: bool,
|
||||
on_change: &Option<ChangeHandler>,
|
||||
focus: &FocusHandle,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
|
||||
match entry {
|
||||
SidebarEntry::Header { label } => div()
|
||||
.px_2()
|
||||
.pt_2()
|
||||
.pb_1()
|
||||
.text_sm()
|
||||
.font_weight(gpui::FontWeight::MEDIUM)
|
||||
.text_color(theme.colors.muted_foreground)
|
||||
.child(label)
|
||||
.into_any_element(),
|
||||
SidebarEntry::Row {
|
||||
label,
|
||||
detail,
|
||||
prefix,
|
||||
suffix,
|
||||
disabled: row_disabled,
|
||||
} => {
|
||||
let label_color = selectable_label_color(cx, selected, row_disabled);
|
||||
match self {
|
||||
SidebarEntry::Header { label } => {
|
||||
// The id is required for the role to surface in the a11y tree.
|
||||
div()
|
||||
.id(("sidebar-header".into(), label.clone()))
|
||||
.px_2()
|
||||
.pt_2()
|
||||
.pb_1()
|
||||
.text_sm()
|
||||
.font_weight(gpui::FontWeight::MEDIUM)
|
||||
.text_color(theme.colors.muted_foreground)
|
||||
.role(Role::Heading)
|
||||
.child(label)
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarEntry::Row {
|
||||
key,
|
||||
label,
|
||||
detail,
|
||||
prefix,
|
||||
suffix,
|
||||
disabled: row_disabled,
|
||||
} => {
|
||||
let label_color = selectable_label_color(cx, selected, row_disabled);
|
||||
|
||||
div()
|
||||
.id(("sidebar-row", index))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(theme.radius.sm)
|
||||
.role(Role::ListItem)
|
||||
.aria_selected(selected)
|
||||
.aria_position_in_set(position_in_set)
|
||||
.aria_size_of_set(size_of_set)
|
||||
.map(|row| {
|
||||
if material && !row_disabled {
|
||||
if selected {
|
||||
row.bg(theme.material.selection)
|
||||
} else {
|
||||
row.hover(|row| row.bg(theme.material.hover))
|
||||
}
|
||||
} else {
|
||||
paint_selectable_row(cx, row, selected, row_disabled)
|
||||
}
|
||||
})
|
||||
.when(!row_disabled, |row| {
|
||||
row.cursor_pointer().on_click({
|
||||
let on_change = on_change.clone();
|
||||
let focus = focus.clone();
|
||||
move |_, window, cx| {
|
||||
window.focus(&focus, cx);
|
||||
if let Some(on_change) = &on_change {
|
||||
on_change(Some(index), window, cx);
|
||||
div()
|
||||
.id((key.clone(), "sidebar-row"))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(theme.radius.sm)
|
||||
.role(Role::ListItem)
|
||||
.aria_selected(selected)
|
||||
.aria_position_in_set(position_in_set)
|
||||
.aria_size_of_set(size_of_set)
|
||||
.map(|row| {
|
||||
if material && !row_disabled {
|
||||
if selected {
|
||||
row.bg(theme.material.selection)
|
||||
} else {
|
||||
row.hover(|row| row.bg(theme.material.hover))
|
||||
}
|
||||
} else {
|
||||
paint_selectable_row(cx, row, selected, row_disabled)
|
||||
}
|
||||
})
|
||||
})
|
||||
.when_some(prefix, |row, prefix| row.child(prefix))
|
||||
.child(div().flex_1().text_color(label_color).child(label))
|
||||
.when_some(detail, |row, detail| {
|
||||
row.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_sm()
|
||||
.text_color(if selected {
|
||||
theme.colors.primary_foreground
|
||||
} else {
|
||||
theme.colors.muted_foreground
|
||||
})
|
||||
.child(detail),
|
||||
)
|
||||
})
|
||||
.when_some(suffix, |row, suffix| row.child(suffix))
|
||||
.into_any_element()
|
||||
.when(!row_disabled, |row| {
|
||||
row.cursor_pointer().on_click({
|
||||
let on_change = on_change.clone();
|
||||
let focus = focus.clone();
|
||||
move |_, window, cx| {
|
||||
window.focus(&focus, cx);
|
||||
if let Some(on_change) = &on_change {
|
||||
on_change(Some(index), window, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.when_some(prefix, |row, prefix| row.child(prefix))
|
||||
.child(div().flex_1().text_color(label_color).child(label))
|
||||
.when_some(detail, |row, detail| {
|
||||
row.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_sm()
|
||||
.text_color(if selected {
|
||||
theme.colors.primary_foreground
|
||||
} else {
|
||||
theme.colors.muted_foreground
|
||||
})
|
||||
.child(detail),
|
||||
)
|
||||
})
|
||||
.when_some(suffix, |row, suffix| row.child(suffix))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+139
-137
@@ -31,6 +31,7 @@ pub enum TabsVariant {
|
||||
}
|
||||
|
||||
/// One tab of a [`Tabs`] strip.
|
||||
#[must_use]
|
||||
pub struct Tab {
|
||||
/// The stable identity for element state; must survive reordering.
|
||||
key: ElementId,
|
||||
@@ -102,6 +103,7 @@ impl FluentBuilder for Tab {}
|
||||
|
||||
/// A tab strip.
|
||||
#[derive(IntoElement)]
|
||||
#[must_use]
|
||||
pub struct Tabs {
|
||||
id: ElementId,
|
||||
selected: usize,
|
||||
@@ -129,7 +131,7 @@ impl Tabs {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the current selection.
|
||||
/// Sets the current selection, as an index into the `tabs` vec — not a tab key.
|
||||
pub fn selected(mut self, selected: usize) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
@@ -141,7 +143,7 @@ impl Tabs {
|
||||
self
|
||||
}
|
||||
|
||||
/// Reports the next selected tab, from clicks and keys alike.
|
||||
/// Reports the next selected tab, as an index into the `tabs` vec — not a tab key; indices shift on reorder.
|
||||
pub fn on_change<F>(mut self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(usize, &mut Window, &mut App) + 'static,
|
||||
@@ -150,7 +152,7 @@ impl Tabs {
|
||||
self
|
||||
}
|
||||
|
||||
/// Reports a closed tab by index.
|
||||
/// Reports a closed tab, as an index into the `tabs` vec — not a tab key; indices shift on reorder.
|
||||
pub fn on_close<F>(mut self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(usize, &mut Window, &mut App) + 'static,
|
||||
@@ -159,7 +161,7 @@ impl Tabs {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the tabs, built fresh each render.
|
||||
/// Sets the tabs; the closure runs during render and must not mutate entities or notify.
|
||||
pub fn tabs<F>(mut self, tabs: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) -> Vec<Tab> + 'static,
|
||||
@@ -204,10 +206,9 @@ impl RenderOnce for Tabs {
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, tab)| {
|
||||
render_tab(
|
||||
tab.render_tab(
|
||||
&self.id,
|
||||
cx,
|
||||
tab,
|
||||
index,
|
||||
self.selected,
|
||||
self.variant,
|
||||
@@ -257,145 +258,146 @@ impl RenderOnce for Tabs {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_tab(
|
||||
tabs_id: &ElementId,
|
||||
cx: &App,
|
||||
tab: Tab,
|
||||
index: usize,
|
||||
selected_index: usize,
|
||||
variant: TabsVariant,
|
||||
on_change: &Option<ChangeHandler>,
|
||||
on_close: &Option<CloseHandler>,
|
||||
focus: &gpui::FocusHandle,
|
||||
) -> AnyElement {
|
||||
let mut tab = tab;
|
||||
let selected = index == selected_index;
|
||||
impl Tab {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_tab(
|
||||
mut self,
|
||||
tabs_id: &ElementId,
|
||||
cx: &App,
|
||||
index: usize,
|
||||
selected_index: usize,
|
||||
variant: TabsVariant,
|
||||
on_change: &Option<ChangeHandler>,
|
||||
on_close: &Option<CloseHandler>,
|
||||
focus: &gpui::FocusHandle,
|
||||
) -> AnyElement {
|
||||
let selected = index == selected_index;
|
||||
|
||||
let theme = cx.theme();
|
||||
let metrics = theme.sizes.resolve(Size::default());
|
||||
let small = theme.sizes.small;
|
||||
let theme = cx.theme();
|
||||
let metrics = theme.sizes.resolve(Size::default());
|
||||
let small = theme.sizes.small;
|
||||
|
||||
let label_color = match variant {
|
||||
TabsVariant::Underline => {
|
||||
if tab.disabled {
|
||||
theme.colors.muted_foreground
|
||||
} else if selected {
|
||||
theme.ramps.primary.base
|
||||
} else {
|
||||
theme.colors.foreground
|
||||
let label_color = match variant {
|
||||
TabsVariant::Underline => {
|
||||
if self.disabled {
|
||||
theme.colors.muted_foreground
|
||||
} else if selected {
|
||||
theme.ramps.primary.base
|
||||
} else {
|
||||
theme.colors.foreground
|
||||
}
|
||||
}
|
||||
}
|
||||
TabsVariant::Pill => {
|
||||
if tab.disabled {
|
||||
theme.colors.muted_foreground
|
||||
} else {
|
||||
theme.colors.foreground
|
||||
TabsVariant::Pill => {
|
||||
if self.disabled {
|
||||
theme.colors.muted_foreground
|
||||
} else {
|
||||
theme.colors.foreground
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
let strip_tab = div()
|
||||
.id((tab.key.clone(), "tab"))
|
||||
.debug_selector({
|
||||
let tabs_id = tabs_id.clone();
|
||||
let label = tab.label.clone();
|
||||
move || format!("{tabs_id}-tab-{}", label)
|
||||
})
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.role(Role::Tab)
|
||||
.aria_selected(selected)
|
||||
.text_color(label_color)
|
||||
.when(!tab.disabled && !selected, |strip_tab| {
|
||||
strip_tab.hover(|strip_tab| strip_tab.bg(theme.colors.muted))
|
||||
})
|
||||
.map(|strip_tab| match variant {
|
||||
TabsVariant::Underline => strip_tab
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded_tl(theme.radius.sm)
|
||||
.rounded_tr(theme.radius.sm)
|
||||
.border_b_2()
|
||||
.border_color(gpui::transparent_black())
|
||||
.when(selected, |strip_tab| {
|
||||
strip_tab.border_color(theme.ramps.primary.base)
|
||||
}),
|
||||
TabsVariant::Pill => strip_tab
|
||||
.h(small.control_height + metrics.plate_inset * 2.0)
|
||||
.px(metrics.dense_padding_x)
|
||||
.pr(metrics.plate_inset)
|
||||
.rounded(theme.radius.control)
|
||||
.when(selected, |strip_tab| {
|
||||
strip_tab
|
||||
.bg(theme.colors.muted)
|
||||
.hover(|strip_tab| strip_tab.bg(theme.colors.muted_hover))
|
||||
}),
|
||||
})
|
||||
.when_some(tab.lead.take(), |strip_tab, lead| strip_tab.child(lead))
|
||||
.child(tab.label.clone())
|
||||
.when(variant == TabsVariant::Pill, |strip_tab| {
|
||||
strip_tab.child(div().flex_none().w(metrics.padding_x * 1.5))
|
||||
})
|
||||
.when(tab.busy && !tab.disabled, |strip_tab| {
|
||||
strip_tab.child(
|
||||
Spinner::new((tab.key.clone(), "tab-busy"))
|
||||
.small()
|
||||
.accessibility_label("Loading"),
|
||||
)
|
||||
})
|
||||
.when(
|
||||
tab.closeable && on_close.is_some() && !tab.disabled,
|
||||
|strip_tab| strip_tab.child(render_tab_close(tabs_id, &tab, index, on_close)),
|
||||
)
|
||||
.when(!tab.disabled, |strip_tab| {
|
||||
strip_tab.cursor_pointer().on_click({
|
||||
let on_change = on_change.clone();
|
||||
let focus = focus.clone();
|
||||
let close = (self.closeable && on_close.is_some() && !self.disabled)
|
||||
.then(|| self.render_tab_close(tabs_id, index, on_close));
|
||||
|
||||
let strip_tab = div()
|
||||
.id((self.key.clone(), "tab"))
|
||||
.debug_selector({
|
||||
let tabs_id = tabs_id.clone();
|
||||
let label = self.label.clone();
|
||||
move || format!("{tabs_id}-tab-{}", label)
|
||||
})
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.role(Role::Tab)
|
||||
.aria_selected(selected)
|
||||
.text_color(label_color)
|
||||
.when(!self.disabled && !selected, |strip_tab| {
|
||||
strip_tab.hover(|strip_tab| strip_tab.bg(theme.colors.muted))
|
||||
})
|
||||
.map(|strip_tab| match variant {
|
||||
TabsVariant::Underline => strip_tab
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded_tl(theme.radius.sm)
|
||||
.rounded_tr(theme.radius.sm)
|
||||
.border_b_2()
|
||||
.border_color(gpui::transparent_black())
|
||||
.when(selected, |strip_tab| {
|
||||
strip_tab.border_color(theme.ramps.primary.base)
|
||||
}),
|
||||
TabsVariant::Pill => strip_tab
|
||||
.h(small.control_height + metrics.plate_inset * 2.0)
|
||||
.px(metrics.dense_padding_x)
|
||||
.pr(metrics.plate_inset)
|
||||
.rounded(theme.radius.control)
|
||||
.when(selected, |strip_tab| {
|
||||
strip_tab
|
||||
.bg(theme.colors.muted)
|
||||
.hover(|strip_tab| strip_tab.bg(theme.colors.muted_hover))
|
||||
}),
|
||||
})
|
||||
.when_some(self.lead.take(), |strip_tab, lead| strip_tab.child(lead))
|
||||
.child(self.label.clone())
|
||||
.when(variant == TabsVariant::Pill, |strip_tab| {
|
||||
strip_tab.child(div().flex_none().w(metrics.padding_x * 1.5))
|
||||
})
|
||||
.when(self.busy && !self.disabled, |strip_tab| {
|
||||
strip_tab.child(
|
||||
Spinner::new((self.key.clone(), "tab-busy"))
|
||||
.small()
|
||||
.accessibility_label("Loading"),
|
||||
)
|
||||
})
|
||||
.when_some(close, |strip_tab, close| strip_tab.child(close))
|
||||
.when(!self.disabled, |strip_tab| {
|
||||
strip_tab.cursor_pointer().on_click({
|
||||
let on_change = on_change.clone();
|
||||
let focus = focus.clone();
|
||||
move |_, window, cx| {
|
||||
window.focus(&focus, cx);
|
||||
if index != selected_index
|
||||
&& let Some(on_change) = &on_change
|
||||
{
|
||||
on_change(index, window, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
match self.wrap {
|
||||
Some(wrap) => wrap(strip_tab),
|
||||
None => strip_tab.into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tab_close(
|
||||
&self,
|
||||
tabs_id: &ElementId,
|
||||
index: usize,
|
||||
on_close: &Option<CloseHandler>,
|
||||
) -> AnyElement {
|
||||
Button::new((self.key.clone(), "tab-close"))
|
||||
.icon(IconRole::Close)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.small()
|
||||
.tab_stop(false)
|
||||
.debug_selector({
|
||||
let tabs_id = tabs_id.clone();
|
||||
let label = self.label.clone();
|
||||
move || format!("{tabs_id}-close-{}", label)
|
||||
})
|
||||
.tooltip(Tooltip::text("Close"))
|
||||
.accessibility_label(SharedString::from(format!("Close {}", self.label)))
|
||||
.on_click({
|
||||
let on_close = on_close.clone();
|
||||
move |_, window, cx| {
|
||||
window.focus(&focus, cx);
|
||||
if index != selected_index
|
||||
&& let Some(on_change) = &on_change
|
||||
{
|
||||
on_change(index, window, cx);
|
||||
cx.stop_propagation();
|
||||
if let Some(on_close) = &on_close {
|
||||
on_close(index, window, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
match tab.wrap {
|
||||
Some(wrap) => wrap(strip_tab),
|
||||
None => strip_tab.into_any_element(),
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tab_close(
|
||||
tabs_id: &ElementId,
|
||||
tab: &Tab,
|
||||
index: usize,
|
||||
on_close: &Option<CloseHandler>,
|
||||
) -> AnyElement {
|
||||
Button::new((tab.key.clone(), "tab-close"))
|
||||
.icon(IconRole::Close)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.small()
|
||||
.tab_stop(false)
|
||||
.debug_selector({
|
||||
let tabs_id = tabs_id.clone();
|
||||
let label = tab.label.clone();
|
||||
move || format!("{tabs_id}-close-{}", label)
|
||||
})
|
||||
.tooltip(Tooltip::text("Close"))
|
||||
.accessibility_label(SharedString::from(format!("Close {}", tab.label)))
|
||||
.on_click({
|
||||
let on_close = on_close.clone();
|
||||
move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(on_close) = &on_close {
|
||||
on_close(index, window, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
+175
-148
@@ -18,7 +18,9 @@ type ItemsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<TreeItem>>;
|
||||
|
||||
/// Tree item.
|
||||
#[derive(Clone)]
|
||||
#[must_use]
|
||||
pub struct TreeItem {
|
||||
key: ElementId,
|
||||
label: SharedString,
|
||||
icon: Option<Icon>,
|
||||
depth: usize,
|
||||
@@ -28,12 +30,14 @@ pub struct TreeItem {
|
||||
}
|
||||
|
||||
impl TreeItem {
|
||||
/// Creates a leaf row with this label at the root depth.
|
||||
pub fn new<L>(label: L) -> Self
|
||||
/// Creates a leaf row shown as `label` at the root depth, identified by `key`.
|
||||
pub fn new<K, L>(key: K, label: L) -> Self
|
||||
where
|
||||
K: Into<ElementId>,
|
||||
L: Into<SharedString>,
|
||||
{
|
||||
Self {
|
||||
key: key.into(),
|
||||
label: label.into(),
|
||||
icon: None,
|
||||
depth: 0,
|
||||
@@ -79,6 +83,7 @@ impl TreeItem {
|
||||
|
||||
/// A tree.
|
||||
#[derive(IntoElement)]
|
||||
#[must_use]
|
||||
pub struct Tree {
|
||||
id: ElementId,
|
||||
selected: Option<usize>,
|
||||
@@ -128,7 +133,7 @@ impl Tree {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the visible rows, built fresh each render.
|
||||
/// Sets the visible rows; the closure runs during render and must not mutate entities or notify.
|
||||
pub fn items<F>(mut self, items: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) -> Vec<TreeItem> + 'static,
|
||||
@@ -145,6 +150,45 @@ impl Tree {
|
||||
self.a11y_label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Maps each visible row to its 1-based position-in-set and set size among its siblings.
|
||||
fn sibling_sets(items: &[TreeItem]) -> Vec<(usize, usize)> {
|
||||
let mut sets = vec![(0usize, 0usize); items.len()];
|
||||
// Open sibling runs along the ancestry path, deeper runs above shallower ones.
|
||||
let mut runs: Vec<(usize, Vec<usize>)> = Vec::new();
|
||||
|
||||
for (index, item) in items.iter().enumerate() {
|
||||
while runs
|
||||
.last()
|
||||
.is_some_and(|(run_depth, _)| *run_depth > item.depth)
|
||||
{
|
||||
if let Some((_, pending)) = runs.pop() {
|
||||
Self::stamp_sibling_run(&mut sets, pending);
|
||||
}
|
||||
}
|
||||
|
||||
match runs.last_mut() {
|
||||
// A row at the running depth extends the open sibling run.
|
||||
Some((run_depth, pending)) if *run_depth == item.depth => pending.push(index),
|
||||
_ => runs.push((item.depth, vec![index])),
|
||||
}
|
||||
}
|
||||
|
||||
for (_, pending) in runs {
|
||||
Self::stamp_sibling_run(&mut sets, pending);
|
||||
}
|
||||
|
||||
sets
|
||||
}
|
||||
|
||||
fn stamp_sibling_run(sets: &mut [(usize, usize)], pending: Vec<usize>) {
|
||||
let size_of_set = pending.len();
|
||||
for (position, index) in pending.into_iter().enumerate() {
|
||||
if let Some(set) = sets.get_mut(index) {
|
||||
*set = (position + 1, size_of_set);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Tree {
|
||||
@@ -169,46 +213,15 @@ impl RenderOnce for Tree {
|
||||
.clone()
|
||||
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
|
||||
|
||||
let sibling_sets: Vec<(usize, usize)> = {
|
||||
let mut sets = vec![(0usize, 0usize); items.len()];
|
||||
let mut start = 0usize;
|
||||
|
||||
while start < items.len() {
|
||||
let depth = items[start].depth;
|
||||
|
||||
let mut end = start + 1;
|
||||
while end < items.len() && items[end].depth > depth {
|
||||
end += 1;
|
||||
}
|
||||
|
||||
let mut position = 0usize;
|
||||
for index in start..end {
|
||||
if items[index].depth == depth {
|
||||
position += 1;
|
||||
sets[index] = (position, 0);
|
||||
}
|
||||
}
|
||||
|
||||
for index in start..end {
|
||||
if items[index].depth == depth {
|
||||
sets[index].1 = position;
|
||||
}
|
||||
}
|
||||
|
||||
start = end;
|
||||
}
|
||||
|
||||
sets
|
||||
};
|
||||
let sibling_sets = Self::sibling_sets(&items);
|
||||
|
||||
let rows: Vec<AnyElement> = items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, _item)| {
|
||||
.map(|(index, item)| {
|
||||
let (position_in_set, size_of_set) = sibling_sets[index];
|
||||
render_row(
|
||||
item.render_row(
|
||||
cx,
|
||||
&items[index],
|
||||
index,
|
||||
position_in_set,
|
||||
size_of_set,
|
||||
@@ -266,121 +279,135 @@ impl RenderOnce for Tree {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_row(
|
||||
cx: &App,
|
||||
item: &TreeItem,
|
||||
index: usize,
|
||||
position_in_set: usize,
|
||||
size_of_set: usize,
|
||||
selected: bool,
|
||||
on_change: &Option<ChangeHandler>,
|
||||
on_disclosure: &Option<DisclosureHandler>,
|
||||
focus: &FocusHandle,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let label_color = selectable_label_color(cx, selected, item.disabled);
|
||||
let disabled = item.disabled;
|
||||
// One indent step per depth level, on the theme's horizontal padding.
|
||||
let indent = theme.sizes.medium.padding_x * (item.depth as f32 + 1.0);
|
||||
impl TreeItem {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_row(
|
||||
&self,
|
||||
cx: &App,
|
||||
index: usize,
|
||||
position_in_set: usize,
|
||||
size_of_set: usize,
|
||||
selected: bool,
|
||||
on_change: &Option<ChangeHandler>,
|
||||
on_disclosure: &Option<DisclosureHandler>,
|
||||
focus: &FocusHandle,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let label_color = selectable_label_color(cx, selected, self.disabled);
|
||||
let disabled = self.disabled;
|
||||
// usize can exceed f32's exact integer range; bound it via u32 before casting.
|
||||
let depth = u32::try_from(self.depth).unwrap_or(u32::MAX) as f32;
|
||||
// One indent step per depth level, on the theme's horizontal padding.
|
||||
let indent = theme.sizes.medium.padding_x * (depth + 1.0);
|
||||
|
||||
div()
|
||||
.id(("tree-item", index))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.pl(indent)
|
||||
.pr_2()
|
||||
.py_1()
|
||||
.rounded(theme.radius.sm)
|
||||
.role(Role::TreeItem)
|
||||
.aria_selected(selected)
|
||||
.aria_level(item.depth + 1)
|
||||
.aria_position_in_set(position_in_set)
|
||||
.aria_size_of_set(size_of_set)
|
||||
.when(item.expandable, |row| row.aria_expanded(item.expanded))
|
||||
.map(|row| paint_selectable_row(cx, row, selected, disabled))
|
||||
.when(!disabled, |row| {
|
||||
row.cursor_pointer().on_click({
|
||||
let on_change = on_change.clone();
|
||||
let focus = focus.clone();
|
||||
move |_, window, cx| {
|
||||
window.focus(&focus, cx);
|
||||
if let Some(on_change) = &on_change {
|
||||
on_change(Some(index), window, cx);
|
||||
div()
|
||||
.id((self.key.clone(), "tree-item"))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.pl(indent)
|
||||
.pr_2()
|
||||
.py_1()
|
||||
.rounded(theme.radius.sm)
|
||||
.role(Role::TreeItem)
|
||||
.aria_selected(selected)
|
||||
.aria_level(self.depth + 1)
|
||||
.aria_position_in_set(position_in_set)
|
||||
.aria_size_of_set(size_of_set)
|
||||
.when(self.expandable, |row| row.aria_expanded(self.expanded))
|
||||
.map(|row| paint_selectable_row(cx, row, selected, disabled))
|
||||
.when(!disabled, |row| {
|
||||
row.cursor_pointer().on_click({
|
||||
let on_change = on_change.clone();
|
||||
let focus = focus.clone();
|
||||
move |_, window, cx| {
|
||||
window.focus(&focus, cx);
|
||||
if let Some(on_change) = &on_change {
|
||||
on_change(Some(index), window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.child(render_disclosure(
|
||||
cx,
|
||||
item,
|
||||
index,
|
||||
disabled,
|
||||
selected,
|
||||
on_disclosure,
|
||||
))
|
||||
.when_some(item.icon.clone(), |row, icon| {
|
||||
row.child(icon.size(theme.sizes.medium.icon_size).color(label_color))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.text_color(label_color)
|
||||
.child(item.label.clone()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// The disclosure slot.
|
||||
fn render_disclosure(
|
||||
cx: &App,
|
||||
item: &TreeItem,
|
||||
index: usize,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
on_disclosure: &Option<DisclosureHandler>,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let glyph_color = if disabled {
|
||||
theme.colors.muted_foreground
|
||||
} else if selected {
|
||||
theme.colors.primary_foreground
|
||||
} else {
|
||||
theme.colors.muted_foreground
|
||||
};
|
||||
|
||||
let cell = div()
|
||||
.flex_none()
|
||||
.size(theme.sizes.medium.icon_size)
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
icon(if item.expanded {
|
||||
IconRole::ChevronDown
|
||||
} else {
|
||||
IconRole::ChevronRight
|
||||
})
|
||||
.size(theme.sizes.medium.dense_icon_size)
|
||||
.color(glyph_color),
|
||||
);
|
||||
|
||||
if item.expandable && !disabled {
|
||||
cell.id(("tree-disclosure", index))
|
||||
.cursor_pointer()
|
||||
.on_click({
|
||||
let on_disclosure = on_disclosure.clone();
|
||||
move |_, window, cx| {
|
||||
if let Some(on_disclosure) = &on_disclosure {
|
||||
on_disclosure(index, window, cx);
|
||||
}
|
||||
// The toggle is not a selection: keep it off the row.
|
||||
cx.stop_propagation();
|
||||
}
|
||||
.child(self.render_disclosure(cx, index, selected, on_disclosure))
|
||||
.when_some(self.icon.clone(), |row, icon| {
|
||||
row.child(icon.size(theme.sizes.medium.icon_size).color(label_color))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.text_color(label_color)
|
||||
.child(self.label.clone()),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
cell.into_any_element()
|
||||
}
|
||||
|
||||
fn render_disclosure(
|
||||
&self,
|
||||
cx: &App,
|
||||
index: usize,
|
||||
selected: bool,
|
||||
on_disclosure: &Option<DisclosureHandler>,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let glyph_color = if self.disabled || !selected {
|
||||
theme.colors.muted_foreground
|
||||
} else {
|
||||
theme.colors.primary_foreground
|
||||
};
|
||||
|
||||
let cell = div()
|
||||
.flex_none()
|
||||
.size(theme.sizes.medium.icon_size)
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
icon(if self.expanded {
|
||||
IconRole::ChevronDown
|
||||
} else {
|
||||
IconRole::ChevronRight
|
||||
})
|
||||
.size(theme.sizes.medium.dense_icon_size)
|
||||
.color(glyph_color),
|
||||
);
|
||||
|
||||
if self.expandable && !self.disabled {
|
||||
cell.id((self.key.clone(), "tree-disclosure"))
|
||||
.cursor_pointer()
|
||||
.on_click({
|
||||
let on_disclosure = on_disclosure.clone();
|
||||
move |_, window, cx| {
|
||||
if let Some(on_disclosure) = &on_disclosure {
|
||||
on_disclosure(index, window, cx);
|
||||
}
|
||||
// The toggle is not a selection: keep it off the row.
|
||||
cx.stop_propagation();
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
} else {
|
||||
cell.into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn item(depth: usize) -> TreeItem {
|
||||
TreeItem::new("item", "Item").depth(depth)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_rows_number_one_of_three() {
|
||||
let sets = Tree::sibling_sets(&[item(0), item(0), item(0)]);
|
||||
assert_eq!(sets, vec![(1, 3), (2, 3), (3, 3)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_rows_number_per_level() {
|
||||
let sets = Tree::sibling_sets(&[item(0), item(1), item(2), item(1), item(0)]);
|
||||
assert_eq!(sets, vec![(1, 2), (1, 2), (1, 1), (2, 2), (2, 2)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@ impl Render for Harness {
|
||||
})
|
||||
.items(|_, _| {
|
||||
vec![
|
||||
ListItem::new("One"),
|
||||
ListItem::new("Two"),
|
||||
ListItem::new("Unavailable").disabled(true),
|
||||
ListItem::new("Three"),
|
||||
ListItem::new("one", "One"),
|
||||
ListItem::new("two", "Two"),
|
||||
ListItem::new("unavailable", "Unavailable").disabled(true),
|
||||
ListItem::new("three", "Three"),
|
||||
]
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -30,11 +30,11 @@ impl Render for Harness {
|
||||
})
|
||||
.entries(|_, _| {
|
||||
vec![
|
||||
SidebarEntry::header("Favorites"), // 0: never selectable
|
||||
SidebarEntry::row("Documents"), // 1
|
||||
SidebarEntry::row("Locked").disabled(true), // 2
|
||||
SidebarEntry::header("Locations"), // 3: never selectable
|
||||
SidebarEntry::row("Trash"), // 4
|
||||
SidebarEntry::header("Favorites"), // 0: never selectable
|
||||
SidebarEntry::row("documents", "Documents"), // 1
|
||||
SidebarEntry::row("locked", "Locked").disabled(true), // 2
|
||||
SidebarEntry::header("Locations"), // 3: never selectable
|
||||
SidebarEntry::row("trash", "Trash"), // 4
|
||||
]
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ use gpui_native_data::tabs::{Tab, Tabs};
|
||||
use gpui_native_theme::Theme;
|
||||
|
||||
type Changes = Rc<RefCell<Vec<usize>>>;
|
||||
type Closes = Rc<RefCell<Vec<usize>>>;
|
||||
|
||||
struct Harness {
|
||||
selected: usize,
|
||||
@@ -64,6 +65,55 @@ fn click(visual: &mut VisualTestContext, position: gpui::Point<gpui::Pixels>) {
|
||||
visual.simulate_mouse_up(position, MouseButton::Left, Modifiers::default());
|
||||
}
|
||||
|
||||
struct CloseHarness {
|
||||
changes: Changes,
|
||||
closes: Closes,
|
||||
}
|
||||
|
||||
impl Render for CloseHarness {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let changes = self.changes.clone();
|
||||
let closes = self.closes.clone();
|
||||
let view = cx.entity();
|
||||
let close_view = view.clone();
|
||||
div().size_full().child(
|
||||
Tabs::new("close-tabs")
|
||||
.on_change(move |next, _, cx| {
|
||||
changes.borrow_mut().push(next);
|
||||
view.update(cx, |_, cx| cx.notify());
|
||||
})
|
||||
.on_close(move |index, _, cx| {
|
||||
closes.borrow_mut().push(index);
|
||||
close_view.update(cx, |_, cx| cx.notify());
|
||||
})
|
||||
.tabs(|_, _| {
|
||||
vec![
|
||||
Tab::new("one", "One").closeable(true),
|
||||
Tab::new("two", "Two"),
|
||||
]
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn close_tabs_window() -> (TestAppContext, VisualTestContext, Changes, Closes) {
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(Theme::init);
|
||||
let changes = Rc::new(RefCell::new(Vec::new()));
|
||||
let closes = Rc::new(RefCell::new(Vec::new()));
|
||||
let harness_changes = changes.clone();
|
||||
let harness_closes = closes.clone();
|
||||
let window = cx.add_window(move |_, _| CloseHarness {
|
||||
changes: harness_changes,
|
||||
closes: harness_closes,
|
||||
});
|
||||
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
||||
visual.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
});
|
||||
(cx, visual, changes, closes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicking_a_tab_selects_it_and_hands_the_strip_the_keyboard() {
|
||||
let (_cx, mut visual, changes) = tabs_window();
|
||||
@@ -100,3 +150,19 @@ fn arrow_keys_skip_disabled_tabs_and_never_wrap() {
|
||||
visual.simulate_keystrokes("end"); // End
|
||||
assert_eq!(*changes.borrow(), vec![1, 3, 1, 0, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicking_the_close_control_reports_a_close_without_selecting() {
|
||||
let (_cx, mut visual, changes, closes) = close_tabs_window();
|
||||
|
||||
// Only the closeable tab ships a close control.
|
||||
let selector: &'static str = Box::leak(String::from("close-tabs-close-One").into_boxed_str());
|
||||
let close_bounds = visual
|
||||
.debug_bounds(selector)
|
||||
.expect("closeable tab renders a close control");
|
||||
assert!(visual.debug_bounds("close-tabs-close-Two").is_none());
|
||||
|
||||
click(&mut visual, close_bounds.center());
|
||||
assert_eq!(*closes.borrow(), vec![0]);
|
||||
assert!(changes.borrow().is_empty(), "a close is not a selection");
|
||||
}
|
||||
|
||||
@@ -38,14 +38,18 @@ impl Render for Harness {
|
||||
})
|
||||
.items(|_, _| {
|
||||
vec![
|
||||
TreeItem::new("Root")
|
||||
TreeItem::new("root", "Root")
|
||||
.depth(0)
|
||||
.expandable(true)
|
||||
.expanded(true),
|
||||
TreeItem::new("Child").depth(1),
|
||||
TreeItem::new("Collapsed").depth(0).expandable(true),
|
||||
TreeItem::new("Untouchable").depth(1).disabled(true),
|
||||
TreeItem::new("Last").depth(0),
|
||||
TreeItem::new("child", "Child").depth(1),
|
||||
TreeItem::new("collapsed", "Collapsed")
|
||||
.depth(0)
|
||||
.expandable(true),
|
||||
TreeItem::new("untouchable", "Untouchable")
|
||||
.depth(1)
|
||||
.disabled(true),
|
||||
TreeItem::new("last", "Last").depth(0),
|
||||
]
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user