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