1use settings::Settings;
2
3use gpui::{
4 AnyWindowHandle, Context, Hsla, InteractiveElement, MouseButton, ParentElement, ScrollHandle,
5 Styled, SystemWindowTab, SystemWindowTabController, Window, WindowId, actions, canvas, div,
6};
7
8use theme::ThemeSettings;
9use ui::{
10 Color, ContextMenu, DynamicSpacing, IconButton, IconButtonShape, IconName, IconSize, Label,
11 LabelSize, Tab, h_flex, prelude::*, right_click_menu,
12};
13use workspace::{
14 CloseWindow, ItemSettings, Workspace,
15 item::{ClosePosition, ShowCloseButton},
16};
17
18actions!(
19 window,
20 [
21 ShowNextWindowTab,
22 ShowPreviousWindowTab,
23 MergeAllWindows,
24 MoveTabToNewWindow
25 ]
26);
27
28#[derive(Clone)]
29pub struct DraggedWindowTab {
30 pub id: WindowId,
31 pub ix: usize,
32 pub handle: AnyWindowHandle,
33 pub title: String,
34 pub width: Pixels,
35 pub is_active: bool,
36 pub active_background_color: Hsla,
37 pub inactive_background_color: Hsla,
38}
39
40pub struct SystemWindowTabs {
41 tab_bar_scroll_handle: ScrollHandle,
42 measured_tab_width: Pixels,
43 last_dragged_tab: Option<DraggedWindowTab>,
44}
45
46impl SystemWindowTabs {
47 pub fn new() -> Self {
48 Self {
49 tab_bar_scroll_handle: ScrollHandle::new(),
50 measured_tab_width: px(0.),
51 last_dragged_tab: None,
52 }
53 }
54
55 pub fn init(cx: &mut App) {
56 cx.observe_new(|workspace: &mut Workspace, _, _| {
57 workspace.register_action_renderer(|div, _, window, cx| {
58 let window_id = window.window_handle().window_id();
59 let controller = cx.global::<SystemWindowTabController>();
60
61 let tab_groups = controller.tab_groups();
62 let tabs = controller.tabs(window_id);
63 let Some(tabs) = tabs else {
64 return div;
65 };
66
67 div.when(tabs.len() > 1, |div| {
68 div.on_action(move |_: &ShowNextWindowTab, window, cx| {
69 SystemWindowTabController::select_next_tab(
70 cx,
71 window.window_handle().window_id(),
72 );
73 })
74 .on_action(move |_: &ShowPreviousWindowTab, window, cx| {
75 SystemWindowTabController::select_previous_tab(
76 cx,
77 window.window_handle().window_id(),
78 );
79 })
80 .on_action(move |_: &MoveTabToNewWindow, window, cx| {
81 SystemWindowTabController::move_tab_to_new_window(
82 cx,
83 window.window_handle().window_id(),
84 );
85 window.move_tab_to_new_window();
86 })
87 })
88 .when(tab_groups.len() > 1, |div| {
89 div.on_action(move |_: &MergeAllWindows, window, cx| {
90 SystemWindowTabController::merge_all_windows(
91 cx,
92 window.window_handle().window_id(),
93 );
94 window.merge_all_windows();
95 })
96 })
97 });
98 })
99 .detach();
100 }
101
102 fn render_tab(
103 &self,
104 ix: usize,
105 item: SystemWindowTab,
106 tabs: Vec<SystemWindowTab>,
107 active_background_color: Hsla,
108 inactive_background_color: Hsla,
109 window: &mut Window,
110 cx: &mut Context<Self>,
111 ) -> impl IntoElement + use<> {
112 let entity = cx.entity();
113 let settings = ItemSettings::get_global(cx);
114 let close_side = &settings.close_position;
115 let show_close_button = &settings.show_close_button;
116
117 let rem_size = window.rem_size();
118 let width = self.measured_tab_width.max(rem_size * 10);
119 let is_active = window.window_handle().window_id() == item.id;
120 let title = item.title.to_string();
121
122 let label = Label::new(&title)
123 .size(LabelSize::Small)
124 .truncate()
125 .color(if is_active {
126 Color::Default
127 } else {
128 Color::Muted
129 });
130
131 let tab = h_flex()
132 .id(ix)
133 .group("tab")
134 .w_full()
135 .overflow_hidden()
136 .h(Tab::content_height(cx))
137 .relative()
138 .px(DynamicSpacing::Base16.px(cx))
139 .justify_center()
140 .border_l_1()
141 .border_color(cx.theme().colors().border)
142 .cursor_pointer()
143 .on_drag(
144 DraggedWindowTab {
145 id: item.id,
146 ix,
147 handle: item.handle,
148 title: item.title.to_string(),
149 width,
150 is_active,
151 active_background_color,
152 inactive_background_color,
153 },
154 move |tab, _, _, cx| {
155 entity.update(cx, |this, _cx| {
156 this.last_dragged_tab = Some(tab.clone());
157 });
158 cx.new(|_| tab.clone())
159 },
160 )
161 .drag_over::<DraggedWindowTab>({
162 let tab_ix = ix;
163 move |element, dragged_tab: &DraggedWindowTab, _, cx| {
164 let mut styled_tab = element
165 .bg(cx.theme().colors().drop_target_background)
166 .border_color(cx.theme().colors().drop_target_border)
167 .border_0();
168
169 if tab_ix < dragged_tab.ix {
170 styled_tab = styled_tab.border_l_2();
171 } else if tab_ix > dragged_tab.ix {
172 styled_tab = styled_tab.border_r_2();
173 }
174
175 styled_tab
176 }
177 })
178 .on_drop({
179 let tab_ix = ix;
180 cx.listener(move |this, dragged_tab: &DraggedWindowTab, _window, cx| {
181 this.last_dragged_tab = None;
182 Self::handle_tab_drop(dragged_tab, tab_ix, cx);
183 })
184 })
185 .on_click(move |_, _, cx| {
186 let _ = item.handle.update(cx, |_, window, _| {
187 window.activate_window();
188 });
189 })
190 .child(label)
191 .map(|this| match show_close_button {
192 ShowCloseButton::Hidden => this,
193 _ => this.child(
194 div()
195 .absolute()
196 .top_2()
197 .w_4()
198 .h_4()
199 .map(|this| match close_side {
200 ClosePosition::Left => this.left_1(),
201 ClosePosition::Right => this.right_1(),
202 })
203 .child(
204 IconButton::new("close", IconName::Close)
205 .shape(IconButtonShape::Square)
206 .icon_color(Color::Muted)
207 .icon_size(IconSize::XSmall)
208 .on_click({
209 move |_, window, cx| {
210 if item.handle.window_id()
211 == window.window_handle().window_id()
212 {
213 window.dispatch_action(Box::new(CloseWindow), cx);
214 } else {
215 let _ = item.handle.update(cx, |_, window, cx| {
216 window.dispatch_action(Box::new(CloseWindow), cx);
217 });
218 }
219 }
220 })
221 .map(|this| match show_close_button {
222 ShowCloseButton::Hover => this.visible_on_hover("tab"),
223 _ => this,
224 }),
225 ),
226 ),
227 })
228 .into_any();
229
230 let menu = right_click_menu(ix)
231 .trigger(|_, _, _| tab)
232 .menu(move |window, cx| {
233 let focus_handle = cx.focus_handle();
234 let tabs = tabs.clone();
235 let other_tabs = tabs.clone();
236 let move_tabs = tabs.clone();
237 let merge_tabs = tabs.clone();
238
239 ContextMenu::build(window, cx, move |mut menu, _window_, _cx| {
240 menu = menu.entry("Close Tab", None, move |window, cx| {
241 Self::handle_right_click_action(
242 cx,
243 window,
244 &tabs,
245 |tab| tab.id == item.id,
246 |window, cx| {
247 window.dispatch_action(Box::new(CloseWindow), cx);
248 },
249 );
250 });
251
252 menu = menu.entry("Close Other Tabs", None, move |window, cx| {
253 Self::handle_right_click_action(
254 cx,
255 window,
256 &other_tabs,
257 |tab| tab.id != item.id,
258 |window, cx| {
259 window.dispatch_action(Box::new(CloseWindow), cx);
260 },
261 );
262 });
263
264 menu = menu.entry("Move Tab to New Window", None, move |window, cx| {
265 Self::handle_right_click_action(
266 cx,
267 window,
268 &move_tabs,
269 |tab| tab.id == item.id,
270 |window, cx| {
271 SystemWindowTabController::move_tab_to_new_window(
272 cx,
273 window.window_handle().window_id(),
274 );
275 window.move_tab_to_new_window();
276 },
277 );
278 });
279
280 menu = menu.entry("Show All Tabs", None, move |window, cx| {
281 Self::handle_right_click_action(
282 cx,
283 window,
284 &merge_tabs,
285 |tab| tab.id == item.id,
286 |window, _cx| {
287 window.toggle_window_tab_overview();
288 },
289 );
290 });
291
292 menu.context(focus_handle)
293 })
294 });
295
296 div()
297 .flex_1()
298 .min_w(rem_size * 10)
299 .when(is_active, |this| this.bg(active_background_color))
300 .border_t_1()
301 .border_color(if is_active {
302 active_background_color
303 } else {
304 cx.theme().colors().border
305 })
306 .child(menu)
307 }
308
309 fn handle_tab_drop(dragged_tab: &DraggedWindowTab, ix: usize, cx: &mut Context<Self>) {
310 SystemWindowTabController::update_tab_position(cx, dragged_tab.id, ix);
311 }
312
313 fn handle_right_click_action<F, P>(
314 cx: &mut App,
315 window: &mut Window,
316 tabs: &Vec<SystemWindowTab>,
317 predicate: P,
318 mut action: F,
319 ) where
320 P: Fn(&SystemWindowTab) -> bool,
321 F: FnMut(&mut Window, &mut App),
322 {
323 for tab in tabs {
324 if predicate(tab) {
325 if tab.id == window.window_handle().window_id() {
326 action(window, cx);
327 } else {
328 let _ = tab.handle.update(cx, |_view, window, cx| {
329 action(window, cx);
330 });
331 }
332 }
333 }
334 }
335}
336
337impl Render for SystemWindowTabs {
338 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
339 let active_background_color = cx.theme().colors().title_bar_background;
340 let inactive_background_color = cx.theme().colors().tab_bar_background;
341 let entity = cx.entity();
342
343 let controller = cx.global::<SystemWindowTabController>();
344 let visible = controller.is_visible();
345 let current_window_tab = vec![SystemWindowTab::new(
346 SharedString::from(window.window_title()),
347 window.window_handle(),
348 )];
349 let tabs = controller
350 .tabs(window.window_handle().window_id())
351 .unwrap_or(¤t_window_tab)
352 .clone();
353
354 let tab_items = tabs
355 .iter()
356 .enumerate()
357 .map(|(ix, item)| {
358 self.render_tab(
359 ix,
360 item.clone(),
361 tabs.clone(),
362 active_background_color,
363 inactive_background_color,
364 window,
365 cx,
366 )
367 })
368 .collect::<Vec<_>>();
369
370 let number_of_tabs = tab_items.len().max(1);
371 if !window.tab_bar_visible() && !visible {
372 return h_flex().into_any_element();
373 }
374
375 h_flex()
376 .w_full()
377 .h(Tab::container_height(cx))
378 .bg(inactive_background_color)
379 .on_mouse_up_out(
380 MouseButton::Left,
381 cx.listener(|this, _event, window, cx| {
382 if let Some(tab) = this.last_dragged_tab.take() {
383 SystemWindowTabController::move_tab_to_new_window(cx, tab.id);
384 if tab.id == window.window_handle().window_id() {
385 window.move_tab_to_new_window();
386 } else {
387 let _ = tab.handle.update(cx, |_, window, _cx| {
388 window.move_tab_to_new_window();
389 });
390 }
391 }
392 }),
393 )
394 .child(
395 h_flex()
396 .id("window tabs")
397 .w_full()
398 .h(Tab::container_height(cx))
399 .bg(inactive_background_color)
400 .overflow_x_scroll()
401 .track_scroll(&self.tab_bar_scroll_handle)
402 .children(tab_items)
403 .child(
404 canvas(
405 |_, _, _| (),
406 move |bounds, _, _, cx| {
407 let entity = entity.clone();
408 entity.update(cx, |this, cx| {
409 let width = bounds.size.width / number_of_tabs as f32;
410 if width != this.measured_tab_width {
411 this.measured_tab_width = width;
412 cx.notify();
413 }
414 });
415 },
416 )
417 .absolute()
418 .size_full(),
419 ),
420 )
421 .child(
422 h_flex()
423 .h_full()
424 .px(DynamicSpacing::Base06.rems(cx))
425 .border_t_1()
426 .border_l_1()
427 .border_color(cx.theme().colors().border)
428 .child(
429 IconButton::new("plus", IconName::Plus)
430 .icon_size(IconSize::Small)
431 .icon_color(Color::Muted)
432 .on_click(|_event, window, cx| {
433 window.dispatch_action(
434 Box::new(zed_actions::OpenRecent {
435 create_new_window: true,
436 }),
437 cx,
438 );
439 }),
440 ),
441 )
442 .into_any_element()
443 }
444}
445
446impl Render for DraggedWindowTab {
447 fn render(
448 &mut self,
449 _window: &mut gpui::Window,
450 cx: &mut gpui::Context<Self>,
451 ) -> impl gpui::IntoElement {
452 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
453 let label = Label::new(self.title.clone())
454 .size(LabelSize::Small)
455 .truncate()
456 .color(if self.is_active {
457 Color::Default
458 } else {
459 Color::Muted
460 });
461
462 h_flex()
463 .h(Tab::container_height(cx))
464 .w(self.width)
465 .px(DynamicSpacing::Base16.px(cx))
466 .justify_center()
467 .bg(if self.is_active {
468 self.active_background_color
469 } else {
470 self.inactive_background_color
471 })
472 .border_1()
473 .border_color(cx.theme().colors().border)
474 .font(ui_font)
475 .child(label)
476 }
477}