1use std::rc::Rc;
2
3use crate::{
4 ActiveTheme, Icon, IconName, InteractiveElementExt as _, Sizable as _, StyledExt, h_flex,
5};
6use gpui::{
7 AnyElement, App, Background, ClickEvent, Context, Decorations, Hsla, InteractiveElement,
8 IntoElement, MouseButton, ParentElement, Pixels, Render, RenderOnce, Rgba,
9 StatefulInteractiveElement as _, StyleRefinement, Styled, TitlebarOptions, Window,
10 WindowControlArea, WindowOptions, div, linear_color_stop, linear_gradient,
11 prelude::FluentBuilder as _, px,
12};
13use gpui_base::TestSupportExt as _;
14use smallvec::SmallVec;
15
16pub const TITLE_BAR_HEIGHT: Pixels = px(34.);
17#[cfg(target_os = "macos")]
18const TITLE_BAR_LEFT_PADDING: Pixels = px(80.);
19#[cfg(not(target_os = "macos"))]
20const TITLE_BAR_LEFT_PADDING: Pixels = px(12.);
21
22fn default_title_bar_background(title_bar: Hsla, background: Hsla) -> Background {
23 let title_bar_rgb = title_bar.to_rgb();
24 let background_rgb = background.to_rgb();
25 let mixed = Hsla::from(Rgba {
26 r: title_bar_rgb.r * 0.55 + background_rgb.r * 0.45,
27 g: title_bar_rgb.g * 0.55 + background_rgb.g * 0.45,
28 b: title_bar_rgb.b * 0.55 + background_rgb.b * 0.45,
29 a: title_bar_rgb.a * 0.55 + background_rgb.a * 0.45,
30 });
31
32 linear_gradient(
33 180.,
34 linear_color_stop(mixed, 0.),
35 linear_color_stop(title_bar, 1.),
36 )
37}
38
39#[derive(IntoElement)]
43pub struct TitleBar {
44 style: StyleRefinement,
45 children: SmallVec<[AnyElement; 1]>,
46 on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
47}
48
49impl TitleBar {
50 pub fn new() -> Self {
52 Self {
53 style: StyleRefinement::default(),
54 children: SmallVec::new(),
55 on_close_window: None,
56 }
57 }
58
59 pub fn title_bar_options() -> TitlebarOptions {
61 TitlebarOptions {
62 title: None,
63 appears_transparent: true,
64 traffic_light_position: Some(gpui::point(px(9.0), px(9.0))),
65 }
66 }
67
68 pub fn window_options() -> WindowOptions {
83 WindowOptions {
84 titlebar: Some(Self::title_bar_options()),
85 app_owns_titlebar_drag: true,
90 ..Default::default()
91 }
92 }
93
94 pub fn on_close_window(
97 mut self,
98 f: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
99 ) -> Self {
100 if cfg!(target_os = "linux") {
101 self.on_close_window = Some(Rc::new(Box::new(f)));
102 }
103 self
104 }
105}
106
107#[derive(IntoElement, Clone)]
112enum ControlIcon {
113 Minimize,
114 Restore,
115 Maximize,
116 Close {
117 on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
118 },
119}
120
121impl ControlIcon {
122 fn minimize() -> Self {
123 Self::Minimize
124 }
125
126 fn restore() -> Self {
127 Self::Restore
128 }
129
130 fn maximize() -> Self {
131 Self::Maximize
132 }
133
134 fn close(on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>) -> Self {
135 Self::Close { on_close_window }
136 }
137
138 fn id(&self) -> &'static str {
139 match self {
140 Self::Minimize => "minimize",
141 Self::Restore => "restore",
142 Self::Maximize => "maximize",
143 Self::Close { .. } => "close",
144 }
145 }
146
147 fn icon(&self) -> IconName {
148 match self {
149 Self::Minimize => IconName::WindowMinimize,
150 Self::Restore => IconName::WindowRestore,
151 Self::Maximize => IconName::WindowMaximize,
152 Self::Close { .. } => IconName::WindowClose,
153 }
154 }
155
156 fn window_control_area(&self) -> WindowControlArea {
157 match self {
158 Self::Minimize => WindowControlArea::Min,
159 Self::Restore | Self::Maximize => WindowControlArea::Max,
160 Self::Close { .. } => WindowControlArea::Close,
161 }
162 }
163
164 fn is_close(&self) -> bool {
165 matches!(self, Self::Close { .. })
166 }
167
168 #[inline]
169 fn hover_fg(&self, cx: &App) -> Hsla {
170 if self.is_close() {
171 cx.theme().danger_foreground
172 } else {
173 cx.theme().secondary_foreground
174 }
175 }
176
177 #[inline]
178 fn hover_bg(&self, cx: &App) -> Hsla {
179 if self.is_close() {
180 cx.theme().danger
181 } else {
182 cx.theme().secondary_hover
183 }
184 }
185
186 #[inline]
187 fn active_bg(&self, cx: &mut App) -> Hsla {
188 if self.is_close() {
189 cx.theme().danger_active
190 } else {
191 cx.theme().secondary_active
192 }
193 }
194}
195
196impl RenderOnce for ControlIcon {
197 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
198 let is_linux = cfg!(target_os = "linux");
199 let is_windows = cfg!(target_os = "windows");
200 let hover_fg = self.hover_fg(cx);
201 let hover_bg = self.hover_bg(cx);
202 let active_bg = self.active_bg(cx);
203 let icon = self.clone();
204 let on_close_window = match &self {
205 ControlIcon::Close { on_close_window } => on_close_window.clone(),
206 _ => None,
207 };
208
209 div()
210 .id(self.id())
211 .test_support()
212 .flex()
213 .w(TITLE_BAR_HEIGHT)
214 .h_full()
215 .flex_shrink_0()
216 .justify_center()
217 .content_center()
218 .items_center()
219 .text_color(cx.theme().foreground)
220 .hover(|style| style.bg(hover_bg).text_color(hover_fg))
221 .active(|style| style.bg(active_bg).text_color(hover_fg))
222 .when(is_windows, |this| {
223 this.window_control_area(self.window_control_area())
224 })
225 .when(is_linux, |this| {
226 this.on_mouse_down(MouseButton::Left, move |_, window, cx| {
227 window.prevent_default();
228 cx.stop_propagation();
229 })
230 .on_click(move |_, window, cx| {
231 cx.stop_propagation();
232 match icon {
233 Self::Minimize => window.minimize_window(),
234 Self::Restore | Self::Maximize => window.zoom_window(),
235 Self::Close { .. } => {
236 if let Some(f) = on_close_window.clone() {
237 f(&ClickEvent::default(), window, cx);
238 } else {
239 window.remove_window();
240 }
241 }
242 }
243 })
244 })
245 .child(Icon::new(self.icon()).small())
246 }
247}
248
249#[derive(IntoElement)]
250struct WindowControls {
251 on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
252}
253
254impl RenderOnce for WindowControls {
255 fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
256 if cfg!(target_os = "macos") || cfg!(target_family = "wasm") {
257 return div().id("window-controls").test_support();
258 }
259
260 #[cfg(target_os = "linux")]
270 if !matches!(window.window_decorations(), Decorations::Client { .. }) {
271 return div().id("window-controls").test_support();
272 }
273
274 let supported = window.window_controls();
278
279 h_flex()
280 .id("window-controls")
281 .test_support()
282 .items_center()
283 .flex_shrink_0()
284 .h_full()
285 .when(supported.minimize, |this| {
286 this.child(ControlIcon::minimize())
287 })
288 .when(supported.maximize, |this| {
289 this.child(if window.is_maximized() {
290 ControlIcon::restore()
291 } else {
292 ControlIcon::maximize()
293 })
294 })
295 .child(ControlIcon::close(self.on_close_window))
296 }
297}
298
299impl Styled for TitleBar {
300 fn style(&mut self) -> &mut gpui::StyleRefinement {
301 &mut self.style
302 }
303}
304
305impl ParentElement for TitleBar {
306 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
307 self.children.extend(elements);
308 }
309}
310
311struct TitleBarState {
312 should_move: bool,
313}
314
315impl Render for TitleBarState {
317 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
318 div()
319 }
320}
321
322impl RenderOnce for TitleBar {
323 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
324 let is_client_decorated = matches!(window.window_decorations(), Decorations::Client { .. });
325 let is_web = cfg!(target_family = "wasm");
326 let is_linux = cfg!(target_os = "linux");
327 let is_macos = cfg!(target_os = "macos");
328
329 let state = window.use_state(cx, |_, _| TitleBarState { should_move: false });
330
331 div().flex_shrink_0().child(
332 div()
333 .id("title-bar")
334 .flex()
335 .flex_row()
336 .items_center()
337 .justify_between()
338 .h(TITLE_BAR_HEIGHT)
339 .pl(TITLE_BAR_LEFT_PADDING)
340 .border_b_1()
341 .border_color(cx.theme().title_bar_border)
342 .bg(default_title_bar_background(
343 cx.theme().title_bar,
344 cx.theme().background,
345 ))
346 .refine_style(&self.style)
347 .when(is_linux, |this| {
348 this.on_double_click(|_, window, _| window.zoom_window())
349 })
350 .when(is_macos, |this| {
351 this.on_double_click(|_, window, _| window.titlebar_double_click())
352 })
353 .test_support()
355 .on_mouse_down_out(window.listener_for(&state, |state, _, _, _| {
356 state.should_move = false;
357 }))
358 .on_mouse_down(
359 MouseButton::Left,
360 window.listener_for(&state, |state, _, _, _| {
361 state.should_move = true;
362 }),
363 )
364 .on_mouse_up(
365 MouseButton::Left,
366 window.listener_for(&state, |state, _, _, _| {
367 state.should_move = false;
368 }),
369 )
370 .on_mouse_move(window.listener_for(&state, |state, _, window, _| {
371 if state.should_move {
372 state.should_move = false;
373 window.start_window_move();
374 }
375 }))
376 .child(
377 h_flex()
378 .id("bar")
379 .h_full()
380 .justify_between()
381 .flex_shrink_0()
382 .flex_1()
383 .when(!is_web, |this| {
384 this.window_control_area(WindowControlArea::Drag)
385 .when(window.is_fullscreen(), |this| this.pl_3())
386 .when(is_linux && is_client_decorated, |this| {
387 this.child(
388 div()
389 .top_0()
390 .left_0()
391 .absolute()
392 .size_full()
393 .h_full()
394 .on_mouse_down(
395 MouseButton::Right,
396 move |ev, window, _| {
397 window.show_window_menu(ev.position)
398 },
399 ),
400 )
401 })
402 })
403 .children(self.children),
404 )
405 .child(WindowControls {
406 on_close_window: self.on_close_window,
407 }),
408 )
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415 use gpui::{Rgba, linear_color_stop, linear_gradient};
416
417 #[test]
418 fn test_default_title_bar_background() {
419 let title_bar = Hsla::black();
420 let background = Hsla::white();
421
422 assert_eq!(
423 default_title_bar_background(title_bar, background),
424 linear_gradient(
425 180.,
426 linear_color_stop(
427 Hsla::from(Rgba {
428 r: 0.45,
429 g: 0.45,
430 b: 0.45,
431 a: 1.,
432 }),
433 0.,
434 ),
435 linear_color_stop(title_bar, 1.),
436 )
437 );
438 }
439}