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