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