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, IconName, IconSize, Label, LabelSize, Tab,
11 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 .child(label)
231 .map(|this| match show_close_button {
232 ShowCloseButton::Hidden => this,
233 _ => this.child(
234 div()
235 .absolute()
236 .top_2()
237 .w_4()
238 .h_4()
239 .map(|this| match close_side {
240 ClosePosition::Left => this.left_1(),
241 ClosePosition::Right => this.right_1(),
242 })
243 .child(
244 IconButton::new("close", IconName::Close)
245 .icon_color(Color::Muted)
246 .icon_size(IconSize::XSmall)
247 .on_click({
248 move |_, window, cx| {
249 if item.handle.window_id()
250 == window.window_handle().window_id()
251 {
252 window.dispatch_action(Box::new(CloseWindow), cx);
253 } else {
254 let _ = item.handle.update(cx, |_, window, cx| {
255 window.dispatch_action(Box::new(CloseWindow), cx);
256 });
257 }
258 }
259 })
260 .map(|this| match show_close_button {
261 ShowCloseButton::Hover => this.visible_on_hover("tab"),
262 _ => this,
263 }),
264 ),
265 ),
266 })
267 .into_any();
268
269 let menu = right_click_menu(ix)
270 .trigger(|_, _, _| tab)
271 .menu(move |window, cx| {
272 let focus_handle = cx.focus_handle();
273 let tabs = tabs.clone();
274 let other_tabs = tabs.clone();
275 let move_tabs = tabs.clone();
276 let merge_tabs = tabs.clone();
277
278 ContextMenu::build(window, cx, move |mut menu, _window_, _cx| {
279 menu = menu.entry("Close Tab", None, move |window, cx| {
280 Self::handle_right_click_action(
281 cx,
282 window,
283 &tabs,
284 |tab| tab.id == item.id,
285 |window, cx| {
286 window.dispatch_action(Box::new(CloseWindow), cx);
287 },
288 );
289 });
290
291 menu = menu.entry("Close Other Tabs", None, move |window, cx| {
292 Self::handle_right_click_action(
293 cx,
294 window,
295 &other_tabs,
296 |tab| tab.id != item.id,
297 |window, cx| {
298 window.dispatch_action(Box::new(CloseWindow), cx);
299 },
300 );
301 });
302
303 menu = menu.entry("Move Tab to New Window", None, move |window, cx| {
304 Self::handle_right_click_action(
305 cx,
306 window,
307 &move_tabs,
308 |tab| tab.id == item.id,
309 |window, cx| {
310 SystemWindowTabController::move_tab_to_new_window(
311 cx,
312 window.window_handle().window_id(),
313 );
314 window.move_tab_to_new_window();
315 },
316 );
317 });
318
319 menu = menu.entry("Show All Tabs", None, move |window, cx| {
320 Self::handle_right_click_action(
321 cx,
322 window,
323 &merge_tabs,
324 |tab| tab.id == item.id,
325 |window, _cx| {
326 window.toggle_window_tab_overview();
327 },
328 );
329 });
330
331 menu.context(focus_handle)
332 })
333 });
334
335 div()
336 .flex_1()
337 .min_w(rem_size * 10)
338 .when(is_active, |this| this.bg(active_background_color))
339 .border_t_1()
340 .border_color(if is_active {
341 active_background_color
342 } else {
343 cx.theme().colors().border
344 })
345 .child(menu)
346 }
347
348 fn handle_tab_drop(dragged_tab: &DraggedWindowTab, ix: usize, cx: &mut Context<Self>) {
349 SystemWindowTabController::update_tab_position(cx, dragged_tab.id, ix);
350 }
351
352 fn handle_right_click_action<F, P>(
353 cx: &mut App,
354 window: &mut Window,
355 tabs: &Vec<SystemWindowTab>,
356 predicate: P,
357 mut action: F,
358 ) where
359 P: Fn(&SystemWindowTab) -> bool,
360 F: FnMut(&mut Window, &mut App),
361 {
362 for tab in tabs {
363 if predicate(tab) {
364 if tab.id == window.window_handle().window_id() {
365 action(window, cx);
366 } else {
367 let _ = tab.handle.update(cx, |_view, window, cx| {
368 action(window, cx);
369 });
370 }
371 }
372 }
373 }
374}
375
376impl Render for SystemWindowTabs {
377 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
378 let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs;
379 let active_background_color = cx.theme().colors().title_bar_background;
380 let inactive_background_color = cx.theme().colors().tab_bar_background;
381 let entity = cx.entity();
382
383 let controller = cx.global::<SystemWindowTabController>();
384 let visible = controller.is_visible();
385 let current_window_tab = vec![SystemWindowTab::new(
386 SharedString::from(window.window_title()),
387 window.window_handle(),
388 )];
389 let tabs = controller
390 .tabs(window.window_handle().window_id())
391 .unwrap_or(¤t_window_tab)
392 .clone();
393
394 let tab_items = tabs
395 .iter()
396 .enumerate()
397 .map(|(ix, item)| {
398 self.render_tab(
399 ix,
400 item.clone(),
401 tabs.clone(),
402 active_background_color,
403 inactive_background_color,
404 window,
405 cx,
406 )
407 })
408 .collect::<Vec<_>>();
409
410 let number_of_tabs = tab_items.len().max(1);
411 if (!window.tab_bar_visible() && !visible)
412 || (!use_system_window_tabs && number_of_tabs == 1)
413 {
414 return h_flex().into_any_element();
415 }
416
417 h_flex()
418 .w_full()
419 .h(Tab::container_height(cx))
420 .bg(inactive_background_color)
421 .on_mouse_up_out(
422 MouseButton::Left,
423 cx.listener(|this, _event, window, cx| {
424 if let Some(tab) = this.last_dragged_tab.take() {
425 SystemWindowTabController::move_tab_to_new_window(cx, tab.id);
426 if tab.id == window.window_handle().window_id() {
427 window.move_tab_to_new_window();
428 } else {
429 let _ = tab.handle.update(cx, |_, window, _cx| {
430 window.move_tab_to_new_window();
431 });
432 }
433 }
434 }),
435 )
436 .child(
437 h_flex()
438 .id("window tabs")
439 .w_full()
440 .h(Tab::container_height(cx))
441 .bg(inactive_background_color)
442 .overflow_x_scroll()
443 .track_scroll(&self.tab_bar_scroll_handle)
444 .children(tab_items)
445 .child(
446 canvas(
447 |_, _, _| (),
448 move |bounds, _, _, cx| {
449 let entity = entity.clone();
450 entity.update(cx, |this, cx| {
451 let width = bounds.size.width / number_of_tabs as f32;
452 if width != this.measured_tab_width {
453 this.measured_tab_width = width;
454 cx.notify();
455 }
456 });
457 },
458 )
459 .absolute()
460 .size_full(),
461 ),
462 )
463 .child(
464 h_flex()
465 .h_full()
466 .px(DynamicSpacing::Base06.rems(cx))
467 .border_t_1()
468 .border_l_1()
469 .border_color(cx.theme().colors().border)
470 .child(
471 IconButton::new("plus", IconName::Plus)
472 .icon_color(Color::Muted)
473 .on_click(|_event, window, cx| {
474 window.dispatch_action(
475 Box::new(zed_actions::OpenRecent {
476 create_new_window: true,
477 }),
478 cx,
479 );
480 }),
481 ),
482 )
483 .into_any_element()
484 }
485}
486
487impl Render for DraggedWindowTab {
488 fn render(
489 &mut self,
490 _window: &mut gpui::Window,
491 cx: &mut gpui::Context<Self>,
492 ) -> impl gpui::IntoElement {
493 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
494 let label = Label::new(self.title.clone())
495 .size(LabelSize::Small)
496 .truncate()
497 .color(if self.is_active {
498 Color::Default
499 } else {
500 Color::Muted
501 });
502
503 h_flex()
504 .h(Tab::container_height(cx))
505 .w(self.width)
506 .px(DynamicSpacing::Base16.px(cx))
507 .justify_center()
508 .bg(if self.is_active {
509 self.active_background_color
510 } else {
511 self.inactive_background_color
512 })
513 .border_1()
514 .border_color(cx.theme().colors().border)
515 .font(ui_font)
516 .child(label)
517 }
518}