1pub mod 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
118/// Renders the platform-appropriate left-side window controls (e.g. Ubuntu/GNOME close button).
119///
120/// Only relevant on Linux with client-side decorations when the window manager
121/// places controls on the left.
122pub fn render_left_window_controls(
123 button_layout: Option<WindowButtonLayout>,
124 close_action: Box<dyn Action>,
125 window: &Window,
126) -> Option<AnyElement> {
127 if PlatformStyle::platform() != PlatformStyle::Linux {
128 return None;
129 }
130 if !matches!(window.window_decorations(), Decorations::Client { .. }) {
131 return None;
132 }
133 let button_layout = button_layout?;
134 if button_layout.left[0].is_none() {
135 return None;
136 }
137 Some(
138 platform_linux::LinuxWindowControls::new(
139 "left-window-controls",
140 button_layout.left,
141 close_action,
142 )
143 .into_any_element(),
144 )
145}
146
147/// Renders the platform-appropriate right-side window controls (close, minimize, maximize).
148///
149/// Returns `None` on Mac or when the platform doesn't need custom controls
150/// (e.g. Linux with server-side decorations).
151pub fn render_right_window_controls(
152 button_layout: Option<WindowButtonLayout>,
153 close_action: Box<dyn Action>,
154 window: &Window,
155) -> Option<AnyElement> {
156 let decorations = window.window_decorations();
157 let height = platform_title_bar_height(window);
158
159 match PlatformStyle::platform() {
160 PlatformStyle::Linux => {
161 if !matches!(decorations, Decorations::Client { .. }) {
162 return None;
163 }
164 let button_layout = button_layout?;
165 if button_layout.right[0].is_none() {
166 return None;
167 }
168 Some(
169 platform_linux::LinuxWindowControls::new(
170 "right-window-controls",
171 button_layout.right,
172 close_action,
173 )
174 .into_any_element(),
175 )
176 }
177 PlatformStyle::Windows => {
178 Some(platform_windows::WindowsWindowControls::new(height).into_any_element())
179 }
180 PlatformStyle::Mac => None,
181 }
182}
183
184impl Render for PlatformTitleBar {
185 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
186 let supported_controls = window.window_controls();
187 let decorations = window.window_decorations();
188 let height = platform_title_bar_height(window);
189 let titlebar_color = self.title_bar_color(window, cx);
190 let close_action = Box::new(workspace::CloseWindow);
191 let children = mem::take(&mut self.children);
192
193 let button_layout = self.effective_button_layout(&decorations, cx);
194 let sidebar = self.sidebar_render_state(cx);
195
196 let title_bar = h_flex()
197 .window_control_area(WindowControlArea::Drag)
198 .w_full()
199 .h(height)
200 .map(|this| {
201 this.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
202 this.should_move = false;
203 }))
204 .on_mouse_up(
205 gpui::MouseButton::Left,
206 cx.listener(move |this, _ev, _window, _cx| {
207 this.should_move = false;
208 }),
209 )
210 .on_mouse_down(
211 gpui::MouseButton::Left,
212 cx.listener(move |this, _ev, _window, _cx| {
213 this.should_move = true;
214 }),
215 )
216 .on_mouse_move(cx.listener(move |this, _ev, window, _| {
217 if this.should_move {
218 this.should_move = false;
219 window.start_window_move();
220 }
221 }))
222 })
223 .map(|this| {
224 // Note: On Windows the title bar behavior is handled by the platform implementation.
225 this.id(self.id.clone())
226 .when(self.platform_style == PlatformStyle::Mac, |this| {
227 this.on_click(|event, window, _| {
228 if event.click_count() == 2 {
229 window.titlebar_double_click();
230 }
231 })
232 })
233 .when(self.platform_style == PlatformStyle::Linux, |this| {
234 this.on_click(|event, window, _| {
235 if event.click_count() == 2 {
236 window.zoom_window();
237 }
238 })
239 })
240 })
241 .map(|this| {
242 let show_left_controls = !(sidebar.open && sidebar.side == SidebarSide::Left);
243
244 if window.is_fullscreen() {
245 this.pl_2()
246 } else if self.platform_style == PlatformStyle::Mac && show_left_controls {
247 this.pl(px(TRAFFIC_LIGHT_PADDING))
248 } else if let Some(controls) = show_left_controls
249 .then(|| {
250 render_left_window_controls(
251 button_layout,
252 close_action.as_ref().boxed_clone(),
253 window,
254 )
255 })
256 .flatten()
257 {
258 this.child(controls)
259 } else {
260 this.pl_2()
261 }
262 })
263 .map(|el| match decorations {
264 Decorations::Server => el,
265 Decorations::Client { tiling, .. } => el
266 .when(
267 !(tiling.top || tiling.right)
268 && !(sidebar.open && sidebar.side == SidebarSide::Right),
269 |el| el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
270 )
271 .when(
272 !(tiling.top || tiling.left)
273 && !(sidebar.open && sidebar.side == SidebarSide::Left),
274 |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
275 )
276 // this border is to avoid a transparent gap in the rounded corners
277 .mt(px(-1.))
278 .mb(px(-1.))
279 .border(px(1.))
280 .border_color(titlebar_color),
281 })
282 .bg(titlebar_color)
283 .content_stretch()
284 .child(
285 div()
286 .id(self.id.clone())
287 .flex()
288 .flex_row()
289 .items_center()
290 .justify_between()
291 .overflow_x_hidden()
292 .w_full()
293 .children(children),
294 )
295 .when(!window.is_fullscreen(), |title_bar| {
296 let show_right_controls = !(sidebar.open && sidebar.side == SidebarSide::Right);
297
298 let title_bar = title_bar.children(
299 show_right_controls
300 .then(|| {
301 render_right_window_controls(
302 button_layout,
303 close_action.as_ref().boxed_clone(),
304 window,
305 )
306 })
307 .flatten(),
308 );
309
310 if self.platform_style == PlatformStyle::Linux
311 && matches!(decorations, Decorations::Client { .. })
312 {
313 title_bar.when(supported_controls.window_menu, |titlebar| {
314 titlebar.on_mouse_down(MouseButton::Right, move |ev, window, _| {
315 window.show_window_menu(ev.position)
316 })
317 })
318 } else {
319 title_bar
320 }
321 });
322
323 v_flex()
324 .w_full()
325 .child(title_bar)
326 .child(self.system_window_tabs.clone().into_any_element())
327 }
328}
329
330impl ParentElement for PlatformTitleBar {
331 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
332 self.children.extend(elements)
333 }
334}