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, 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
118impl Render for PlatformTitleBar {
119 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
120 let supported_controls = window.window_controls();
121 let decorations = window.window_decorations();
122 let height = platform_title_bar_height(window);
123 let titlebar_color = self.title_bar_color(window, cx);
124 let close_action = Box::new(workspace::CloseWindow);
125 let children = mem::take(&mut self.children);
126
127 let button_layout = self.effective_button_layout(&decorations, cx);
128 let sidebar = self.sidebar_render_state(cx);
129
130 let title_bar = h_flex()
131 .window_control_area(WindowControlArea::Drag)
132 .w_full()
133 .h(height)
134 .map(|this| {
135 this.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
136 this.should_move = false;
137 }))
138 .on_mouse_up(
139 gpui::MouseButton::Left,
140 cx.listener(move |this, _ev, _window, _cx| {
141 this.should_move = false;
142 }),
143 )
144 .on_mouse_down(
145 gpui::MouseButton::Left,
146 cx.listener(move |this, _ev, _window, _cx| {
147 this.should_move = true;
148 }),
149 )
150 .on_mouse_move(cx.listener(move |this, _ev, window, _| {
151 if this.should_move {
152 this.should_move = false;
153 window.start_window_move();
154 }
155 }))
156 })
157 .map(|this| {
158 // Note: On Windows the title bar behavior is handled by the platform implementation.
159 this.id(self.id.clone())
160 .when(self.platform_style == PlatformStyle::Mac, |this| {
161 this.on_click(|event, window, _| {
162 if event.click_count() == 2 {
163 window.titlebar_double_click();
164 }
165 })
166 })
167 .when(self.platform_style == PlatformStyle::Linux, |this| {
168 this.on_click(|event, window, _| {
169 if event.click_count() == 2 {
170 window.zoom_window();
171 }
172 })
173 })
174 })
175 .map(|this| {
176 if window.is_fullscreen() {
177 this.pl_2()
178 } else if self.platform_style == PlatformStyle::Mac
179 && !(sidebar.open && sidebar.side == SidebarSide::Left)
180 {
181 this.pl(px(TRAFFIC_LIGHT_PADDING))
182 } else if let Some(button_layout) =
183 button_layout.filter(|button_layout| button_layout.left[0].is_some())
184 {
185 this.child(platform_linux::LinuxWindowControls::new(
186 "left-window-controls",
187 button_layout.left,
188 close_action.as_ref().boxed_clone(),
189 ))
190 } else {
191 this.pl_2()
192 }
193 })
194 .map(|el| match decorations {
195 Decorations::Server => el,
196 Decorations::Client { tiling, .. } => el
197 .when(
198 !(tiling.top || tiling.right)
199 && !(sidebar.open && sidebar.side == SidebarSide::Right),
200 |el| el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
201 )
202 .when(
203 !(tiling.top || tiling.left)
204 && !(sidebar.open && sidebar.side == SidebarSide::Left),
205 |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
206 )
207 // this border is to avoid a transparent gap in the rounded corners
208 .mt(px(-1.))
209 .mb(px(-1.))
210 .border(px(1.))
211 .border_color(titlebar_color),
212 })
213 .bg(titlebar_color)
214 .content_stretch()
215 .child(
216 div()
217 .id(self.id.clone())
218 .flex()
219 .flex_row()
220 .items_center()
221 .justify_between()
222 .overflow_x_hidden()
223 .w_full()
224 .children(children),
225 )
226 .when(!window.is_fullscreen(), |title_bar| {
227 match self.platform_style {
228 PlatformStyle::Mac => title_bar,
229 PlatformStyle::Linux => {
230 if matches!(decorations, Decorations::Client { .. }) {
231 let mut result = title_bar;
232 if let Some(button_layout) = button_layout
233 .filter(|button_layout| button_layout.right[0].is_some())
234 {
235 result = result.child(platform_linux::LinuxWindowControls::new(
236 "right-window-controls",
237 button_layout.right,
238 close_action.as_ref().boxed_clone(),
239 ));
240 }
241
242 result.when(supported_controls.window_menu, |titlebar| {
243 titlebar.on_mouse_down(MouseButton::Right, move |ev, window, _| {
244 window.show_window_menu(ev.position)
245 })
246 })
247 } else {
248 title_bar
249 }
250 }
251 PlatformStyle::Windows => {
252 title_bar.child(platform_windows::WindowsWindowControls::new(height))
253 }
254 }
255 });
256
257 v_flex()
258 .w_full()
259 .child(title_bar)
260 .child(self.system_window_tabs.clone().into_any_element())
261 }
262}
263
264impl ParentElement for PlatformTitleBar {
265 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
266 self.children.extend(elements)
267 }
268}