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