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