platform_title_bar.rs

  1mod platforms;
  2mod system_window_tabs;
  3
  4use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
  5use gpui::{
  6    AnyElement, App, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement,
  7    MouseButton, ParentElement, StatefulInteractiveElement, Styled, Window, WindowControlArea, div,
  8    px,
  9};
 10use project::DisableAiSettings;
 11use settings::Settings;
 12use smallvec::SmallVec;
 13use std::mem;
 14use ui::{
 15    prelude::*,
 16    utils::{TRAFFIC_LIGHT_PADDING, platform_title_bar_height},
 17};
 18
 19use crate::{
 20    platforms::{platform_linux, platform_windows},
 21    system_window_tabs::SystemWindowTabs,
 22};
 23
 24pub use system_window_tabs::{
 25    DraggedWindowTab, MergeAllWindows, MoveTabToNewWindow, ShowNextWindowTab, ShowPreviousWindowTab,
 26};
 27
 28pub struct PlatformTitleBar {
 29    id: ElementId,
 30    platform_style: PlatformStyle,
 31    children: SmallVec<[AnyElement; 2]>,
 32    should_move: bool,
 33    system_window_tabs: Entity<SystemWindowTabs>,
 34    workspace_sidebar_open: bool,
 35    sidebar_has_notifications: bool,
 36    is_singleton: bool,
 37}
 38
 39impl PlatformTitleBar {
 40    pub fn new(id: impl Into<ElementId>, cx: &mut Context<Self>) -> Self {
 41        let platform_style = PlatformStyle::platform();
 42        let system_window_tabs = cx.new(|_cx| SystemWindowTabs::new());
 43
 44        Self {
 45            id: id.into(),
 46            platform_style,
 47            children: SmallVec::new(),
 48            should_move: false,
 49            system_window_tabs,
 50            workspace_sidebar_open: false,
 51            sidebar_has_notifications: false,
 52            is_singleton: false,
 53        }
 54    }
 55
 56    pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
 57        if cfg!(any(target_os = "linux", target_os = "freebsd")) {
 58            if window.is_window_active() && !self.should_move {
 59                cx.theme().colors().title_bar_background
 60            } else {
 61                cx.theme().colors().title_bar_inactive_background
 62            }
 63        } else {
 64            cx.theme().colors().title_bar_background
 65        }
 66    }
 67
 68    pub fn set_children<T>(&mut self, children: T)
 69    where
 70        T: IntoIterator<Item = AnyElement>,
 71    {
 72        self.children = children.into_iter().collect();
 73    }
 74
 75    pub fn init(cx: &mut App) {
 76        SystemWindowTabs::init(cx);
 77    }
 78
 79    pub fn is_workspace_sidebar_open(&self) -> bool {
 80        self.workspace_sidebar_open
 81    }
 82
 83    pub fn set_workspace_sidebar_open(&mut self, open: bool, cx: &mut Context<Self>) {
 84        self.workspace_sidebar_open = open;
 85        cx.notify();
 86    }
 87
 88    pub fn sidebar_has_notifications(&self) -> bool {
 89        self.sidebar_has_notifications
 90    }
 91
 92    pub fn set_sidebar_has_notifications(
 93        &mut self,
 94        has_notifications: bool,
 95        cx: &mut Context<Self>,
 96    ) {
 97        self.sidebar_has_notifications = has_notifications;
 98        cx.notify();
 99    }
