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