platform_title_bar.rs

  1mod 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, Window, WindowButtonLayout,
  8    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};
 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    workspace_sidebar_open: bool,
 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            workspace_sidebar_open: false,
 51        }
 52    }
 53
 54    pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
 55        if cfg!(any(target_os = "linux", target_os = "freebsd")) {
 56            if window.is_window_active() && !self.should_move {
 57                cx.theme().colors().title_bar_background
 58            } else {
 59                cx.theme().colors().title_bar_inactive_background
 60            }
 61        } else {
 62            cx.theme().colors().title_bar_background
 63        }
 64    }
 65
 66    pub fn set_children<T>(&mut self, children: T)
 67    where
 68        T: IntoIterator<Item = AnyElement>,
 69    {
 70        self.children = children.into_iter().collect();
 71    }
 72
 73    pub fn set_button_layout(&mut self, button_layout: Option<WindowButtonLayout>) {
 74        self.button_layout = button_layout;
 75    }
 76
 77    fn effective_button_layout(
 78        &self,
 79        decorations: &Decorations,
 80        cx: &App,
 81    ) -> Option<WindowButtonLayout> {
 82        if self.platform_style == PlatformStyle::Linux
 83            && matches!(decorations, Decorations::Client { .. })
 84        {
 85            self.button_layout.or_else(|| cx.button_layout())
 86        } else {
 87            None
 88        }
 89    }
 90
 91    pub fn init(cx: &mut App) {
 92        SystemWindowTabs::init(cx);
 93    }
 94
 95    pub fn is_workspace_sidebar_open(&self) -> bool {
 96        self.workspace_sidebar_open
 97    }
 98
 99    pub fn set_workspace_sidebar_open(&mut self, open: bool, cx: &mut Context<Self>) {
100        self.workspace_sidebar_open = open;
101        cx.notify();
102    }
103
104    pub fn is_multi_workspace_enabled(cx: &App) -> bool {
105        cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
106    }
107}
108
109impl Render for PlatformTitleBar {
110    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
111        let supported_controls = window.window_controls();
112        let decorations = window.window_decorations();
113        let height = platform_title_bar_height(window);
114        let titlebar_color = self.title_bar_color(window, cx);
115        let close_action = Box::new(workspace::CloseWindow);
116        let children = mem::take(&mut self.children);
117
118        let button_layout = self.effective_button_layout(&decorations, cx);
119        let is_multiworkspace_sidebar_open =
120            PlatformTitleBar::is_multi_workspace_enabled(cx) && self.is_workspace_sidebar_open();
121
122        let title_bar = h_flex()
123            .window_control_area(WindowControlArea::Drag)
124            .w_full()
125            .h(height)
126            .map(|this| {
127                this.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
128                    this.should_move = false;
129                }))
130                .on_mouse_up(
131                    gpui::MouseButton::Left,
132                    cx.listener(move |this, _ev, _window, _cx| {
133                        this.should_move = false;
134                    }),
135                )
136                .on_mouse_down(
137                    gpui::MouseButton::Left,
138                    cx.listener(move |this, _ev, _window, _cx| {
139                        this.should_move = true;
140                    }),
141                )
142                .on_mouse_move(cx.listener(move |this, _ev, window, _| {
143                    if this.should_move {
144                        this.should_move = false;
145                        window.start_window_move();
146                    }
147                }))
148            })
149            .map(|this| {
150                // Note: On Windows the title bar behavior is handled by the platform implementation.
151                this.id(self.id.clone())
152                    .when(self.platform_style == PlatformStyle::Mac, |this| {
153                        this.on_click(|event, window, _| {
154                            if event.click_count() == 2 {
155                                window.titlebar_double_click();
156                            }
157                        })
158                    })
159                    .when(self.platform_style == PlatformStyle::Linux, |this| {
160                        this.on_click(|event, window, _| {
161                            if event.click_count() == 2 {
162                                window.zoom_window();
163                            }
164                        })
165                    })
166            })
167            .map(|this| {
168                if window.is_fullscreen() {
169                    this.pl_2()
170                } else if self.platform_style == PlatformStyle::Mac
171                    && !is_multiworkspace_sidebar_open
172                {
173                    this.pl(px(TRAFFIC_LIGHT_PADDING))
174                } else if let Some(button_layout) =
175                    button_layout.filter(|button_layout| button_layout.left[0].is_some())
176                {
177                    this.child(platform_linux::LinuxWindowControls::new(
178                        "left-window-controls",
179                        button_layout.left,
180                        close_action.as_ref().boxed_clone(),
181                    ))
182                } else {
183                    this.pl_2()
184                }
185            })
186            .map(|el| match decorations {
187                Decorations::Server => el,
188                Decorations::Client { tiling, .. } => el
189                    .when(!(tiling.top || tiling.right), |el| {
190                        el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
191                    })
192                    .when(
193                        !(tiling.top || tiling.left) && !is_multiworkspace_sidebar_open,
194                        |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
195                    )
196                    // this border is to avoid a transparent gap in the rounded corners
197                    .mt(px(-1.))
198                    .mb(px(-1.))
199                    .border(px(1.))
200                    .border_color(titlebar_color),
201            })
202            .bg(titlebar_color)
203            .content_stretch()
204            .child(
205                div()
206                    .id(self.id.clone())
207                    .flex()
208                    .flex_row()
209                    .items_center()
210                    .justify_between()
211                    .overflow_x_hidden()
212                    .w_full()
213                    .children(children),
214            )
215            .when(!window.is_fullscreen(), |title_bar| {
216                match self.platform_style {
217                    PlatformStyle::Mac => title_bar,
218                    PlatformStyle::Linux => {
219                        if matches!(decorations, Decorations::Client { .. }) {
220                            let mut result = title_bar;
221                            if let Some(button_layout) = button_layout
222                                .filter(|button_layout| button_layout.right[0].is_some())
223                            {
224                                result = result.child(platform_linux::LinuxWindowControls::new(
225                                    "right-window-controls",
226                                    button_layout.right,
227                                    close_action.as_ref().boxed_clone(),
228                                ));
229                            }
230
231                            result.when(supported_controls.window_menu, |titlebar| {
232                                titlebar.on_mouse_down(MouseButton::Right, move |ev, window, _| {
233                                    window.show_window_menu(ev.position)
234                                })
235                            })
236                        } else {
237                            title_bar
238                        }
239                    }
240                    PlatformStyle::Windows => {
241                        title_bar.child(platform_windows::WindowsWindowControls::new(height))
242                    }
243                }
244            });
245
246        v_flex()
247            .w_full()
248            .child(title_bar)
249            .child(self.system_window_tabs.clone().into_any_element())
250    }
251}
252
253impl ParentElement for PlatformTitleBar {
254    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
255        self.children.extend(elements)
256    }
257}