platform_title_bar.rs

  1pub mod platforms;
  2mod system_window_tabs;
  3
  4use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
  5use gpui::{
  6    Action, AnyElement, App, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement,
  7    MouseButton, ParentElement, StatefulInteractiveElement, Styled, WeakEntity, Window,
  8    WindowButtonLayout, WindowControlArea, div, 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};
 18use workspace::{MultiWorkspace, SidebarRenderState, SidebarSide};
 19
 20use crate::{
 21    platforms::{platform_linux, platform_windows},
 22    system_window_tabs::SystemWindowTabs,
 23};
 24
 25pub use system_window_tabs::{
 26    DraggedWindowTab, MergeAllWindows, MoveTabToNewWindow, ShowNextWindowTab, ShowPreviousWindowTab,
 27};
 28
 29pub struct PlatformTitleBar {
 30    id: ElementId,
 31    platform_style: PlatformStyle,
 32    children: SmallVec<[AnyElement; 2]>,
 33    should_move: bool,
 34    system_window_tabs: Entity<SystemWindowTabs>,
 35    button_layout: Option<WindowButtonLayout>,
 36    multi_workspace: Option<WeakEntity<MultiWorkspace>>,
 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            button_layout: None,
 51            multi_workspace: None,
 52        }
 53    }
 54
 55    pub fn with_multi_workspace(mut self, multi_workspace: WeakEntity<MultiWorkspace>) -> Self {
 56        self.multi_workspace = Some(multi_workspace);
 57        self
 58    }
 59
 60    pub fn set_multi_workspace(&mut self, multi_workspace: WeakEntity<MultiWorkspace>) {
 61        self.multi_workspace = Some(multi_workspace);
 62    }
 63
 64    pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
 65        if cfg!(any(target_os = "linux", target_os = "freebsd")) {
 66            if window.is_window_active() && !self.should_move {
 67                cx.theme().colors().title_bar_background
 68            } else {
 69                cx.theme().colors().title_bar_inactive_background
 70            }
 71        } else {
 72            cx.theme().colors().title_bar_background
 73        }
 74    }
 75
 76    pub fn set_children<T>(&mut self, children: T)
 77    where
 78        T: IntoIterator<Item = AnyElement>,
 79    {
 80        self.children = children.into_iter().collect();
 81    }
 82
 83    pub fn set_button_layout(&mut self, button_layout: Option<WindowButtonLayout>) {
 84        self.button_layout = button_layout;
 85    }
 86
 87    fn effective_button_layout(
 88        &self,
 89        decorations: &Decorations,
 90        cx: &App,
 91    ) -> Option<WindowButtonLayout> {
 92        if self.platform_style == PlatformStyle::Linux
 93            && matches!(decorations, Decorations::Client { .. })
 94        {
 95            self.button_layout.or_else(|| cx.button_layout())
 96        } else {
 97            None
 98        }
 99    }
