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