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