system_window_tabs.rs

  1use settings::Settings;
  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,
 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        cx.observe_new(|workspace: &mut Workspace, _, _| {
 57            workspace.register_action_renderer(|div, _, window, cx| {
 58                let window_id = window.window_handle().window_id();
 59                let controller = cx.global::<SystemWindowTabController>();
 60
 61                let tab_groups = controller.tab_groups();
 62                let tabs = controller.tabs(window_id);
 63                let Some(tabs) = tabs else {
 64                    return div;
 65                };
 66
 67                div.when(tabs.len() > 1, |div| {
 68                    div.on_action(move |_: &ShowNextWindowTab, window, cx| {
 69                        SystemWindowTabController::select_next_tab(
 70                            cx,
 71                            window.window_handle().window_id(),
 72                        );
 73                    })
 74                    .on_action(move |_: &ShowPreviousWindowTab, window, cx| {
 75                        SystemWindowTabController::select_previous_tab(
 76                            cx,
 77                            window.window_handle().window_id(),
 78                        );
 79                    })
 80                    .on_action(move |_: &MoveTabToNewWindow, window, cx| {
 81                        SystemWindowTabController::move_tab_to_new_window(
 82                            cx,
 83                            window.window_handle().window_id(),
 84                        );
 85                        window.move_tab_to_new_window();
 86                    })
 87                })
 88                .when(tab_groups.len() > 1, |div| {
 89                    div.on_action(move |_: &MergeAllWindows, window, cx| {
 90                        SystemWindowTabController::merge_all_windows(
 91                            cx,
 92                            window.window_handle().window_id(),
 93                        );
 94                        window.merge_all_windows();
 95                    })
 96                })
 97            });
 98        })
 99        .detach();
