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}
36
37impl PlatformTitleBar {
38 pub fn new(id: impl Into<ElementId>, cx: &mut Context<Self>) -> Self {
39 let platform_style = PlatformStyle::platform();
40 let system_window_tabs = cx.new(|_cx| SystemWindowTabs::new());
41
42 Self {
43 id: id.into(),
44 platform_style,
45 children: SmallVec::new(),
46 should_move: false,
47 system_window_tabs,
48 workspace_sidebar_open: 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 is_multi_workspace_enabled(cx: &App) -> bool {
85 cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
86 }
87}
88
89impl Render for PlatformTitleBar {
90 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
91 let supported_controls = window.window_controls();
92 let decorations = window.window_decorations();
93 let height = platform_title_bar_height(window);
94 let titlebar_color = self.title_bar_color(window, cx);
95 let close_action = Box::new(workspace::CloseWindow);
96 let children = mem::take(&mut self.children);
97
98 let is_multiworkspace_sidebar_open =
99 PlatformTitleBar::is_multi_workspace_enabled(cx) && self.is_workspace_sidebar_open();
100
101 let title_bar = h_flex()
102 .window_control_area(WindowControlArea::Drag)
103 .w_full()
104 .h(height)
105 .map(|this| {
106 this.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
107 this.should_move = false;
108 }))
109 .on_mouse_up(
110 gpui::MouseButton::Left,
111 cx.listener(move |this, _ev, _window, _cx| {
112 this.should_move = false;
113 }),
114 )
115 .on_mouse_down(
116 gpui::MouseButton::Left,
117 cx.listener(move |this, _ev, _window, _cx| {
118 this.should_move = true;
119 }),
120 )
121 .on_mouse_move(cx.listener(move |this, _ev, window, _| {
122 if this.should_move {
123 this.should_move = false;
124 window.start_window_move();
125 }
126 }))
127 })
128 .map(|this| {
129 // Note: On Windows the title bar behavior is handled by the platform implementation.
130 this.id(self.id.clone())
131 .when(self.platform_style == PlatformStyle::Mac, |this| {
132 this.on_click(|event, window, _| {
133 if event.click_count() == 2 {
134 window.titlebar_double_click();
135 }
136 })
137 })
138 .when(self.platform_style == PlatformStyle::Linux, |this| {
139 this.on_click(|event, window, _| {
140 if event.click_count() == 2 {
141 window.zoom_window();
142 }
143 })
144 })
145 })
146 .map(|this| {
147 if window.is_fullscreen() {
148 this.pl_2()
149 } else if self.platform_style == PlatformStyle::Mac
150 && !is_multiworkspace_sidebar_open
151 {
152 this.pl(px(TRAFFIC_LIGHT_PADDING))
153 } else {
154 this.pl_2()
155 }
156 })
157 .map(|el| match decorations {
158 Decorations::Server => el,
159 Decorations::Client { tiling, .. } => el
160 .when(!(tiling.top || tiling.right), |el| {
161 el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
162 })
163 .when(
164 !(tiling.top || tiling.left) && !is_multiworkspace_sidebar_open,
165 |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
166 )
167 // this border is to avoid a transparent gap in the rounded corners
168 .mt(px(-1.))
169 .mb(px(-1.))
170 .border(px(1.))
171 .border_color(titlebar_color),
172 })
173 .bg(titlebar_color)
174 .content_stretch()
175 .child(
176 div()
177 .id(self.id.clone())
178 .flex()
179 .flex_row()
180 .items_center()
181 .justify_between()
182 .overflow_x_hidden()
183 .w_full()
184 .children(children),
185 )
186 .when(!window.is_fullscreen(), |title_bar| {
187 match self.platform_style {
188 PlatformStyle::Mac => title_bar,
189 PlatformStyle::Linux => {
190 if matches!(decorations, Decorations::Client { .. }) {
191 title_bar
192 .child(platform_linux::LinuxWindowControls::new(close_action))
193 .when(supported_controls.window_menu, |titlebar| {
194 titlebar
195 .on_mouse_down(MouseButton::Right, move |ev, window, _| {
196 window.show_window_menu(ev.position)
197 })
198 })
199 } else {
200 title_bar
201 }
202 }
203 PlatformStyle::Windows => {
204 title_bar.child(platform_windows::WindowsWindowControls::new(height))
205 }
206 }
207 });
208
209 v_flex()
210 .w_full()
211 .child(title_bar)
212 .child(self.system_window_tabs.clone().into_any_element())
213 }
214}
215
216impl ParentElement for PlatformTitleBar {
217 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
218 self.children.extend(elements)
219 }
220}