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