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