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::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 smol::block_on(fuzzy::match_strings(
445 &candidates,
446 &query,
447 true,
448 true,
449 10000,
450 &Default::default(),
451 cx.background_executor().clone(),
452 ))
453 .into_iter()
454 .map(|m| all_items[m.candidate_id].clone())
455 .collect()
456 };
457
458 if self.open_in_active_pane {
459 let mut seen_paths: HashSet<project::ProjectPath> = HashSet::default();
460 matches.retain(|tab| {
461 if let Some(path) = tab.item.project_path(cx) {
462 seen_paths.insert(path)
463 } else {
464 true
465 }
466 });
467 }
468
469 let selected_item_id = self.selected_item_id();
470 self.matches = matches;
471 self.selected_index = self.compute_selected_index(selected_item_id, window, cx);
472 }
473
474 fn update_matches(
475 &mut self,
476 query: String,
477 window: &mut Window,
478 cx: &mut Context<Picker<Self>>,
479 ) {
480 if self.is_all_panes {
481 // needed because we need to borrow the workspace, but that may be borrowed when the picker
482 // calls update_matches.
483 let this = cx.entity();
484 window.defer(cx, move |window, cx| {
485 this.update(cx, |this, cx| {
486 this.delegate.update_all_pane_matches(query, window, cx);
487 })
488 });
489 return;
490 }
491 let selected_item_id = self.selected_item_id();
492 self.matches.clear();
493 let Some(pane) = self.pane.upgrade() else {
494 return;
495 };
496
497 let pane = pane.read(cx);
498 let mut history_indices = HashMap::default();
499 pane.activation_history().iter().rev().enumerate().for_each(
500 |(history_index, history_entry)| {
501 history_indices.insert(history_entry.entity_id, history_index);
502 },
503 );
504
505 let items: Vec<Box<dyn ItemHandle>> = pane.items().map(|item| item.boxed_clone()).collect();
506 items
507 .iter()
508 .enumerate()
509 .zip(tab_details(&items, window, cx))
510 .map(|((item_index, item), detail)| TabMatch {
511 pane: self.pane.clone(),
512 item_index,
513 item: item.boxed_clone(),
514 detail,
515 preview: pane.is_active_preview_item(item.item_id()),
516 })
517 .for_each(|tab_match| self.matches.push(tab_match));
518
519 let non_history_base = history_indices.len();
520 self.matches.sort_by(move |a, b| {
521 let a_score = *history_indices
522 .get(&a.item.item_id())
523 .unwrap_or(&(a.item_index + non_history_base));
524 let b_score = *history_indices
525 .get(&b.item.item_id())
526 .unwrap_or(&(b.item_index + non_history_base));
527 a_score.cmp(&b_score)
528 });
529
530 self.selected_index = self.compute_selected_index(selected_item_id, window, cx);
531 }
532
533 fn selected_item_id(&self) -> Option<EntityId> {
534 self.matches
535 .get(self.selected_index())
536 .map(|tab_match| tab_match.item.item_id())
537 }
538
539 fn compute_selected_index(
540 &mut self,
541 prev_selected_item_id: Option<EntityId>,
542 window: &mut Window,
543 cx: &mut Context<Picker<Self>>,
544 ) -> usize {
545 if self.matches.is_empty() {
546 return 0;
547 }
548
549 if let Some(selected_item_id) = prev_selected_item_id {
550 // If the previously selected item is still in the list, select its new position.
551 if let Some(item_index) = self
552 .matches
553 .iter()
554 .position(|tab_match| tab_match.item.item_id() == selected_item_id)
555 {
556 return item_index;
557 }
558 // Otherwise, try to preserve the previously selected index.
559 return self.selected_index.min(self.matches.len() - 1);
560 }
561
562 if self.select_last {
563 let item_index = self.matches.len() - 1;
564 self.set_selected_index(item_index, window, cx);
565 return item_index;
566 }
567
568 // This only runs when initially opening the picker
569 // Index 0 is already active, so don't preselect it for switching.
570 if self.matches.len() > 1 {
571 self.set_selected_index(1, window, cx);
572 return 1;
573 }
574
575 0
576 }
577
578 fn close_item_at(
579 &mut self,
580 ix: usize,
581 window: &mut Window,
582 cx: &mut Context<Picker<TabSwitcherDelegate>>,
583 ) {
584 let Some(tab_match) = self.matches.get(ix) else {
585 return;
586 };
587
588 if self.open_in_active_pane
589 && let Some(project_path) = tab_match.item.project_path(cx)
590 {
591 let Some(workspace) = self.workspace.upgrade() else {
592 return;
593 };
594 workspace.update(cx, |workspace, cx| {
595 workspace.close_items_with_project_path(
596 &project_path,
597 SaveIntent::Close,
598 true,
599 window,
600 cx,
601 );
602 });
603 } else {
604 let Some(pane) = tab_match.pane.upgrade() else {
605 return;
606 };
607 pane.update(cx, |pane, cx| {
608 pane.close_item_by_id(tab_match.item.item_id(), SaveIntent::Close, window, cx)
609 .detach_and_log_err(cx);
610 });
611 }
612 }
613
614 /// Updates the selected index to ensure it matches the pane's active item,
615 /// as the pane's active item can be indirectly updated and this method
616 /// ensures that the picker can react to those changes.
617 fn sync_selected_index(&mut self, cx: &mut Context<Picker<TabSwitcherDelegate>>) {
618 let item = if self.is_all_panes {
619 self.workspace
620 .read_with(cx, |workspace, cx| workspace.active_item(cx))
621 } else {
622 self.pane.read_with(cx, |pane, _cx| pane.active_item())
623 };
624
625 let Ok(Some(item)) = item else {
626 return;
627 };
628
629 let item_id = item.item_id();
630 let Some((index, _tab_match)) = self
631 .matches
632 .iter()
633 .enumerate()
634 .find(|(_index, tab_match)| tab_match.item.item_id() == item_id)
635 else {
636 return;
637 };
638
639 self.selected_index = index;
640 }
641
642 fn confirm_open_in_active_pane(
643 &mut self,
644 selected_match: TabMatch,
645 window: &mut Window,
646 cx: &mut Context<Picker<TabSwitcherDelegate>>,
647 ) {
648 let Some(workspace) = self.workspace.upgrade() else {
649 return;
650 };
651
652 let current_pane = self
653 .pane
654 .upgrade()
655 .filter(|pane| {
656 workspace
657 .read(cx)
658 .panes()
659 .iter()
660 .any(|p| p.entity_id() == pane.entity_id())
661 })
662 .or_else(|| selected_match.pane.upgrade());
663
664 let Some(current_pane) = current_pane else {
665 return;
666 };
667
668 if let Some(index) = current_pane
669 .read(cx)
670 .index_for_item(selected_match.item.as_ref())
671 {
672 current_pane.update(cx, |pane, cx| {
673 pane.activate_item(index, true, true, window, cx);
674 });
675 } else if selected_match.item.project_path(cx).is_some()
676 && selected_match.item.can_split(cx)
677 {
678 let Some(workspace) = self.workspace.upgrade() else {
679 return;
680 };
681 let database_id = workspace.read(cx).database_id();
682 let task = selected_match.item.clone_on_split(database_id, window, cx);
683 let current_pane = current_pane.downgrade();
684 cx.spawn_in(window, async move |_, cx| {
685 if let Some(clone) = task.await {
686 current_pane
687 .update_in(cx, |pane, window, cx| {
688 pane.add_item(clone, true, true, None, window, cx);
689 })
690 .log_err();
691 }
692 })
693 .detach();
694 } else {
695 let Some(source_pane) = selected_match.pane.upgrade() else {
696 return;
697 };
698 workspace::move_item(
699 &source_pane,
700 ¤t_pane,
701 selected_match.item.item_id(),
702 current_pane.read(cx).items_len(),
703 true,
704 window,
705 cx,
706 );
707 }
708 }
709}
710
711impl PickerDelegate for TabSwitcherDelegate {
712 type ListItem = ListItem;
713
714 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
715 "Search all tabs…".into()
716 }
717
718 fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
719 Some("No tabs".into())
720 }
721
722 fn match_count(&self) -> usize {
723 self.matches.len()
724 }
725
726 fn selected_index(&self) -> usize {
727 self.selected_index
728 }
729
730 fn set_selected_index(
731 &mut self,
732 ix: usize,
733 window: &mut Window,
734 cx: &mut Context<Picker<Self>>,
735 ) {
736 self.selected_index = ix;
737
738 if !self.open_in_active_pane {
739 let Some(selected_match) = self.matches.get(self.selected_index()) else {
740 return;
741 };
742 selected_match
743 .pane
744 .update(cx, |pane, cx| {
745 if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) {
746 pane.activate_item(index, false, false, window, cx);
747 }
748 })
749 .ok();
750 }
751 cx.notify();
752 }
753
754 fn separators_after_indices(&self) -> Vec<usize> {
755 Vec::new()
756 }
757
758 fn update_matches(
759 &mut self,
760 raw_query: String,
761 window: &mut Window,
762 cx: &mut Context<Picker<Self>>,
763 ) -> Task<()> {
764 self.update_matches(raw_query, window, cx);
765 Task::ready(())
766 }
767
768 fn confirm(
769 &mut self,
770 _secondary: bool,
771 window: &mut Window,
772 cx: &mut Context<Picker<TabSwitcherDelegate>>,
773 ) {
774 let Some(selected_match) = self.matches.get(self.selected_index()).cloned() else {
775 return;
776 };
777
778 self.restored_items = true;
779 for (pane, index) in self.original_items.iter() {
780 pane.update(cx, |this, cx| {
781 this.activate_item(*index, false, false, window, cx);
782 })
783 }
784
785 if self.open_in_active_pane {
786 self.confirm_open_in_active_pane(selected_match, window, cx);
787 } else {
788 selected_match
789 .pane
790 .update(cx, |pane, cx| {
791 if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) {
792 pane.activate_item(index, true, true, window, cx);
793 }
794 })
795 .ok();
796 }
797 }
798
799 fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<TabSwitcherDelegate>>) {
800 if !self.restored_items {
801 for (pane, index) in self.original_items.iter() {
802 pane.update(cx, |this, cx| {
803 this.activate_item(*index, false, false, window, cx);
804 })
805 }
806 }
807
808 self.tab_switcher
809 .update(cx, |_, cx| cx.emit(DismissEvent))
810 .log_err();
811 }
812
813 fn render_match(
814 &self,
815 ix: usize,
816 selected: bool,
817 window: &mut Window,
818 cx: &mut Context<Picker<Self>>,
819 ) -> Option<Self::ListItem> {
820 let tab_match = self.matches.get(ix)?;
821
822 let params = TabContentParams {
823 detail: Some(tab_match.detail),
824 selected: true,
825 preview: tab_match.preview,
826 deemphasized: false,
827 };
828 let label = tab_match.item.tab_content(params, window, cx);
829
830 let icon = tab_match.icon(&self.project, selected, window, cx);
831
832 let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx);
833 let indicator_color = if let Some(ref indicator) = indicator {
834 indicator.color
835 } else {
836 Color::default()
837 };
838 let indicator = h_flex()
839 .flex_shrink_0()
840 .children(indicator)
841 .child(div().w_2())
842 .into_any_element();
843 let close_button = div()
844 .id("close-button")
845 .on_mouse_up(
846 // We need this on_mouse_up here because on macOS you may have ctrl held
847 // down to open the menu, and a ctrl-click comes through as a right click.
848 MouseButton::Right,
849 cx.listener(move |picker, _: &MouseUpEvent, window, cx| {
850 cx.stop_propagation();
851 picker.delegate.close_item_at(ix, window, cx);
852 }),
853 )
854 .child(
855 IconButton::new("close_tab", IconName::Close)
856 .icon_size(IconSize::Small)
857 .icon_color(indicator_color)
858 .tooltip(Tooltip::for_action_title("Close", &CloseSelectedItem))
859 .on_click(cx.listener(move |picker, _, window, cx| {
860 cx.stop_propagation();
861 picker.delegate.close_item_at(ix, window, cx);
862 })),
863 )
864 .into_any_element();
865
866 Some(
867 ListItem::new(ix)
868 .spacing(ListItemSpacing::Sparse)
869 .inset(true)
870 .toggle_state(selected)
871 .child(h_flex().w_full().child(label))
872 .start_slot::<DecoratedIcon>(icon)
873 .map(|el| {
874 if self.selected_index == ix {
875 el.end_slot::<AnyElement>(close_button)
876 } else {
877 el.end_slot::<AnyElement>(indicator)
878 .end_hover_slot::<AnyElement>(close_button)
879 }
880 }),
881 )
882 }
883}