1use std::rc::Rc;
2
3use gpui::{
4 AnyElement, App, Context, FocusHandle, Focusable, IntoElement, KeyDownEvent, Pixels, Render,
5 Window, div, px, rgb,
6};
7
8use crate::{IconName, List, ListItem, overlay_backdrop, overlay_panel, prelude::*};
9
10pub const TAB_SWITCHER_WIDTH: Pixels = px(420.);
12
13pub struct TabSwitcherItem {
15 label: SharedString,
16 subtitle: Option<SharedString>,
17 icon: Option<IconName>,
18 on_select: Rc<dyn Fn(&mut Window, &mut App)>,
19}
20
21impl TabSwitcherItem {
22 pub fn new(
23 label: impl Into<SharedString>,
24 on_select: impl Fn(&mut Window, &mut App) + 'static,
25 ) -> Self {
26 Self {
27 label: label.into(),
28 subtitle: None,
29 icon: None,
30 on_select: Rc::new(on_select),
31 }
32 }
33
34 pub fn subtitle(mut self, subtitle: impl Into<SharedString>) -> Self {
35 self.subtitle = Some(subtitle.into());
36 self
37 }
38
39 pub fn icon(mut self, icon: IconName) -> Self {
40 self.icon = Some(icon);
41 self
42 }
43}
44
45pub struct TabSwitcher {
59 items: Vec<TabSwitcherItem>,
60 selected_ix: usize,
61 focus_handle: FocusHandle,
62 on_dismiss: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
63}
64
65impl TabSwitcher {
66 pub fn new(cx: &mut Context<Self>, items: Vec<TabSwitcherItem>) -> Self {
67 Self {
68 items,
69 selected_ix: 0,
70 focus_handle: cx.focus_handle(),
71 on_dismiss: None,
72 }
73 }
74
75 pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
78 self.on_dismiss = Some(Rc::new(handler));
79 self
80 }
81
82 pub fn focus(&self, window: &mut Window, cx: &mut App) {
85 window.focus(&self.focus_handle, cx);
86 }
87
88 fn select_next(&mut self, cx: &mut Context<Self>) {
89 if self.items.is_empty() {
90 return;
91 }
92 self.selected_ix = (self.selected_ix + 1) % self.items.len();
93 cx.notify();
94 }
95
96 fn select_previous(&mut self, cx: &mut Context<Self>) {
97 if self.items.is_empty() {
98 return;
99 }
100 self.selected_ix = if self.selected_ix == 0 {
101 self.items.len() - 1
102 } else {
103 self.selected_ix - 1
104 };
105 cx.notify();
106 }
107
108 fn handle_key_down(
109 &mut self,
110 event: &KeyDownEvent,
111 window: &mut Window,
112 cx: &mut Context<Self>,
113 ) {
114 let shift = event.keystroke.modifiers.shift;
115 match event.keystroke.key.as_str() {
116 "down" => self.select_next(cx),
117 "tab" if !shift => self.select_next(cx),
118 "up" => self.select_previous(cx),
119 "tab" if shift => self.select_previous(cx),
120 "enter" => {
121 if let Some(item) = self.items.get(self.selected_ix) {
122 let on_select = item.on_select.clone();
123 on_select(window, cx);
124 }
125 }
126 "escape" => {
127 if let Some(on_dismiss) = self.on_dismiss.clone() {
128 on_dismiss(window, cx);
129 }
130 }
131 _ => {}
132 }
133 }
134
135 fn render_row(&self, item_ix: usize, is_selected: bool) -> AnyElement {
136 let item = &self.items[item_ix];
137 let on_select = item.on_select.clone();
138
139 ListItem::new(("tab-switcher-item", item_ix))
140 .focused(is_selected)
141 .when_some(item.icon, |this, icon| this.start_slot(Icon::new(icon)))
142 .child(
143 v_flex()
144 .gap_0p5()
145 .child(Label::new(item.label.clone()))
146 .when_some(item.subtitle.clone(), |this, subtitle| {
147 this.child(
148 Label::new(subtitle)
149 .size(LabelSize::Small)
150 .color(Color::Muted),
151 )
152 }),
153 )
154 .on_click(move |_, window, cx| on_select(window, cx))
155 .into_any_element()
156 }
157}
158
159impl Focusable for TabSwitcher {
160 fn focus_handle(&self, _cx: &App) -> FocusHandle {
161 self.focus_handle.clone()
162 }
163}
164
165impl Render for TabSwitcher {
166 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
167 if self.selected_ix >= self.items.len() {
168 self.selected_ix = 0;
169 }
170 let selected_ix = self.selected_ix;
171
172 let rows: Vec<AnyElement> = (0..self.items.len())
173 .map(|item_ix| self.render_row(item_ix, item_ix == selected_ix))
174 .collect();
175
176 overlay_backdrop()
177 .track_focus(&self.focus_handle)
178 .on_key_down(cx.listener(Self::handle_key_down))
179 .child(
180 overlay_panel()
181 .w(TAB_SWITCHER_WIDTH)
182 .max_h(px(360.))
183 .child(
184 div()
185 .px(px(20.))
186 .py(px(12.))
187 .border_b_1()
188 .border_color(rgb(0x2A313B))
189 .child(
190 Label::new("Switch Tab")
191 .size(LabelSize::Small)
192 .color(Color::Muted),
193 ),
194 )
195 .child(
196 div()
197 .id("tab-switcher-results")
198 .flex_1()
199 .overflow_y_scroll()
200 .child(List::new().empty_message("No open tabs").children(rows)),
201 ),
202 )
203 }
204}
205
206pub fn tab_switcher_preview(_window: &mut Window, cx: &mut App) -> AnyElement {
210 div()
214 .relative()
215 .h(px(400.))
216 .child(cx.new(|cx| {
217 TabSwitcher::new(
218 cx,
219 vec![
220 TabSwitcherItem::new("main.rs", |_, _| {}).icon(IconName::File),
221 TabSwitcherItem::new("lib.rs", |_, _| {})
222 .icon(IconName::File)
223 .subtitle("crates/ui/src"),
224 TabSwitcherItem::new("Cargo.toml", |_, _| {}).icon(IconName::File),
225 ],
226 )
227 }))
228 .into_any_element()
229}