100
101    pub fn is_singleton(&self) -> bool {
102        self.is_singleton
103    }
104
105    pub fn set_singleton(&mut self, is_singleton: bool, cx: &mut Context<Self>) {
106        self.is_singleton = is_singleton;
107        cx.notify();
108    }
109
110    pub fn is_multi_workspace_enabled(cx: &App) -> bool {
111        cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
112    }
113}
114
115impl Render for PlatformTitleBar {
116    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
117        let supported_controls = window.window_controls();
118        let decorations = window.window_decorations();
119        let height = platform_title_bar_height(window);
120        let titlebar_color = self.title_bar_color(window, cx);
121        let close_action = Box::new(workspace::CloseWindow);
122        let children = mem::take(&mut self.children);
123
124        let is_multiworkspace_sidebar_open =
125            PlatformTitleBar::is_multi_workspace_enabled(cx) && self.is_workspace_sidebar_open();
126
127        let title_bar = h_flex()
128            .window_control_area(WindowControlArea::Drag)
129            .w_full()
130            .h(height)
131            .map(|this| {
132                this.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
133                    this.should_move = false;
134                }))
135                .on_mouse_up(
136                    gpui::MouseButton::Left,
137                    cx.listener(move |this, _ev, _window, _cx| {
138                        this.should_move = false;
139                    }),
140                )
141                .on_mouse_down(
142                    gpui::MouseButton::Left,
143                    cx.listener(move |this, _ev, _window, _cx| {
144                        this.should_move = true;
145                    }),
146                )
147                .on_mouse_move(cx.listener(move |this, _ev, window, _| {
148                    if this.should_move {
149                        this.should_move = false;
150                        window.start_window_move();
151                    }
152                }))
153            })
154            .map(|this| {
155                // Note: On Windows the title bar behavior is handled by the platform implementation.
156                this.id(self.id.clone())
157                    .when(self.platform_style == PlatformStyle::Mac, |this| {
158                        this.on_click(|event, window, _| {
159                            if event.click_count() == 2 {
160                                window.titlebar_double_click();
161                            }
162                        })
163                    })
164                    .when(self.platform_style == PlatformStyle::Linux, |this| {
165                        this.on_click(|event, window, _| {
166                            if event.click_count() == 2 {
167                                window.zoom_window();
168                            }
169                        })
170                    })
171            })
172            .map(|this| {
173                if window.is_fullscreen() {
174                    this.pl_2()
175                } else if self.platform_style == PlatformStyle::Mac
176                    && !is_multiworkspace_sidebar_open
177                {
178                    this.pl(px(TRAFFIC_LIGHT_PADDING))
179                } else {
180                    this.pl_2()
181                }
182            })
183            .map(|el| match decorations {
184                Decorations::Server => el,
185                Decorations::Client { tiling, .. } => el
186                    .when(!(tiling.top || tiling.right), |el| {
187                        el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
188                    })
189                    .when(
190                        !(tiling.top || tiling.left) && !is_multiworkspace_sidebar_open,
191                        |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
192                    )
193                    // this border is to avoid a transparent gap in the rounded corners
194                    .mt(px(-1.))
195                    .mb(px(-1.))
196                    .border(px(1.))
197                    .border_color(titlebar_color),
198            })
199            .bg(titlebar_color)
200            .content_stretch()
201            .child(
202                div()
203                    .id(self.id.clone())
204                    .flex()
205                    .flex_row()
206                    .items_center()
207                    .justify_between()
208                    .overflow_x_hidden()
209                    .w_full()
210                    .children(children),
211            )
212            .when(!window.is_fullscreen(), |title_bar| {
213                match self.platform_style {
214                    PlatformStyle::Mac => title_bar,
215                    PlatformStyle::Linux => {
216                        if matches!(decorations, Decorations::Client { .. }) {
217                            title_bar
218                                .child(platform_linux::LinuxWindowControls::new(close_action))
219                                .when(supported_controls.window_menu, |titlebar| {
220                                    titlebar
221                                        .on_mouse_down(MouseButton::Right, move |ev, window, _| {
222                                            window.show_window_menu(ev.position)
223                                        })
224                                })
225                        } else {
226                            title_bar
227                        }
228                    }
229                    PlatformStyle::Windows => {
230                        title_bar.child(platform_windows::WindowsWindowControls::new(height))
231                    }
232                }
233            });
234
235        v_flex()
236            .w_full()
237            .child(title_bar)
238            .child(self.system_window_tabs.clone().into_any_element())
239    }
240}
241
242impl ParentElement for PlatformTitleBar {
243    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
244        self.children.extend(elements)
245    }
246}