100    }
101
102    fn render_tab(
103        &self,
104        ix: usize,
105        item: SystemWindowTab,
106        tabs: Vec<SystemWindowTab>,
107        active_background_color: Hsla,
108        inactive_background_color: Hsla,
109        window: &mut Window,
110        cx: &mut Context<Self>,
111    ) -> impl IntoElement + use<> {
112        let entity = cx.entity();
113        let settings = ItemSettings::get_global(cx);
114        let close_side = &settings.close_position;
115        let show_close_button = &settings.show_close_button;
116
117        let rem_size = window.rem_size();
118        let width = self.measured_tab_width.max(rem_size * 10);
119        let is_active = window.window_handle().window_id() == item.id;
120        let title = item.title.to_string();
121
122        let label = Label::new(&title)
123            .size(LabelSize::Small)
124            .truncate()
125            .color(if is_active {
126                Color::Default
127            } else {
128                Color::Muted
129            });
130
131        let tab = h_flex()
132            .id(ix)
133            .group("tab")
134            .w_full()
135            .overflow_hidden()
136            .h(Tab::content_height(cx))
137            .relative()
138            .px(DynamicSpacing::Base16.px(cx))
139            .justify_center()
140            .border_l_1()
141            .border_color(cx.theme().colors().border)
142            .cursor_pointer()
143            .on_drag(
144                DraggedWindowTab {
145                    id: item.id,
146                    ix,
147                    handle: item.handle,
148                    title: item.title.to_string(),
149                    width,
150                    is_active,
151                    active_background_color,
152                    inactive_background_color,
153                },
154                move |tab, _, _, cx| {
155                    entity.update(cx, |this, _cx| {
156                        this.last_dragged_tab = Some(tab.clone());
157                    });
158                    cx.new(|_| tab.clone())
159                },
160            )
161            .drag_over::<DraggedWindowTab>({
162                let tab_ix = ix;
163                move |element, dragged_tab: &DraggedWindowTab, _, cx| {
164                    let mut styled_tab = element
165                        .bg(cx.theme().colors().drop_target_background)
166                        .border_color(cx.theme().colors().drop_target_border)
167                        .border_0();
168
169                    if tab_ix < dragged_tab.ix {
170                        styled_tab = styled_tab.border_l_2();
171                    } else if tab_ix > dragged_tab.ix {
172                        styled_tab = styled_tab.border_r_2();
173                    }
174
175                    styled_tab
176                }
177            })
178            .on_drop({
179                let tab_ix = ix;
180                cx.listener(move |this, dragged_tab: &DraggedWindowTab, _window, cx| {
181                    this.last_dragged_tab = None;
182                    Self::handle_tab_drop(dragged_tab, tab_ix, cx);
183                })
184            })
185            .on_click(move |_, _, cx| {
186                let _ = item.handle.update(cx, |_, window, _| {
187                    window.activate_window();
188                });
189            })
190            .child(label)
191            .map(|this| match show_close_button {
192                ShowCloseButton::Hidden => this,
193                _ => this.child(
194                    div()
195                        .absolute()
196                        .top_2()
197                        .w_4()
198                        .h_4()
199                        .map(|this| match close_side {
200                            ClosePosition::Left => this.left_1(),
201                            ClosePosition::Right => this.right_1(),
202                        })
203                        .child(
204                            IconButton::new("close", IconName::Close)
205                                .shape(IconButtonShape::Square)
206                                .icon_color(Color::Muted)
207                                .icon_size(IconSize::XSmall)
208                                .on_click({
209                                    move |_, window, cx| {
210                                        if item.handle.window_id()
211                                            == window.window_handle().window_id()
212                                        {
213                                            window.dispatch_action(Box::new(CloseWindow), cx);
214                                        } else {
215                                            let _ = item.handle.update(cx, |_, window, cx| {
216                                                window.dispatch_action(Box::new(CloseWindow), cx);
217                                            });
218                                        }
219                                    }
220                                })
221                                .map(|this| match show_close_button {
222                                    ShowCloseButton::Hover => this.visible_on_hover("tab"),
223                                    _ => this,
224                                }),
225                        ),
226                ),
227            })
228            .into_any();
229
230        let menu = right_click_menu(ix)
231            .trigger(|_, _, _| tab)
232            .menu(move |window, cx| {
233                let focus_handle = cx.focus_handle();
234                let tabs = tabs.clone();
235                let other_tabs = tabs.clone();
236                let move_tabs = tabs.clone();
237                let merge_tabs = tabs.clone();
238
239                ContextMenu::build(window, cx, move |mut menu, _window_, _cx| {
240                    menu = menu.entry("Close Tab", None, move |window, cx| {
241                        Self::handle_right_click_action(
242                            cx,
243                            window,
244                            &tabs,
245                            |tab| tab.id == item.id,
246                            |window, cx| {
247                                window.dispatch_action(Box::new(CloseWindow), cx);
248                            },
249                        );
250                    });
251
252                    menu = menu.entry("Close Other Tabs", None, move |window, cx| {
253                        Self::handle_right_click_action(
254                            cx,
255                            window,
256                            &other_tabs,
257                            |tab| tab.id != item.id,
258                            |window, cx| {
259                                window.dispatch_action(Box::new(CloseWindow), cx);
260                            },
261                        );
262                    });
263
264                    menu = menu.entry("Move Tab to New Window", None, move |window, cx| {
265                        Self::handle_right_click_action(
266                            cx,
267                            window,
268                            &move_tabs,
269                            |tab| tab.id == item.id,
270                            |window, cx| {
271                                SystemWindowTabController::move_tab_to_new_window(
272                                    cx,
273                                    window.window_handle().window_id(),
274                                );
275                                window.move_tab_to_new_window();
276                            },
277                        );
278                    });
279
280                    menu = menu.entry("Show All Tabs", None, move |window, cx| {
281                        Self::handle_right_click_action(
282                            cx,
283                            window,
284                            &merge_tabs,
285                            |tab| tab.id == item.id,
286                            |window, _cx| {
287                                window.toggle_window_tab_overview();
288                            },
289                        );
290                    });
291
292                    menu.context(focus_handle)
293                })
294            });
295
296        div()
297            .flex_1()
298            .min_w(rem_size * 10)
299            .when(is_active, |this| this.bg(active_background_color))
300            .border_t_1()
301            .border_color(if is_active {
302                active_background_color
303            } else {
304                cx.theme().colors().border
305            })
306            .child(menu)
307    }
308
309    fn handle_tab_drop(dragged_tab: &DraggedWindowTab, ix: usize, cx: &mut Context<Self>) {
310        SystemWindowTabController::update_tab_position(cx, dragged_tab.id, ix);
311    }
312
313    fn handle_right_click_action<F, P>(
314        cx: &mut App,
315        window: &mut Window,
316        tabs: &Vec<SystemWindowTab>,
317        predicate: P,
318        mut action: F,
319    ) where
320        P: Fn(&SystemWindowTab) -> bool,
321        F: FnMut(&mut Window, &mut App),
322    {
323        for tab in tabs {
324            if predicate(tab) {
325                if tab.id == window.window_handle().window_id() {
326                    action(window, cx);
327                } else {
328                    let _ = tab.handle.update(cx, |_view, window, cx| {
329                        action(window, cx);
330                    });
331                }
332            }
333        }
334    }
335}
336
337impl Render for SystemWindowTabs {
338    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
339        let active_background_color = cx.theme().colors().title_bar_background;
340        let inactive_background_color = cx.theme().colors().tab_bar_background;
341        let entity = cx.entity();
342
343        let controller = cx.global::<SystemWindowTabController>();
344        let visible = controller.is_visible();
345        let current_window_tab = vec![SystemWindowTab::new(
346            SharedString::from(window.window_title()),
347            window.window_handle(),
348        )];
349        let tabs = controller
350            .tabs(window.window_handle().window_id())
351            .unwrap_or(&current_window_tab)
352            .clone();
353
354        let tab_items = tabs
355            .iter()
356            .enumerate()
357            .map(|(ix, item)| {
358                self.render_tab(
359                    ix,
360                    item.clone(),
361                    tabs.clone(),
362                    active_background_color,
363                    inactive_background_color,
364                    window,
365                    cx,
366                )
367            })
368            .collect::<Vec<_>>();
369
370        let number_of_tabs = tab_items.len().max(1);
371        if !window.tab_bar_visible() && !visible {
372            return h_flex().into_any_element();
373        }
374
375        h_flex()
376            .w_full()
377            .h(Tab::container_height(cx))
378            .bg(inactive_background_color)
379            .on_mouse_up_out(
380                MouseButton::Left,
381                cx.listener(|this, _event, window, cx| {
382                    if let Some(tab) = this.last_dragged_tab.take() {
383                        SystemWindowTabController::move_tab_to_new_window(cx, tab.id);
384                        if tab.id == window.window_handle().window_id() {
385                            window.move_tab_to_new_window();
386                        } else {
387                            let _ = tab.handle.update(cx, |_, window, _cx| {
388                                window.move_tab_to_new_window();
389                            });
390                        }
391                    }
392                }),
393            )
394            .child(
395                h_flex()
396                    .id("window tabs")
397                    .w_full()
398                    .h(Tab::container_height(cx))
399                    .bg(inactive_background_color)
400                    .overflow_x_scroll()
401                    .track_scroll(&self.tab_bar_scroll_handle)
402                    .children(tab_items)
403                    .child(
404                        canvas(
405                            |_, _, _| (),
406                            move |bounds, _, _, cx| {
407                                let entity = entity.clone();
408                                entity.update(cx, |this, cx| {
409                                    let width = bounds.size.width / number_of_tabs as f32;
410                                    if width != this.measured_tab_width {
411                                        this.measured_tab_width = width;
412                                        cx.notify();
413                                    }
414                                });
415                            },
416                        )
417                        .absolute()
418                        .size_full(),
419                    ),
420            )
421            .child(
422                h_flex()
423                    .h_full()
424                    .px(DynamicSpacing::Base06.rems(cx))
425                    .border_t_1()
426                    .border_l_1()
427                    .border_color(cx.theme().colors().border)
428                    .child(
429                        IconButton::new("plus", IconName::Plus)
430                            .icon_size(IconSize::Small)
431                            .icon_color(Color::Muted)
432                            .on_click(|_event, window, cx| {
433                                window.dispatch_action(
434                                    Box::new(zed_actions::OpenRecent {
435                                        create_new_window: true,
436                                    }),
437                                    cx,
438                                );
439                            }),
440                    ),
441            )
442            .into_any_element()
443    }
444}
445
446impl Render for DraggedWindowTab {
447    fn render(
448        &mut self,
449        _window: &mut gpui::Window,
450        cx: &mut gpui::Context<Self>,
451    ) -> impl gpui::IntoElement {
452        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
453        let label = Label::new(self.title.clone())
454            .size(LabelSize::Small)
455            .truncate()
456            .color(if self.is_active {
457                Color::Default
458            } else {
459                Color::Muted
460            });
461
462        h_flex()
463            .h(Tab::container_height(cx))
464            .w(self.width)
465            .px(DynamicSpacing::Base16.px(cx))
466            .justify_center()
467            .bg(if self.is_active {
468                self.active_background_color
469            } else {
470                self.inactive_background_color
471            })
472            .border_1()
473            .border_color(cx.theme().colors().border)
474            .font(ui_font)
475            .child(label)
476    }
477}