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