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