1use fluent_core::{Theme, ThemeProvider as _};
2use gpui::{
3 div, prelude::*, px, svg, ClickEvent, Context, IntoElement, MouseButton, MouseDownEvent,
4 Render, SharedString, Window,
5};
6
7const DOUBLE_CLICK_WINDOW_MS: u64 = 350;
10
11pub struct TitleBar {
17 pub title: SharedString,
18 pub icon: Option<SharedString>,
19 pub show_controls: bool,
20 last_press_ms: u64,
21}
22
23impl TitleBar {
24 pub fn new(cx: &mut Context<Self>, title: impl Into<SharedString>) -> Self {
25 cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
26 Self {
27 title: title.into(),
28 icon: None,
29 show_controls: true,
30 last_press_ms: 0,
31 }
32 }
33
34 pub fn show_controls(mut self, show: bool) -> Self {
35 self.show_controls = show;
36 self
37 }
38
39 pub fn icon(mut self, path: impl Into<SharedString>) -> Self {
42 self.icon = Some(path.into());
43 self
44 }
45}
46
47impl Render for TitleBar {
48 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
49 let theme = cx.theme();
50 let colors = theme.colors.clone();
51 let spacing = theme.spacing;
52 let typography = theme.typography;
53
54 let title = self.title.clone();
55 let show_controls = self.show_controls;
56
57 let bar_bg = colors.surface_dim;
58 let fg = colors.on_neutral;
59 let ctrl_hover = colors.subtle_hover;
60 let close_hover: gpui::Hsla = gpui::rgb(0xC42B1C).into();
61
62 let drag_handler = cx.listener(
65 |this: &mut TitleBar, _: &MouseDownEvent, window: &mut Window, _| {
66 let now = std::time::SystemTime::now()
67 .duration_since(std::time::UNIX_EPOCH)
68 .map(|d| d.as_millis() as u64)
69 .unwrap_or(0);
70 if now.saturating_sub(this.last_press_ms) < DOUBLE_CLICK_WINDOW_MS {
71 this.last_press_ms = 0;
72 window.zoom_window();
73 return;
74 }
75 this.last_press_ms = now;
76 window.start_window_move();
77 },
78 );
79
80 let min_handler =
81 cx.listener(|_: &mut TitleBar, _: &ClickEvent, window: &mut Window, _| {
82 window.minimize_window();
83 });
84 let max_handler =
85 cx.listener(|_: &mut TitleBar, _: &ClickEvent, window: &mut Window, _| {
86 window.zoom_window();
87 });
88 let close_handler =
89 cx.listener(|_: &mut TitleBar, _: &ClickEvent, window: &mut Window, _| {
90 window.remove_window();
91 });
92
93 let icon = self.icon.clone();
96 let accent_fg = colors.on_neutral_accent;
97 let mut title_area = div()
98 .flex_1()
99 .h_full()
100 .flex()
101 .items_center()
102 .gap(px(spacing.sm))
103 .pl(px(spacing.md))
104 .text_size(px(typography.caption.size))
105 .text_color(fg)
106 .on_mouse_down(MouseButton::Left, drag_handler);
107 if let Some(path) = icon {
108 title_area = title_area.child(svg().path(path).size(px(16.0)).text_color(accent_fg));
109 }
110 let title_area = title_area.child(title);
111
112 let bar = div()
113 .flex()
114 .flex_row()
115 .h(px(36.0))
116 .bg(bar_bg)
117 .child(title_area);
118
119 if !show_controls {
120 return bar;
121 }
122
123 bar.child(
125 div()
126 .flex()
127 .flex_row()
128 .h_full()
129 .child(
130 div()
131 .id("titlebar-min")
132 .w(px(46.0))
133 .h_full()
134 .flex()
135 .items_center()
136 .justify_center()
137 .cursor_pointer()
138 .hover(move |s| s.bg(ctrl_hover))
139 .on_click(min_handler)
140 .child(
141 svg()
142 .path("icons/minimize.svg")
143 .size(px(10.0))
144 .text_color(fg),
145 ),
146 )
147 .child(
148 div()
149 .id("titlebar-max")
150 .w(px(46.0))
151 .h_full()
152 .flex()
153 .items_center()
154 .justify_center()
155 .cursor_pointer()
156 .hover(move |s| s.bg(ctrl_hover))
157 .on_click(max_handler)
158 .child(
159 svg()
160 .path("icons/maximize.svg")
161 .size(px(10.0))
162 .text_color(fg),
163 ),
164 )
165 .child(
166 div()
167 .id("titlebar-close")
168 .w(px(46.0))
169 .h_full()
170 .flex()
171 .items_center()
172 .justify_center()
173 .cursor_pointer()
174 .hover(move |s| s.bg(close_hover))
175 .on_click(close_handler)
176 .child(
177 svg()
178 .path("icons/dismiss.svg")
179 .size(px(10.0))
180 .text_color(fg),
181 ),
182 ),
183 )
184 }
185}