1pub mod platforms;
2mod system_window_tabs;
3
4use gpui::{
5 Action, AnyElement, App, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement,
6 MouseButton, ParentElement, StatefulInteractiveElement, Styled, WeakEntity, Window,
7 WindowButtonLayout, WindowControlArea, div, px,
8};
9use project::DisableAiSettings;
10use settings::Settings;
11use smallvec::SmallVec;
12use std::mem;
13use ui::{
14 prelude::*,
15 utils::{TRAFFIC_LIGHT_PADDING, platform_title_bar_height},
16};
17use workspace::{MultiWorkspace, SidebarRenderState, SidebarSide};
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 button_layout: Option<WindowButtonLayout>,
35 multi_workspace: Option<WeakEntity<MultiWorkspace>>,
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 button_layout: None,
50 multi_workspace: None,
51 }
52 }
53
54 pub fn with_multi_workspace(mut self, multi_workspace: WeakEntity<MultiWorkspace>) -> Self {
55 self.multi_workspace = Some(multi_workspace);
56 self
57 }
58
59 pub fn set_multi_workspace(&mut self, multi_workspace: WeakEntity<MultiWorkspace>) {
60 self.multi_workspace = Some(multi_workspace);
61 }
62
63 pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
64 if cfg!(any(target_os = "linux", target_os = "freebsd")) {
65 if window.is_window_active() && !self.should_move {
66 cx.theme().colors().title_bar_background
67 } else {
68 cx.theme().colors().title_bar_inactive_background
69 }
70 } else {
71 cx.theme().colors().title_bar_background
72 }
73 }
74
75 pub fn set_children<T>(&mut self, children: T)
76 where
77 T: IntoIterator<Item = AnyElement>,
78 {
79 self.children = children.into_iter().collect();
80 }
81
82 pub fn set_button_layout(&mut self, button_layout: Option<WindowButtonLayout>) {
83 self.button_layout = button_layout;
84 }
85
86 fn effective_button_layout(
87 &self,
88 decorations: &Decorations,
89 cx: &App,
90 ) -> Option<WindowButtonLayout> {
91 if self.platform_style == PlatformStyle::Linux
92 && matches!(decorations, Decorations::Client { .. })
93 {
94 self.button_layout.or_else(|| cx.button_layout())
95 } else {
96 None
97 }
98 }
99
100 pub fn init(cx: &mut App) {
101 SystemWindowTabs::init(cx);
102 }
103
104 fn sidebar_render_state(&self, cx: &App) -> SidebarRenderState {
105 self.multi_workspace
106 .as_ref()
107 .and_then(|mw| mw.upgrade())
108 .map(|mw| mw.read(cx).sidebar_render_state(cx))
109 .unwrap_or_default()
110 }
111
112 pub fn is_multi_workspace_enabled(cx: &App) -> bool {
113 !DisableAiSettings::get_global(cx).disable_ai
114 }
115}
116
117/// Renders the platform-appropriate left-side window controls (e.g. Ubuntu/GNOME close button).
118///
119/// Only relevant on Linux with client-side decorations when the window manager
120/// places controls on the left.
121pub fn render_left_window_controls(
122 button_layout: Option<WindowButtonLayout>,
123 close_action: Box<dyn Action>,
124 window: &Window,
125) -> Option<AnyElement> {
126 if PlatformStyle::platform() != PlatformStyle::Linux {
127 return None;
128 }
129 if !matches!(window.window_decorations(), Decorations::Client { .. }) {
130 return None;
131 }
132 let button_layout = button_layout?;
133 if button_layout.left[0].is_none() {
134 return None;
135 }
136 Some(
137 platform_linux::LinuxWindowControls::new(
138 "left-window-controls",
139 button_layout.left,
140 close_action,
141 )
142 .into_any_element(),
143 )
144}
145
146/// Renders the platform-appropriate right-side window controls (close, minimize, maximize).
147///
148/// Returns `None` on Mac or when the platform doesn't need custom controls
149/// (e.g. Linux with server-side decorations).
150pub fn render_right_window_controls(
151 button_layout: Option<WindowButtonLayout>,
152 close_action: Box<dyn Action>,
153 window: &Window,
154) -> Option<AnyElement> {
155 let decorations = window.window_decorations();
156 let height = platform_title_bar_height(window);
157
158 match PlatformStyle::platform() {
159 PlatformStyle::Linux => {
160 if !matches!(decorations, Decorations::Client { .. }) {
161 return None;
162 }
163 let button_layout = button_layout?;
164 if button_layout.right[0].is_none() {
165 return None;
166 }
167 Some(
168 platform_linux::LinuxWindowControls::new(
169 "right-window-controls",
170 button_layout.right,
171 close_action,
172 )
173 .into_any_element(),
174 )
175 }
176 PlatformStyle::Windows => {
177 Some(platform_windows::WindowsWindowControls::new(height).into_any_element())
178 }
179 PlatformStyle::Mac => None,
180 }
181}
182
183impl Render for PlatformTitleBar {
184 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
185 let supported_controls = window.window_controls();
186 let decorations = window.window_decorations();
187 let height = platform_title_bar_height(window);
188 let titlebar_color = self.title_bar_color(window, cx);
189 let close_action = Box::new(workspace::CloseWindow);
190 let children = mem::take(&mut self.children);
191
192 let button_layout = self.effective_button_layout(&decorations, cx);
193 let sidebar = self.sidebar_render_state(cx);
194
195 let title_bar = h_flex()
196 .window_control_area(WindowControlArea::Drag)
197 .w_full()
198 .h(height)
199 .map(|this| {
200 this.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
201 this.should_move = false;
202 }))
203 .on_mouse_up(
204 gpui::MouseButton::Left,
205 cx.listener(move |this, _ev, _window, _cx| {
206 this.should_move = false;
207 }),
208 )
209 .on_mouse_down(
210 gpui::MouseButton::Left,
211 cx.listener(move |this, _ev, _window, _cx| {
212 this.should_move = true;
213 }),
214 )
215 .on_mouse_move(cx.listener(move |this, _ev, window, _| {
216 if this.should_move {
217 this.should_move = false;
218 window.start_window_move();
219 }
220 }))
221 })
222 .map(|this| {
223 // Note: On Windows the title bar behavior is handled by the platform implementation.
224 this.id(self.id.clone())
225 .when(self.platform_style == PlatformStyle::Mac, |this| {
226 this.on_click(|event, window, _| {
227 if event.click_count() == 2 {
228 window.titlebar_double_click();
229 }
230 })
231 })
232 .when(self.platform_style == PlatformStyle::Linux, |this| {
233 this.on_click(|event, window, _| {
234 if event.click_count() == 2 {
235 window.zoom_window();
236 }
237 })
238 })
239 })
240 .map(|this| {
241 let show_left_controls = !(sidebar.open && sidebar.side == SidebarSide::Left);
242
243 if window.is_fullscreen() {
244 this.pl_2()
245 } else if self.platform_style == PlatformStyle::Mac && show_left_controls {
246 this.pl(px(TRAFFIC_LIGHT_PADDING))
247 } else if let Some(controls) = show_left_controls
248 .then(|| {
249 render_left_window_controls(
250 button_layout,
251 close_action.as_ref().boxed_clone(),
252 window,
253 )
254 })
255 .flatten()
256 {
257 this.child(controls)
258 } else {
259 this.pl_2()
260 }
261 })
262 .map(|el| match decorations {
263 Decorations::Server => el,
264 Decorations::Client { tiling, .. } => el
265 .when(
266 !(tiling.top || tiling.right)
267 && !(sidebar.open && sidebar.side == SidebarSide::Right),
268 |el| el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
269 )
270 .when(
271 !(tiling.top || tiling.left)
272 && !(sidebar.open && sidebar.side == SidebarSide::Left),
273 |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
274 )
275 // this border is to avoid a transparent gap in the rounded corners
276 .mt(px(-1.))
277 .mb(px(-1.))
278 .border(px(1.))
279 .border_color(titlebar_color),
280 })
281 .bg(titlebar_color)
282 .content_stretch()
283 .child(
284 div()
285 .id(self.id.clone())
286 .flex()
287 .flex_row()
288 .items_center()
289 .justify_between()
290 .overflow_x_hidden()
291 .w_full()
292 .children(children),
293 )
294 .when(!window.is_fullscreen(), |title_bar| {
295 let show_right_controls = !(sidebar.open && sidebar.side == SidebarSide::Right);
296
297 let title_bar = title_bar.children(
298 show_right_controls
299 .then(|| {
300 render_right_window_controls(
301 button_layout,
302 close_action.as_ref().boxed_clone(),
303 window,
304 )
305 })
306 .flatten(),
307 );
308
309 if self.platform_style == PlatformStyle::Linux
310 && matches!(decorations, Decorations::Client { .. })
311 {
312 title_bar.when(supported_controls.window_menu, |titlebar| {
313 titlebar.on_mouse_down(MouseButton::Right, move |ev, window, _| {
314 window.show_window_menu(ev.position)
315 })
316 })
317 } else {
318 title_bar
319 }
320 });
321
322 v_flex()
323 .w_full()
324 .child(title_bar)
325 .child(self.system_window_tabs.clone().into_any_element())
326 }
327}
328
329impl ParentElement for PlatformTitleBar {
330 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
331 self.children.extend(elements)
332 }
333}