system_window_tabs.rs

  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(&current_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}