1#[cfg(test)]
2mod tab_switcher_tests;
3
4use collections::{HashMap, HashSet};
5use editor::items::{
6 entry_diagnostic_aware_icon_decoration_and_color, entry_git_aware_label_color,
7};
8use fuzzy_nucleo::StringMatchCandidate;
9use gpui::{
10 Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle,
11 Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Point,
12 Render, Styled, Task, WeakEntity, Window, actions, rems,
13};
14use picker::{Picker, PickerDelegate};
15use project::Project;
16use schemars::JsonSchema;
17use serde::Deserialize;
18use settings::Settings;
19use std::{cmp::Reverse, sync::Arc};
20use ui::{
21 DecoratedIcon, IconDecoration, IconDecorationKind, ListItem, ListItemSpacing, Tooltip,
22 prelude::*,
23};
24use util::ResultExt;
25use workspace::{
26 Event as WorkspaceEvent, ModalView, Pane, SaveIntent, Workspace,
27 item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams},
28 pane::{render_item_indicator, tab_details},
29};
30
31const PANEL_WIDTH_REMS: f32 = 28.;
32
33/// Toggles the tab switcher interface.
34#[derive(PartialEq, Clone, Deserialize, JsonSchema, Default, Action)]
35#[action(namespace = tab_switcher)]
36#[serde(deny_unknown_fields)]
37pub struct Toggle {
38 #[serde(default)]
39 pub select_last: bool,
40}
41actions!(
42 tab_switcher,
43 [
44 /// Closes the selected item in the tab switcher.
45 CloseSelectedItem,
46 /// Toggles between showing all tabs or just the current pane's tabs.
47 ToggleAll,
48 /// Toggles the tab switcher showing all tabs across all panes, deduplicated by path.
49 /// Opens selected items in the active pane.
50 OpenInActivePane,
51 ]
52);
53
54pub struct TabSwitcher {
55 picker: Entity<Picker<TabSwitcherDelegate>>,
56 init_modifiers: Option<Modifiers>,
57}
58
59impl ModalView for TabSwitcher {}
60
61pub fn init(cx: &mut App) {
62 cx.observe_new(TabSwitcher::register).detach();
63}
64
65impl TabSwitcher {
66 fn register(
67 workspace: &mut Workspace,
68 _window: Option<&mut Window>,
69 _: &mut Context<Workspace>,
70 ) {
71 workspace.register_action(|workspace, action: &Toggle, window, cx| {
72 let Some(tab_switcher) = workspace.active_modal::<Self>(cx) else {
73 Self::open(workspace, action.select_last, false, false, window, cx);
74 return;
75 };
76
77 tab_switcher.update(cx, |tab_switcher, cx| {
78 tab_switcher
79 .picker
80 .update(cx, |picker, cx| picker.cycle_selection(window, cx))
81 });
82 });
83 workspace.register_action(|workspace, _action: &ToggleAll, window, cx| {
84 let Some(tab_switcher) = workspace.active_modal::<Self>(cx) else {
85 Self::open(workspace, false, true, false, window, cx);
86 return;
87 };
88
89 tab_switcher.update(cx, |tab_switcher, cx| {
90 tab_switcher
91 .picker
92 .update(cx, |picker, cx| picker.cycle_selection(window, cx))
93 });
94 });
95 workspace.register_action(|workspace, _action: &OpenInActivePane, window, cx| {
96 let Some(tab_switcher) = workspace.active_modal::<Self>(cx) else {
97 Self::open(workspace, false, true, true, window, cx);
98 return;
99 };
100
101 tab_switcher.update(cx, |tab_switcher, cx| {
102 tab_switcher
103 .picker
104 .update(cx, |picker, cx| picker.cycle_selection(window, cx))
105 });
106 });
107 }
108
109 fn open(
110 workspace: &mut Workspace,
111 select_last: bool,
112 is_global: bool,
113 open_in_active_pane: bool,
114 window: &mut Window,
115 cx: &mut Context<Workspace>,
116 ) {
117 let mut weak_pane = workspace.active_pane().downgrade();
118 for dock in [
119 workspace.left_dock(),
120 workspace.bottom_dock(),
121 workspace.right_dock(),
122 ] {
123 dock.update(cx, |this, cx| {
124 let Some(panel) = this
125 .active_panel()
126 .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx))
127 else {
128 return;
129 };
130 if let Some(pane) = panel.pane(cx) {
131 weak_pane = pane.downgrade();
132 }
133 })
134 }
135
136 let weak_workspace = workspace.weak_handle();
137
138 let project = workspace.project().clone();
139 let original_items: Vec<_> = workspace
140 .panes()
141 .iter()
142 .map(|p| (p.clone(), p.read(cx).active_item_index()))
143 .collect();
144 workspace.toggle_modal(window, cx, |window, cx| {
145 let delegate = TabSwitcherDelegate::new(
146 project,
147 select_last,
148 cx.entity().downgrade(),
149 weak_pane,
150 weak_workspace,
151 is_global,
152 open_in_active_pane,
153 window,
154 cx,
155 original_items,
156 );
157 TabSwitcher::new(delegate, window, is_global, cx)
158 });
159 }
160
161 fn new(
162 delegate: TabSwitcherDelegate,
163 window: &mut Window,
164 is_global: bool,
165 cx: &mut Context<Self>,
166 ) -> Self {
167 let init_modifiers = if is_global {
168 None
169 } else {
170 window.modifiers().modified().then_some(window.modifiers())
171 };
172 Self {
173 picker: cx.new(|cx| {
174 if is_global {
175 Picker::list(delegate, window, cx)
176 } else {
177 Picker::nonsearchable_list(delegate, window, cx)
178 }
179 }),
180 init_modifiers,
181 }
182 }
183
184 fn handle_modifiers_changed(
185 &mut self,
186 event: &ModifiersChangedEvent,
187 window: &mut Window,
188 cx: &mut Context<Self>,
189 ) {
190 let Some(init_modifiers) = self.init_modifiers else {
191 return;
192 };
193 if !event.modified() || !init_modifiers.is_subset_of(event) {
194 self.init_modifiers = None;
195 if self.picker.read(cx).delegate.matches.is_empty() {
196 cx.emit(DismissEvent)
197 } else {
198 window.dispatch_action(menu::Confirm.boxed_clone(), cx);
199 }
200 }
201 }
202
203 fn handle_close_selected_item(
204 &mut self,
205 _: &CloseSelectedItem,
206 window: &mut Window,
207 cx: &mut Context<Self>,
208 ) {
209 self.picker.update(cx, |picker, cx| {
210 picker
211 .delegate
212 .close_item_at(picker.delegate.selected_index(), window, cx)
213 });
214 }
215}
216
217impl EventEmitter<DismissEvent> for TabSwitcher {}
218
219impl Focusable for TabSwitcher {
220 fn focus_handle(&self, cx: &App) -> FocusHandle {
221 self.picker.focus_handle(cx)
222 }
223}
224
225impl Render for TabSwitcher {
226 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
227 v_flex()
228 .key_context("TabSwitcher")
229 .w(rems(PANEL_WIDTH_REMS))
230 .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
231 .on_action(cx.listener(Self::handle_close_selected_item))
232 .child(self.picker.clone())
233 }
234}
235
236#[derive(Clone)]
237struct TabMatch {
238 pane: WeakEntity<Pane>,
239 item_index: usize,
240 item: Box<dyn ItemHandle>,
241 detail: usize,
242 preview: bool,
243}
244
245pub struct TabSwitcherDelegate {
246 select_last: bool,
247 tab_switcher: WeakEntity<TabSwitcher>,
248 selected_index: usize,
249 pane: WeakEntity<Pane>,
250 workspace: WeakEntity<Workspace>,
251 project: Entity<Project>,
252 matches: Vec<TabMatch>,
253 original_items: Vec<(Entity<Pane>, usize)>,
254 is_all_panes: bool,
255 open_in_active_pane: bool,
256 restored_items: bool,
257}
258
259impl TabMatch {
260 fn icon(
261 &self,
262 project: &Entity<Project>,
263 selected: bool,
264 window: &Window,
265 cx: &App,
266 ) -> Option<DecoratedIcon> {
267 let icon = self.item.tab_icon(window, cx)?;
268 let item_settings = ItemSettings::get_global(cx);
269 let show_diagnostics = item_settings.show_diagnostics;
270 let git_status_color = item_settings
271 .git_status
272 .then(|| {
273 let path = self.item.project_path(cx)?;
274 let project = project.read(cx);
275 let entry = project.entry_for_path(&path, cx)?;
276 let git_status = project
277 .project_path_git_status(&path, cx)
278 .map(|status| status.summary())
279 .unwrap_or_default();
280 Some(entry_git_aware_label_color(
281 git_status,
282 entry.is_ignored,
283 selected,
284 ))
285 })
286 .flatten();
287 let colored_icon = icon.color(git_status_color.unwrap_or_default());
288
289 let most_severe_diagnostic_level = if show_diagnostics == ShowDiagnostics::Off {
290 None
291 } else {
292 let buffer_store = project.read(cx).buffer_store().read(cx);
293 let buffer = self
294 .item
295 .project_path(cx)
296 .and_then(|path| buffer_store.get_by_path(&path))
297 .map(|buffer| buffer.read(cx));
298 buffer.and_then(|buffer| {
299 buffer
300 .buffer_diagnostics(None)
301 .iter()
302 .map(|diagnostic_entry| diagnostic_entry.diagnostic.severity)
303 .min()
304 })
305 };
306
307 let decorations =
308 entry_diagnostic_aware_icon_decoration_and_color(most_severe_diagnostic_level)
309 .filter(|(d, _)| {
310 *d != IconDecorationKind::Triangle
311 || show_diagnostics != ShowDiagnostics::Errors
312 })
313 .map(|(icon, color)| {
314 let knockout_item_color = if selected {
315 cx.theme().colors().element_selected
316 } else {
317 cx.theme().colors().element_background
318 };
319 IconDecoration::new(icon, knockout_item_color, cx)
320 .color(color.color(cx))
321 .position(Point {
322 x: px(-2.),
323 y: px(-2.),
324 })
325 });
326 Some(DecoratedIcon::new(colored_icon, decorations))
327 }
328}
329
330impl TabSwitcherDelegate {
331 #[allow(clippy::complexity)]
332 fn new(
333 project: Entity<Project>,
334 select_last: bool,
335 tab_switcher: WeakEntity<TabSwitcher>,
336 pane: WeakEntity<Pane>,
337 workspace: WeakEntity<Workspace>,
338 is_all_panes: bool,
339 open_in_active_pane: bool,
340 window: &mut Window,
341 cx: &mut Context<TabSwitcher>,
342 original_items: Vec<(Entity<Pane>, usize)>,
343 ) -> Self {
344 Self::subscribe_to_updates(&workspace, window, cx);
345 Self {
346 select_last,
347 tab_switcher,
348 selected_index: 0,
349 pane,
350 workspace,
351 project,
352 matches: Vec::new(),
353 is_all_panes,
354 open_in_active_pane,
355 original_items,
356 restored_items: false,
357 }
358 }
359
360 fn subscribe_to_updates(
361 workspace: &WeakEntity<Workspace>,
362 window: &mut Window,
363 cx: &mut Context<TabSwitcher>,
364 ) {
365 let Some(workspace) = workspace.upgrade() else {
366 return;
367 };
368 cx.subscribe_in(&workspace, window, |tab_switcher, _, event, window, cx| {
369 match event {
370 WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::PaneRemoved => {
371 tab_switcher.picker.update(cx, |picker, cx| {
372 let query = picker.query(cx);
373 picker.delegate.update_matches(query, window, cx);
374 cx.notify();
375 })
376 }
377 WorkspaceEvent::ItemRemoved { .. } => {
378 tab_switcher.picker.update(cx, |picker, cx| {
379 let query = picker.query(cx);
380 picker.delegate.update_matches(query, window, cx);
381
382 // When the Tab Switcher is being used and an item is
383 // removed, there's a chance that the new selected index
384 // will not match the actual tab that is now being displayed
385 // by the pane, as such, the selected index needs to be
386 // updated to match the pane's state.
387 picker.delegate.sync_selected_index(cx);
388 cx.notify();
389 })
390 }
391 _ => {}
392 };
393 })
394 .detach();
395 }
396
397 fn update_all_pane_matches(
398 &mut self,
399 query: String,
400 window: &mut Window,
401 cx: &mut Context<Picker<Self>>,
402 ) {
403 let Some(workspace) = self.workspace.upgrade() else {
404 return;
405 };
406 let mut all_items = Vec::new();
407 let mut item_index = 0;
408 for pane_handle in workspace.read(cx).panes() {
409 let pane = pane_handle.read(cx);
410 let items: Vec<Box<dyn ItemHandle>> =
411 pane.items().map(|item| item.boxed_clone()).collect();
412 for ((_detail, item), detail) in items
413 .iter()
414 .enumerate()
415 .zip(tab_details(&items, window, cx))
416 {
417 all_items.push(TabMatch {
418 pane: pane_handle.downgrade(),
419 item_index,
420 item: item.clone(),
421 detail,
422 preview: pane.is_active_preview_item(item.item_id()),
423 });
424 item_index += 1;
425 }
426 }
427
428 let mut matches = if query.is_empty() {
429 let history = workspace.read(cx).recently_activated_items(cx);
430 all_items
431 .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index));
432 all_items
433 } else {
434 let candidates = all_items
435 .iter()
436 .enumerate()
437 .flat_map(|(ix, tab_match)| {
438 Some(StringMatchCandidate::new(
439 ix,
440 &tab_match.item.tab_content_text(0, cx),
441 ))
442 })
443 .collect::<Vec<_>>();
444 fuzzy_nucleo::match_strings(
445 &candidates,
446 &query,
447 fuzzy_nucleo::Case::Smart,
448 fuzzy_nucleo::LengthPenalty::On,
449 10000,
450 )
451 .into_iter()
452 .map(|m| all_items[m.candidate_id].clone())
453 .collect()
454 };
455
456 if self.open_in_active_pane {
457 let mut seen_paths: HashSet<project::ProjectPath> = HashSet::default();
458 matches.retain(|tab| {
459 if let Some(path) = tab.item.project_path(cx) {
460 seen_paths.insert(path)
461 } else {
462 true
463 }
464 });
465 }
466
467 let selected_item_id = self.selected_item_id();
468 self.matches = matches;
469 self.selected_index = self.compute_selected_index(selected_item_id, window, cx);
470 }
471
472 fn update_matches(
473 &mut self,
474 query: String,
475 window: &mut Window,
476 cx: &mut Context<Picker<Self>>,
477 ) {
478 if self.is_all_panes {
479 // needed because we need to borrow the workspace, but that may be borrowed when the picker
480 // calls update_matches.
481 let this = cx.entity();
482 window.defer(cx, move |window, cx| {
483 this.update(cx, |this, cx| {
484 this.delegate.update_all_pane_matches(query, window, cx);
485 })
486 });
487 return;
488 }
489 let selected_item_id = self.selected_item_id();
490 self.matches.clear();
491 let Some(pane) = self.pane.upgrade() else {
492 return;
493 };
494
495 let pane = pane.read(cx);
496 let mut history_indices = HashMap::default();
497 pane.activation_history().iter().rev().enumerate().for_each(
498 |(history_index, history_entry)| {
499 history_indices.insert(history_entry.entity_id, history_index);
500 },
501 );
502
503 let items: Vec<Box<dyn ItemHandle>> = pane.items().map(|item| item.boxed_clone()).collect();
504 items
505 .iter()
506 .enumerate()
507 .zip(tab_details(&items, window, cx))
508 .map(|((item_index, item), detail)| TabMatch {
509 pane: self.pane.clone(),
510 item_index,
511 item: item.boxed_clone(),
512 detail,
513 preview: pane.is_active_preview_item(item.item_id()),
514 })
515 .for_each(|tab_match| self.matches.push(tab_match));
516
517 let non_history_base = history_indices.len();
518 self.matches.sort_by(move |a, b| {
519 let a_score = *history_indices
520 .get(&a.item.item_id())
521 .unwrap_or(&(a.item_index + non_history_base));
522 let b_score = *history_indices
523 .get(&b.item.item_id())
524 .unwrap_or(&(b.item_index + non_history_base));
525 a_score.cmp(&b_score)
526 });
527
528 self.selected_index = self.compute_selected_index(selected_item_id, window, cx);
529 }
530
531 fn selected_item_id(&self) -> Option<EntityId> {
532 self.matches
533 .get(self.selected_index())
534 .map(|tab_match| tab_match.item.item_id())
535 }
536
537 fn compute_selected_index(
538 &mut self,
539 prev_selected_item_id: Option<EntityId>,
540 window: &mut Window,
541 cx: &mut Context<Picker<Self>>,
542 ) -> usize {
543 if self.matches.is_empty() {
544 return 0;
545 }
546
547 if let Some(selected_item_id) = prev_selected_item_id {
548 // If the previously selected item is still in the list, select its new position.
549 if let Some(item_index) = self
550 .matches
551 .iter()
552 .position(|tab_match| tab_match.item.item_id() == selected_item_id)
553 {
554 return item_index;
555 }
556 // Otherwise, try to preserve the previously selected index.
557 return self.selected_index.min(self.matches.len() - 1);
558 }
559
560 if self.select_last {
561 let item_index = self.matches.len() - 1;
562 self.set_selected_index(item_index, window, cx);
563 return item_index;
564 }
565
566 // This only runs when initially opening the picker
567 // Index 0 is already active, so don't preselect it for switching.
568 if self.matches.len() > 1 {
569 self.set_selected_index(1, window, cx);
570 return 1;
571 }
572
573 0
574 }
575
576 fn close_item_at(
577 &mut self,
578 ix: usize,
579 window: &mut Window,
580 cx: &mut Context<Picker<TabSwitcherDelegate>>,
581 ) {
582 let Some(tab_match) = self.matches.get(ix) else {
583 return;
584 };
585
586 if self.open_in_active_pane
587 && let Some(project_path) = tab_match.item.project_path(cx)
588 {
589 let Some(workspace) = self.workspace.upgrade() else {
590 return;
591 };
592 workspace.update(cx, |workspace, cx| {
593 workspace.close_items_with_project_path(
594 &project_path,
595 SaveIntent::Close,
596 true,
597 window,
598 cx,
599 );
600 });
601 } else {
602 let Some(pane) = tab_match.pane.upgrade() else {
603 return;
604 };
605 pane.update(cx, |pane, cx| {
606 pane.close_item_by_id(tab_match.item.item_id(), SaveIntent::Close, window, cx)
607 .detach_and_log_err(cx);
608 });
609 }
610 }
611
612 /// Updates the selected index to ensure it matches the pane's active item,
613 /// as the pane's active item can be indirectly updated and this method
614 /// ensures that the picker can react to those changes.
615 fn sync_selected_index(&mut self, cx: &mut Context<Picker<TabSwitcherDelegate>>) {
616 let item = if self.is_all_panes {
617 self.workspace
618 .read_with(cx, |workspace, cx| workspace.active_item(cx))
619 } else {
620 self.pane.read_with(cx, |pane, _cx| pane.active_item())
621 };
622
623 let Ok(Some(item)) = item else {
624 return;
625 };
626
627 let item_id = item.item_id();
628 let Some((index, _tab_match)) = self
629 .matches
630 .iter()
631 .enumerate()
632 .find(|(_index, tab_match)| tab_match.item.item_id() == item_id)
633 else {
634 return;
635 };
636
637 self.selected_index = index;
638 }
639
640 fn confirm_open_in_active_pane(
641 &mut self,
642 selected_match: TabMatch,
643 window: &mut Window,
644 cx: &mut Context<Picker<TabSwitcherDelegate>>,
645 ) {
646 let Some(workspace) = self.workspace.upgrade() else {
647 return;
648 };
649
650 let current_pane = self
651 .pane
652 .upgrade()
653 .filter(|pane| {
654 workspace
655 .read(cx)
656 .panes()
657 .iter()
658 .any(|p| p.entity_id() == pane.entity_id())
659 })
660 .or_else(|| selected_match.pane.upgrade());
661
662 let Some(current_pane) = current_pane else {
663 return;
664 };
665
666 if let Some(index) = current_pane
667 .read(cx)
668 .index_for_item(selected_match.item.as_ref())
669 {
670 current_pane.update(cx, |pane, cx| {
671 pane.activate_item(index, true, true, window, cx);
672 });
673 } else if selected_match.item.project_path(cx).is_some()
674 && selected_match.item.can_split(cx)
675 {
676 let Some(workspace) = self.workspace.upgrade() else {
677 return;
678 };
679 let database_id = workspace.read(cx).database_id();
680 let task = selected_match.item.clone_on_split(database_id, window, cx);
681 let current_pane = current_pane.downgrade();
682 cx.spawn_in(window, async move |_, cx| {
683 if let Some(clone) = task.await {
684 current_pane
685 .update_in(cx, |pane, window, cx| {
686 pane.add_item(clone, true, true, None, window, cx);
687 })
688 .log_err();
689 }
690 })
691 .detach();
692 } else {
693 let Some(source_pane) = selected_match.pane.upgrade() else {
694 return;
695 };
696 workspace::move_item(
697 &source_pane,
698 ¤t_pane,
699 selected_match.item.item_id(),
700 current_pane.read(cx).items_len(),
701 true,
702 window,
703 cx,
704 );
705 }
706 }
707}
708
709impl PickerDelegate for TabSwitcherDelegate {
710 type ListItem = ListItem;
711
712 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
713 "Search all tabs…".into()
714 }
715
716 fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
717 Some("No tabs".into())
718 }
719
720 fn match_count(&self) -> usize {
721 self.matches.len()
722 }
723
724 fn selected_index(&self) -> usize {
725 self.selected_index
726 }
727
728 fn set_selected_index(
729 &mut self,
730 ix: usize,
731 window: &mut Window,
732 cx: &mut Context<Picker<Self>>,
733 ) {
734 self.selected_index = ix;
735
736 if !self.open_in_active_pane {
737 let Some(selected_match) = self.matches.get(self.selected_index()) else {
738 return;
739 };
740 selected_match
741 .pane
742 .update(cx, |pane, cx| {
743 if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) {
744 pane.activate_item(index, false, false, window, cx);
745 }
746 })
747 .ok();
748 }
749 cx.notify();
750 }
751
752 fn separators_after_indices(&self) -> Vec<usize> {
753 Vec::new()
754 }
755
756 fn update_matches(
757 &mut self,
758 raw_query: String,
759 window: &mut Window,
760 cx: &mut Context<Picker<Self>>,
761 ) -> Task<()> {
762 self.update_matches(raw_query, window, cx);
763 Task::ready(())
764 }
765
766 fn confirm(
767 &mut self,
768 _secondary: bool,
769 window: &mut Window,
770 cx: &mut Context<Picker<TabSwitcherDelegate>>,
771 ) {
772 let Some(selected_match) = self.matches.get(self.selected_index()).cloned() else {
773 return;
774 };
775
776 self.restored_items = true;
777 for (pane, index) in self.original_items.iter() {
778 pane.update(cx, |this, cx| {
779 this.activate_item(*index, false, false, window, cx);
780 })
781 }
782
783 if self.open_in_active_pane {
784 self.confirm_open_in_active_pane(selected_match, window, cx);
785 } else {
786 selected_match
787 .pane
788 .update(cx, |pane, cx| {
789 if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) {
790 pane.activate_item(index, true, true, window, cx);
791 }
792 })
793 .ok();
794 }
795 }
796
797 fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<TabSwitcherDelegate>>) {
798 if !self.restored_items {
799 for (pane, index) in self.original_items.iter() {
800 pane.update(cx, |this, cx| {
801 this.activate_item(*index, false, false, window, cx);
802 })
803 }
804 }
805
806 self.tab_switcher
807 .update(cx, |_, cx| cx.emit(DismissEvent))
808 .log_err();
809 }
810
811 fn render_match(
812 &self,
813 ix: usize,
814 selected: bool,
815 window: &mut Window,
816 cx: &mut Context<Picker<Self>>,
817 ) -> Option<Self::ListItem> {
818 let tab_match = self.matches.get(ix)?;
819
820 let params = TabContentParams {
821 detail: Some(tab_match.detail),
822 selected: true,
823 preview: tab_match.preview,
824 deemphasized: false,
825 };
826 let label = tab_match.item.tab_content(params, window, cx);
827
828 let icon = tab_match.icon(&self.project, selected, window, cx);
829
830 let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx);
831 let indicator_color = if let Some(ref indicator) = indicator {
832 indicator.color
833 } else {
834 Color::default()
835 };
836 let indicator = h_flex()
837 .flex_shrink_0()
838 .children(indicator)
839 .child(div().w_2())
840 .into_any_element();
841 let close_button = div()
842 .id("close-button")
843 .on_mouse_up(
844 // We need this on_mouse_up here because on macOS you may have ctrl held
845 // down to open the menu, and a ctrl-click comes through as a right click.
846 MouseButton::Right,
847 cx.listener(move |picker, _: &MouseUpEvent, window, cx| {
848 cx.stop_propagation();
849 picker.delegate.close_item_at(ix, window, cx);
850 }),
851 )
852 .child(
853 IconButton::new("close_tab", IconName::Close)
854 .icon_size(IconSize::Small)
855 .icon_color(indicator_color)
856 .tooltip(Tooltip::for_action_title("Close", &CloseSelectedItem))
857 .on_click(cx.listener(move |picker, _, window, cx| {
858 cx.stop_propagation();
859 picker.delegate.close_item_at(ix, window, cx);
860 })),
861 )
862 .into_any_element();
863
864 Some(
865 ListItem::new(ix)
866 .spacing(ListItemSpacing::Sparse)
867 .inset(true)
868 .toggle_state(selected)
869 .child(h_flex().w_full().child(label))
870 .start_slot::<DecoratedIcon>(icon)
871 .map(|el| {
872 if self.selected_index == ix {
873 el.end_slot::<AnyElement>(close_button)
874 } else {
875 el.end_slot::<AnyElement>(indicator)
876 .end_slot_on_hover::<AnyElement>(close_button)
877 }
878 }),
879 )
880 }
881}