platform_title_bar.rs

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