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, 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
118impl Render for PlatformTitleBar {
119    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
120        let supported_controls = window.window_controls();
121        let decorations = window.window_decorations();
122        let height = platform_title_bar_height(window);
123        let titlebar_color = self.title_bar_color(window, cx);
124        let close_action = Box::new(workspace::CloseWindow);
125        let children = mem::take(&mut self.children);
126
127        let button_layout = self.effective_button_layout(&decorations, cx);
128        let sidebar = self.sidebar_render_state(cx);
129
130        let title_bar = h_flex()
131            .window_control_area(WindowControlArea::Drag)
132            .w_full()
133            .h(height)
134            .map(|this| {
135                this.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
136                    this.should_move = false;
137                }))
138                .on_mouse_up(
139                    gpui::MouseButton::Left,
140                    cx.listener(move |this, _ev, _window, _cx| {
141                        this.should_move = false;
142                    }),
143                )
144                .on_mouse_down(
145                    gpui::MouseButton::Left,
146                    cx.listener(move |this, _ev, _window, _cx| {
147                        this.should_move = true;
148                    }),
149                )
150                .on_mouse_move(cx.listener(move |this, _ev, window, _| {
151                    if this.should_move {
152                        this.should_move = false;
153                        window.start_window_move();
154                    }
155                }))
156            })
157            .map(|this| {
158                // Note: On Windows the title bar behavior is handled by the platform implementation.
159                this.id(self.id.clone())
160                    .when(self.platform_style == PlatformStyle::Mac, |this| {
161                        this.on_click(|event, window, _| {
162                            if event.click_count() == 2 {
163                                window.titlebar_double_click();
164                            }
165                        })
166                    })
167                    .when(self.platform_style == PlatformStyle::Linux, |this| {
168                        this.on_click(|event, window, _| {
169                            if event.click_count() == 2 {
170                                window.zoom_window();
171                            }
172                        })
173                    })
174            })
175            .map(|this| {
176                if window.is_fullscreen() {
177                    this.pl_2()
178                } else if self.platform_style == PlatformStyle::Mac
179                    && !(sidebar.open && sidebar.side == SidebarSide::Left)
180                {
181                    this.pl(px(TRAFFIC_LIGHT_PADDING))
182                } else if let Some(button_layout) =
183                    button_layout.filter(|button_layout| button_layout.left[0].is_some())
184                {
185                    this.child(platform_linux::LinuxWindowControls::new(
186                        "left-window-controls",
187                        button_layout.left,
188                        close_action.as_ref().boxed_clone(),
189                    ))
190                } else {
191                    this.pl_2()
192                }
193            })
194            .map(|el| match decorations {
195                Decorations::Server => el,
196                Decorations::Client { tiling, .. } => el
197                    .when(
198                        !(tiling.top || tiling.right)
199                            && !(sidebar.open && sidebar.side == SidebarSide::Right),
200                        |el| el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
201                    )
202                    .when(
203                        !(tiling.top || tiling.left)
204                            && !(sidebar.open && sidebar.side == SidebarSide::Left),
205                        |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
206                    )
207                    // this border is to avoid a transparent gap in the rounded corners
208                    .mt(px(-1.))
209                    .mb(px(-1.))
210                    .border(px(1.))
211                    .border_color(titlebar_color),
212            })
213            .bg(titlebar_color)
214            .content_stretch()
215            .child(
216                div()
217                    .id(self.id.clone())
218                    .flex()
219                    .flex_row()
220                    .items_center()
221                    .justify_between()
222                    .overflow_x_hidden()
223                    .w_full()
224                    .children(children),
225            )
226            .when(!window.is_fullscreen(), |title_bar| {
227                match self.platform_style {
228                    PlatformStyle::Mac => title_bar,
229                    PlatformStyle::Linux => {
230                        if matches!(decorations, Decorations::Client { .. }) {
231                            let mut result = title_bar;
232                            if let Some(button_layout) = button_layout
233                                .filter(|button_layout| button_layout.right[0].is_some())
234                            {
235                                result = result.child(platform_linux::LinuxWindowControls::new(
236                                    "right-window-controls",
237                                    button_layout.right,
238                                    close_action.as_ref().boxed_clone(),
239                                ));
240                            }
241
242                            result.when(supported_controls.window_menu, |titlebar| {
243                                titlebar.on_mouse_down(MouseButton::Right, move |ev, window, _| {
244                                    window.show_window_menu(ev.position)
245                                })
246                            })
247                        } else {
248                            title_bar
249                        }
250                    }
251                    PlatformStyle::Windows => {
252                        title_bar.child(platform_windows::WindowsWindowControls::new(height))
253                    }
254                }
255            });
256
257        v_flex()
258            .w_full()
259            .child(title_bar)
260            .child(self.system_window_tabs.clone().into_any_element())
261    }
262}
263
264impl ParentElement for PlatformTitleBar {
265    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
266        self.children.extend(elements)
267    }
268}