100
101    pub fn init(cx: &mut App) {
102        SystemWindowTabs::init(cx);
103    }
104
105    fn sidebar_render_state(&self, cx: &App) -> SidebarRenderState {
106        self.multi_workspace
107            .as_ref()
108            .and_then(|mw| mw.upgrade())
109            .map(|mw| mw.read(cx).sidebar_render_state(cx))
110            .unwrap_or_default()
111    }
112
113    pub fn is_multi_workspace_enabled(cx: &App) -> bool {
114        cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
115    }
116}
117
118/// Renders the platform-appropriate left-side window controls (e.g. Ubuntu/GNOME close button).
119///
120/// Only relevant on Linux with client-side decorations when the window manager
121/// places controls on the left.
122pub fn render_left_window_controls(
123    button_layout: Option<WindowButtonLayout>,
124    close_action: Box<dyn Action>,
125    window: &Window,
126) -> Option<AnyElement> {
127    if PlatformStyle::platform() != PlatformStyle::Linux {
128        return None;
129    }
130    if !matches!(window.window_decorations(), Decorations::Client { .. }) {
131        return None;
132    }
133    let button_layout = button_layout?;
134    if button_layout.left[0].is_none() {
135        return None;
136    }
137    Some(
138        platform_linux::LinuxWindowControls::new(
139            "left-window-controls",
140            button_layout.left,
141            close_action,
142        )
143        .into_any_element(),
144    )
145}
146
147/// Renders the platform-appropriate right-side window controls (close, minimize, maximize).
148///
149/// Returns `None` on Mac or when the platform doesn't need custom controls
150/// (e.g. Linux with server-side decorations).
151pub fn render_right_window_controls(
152    button_layout: Option<WindowButtonLayout>,
153    close_action: Box<dyn Action>,
154    window: &Window,
155) -> Option<AnyElement> {
156    let decorations = window.window_decorations();
157    let height = platform_title_bar_height(window);
158
159    match PlatformStyle::platform() {
160        PlatformStyle::Linux => {
161            if !matches!(decorations, Decorations::Client { .. }) {
162                return None;
163            }
164            let button_layout = button_layout?;
165            if button_layout.right[0].is_none() {
166                return None;
167            }
168            Some(
169                platform_linux::LinuxWindowControls::new(
170                    "right-window-controls",
171                    button_layout.right,
172                    close_action,
173                )
174                .into_any_element(),
175            )
176        }
177        PlatformStyle::Windows => {
178            Some(platform_windows::WindowsWindowControls::new(height).into_any_element())
179        }
180        PlatformStyle::Mac => None,
181    }
182}
183
184impl Render for PlatformTitleBar {
185    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
186        let supported_controls = window.window_controls();
187        let decorations = window.window_decorations();
188        let height = platform_title_bar_height(window);
189        let titlebar_color = self.title_bar_color(window, cx);
190        let close_action = Box::new(workspace::CloseWindow);
191        let children = mem::take(&mut self.children);
192
193        let button_layout = self.effective_button_layout(&decorations, cx);
194        let sidebar = self.sidebar_render_state(cx);
195
196        let title_bar = h_flex()
197            .window_control_area(WindowControlArea::Drag)
198            .w_full()
199            .h(height)
200            .map(|this| {
201                this.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
202                    this.should_move = false;
203                }))
204                .on_mouse_up(
205                    gpui::MouseButton::Left,
206                    cx.listener(move |this, _ev, _window, _cx| {
207                        this.should_move = false;
208                    }),
209                )
210                .on_mouse_down(
211                    gpui::MouseButton::Left,
212                    cx.listener(move |this, _ev, _window, _cx| {
213                        this.should_move = true;
214                    }),
215                )
216                .on_mouse_move(cx.listener(move |this, _ev, window, _| {
217                    if this.should_move {
218                        this.should_move = false;
219                        window.start_window_move();
220                    }
221                }))
222            })
223            .map(|this| {
224                // Note: On Windows the title bar behavior is handled by the platform implementation.
225                this.id(self.id.clone())
226                    .when(self.platform_style == PlatformStyle::Mac, |this| {
227                        this.on_click(|event, window, _| {
228                            if event.click_count() == 2 {
229                                window.titlebar_double_click();
230                            }
231                        })
232                    })
233                    .when(self.platform_style == PlatformStyle::Linux, |this| {
234                        this.on_click(|event, window, _| {
235                            if event.click_count() == 2 {
236                                window.zoom_window();
237                            }
238                        })
239                    })
240            })
241            .map(|this| {
242                let show_left_controls = !(sidebar.open && sidebar.side == SidebarSide::Left);
243
244                if window.is_fullscreen() {
245                    this.pl_2()
246                } else if self.platform_style == PlatformStyle::Mac && show_left_controls {
247                    this.pl(px(TRAFFIC_LIGHT_PADDING))
248                } else if let Some(controls) = show_left_controls
249                    .then(|| {
250                        render_left_window_controls(
251                            button_layout,
252                            close_action.as_ref().boxed_clone(),
253                            window,
254                        )
255                    })
256                    .flatten()
257                {
258                    this.child(controls)
259                } else {
260                    this.pl_2()
261                }
262            })
263            .map(|el| match decorations {
264                Decorations::Server => el,
265                Decorations::Client { tiling, .. } => el
266                    .when(
267                        !(tiling.top || tiling.right)
268                            && !(sidebar.open && sidebar.side == SidebarSide::Right),
269                        |el| el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
270                    )
271                    .when(
272                        !(tiling.top || tiling.left)
273                            && !(sidebar.open && sidebar.side == SidebarSide::Left),
274                        |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
275                    )
276                    // this border is to avoid a transparent gap in the rounded corners
277                    .mt(px(-1.))
278                    .mb(px(-1.))
279                    .border(px(1.))
280                    .border_color(titlebar_color),
281            })
282            .bg(titlebar_color)
283            .content_stretch()
284            .child(
285                div()
286                    .id(self.id.clone())
287                    .flex()
288                    .flex_row()
289                    .items_center()
290                    .justify_between()
291                    .overflow_x_hidden()
292                    .w_full()
293                    .children(children),
294            )
295            .when(!window.is_fullscreen(), |title_bar| {
296                let show_right_controls = !(sidebar.open && sidebar.side == SidebarSide::Right);
297
298                let title_bar = title_bar.children(
299                    show_right_controls
300                        .then(|| {
301                            render_right_window_controls(
302                                button_layout,
303                                close_action.as_ref().boxed_clone(),
304                                window,
305                            )
306                        })
307                        .flatten(),
308                );
309
310                if self.platform_style == PlatformStyle::Linux
311                    && matches!(decorations, Decorations::Client { .. })
312                {
313                    title_bar.when(supported_controls.window_menu, |titlebar| {
314                        titlebar.on_mouse_down(MouseButton::Right, move |ev, window, _| {
315                            window.show_window_menu(ev.position)
316                        })
317                    })
318                } else {
319                    title_bar
320                }
321            });
322
323        v_flex()
324            .w_full()
325            .child(title_bar)
326            .child(self.system_window_tabs.clone().into_any_element())
327    }
328}
329
330impl ParentElement for PlatformTitleBar {
331    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
332        self.children.extend(elements)
333    }
334}