Files
zed/crates/tab_switcher/src/tab_switcher.rs
T
Om ChillureandPiotr Osiewicz 24b222a981 Fix tab switcher random jumps (#52671)
#### Fixes #52579

When quickly pressing and releasing Ctrl+Tab, the tab switcher popover
would briefly appear, and any mouse movement over it would select a
random tab instead of the intended recently-focused file.

### Fix : 
This adds a 300ms delay before showing the tab switcher UI when opened
via modifier keys. If the modifier is released before the delay elapses,
the selected item is confirmed directly without ever showing the
popover. This matches the behavior of system window switchers (e.g.
macOS Cmd+Tab), where quick presses never flash the UI.

- `visible` and `_show_task` fields track whether the popover should be
rendered
- When not yet visible, the picker is mounted in a zero-size
overflow-hidden container to preserve focus and keyboard state
- When modifiers are released before the popover is visible, `confirm()`
is called directly instead of dispatching `menu::Confirm`, avoiding any
mouse-hover-based selection

### Self-Review Checklist

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments — *N/A, no unsafe
blocks*
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
— *Matches system window switcher behavior (e.g. macOS Cmd+Tab); no
visual change for slow/held switches*
- [x] Tests cover the new/changed behavior — *Added
`test_quick_switch_before_popover_visible`*
- [x] Performance impact has been considered and is acceptable — *Only
adds a lightweight 300ms timer task; no extra allocations or rendering
cost*

### Video 

[Screencast from 2026-03-29
11-52-07.webm](https://github.com/user-attachments/assets/8e148a74-75ae-4c36-8d44-594c3c9097e6)



### Note : Reopens previous work from closed PR #52648 (fork was deleted
by mistake)


Release Notes:

- Fixed Ctrl+Tab jumping to random documents when the mouse is moved
during a quick tab switch

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2026-09-09 09:09:25 +00:00

924 lines
30 KiB
Rust

#[cfg(test)]
mod tab_switcher_tests;
use collections::{HashMap, HashSet};
use editor::items::{
entry_diagnostic_aware_icon_decoration_and_color, entry_git_aware_label_color,
};
use fuzzy_nucleo::StringMatchCandidate;
use gpui::{
Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle,
Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Point,
Render, Styled, Task, TaskExt, WeakEntity, Window, actions, rems,
};
use picker::{Picker, PickerDelegate};
use project::Project;
use schemars::JsonSchema;
use serde::Deserialize;
use settings::Settings;
use std::time::Duration;
use std::{cmp::Reverse, sync::Arc};
use ui::{
DecoratedIcon, IconDecoration, IconDecorationKind, ListItem, ListItemSpacing, Tooltip,
prelude::*,
};
use util::ResultExt;
use workspace::{
Event as WorkspaceEvent, ModalView, Pane, SaveIntent, Workspace,
item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams},
pane::{render_item_indicator, tab_details},
};
const PANEL_WIDTH_REMS: f32 = 28.;
const POPOVER_DELAY: Duration = Duration::from_millis(300);
/// Toggles the tab switcher interface.
#[derive(PartialEq, Clone, Deserialize, JsonSchema, Default, Action)]
#[action(namespace = tab_switcher)]
#[serde(deny_unknown_fields)]
pub struct Toggle {
#[serde(default)]
pub select_last: bool,
}
actions!(
tab_switcher,
[
/// Closes the selected item in the tab switcher.
CloseSelectedItem,
/// Toggles between showing all tabs or just the current pane's tabs.
ToggleAll,
/// Toggles the tab switcher showing all tabs across all panes, deduplicated by path.
/// Opens selected items in the active pane.
OpenInActivePane,
]
);
pub struct TabSwitcher {
picker: Entity<Picker<TabSwitcherDelegate>>,
init_modifiers: Option<Modifiers>,
visible: bool,
_show_task: Option<Task<()>>,
}
impl ModalView for TabSwitcher {}
pub fn init(cx: &mut App) {
cx.observe_new(TabSwitcher::register).detach();
}
impl TabSwitcher {
fn register(
workspace: &mut Workspace,
_window: Option<&mut Window>,
_: &mut Context<Workspace>,
) {
workspace.register_action(|workspace, action: &Toggle, window, cx| {
let Some(tab_switcher) = workspace.active_modal::<Self>(cx) else {
Self::open(workspace, action.select_last, false, false, window, cx);
return;
};
tab_switcher.update(cx, |tab_switcher, cx| {
tab_switcher
.picker
.update(cx, |picker, cx| picker.cycle_selection(window, cx))
});
});
workspace.register_action(|workspace, _action: &ToggleAll, window, cx| {
let Some(tab_switcher) = workspace.active_modal::<Self>(cx) else {
Self::open(workspace, false, true, false, window, cx);
return;
};
tab_switcher.update(cx, |tab_switcher, cx| {
tab_switcher
.picker
.update(cx, |picker, cx| picker.cycle_selection(window, cx))
});
});
workspace.register_action(|workspace, _action: &OpenInActivePane, window, cx| {
let Some(tab_switcher) = workspace.active_modal::<Self>(cx) else {
Self::open(workspace, false, true, true, window, cx);
return;
};
tab_switcher.update(cx, |tab_switcher, cx| {
tab_switcher
.picker
.update(cx, |picker, cx| picker.cycle_selection(window, cx))
});
});
}
fn open(
workspace: &mut Workspace,
select_last: bool,
is_global: bool,
open_in_active_pane: bool,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let mut weak_pane = workspace.active_pane().downgrade();
for dock in [
workspace.left_dock(),
workspace.bottom_dock(),
workspace.right_dock(),
] {
dock.update(cx, |this, cx| {
let Some(panel) = this
.active_panel()
.filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx))
else {
return;
};
if let Some(pane) = panel.pane(cx) {
weak_pane = pane.downgrade();
}
})
}
let weak_workspace = workspace.weak_handle();
let project = workspace.project().clone();
let original_items: Vec<_> = workspace
.panes()
.iter()
.map(|p| (p.clone(), p.read(cx).active_item_index()))
.collect();
workspace.toggle_modal(window, cx, |window, cx| {
let delegate = TabSwitcherDelegate::new(
project,
select_last,
cx.entity().downgrade(),
weak_pane,
weak_workspace,
is_global,
open_in_active_pane,
window,
cx,
original_items,
);
TabSwitcher::new(delegate, window, is_global, cx)
});
}
fn new(
delegate: TabSwitcherDelegate,
window: &mut Window,
is_global: bool,
cx: &mut Context<Self>,
) -> Self {
let init_modifiers = if is_global {
None
} else {
window.modifiers().modified().then_some(window.modifiers())
};
let has_modifiers = init_modifiers.is_some();
let _show_task = has_modifiers.then(|| {
cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(POPOVER_DELAY).await;
this.update_in(cx, |this, _window, cx| {
this.visible = true;
cx.notify();
})
.ok();
})
});
Self {
picker: cx.new(|cx| {
if is_global {
Picker::list(delegate, window, cx)
} else {
Picker::nonsearchable_list(delegate, window, cx)
}
.initial_width(rems(PANEL_WIDTH_REMS))
}),
init_modifiers,
visible: !has_modifiers,
_show_task,
}
}
fn handle_modifiers_changed(
&mut self,
event: &ModifiersChangedEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(init_modifiers) = self.init_modifiers else {
return;
};
if !event.modified() || !init_modifiers.is_subset_of(event) {
self.init_modifiers = None;
if self.picker.read(cx).delegate.matches.is_empty() {
cx.emit(DismissEvent)
} else if self.visible {
window.dispatch_action(menu::Confirm.boxed_clone(), cx);
} else {
self.picker.update(cx, |picker, cx| {
picker.delegate.confirm(false, window, cx);
});
}
}
}
fn handle_close_selected_item(
&mut self,
_: &CloseSelectedItem,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.picker.update(cx, |picker, cx| {
picker
.delegate
.close_item_at(picker.delegate.selected_index(), window, cx)
});
}
}
impl EventEmitter<DismissEvent> for TabSwitcher {}
impl Focusable for TabSwitcher {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for TabSwitcher {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let picker = self.picker.clone();
v_flex()
.key_context("TabSwitcher")
.w(rems(PANEL_WIDTH_REMS))
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.on_action(cx.listener(Self::handle_close_selected_item))
.when(self.visible, |el| el.child(picker.clone()))
.when(!self.visible, |el| {
el.child(div().size_0().overflow_hidden().child(picker.clone()))
})
}
}
#[derive(Clone)]
struct TabMatch {
pane: WeakEntity<Pane>,
item_index: usize,
item: Box<dyn ItemHandle>,
detail: usize,
preview: bool,
}
pub struct TabSwitcherDelegate {
select_last: bool,
tab_switcher: WeakEntity<TabSwitcher>,
selected_index: usize,
pane: WeakEntity<Pane>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
matches: Vec<TabMatch>,
original_items: Vec<(Entity<Pane>, usize)>,
is_all_panes: bool,
open_in_active_pane: bool,
restored_items: bool,
}
impl TabMatch {
fn icon(
&self,
project: &Entity<Project>,
selected: bool,
window: &Window,
cx: &App,
) -> Option<DecoratedIcon> {
let icon = self.item.tab_icon(window, cx)?;
let item_settings = ItemSettings::get_global(cx);
let show_diagnostics = item_settings.show_diagnostics;
let git_status_color = item_settings
.git_status
.then(|| {
let path = self.item.project_path(cx)?;
let project = project.read(cx);
let entry = project.entry_for_path(&path, cx)?;
let git_status = project
.git_store()
.read(cx)
.display_status_for_project_path(&path, cx)
.map(|status| status.summary())
.unwrap_or_default();
Some(entry_git_aware_label_color(
git_status,
entry.is_ignored,
selected,
))
})
.flatten();
let colored_icon = icon.color(git_status_color.unwrap_or_default());
let most_severe_diagnostic_level = if show_diagnostics == ShowDiagnostics::Off {
None
} else {
let buffer_store = project.read(cx).buffer_store().read(cx);
let buffer = self
.item
.project_path(cx)
.and_then(|path| buffer_store.get_by_path(&path))
.map(|buffer| buffer.read(cx));
buffer.and_then(|buffer| {
buffer
.buffer_diagnostics(None)
.iter()
.map(|diagnostic_entry| diagnostic_entry.diagnostic.severity)
.min()
})
};
let decorations =
entry_diagnostic_aware_icon_decoration_and_color(most_severe_diagnostic_level)
.filter(|(d, _)| {
*d != IconDecorationKind::Triangle
|| show_diagnostics != ShowDiagnostics::Errors
})
.map(|(icon, color)| {
let knockout_item_color = if selected {
cx.theme().colors().element_selected
} else {
cx.theme().colors().element_background
};
IconDecoration::new(icon, knockout_item_color, cx)
.color(color.color(cx))
.position(Point {
x: px(-2.),
y: px(-2.),
})
});
Some(DecoratedIcon::new(colored_icon, decorations))
}
}
impl TabSwitcherDelegate {
#[allow(clippy::complexity)]
fn new(
project: Entity<Project>,
select_last: bool,
tab_switcher: WeakEntity<TabSwitcher>,
pane: WeakEntity<Pane>,
workspace: WeakEntity<Workspace>,
is_all_panes: bool,
open_in_active_pane: bool,
window: &mut Window,
cx: &mut Context<TabSwitcher>,
original_items: Vec<(Entity<Pane>, usize)>,
) -> Self {
Self::subscribe_to_updates(&workspace, window, cx);
Self {
select_last,
tab_switcher,
selected_index: 0,
pane,
workspace,
project,
matches: Vec::new(),
is_all_panes,
open_in_active_pane,
original_items,
restored_items: false,
}
}
fn subscribe_to_updates(
workspace: &WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<TabSwitcher>,
) {
let Some(workspace) = workspace.upgrade() else {
return;
};
cx.subscribe_in(&workspace, window, |tab_switcher, _, event, window, cx| {
match event {
WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::PaneRemoved => {
tab_switcher.picker.update(cx, |picker, cx| {
let query = picker.query(cx);
picker.delegate.update_matches(query, window, cx);
cx.notify();
})
}
WorkspaceEvent::ItemRemoved { .. } => {
tab_switcher.picker.update(cx, |picker, cx| {
let query = picker.query(cx);
picker.delegate.update_matches(query, window, cx);
// When the Tab Switcher is being used and an item is
// removed, there's a chance that the new selected index
// will not match the actual tab that is now being displayed
// by the pane, as such, the selected index needs to be
// updated to match the pane's state.
picker.delegate.sync_selected_index(cx);
cx.notify();
})
}
_ => {}
};
})
.detach();
}
fn update_all_pane_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let mut all_items = Vec::new();
let mut item_index = 0;
for pane_handle in workspace.read(cx).panes() {
let pane = pane_handle.read(cx);
let items: Vec<Box<dyn ItemHandle>> =
pane.items().map(|item| item.boxed_clone()).collect();
for ((_detail, item), detail) in items
.iter()
.enumerate()
.zip(tab_details(&items, window, cx))
{
all_items.push(TabMatch {
pane: pane_handle.downgrade(),
item_index,
item: item.clone(),
detail,
preview: pane.is_active_preview_item(item.item_id()),
});
item_index += 1;
}
}
let mut matches = if query.is_empty() {
let history = workspace.read(cx).recently_activated_items(cx);
all_items
.sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index));
all_items
} else {
let candidates = all_items
.iter()
.enumerate()
.flat_map(|(ix, tab_match)| {
Some(StringMatchCandidate::new(
ix,
&tab_match.item.tab_content_text(0, cx),
))
})
.collect::<Vec<_>>();
fuzzy_nucleo::match_strings(
&candidates,
&query,
fuzzy_nucleo::Case::Smart,
fuzzy_nucleo::LengthPenalty::On,
10000,
)
.into_iter()
.map(|m| all_items[m.candidate_id].clone())
.collect()
};
if self.open_in_active_pane {
let mut seen_paths: HashSet<project::ProjectPath> = HashSet::default();
matches.retain(|tab| {
if let Some(path) = tab.item.project_path(cx) {
seen_paths.insert(path)
} else {
true
}
});
}
let selected_item_id = self.selected_item_id();
self.matches = matches;
self.selected_index = if query.is_empty() {
self.compute_selected_index(selected_item_id, window, cx)
} else {
0
}
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
if self.is_all_panes {
// needed because we need to borrow the workspace, but that may be borrowed when the picker
// calls update_matches.
let this = cx.entity();
window.defer(cx, move |window, cx| {
this.update(cx, |this, cx| {
this.delegate.update_all_pane_matches(query, window, cx);
})
});
return;
}
let selected_item_id = self.selected_item_id();
self.matches.clear();
let Some(pane) = self.pane.upgrade() else {
return;
};
let pane = pane.read(cx);
let mut history_indices = HashMap::default();
pane.activation_history().iter().rev().enumerate().for_each(
|(history_index, history_entry)| {
history_indices.insert(history_entry.entity_id, history_index);
},
);
let items: Vec<Box<dyn ItemHandle>> = pane.items().map(|item| item.boxed_clone()).collect();
items
.iter()
.enumerate()
.zip(tab_details(&items, window, cx))
.map(|((item_index, item), detail)| TabMatch {
pane: self.pane.clone(),
item_index,
item: item.boxed_clone(),
detail,
preview: pane.is_active_preview_item(item.item_id()),
})
.for_each(|tab_match| self.matches.push(tab_match));
let non_history_base = history_indices.len();
self.matches.sort_by(move |a, b| {
let a_score = *history_indices
.get(&a.item.item_id())
.unwrap_or(&(a.item_index + non_history_base));
let b_score = *history_indices
.get(&b.item.item_id())
.unwrap_or(&(b.item_index + non_history_base));
a_score.cmp(&b_score)
});
self.selected_index = if query.is_empty() {
self.compute_selected_index(selected_item_id, window, cx)
} else {
0
}
}
fn selected_item_id(&self) -> Option<EntityId> {
self.matches
.get(self.selected_index())
.map(|tab_match| tab_match.item.item_id())
}
fn compute_selected_index(
&mut self,
prev_selected_item_id: Option<EntityId>,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> usize {
if self.matches.is_empty() {
return 0;
}
if let Some(selected_item_id) = prev_selected_item_id {
// If the previously selected item is still in the list, select its new position.
if let Some(item_index) = self
.matches
.iter()
.position(|tab_match| tab_match.item.item_id() == selected_item_id)
{
return item_index;
}
// Otherwise, try to preserve the previously selected index.
return self.selected_index.min(self.matches.len() - 1);
}
if self.select_last {
let item_index = self.matches.len() - 1;
self.set_selected_index(item_index, window, cx);
return item_index;
}
// This only runs when initially opening the picker
// Index 0 is already active, so don't preselect it for switching.
if self.matches.len() > 1 {
self.set_selected_index(1, window, cx);
return 1;
}
0
}
fn close_item_at(
&mut self,
ix: usize,
window: &mut Window,
cx: &mut Context<Picker<TabSwitcherDelegate>>,
) {
let Some(tab_match) = self.matches.get(ix) else {
return;
};
if self.open_in_active_pane
&& let Some(project_path) = tab_match.item.project_path(cx)
{
let Some(workspace) = self.workspace.upgrade() else {
return;
};
workspace.update(cx, |workspace, cx| {
workspace.close_items_with_project_path(
&project_path,
SaveIntent::Close,
true,
window,
cx,
);
});
} else {
let Some(pane) = tab_match.pane.upgrade() else {
return;
};
pane.update(cx, |pane, cx| {
pane.close_item_by_id(tab_match.item.item_id(), SaveIntent::Close, window, cx)
.detach_and_log_err(cx);
});
}
}
/// Updates the selected index to ensure it matches the pane's active item,
/// as the pane's active item can be indirectly updated and this method
/// ensures that the picker can react to those changes.
fn sync_selected_index(&mut self, cx: &mut Context<Picker<TabSwitcherDelegate>>) {
let item = if self.is_all_panes {
self.workspace
.read_with(cx, |workspace, cx| workspace.active_item(cx))
} else {
self.pane.read_with(cx, |pane, _cx| pane.active_item())
};
let Ok(Some(item)) = item else {
return;
};
let item_id = item.item_id();
let Some((index, _tab_match)) = self
.matches
.iter()
.enumerate()
.find(|(_index, tab_match)| tab_match.item.item_id() == item_id)
else {
return;
};
self.selected_index = index;
}
fn confirm_open_in_active_pane(
&mut self,
selected_match: TabMatch,
window: &mut Window,
cx: &mut Context<Picker<TabSwitcherDelegate>>,
) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let current_pane = self
.pane
.upgrade()
.filter(|pane| {
workspace
.read(cx)
.panes()
.iter()
.any(|p| p.entity_id() == pane.entity_id())
})
.or_else(|| selected_match.pane.upgrade());
let Some(current_pane) = current_pane else {
return;
};
if let Some(index) = current_pane
.read(cx)
.index_for_item(selected_match.item.as_ref())
{
current_pane.update(cx, |pane, cx| {
pane.activate_item(index, true, true, window, cx);
});
} else if selected_match.item.project_path(cx).is_some()
&& selected_match.item.can_split(cx)
{
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let database_id = workspace.read(cx).database_id();
let task = selected_match.item.clone_on_split(database_id, window, cx);
let current_pane = current_pane.downgrade();
cx.spawn_in(window, async move |_, cx| {
if let Some(clone) = task.await {
current_pane
.update_in(cx, |pane, window, cx| {
pane.add_item(clone, true, true, None, window, cx);
})
.log_err();
}
})
.detach();
} else {
let Some(source_pane) = selected_match.pane.upgrade() else {
return;
};
workspace::move_item(
&source_pane,
&current_pane,
selected_match.item.item_id(),
current_pane.read(cx).items_len(),
true,
window,
cx,
);
}
}
}
impl PickerDelegate for TabSwitcherDelegate {
type ListItem = ListItem;
fn name() -> &'static str {
"tab switcher"
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search all tabs…".into()
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
Some("No tabs".into())
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
if !self.open_in_active_pane {
let Some(selected_match) = self.matches.get(self.selected_index()) else {
return;
};
selected_match
.pane
.update(cx, |pane, cx| {
if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) {
pane.activate_item(index, false, false, window, cx);
}
})
.ok();
}
cx.notify();
}
fn separators_after_indices(&self) -> Vec<usize> {
Vec::new()
}
fn update_matches(
&mut self,
raw_query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
self.update_matches(raw_query, window, cx);
Task::ready(())
}
fn confirm(
&mut self,
_secondary: bool,
window: &mut Window,
cx: &mut Context<Picker<TabSwitcherDelegate>>,
) {
let Some(selected_match) = self.matches.get(self.selected_index()).cloned() else {
return;
};
self.restored_items = true;
for (pane, index) in self.original_items.iter() {
pane.update(cx, |this, cx| {
this.activate_item(*index, false, false, window, cx);
})
}
if self.open_in_active_pane {
self.confirm_open_in_active_pane(selected_match, window, cx);
} else {
selected_match
.pane
.update(cx, |pane, cx| {
if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) {
pane.activate_item(index, true, true, window, cx);
}
})
.ok();
}
}
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<TabSwitcherDelegate>>) {
if !self.restored_items {
for (pane, index) in self.original_items.iter() {
pane.update(cx, |this, cx| {
this.activate_item(*index, false, false, window, cx);
})
}
}
self.tab_switcher
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let tab_match = self.matches.get(ix)?;
let params = TabContentParams {
detail: Some(tab_match.detail),
selected: true,
preview: tab_match.preview,
deemphasized: false,
max_title_len: Some(usize::MAX),
truncate_title_middle: true,
};
let label = tab_match.item.tab_content(params, window, cx);
let icon = tab_match.icon(&self.project, selected, window, cx);
let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx);
let indicator_color = if let Some(ref indicator) = indicator {
indicator.color
} else {
Color::default()
};
let indicator = h_flex()
.flex_shrink_0()
.children(indicator)
.child(div().w_2())
.into_any_element();
let close_button = div()
.id("close-button")
.on_mouse_up(
// We need this on_mouse_up here because on macOS you may have ctrl held
// down to open the menu, and a ctrl-click comes through as a right click.
MouseButton::Right,
cx.listener(move |picker, _: &MouseUpEvent, window, cx| {
cx.stop_propagation();
picker.delegate.close_item_at(ix, window, cx);
}),
)
.child(
IconButton::new("close_tab", IconName::Close)
.icon_size(IconSize::Small)
.icon_color(indicator_color)
.tooltip(Tooltip::for_action_title("Close", &CloseSelectedItem))
.on_click(cx.listener(move |picker, _, window, cx| {
cx.stop_propagation();
picker.delegate.close_item_at(ix, window, cx);
})),
)
.into_any_element();
Some(
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.inset(true)
.toggle_state(selected)
.child(h_flex().w_full().min_w_0().overflow_hidden().child(label))
.start_slot::<DecoratedIcon>(icon)
.map(|el| {
if self.selected_index == ix {
el.end_slot::<AnyElement>(close_button)
} else {
el.end_slot::<AnyElement>(indicator)
.end_slot_on_hover::<AnyElement>(close_button)
}
}),
)
}
}