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 smallvec::SmallVec;
 11use std::mem;
 12use ui::{
 13    prelude::*,
 14    utils::{TRAFFIC_LIGHT_PADDING, platform_title_bar_height},
 15};
 16
 17use crate::{
 18    platforms::{platform_linux, platform_windows},
 19    system_window_tabs::SystemWindowTabs,
 20};
 21
 22pub use system_window_tabs::{
 23    DraggedWindowTab, MergeAllWindows, MoveTabToNewWindow, ShowNextWindowTab, ShowPreviousWindowTab,
 24};
 25
 26pub struct PlatformTitleBar {
 27    id: ElementId,
 28    platform_style: PlatformStyle,
 29    children: SmallVec<[AnyElement; 2]>,
 30    should_move: bool,
 31    system_window_tabs: Entity<SystemWindowTabs>,
 32    workspace_sidebar_open: bool,
 33    sidebar_has_notifications: bool,
 34}
 35
 36impl PlatformTitleBar {
 37    pub fn new(id: impl Into<ElementId>, cx: &mut Context<Self>) -> Self {
 38        let platform_style = PlatformStyle::platform();
 39        let system_window_tabs = cx.new(|_cx| SystemWindowTabs::new());
 40
 41        Self {
 42            id: id.into(),
 43            platform_style,
 44            children: SmallVec::new(),
 45            should_move: false,
 46            system_window_tabs,
 47            workspace_sidebar_open: false,
 48            sidebar_has_notifications: false,
 49        }
 50    }
 51
 52    pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
 53        if cfg!(any(target_os = "linux", target_os = "freebsd")) {
 54            if window.is_window_active() && !self.should_move {
 55                cx.theme().colors().title_bar_background
 56            } else {
 57                cx.theme().colors().title_bar_inactive_background
 58            }
 59        } else {
 60            cx.theme().colors().title_bar_background
 61        }
 62    }
 63
 64    pub fn set_children<T>(&mut self, children: T)
 65    where
 66        T: IntoIterator<Item = AnyElement>,
 67    {
 68        self.children = children.into_iter().collect();
 69    }
 70
 71    pub fn init(cx: &mut App) {
 72        SystemWindowTabs::init(cx);
 73    }
 74
 75    pub fn is_workspace_sidebar_open(&self) -> bool {
 76        self.workspace_sidebar_open
 77    }
 78
 79    pub fn set_workspace_sidebar_open(&mut self, open: bool, cx: &mut Context<Self>) {
 80        self.workspace_sidebar_open = open;
 81        cx.notify();
 82    }
 83
 84    pub fn sidebar_has_notifications(&self) -> bool {
 85        self.sidebar_has_notifications
 86    }
 87
 88    pub fn set_sidebar_has_notifications(
 89        &mut self,
 90        has_notifications: bool,
 91        cx: &mut Context<Self>,
 92    ) {
 93        self.sidebar_has_notifications = has_notifications;
 94        cx.notify();
 95    }
 96
 97    pub fn is_multi_workspace_enabled(cx: &App) -> bool {
 98        cx.has_flag::<AgentV2FeatureFlag>()
 99    }
100}
101
102impl Render for PlatformTitleBar {
103    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
104        let supported_controls = window.window_controls();
105        let decorations = window.window_decorations();
106        let height = platform_title_bar_height(window);
107        let titlebar_color = self.title_bar_color(window, cx);
108        let close_action = Box::new(workspace::CloseWindow);
109        let children = mem::take(&mut self.children);
110
111        let is_multiworkspace_sidebar_open =
112            PlatformTitleBar::is_multi_workspace_enabled(cx) && self.is_workspace_sidebar_open();
113
114        let title_bar = h_flex()
115            .window_control_area(WindowControlArea::Drag)
116            .w_full()
117            .h(height)
118            .map(|this| {
119                this.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
120                    this.should_move = false;
121                }))
122                .on_mouse_up(
123                    gpui::MouseButton::Left,
124                    cx.listener(move |this, _ev, _window, _cx| {
125                        this.should_move = false;
126                    }),
127                )
128                .on_mouse_down(
129                    gpui::MouseButton::Left,
130                    cx.listener(move |this, _ev, _window, _cx| {
131                        this.should_move = true;
132                    }),
133                )
134                .on_mouse_move(cx.listener(move |this, _ev, window, _| {
135                    if this.should_move {
136                        this.should_move = false;
137                        window.start_window_move();
138                    }
139                }))
140            })
141            .map(|this| {
142                // Note: On Windows the title bar behavior is handled by the platform implementation.
143                this.id(self.id.clone())
144                    .when(self.platform_style == PlatformStyle::Mac, |this| {
145                        this.on_click(|event, window, _| {
146                            if event.click_count() == 2 {
147                                window.titlebar_double_click();
148                            }
149                        })
150                    })
151                    .when(self.platform_style == PlatformStyle::Linux, |this| {
152                        this.on_click(|event, window, _| {
153                            if event.click_count() == 2 {
154                                window.zoom_window();
155                            }
156                        })
157                    })
158            })
159            .map(|this| {
160                if window.is_fullscreen() {
161                    this.pl_2()
162                } else if self.platform_style == PlatformStyle::Mac
163                    && !is_multiworkspace_sidebar_open
164                {
165                    this.pl(px(TRAFFIC_LIGHT_PADDING))
166                } else {
167                    this.pl_2()
168                }
169            })
170            .map(|el| match decorations {
171                Decorations::Server => el,
172                Decorations::Client { tiling, .. } => el
173                    .when(!(tiling.top || tiling.right), |el| {
174                        el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
175                    })
176                    .when(
177                        !(tiling.top || tiling.left) && !is_multiworkspace_sidebar_open,
178                        |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
179                    )
180                    // this border is to avoid a transparent gap in the rounded corners
181                    .mt(px(-1.))
182                    .mb(px(-1.))
183                    .border(px(1.))
184                    .border_color(titlebar_color),
185            })
186            .bg(titlebar_color)
187            .content_stretch()
188            .child(
189                div()
190                    .id(self.id.clone())
191                    .flex()
192                    .flex_row()
193                    .items_center()
194                    .justify_between()
195                    .overflow_x_hidden()
196                    .w_full()
197                    .children(children),
198            )
199            .when(!window.is_fullscreen(), |title_bar| {
200                match self.platform_style {
201                    PlatformStyle::Mac => title_bar,
202                    PlatformStyle::Linux => {
203                        if matches!(decorations, Decorations::Client { .. }) {
204                            title_bar
205                                .child(platform_linux::LinuxWindowControls::new(close_action))
206                                .when(supported_controls.window_menu, |titlebar| {
207                                    titlebar
208                                        .on_mouse_down(MouseButton::Right, move |ev, window, _| {
209                                            window.show_window_menu(ev.position)
210                                        })
211                                })
212                        } else {
213                            title_bar
214                        }
215                    }
216                    PlatformStyle::Windows => {
217                        title_bar.child(platform_windows::WindowsWindowControls::new(height))
218                    }
219                }
220            });
221
222        v_flex()
223            .w_full()
224            .child(title_bar)
225            .child(self.system_window_tabs.clone().into_any_element())
226    }
227}
228
229impl ParentElement for PlatformTitleBar {
230    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
231        self.children.extend(elements)
232    }
233}