1use std::rc::Rc;
6
7use gpui_kit::{
8 AnyElement, App, ClickEvent, Context, Decorations, Hsla, InteractiveElement as _, IntoElement,
9 MouseButton, ParentElement, Render, RenderOnce, StatefulInteractiveElement as _,
10 StyleRefinement, Styled, TitlebarOptions, Window, WindowControlArea, WindowOptions,
11 base::{InteractiveElementExt as _, StyledExt as _, TestSupportExt as _},
12 div, point,
13 prelude::FluentBuilder as _,
14};
15
16use crate::{ActiveTheme as _, Icon};
17use gpui_kit::assets::IconName;
18
19type CloseHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
20type DoubleClickHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
21
22#[derive(IntoElement)]
44pub struct TitleBar {
45 children: Vec<AnyElement>,
46 style: StyleRefinement,
47 inset: bool,
48 on_close_window: Option<CloseHandler>,
49 on_double_click: Option<DoubleClickHandler>,
50}
51
52impl Default for TitleBar {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58impl TitleBar {
59 pub fn new() -> Self {
61 Self {
62 children: Vec::new(),
63 style: StyleRefinement::default(),
64 inset: true,
65 on_close_window: None,
66 on_double_click: None,
67 }
68 }
69
70 pub fn on_double_click(
75 mut self,
76 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
77 ) -> Self {
78 self.on_double_click = Some(Rc::new(handler));
79 self
80 }
81
82 pub fn title_bar_options(cx: &App) -> TitlebarOptions {
86 let position = cx.theme().metrics.window_controls_position;
87 TitlebarOptions {
88 title: None,
89 appears_transparent: true,
90 traffic_light_position: Some(point(position, position)),
91 }
92 }
93
94 pub fn window_options(cx: &App) -> WindowOptions {
97 WindowOptions {
98 titlebar: Some(Self::title_bar_options(cx)),
99 app_owns_titlebar_drag: true,
103 ..Default::default()
104 }
105 }
106
107 pub fn inset(mut self, inset: bool) -> Self {
112 self.inset = inset;
113 self
114 }
115
116 pub fn on_close_window(
119 mut self,
120 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
121 ) -> Self {
122 if cfg!(target_os = "linux") {
123 self.on_close_window = Some(Rc::new(handler));
124 }
125 self
126 }
127}
128
129impl Styled for TitleBar {
130 fn style(&mut self) -> &mut StyleRefinement {
131 &mut self.style
132 }
133}
134
135impl ParentElement for TitleBar {
136 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
137 self.children.extend(elements);
138 }
139}
140
141struct DragState {
143 pending: bool,
145 on_control: bool,
147}
148
149impl Render for DragState {
150 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
151 div()
152 }
153}
154
155impl RenderOnce for TitleBar {
156 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
157 let is_macos = cfg!(target_os = "macos");
158 let is_linux = cfg!(target_os = "linux");
159 let is_web = cfg!(target_family = "wasm");
160 let client_decorated = matches!(window.window_decorations(), Decorations::Client { .. });
161 let macos_inset = is_macos && self.inset && !window.is_fullscreen();
162 let draws_controls = draws_window_controls(window);
168 let (height, offset, inset, gap) = {
169 let theme = cx.theme();
170 (
171 theme.metrics.title_bar,
172 theme.metrics.title_bar_content_offset,
173 theme.metrics.window_controls_inset,
174 theme.base.spacing.xs,
175 )
176 };
177
178 let drag = window.use_state(cx, |_, _| DragState {
179 pending: false,
180 on_control: false,
181 });
182 let on_double_click = self.on_double_click.clone();
183
184 div()
185 .id("title-bar")
186 .on_double_click(window.listener_for(&drag, move |drag, event, window, cx| {
187 if drag.on_control {
190 return;
191 }
192 match &on_double_click {
193 Some(handler) => handler(event, window, cx),
194 None if is_macos => window.titlebar_double_click(),
195 None if is_linux => window.zoom_window(),
196 None => {}
197 }
198 }))
199 .test_support()
200 .h_flex()
201 .flex_shrink_0()
202 .w_full()
203 .h(height)
204 .when(!draws_controls, |this| this.pt(offset))
205 .map(|this| {
206 if macos_inset {
207 this.pl(inset)
208 } else {
209 this.pl_3()
210 }
211 })
212 .when(!draws_controls, |this| this.pr_3())
213 .refine_style(&self.style)
214 .on_mouse_down(
215 MouseButton::Left,
216 window.listener_for(&drag, |drag, _, window, _| {
217 drag.on_control = window.default_prevented();
221 drag.pending = !drag.on_control;
222 }),
223 )
224 .on_mouse_up(
225 MouseButton::Left,
226 window.listener_for(&drag, |drag, _, _, _| drag.pending = false),
227 )
228 .on_mouse_down_out(window.listener_for(&drag, |drag, _, _, _| drag.pending = false))
229 .on_mouse_move(window.listener_for(&drag, |drag, _, window, _| {
230 if drag.pending {
231 drag.pending = false;
232 window.start_window_move();
233 }
234 }))
235 .child(
236 div()
237 .id("title-bar-content")
238 .h_flex()
239 .flex_1()
240 .min_w_0()
241 .h_full()
242 .gap(gap)
243 .when(draws_controls, |this| this.pt(offset).pr_3())
244 .when(!is_web, |this| {
245 this.window_control_area(WindowControlArea::Drag).when(
246 is_linux && client_decorated,
247 |this| {
248 this.on_mouse_down(MouseButton::Right, |event, window, _| {
249 window.show_window_menu(event.position)
250 })
251 },
252 )
253 })
254 .children(self.children),
255 )
256 .child(WindowControls {
257 on_close_window: self.on_close_window,
258 })
259 }
260}
261
262#[derive(IntoElement)]
264struct WindowControls {
265 on_close_window: Option<CloseHandler>,
266}
267
268fn draws_window_controls(window: &Window) -> bool {
273 cfg!(target_os = "windows")
274 || (cfg!(target_os = "linux")
275 && matches!(window.window_decorations(), Decorations::Client { .. }))
276}
277
278impl RenderOnce for WindowControls {
279 fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
280 if !draws_window_controls(window) {
281 return div().id("window-controls");
282 }
283 let supported = window.window_controls();
284 div()
285 .id("window-controls")
286 .h_flex()
287 .flex_shrink_0()
288 .h_full()
289 .when(supported.minimize, |this| this.child(Control::Minimize))
290 .when(supported.maximize, |this| {
291 this.child(if window.is_maximized() {
292 Control::Restore
293 } else {
294 Control::Maximize
295 })
296 })
297 .child(Control::Close {
298 on_close_window: self.on_close_window,
299 })
300 }
301}
302
303#[derive(IntoElement, Clone)]
306enum Control {
307 Minimize,
308 Maximize,
309 Restore,
310 Close {
311 on_close_window: Option<CloseHandler>,
312 },
313}
314
315impl Control {
316 fn id(&self) -> &'static str {
317 match self {
318 Self::Minimize => "minimize",
319 Self::Maximize => "maximize",
320 Self::Restore => "restore",
321 Self::Close { .. } => "close",
322 }
323 }
324
325 fn icon(&self) -> Icon {
326 Icon::from(match self {
327 Self::Minimize => IconName::WindowMinimize,
328 Self::Maximize => IconName::WindowMaximize,
329 Self::Restore => IconName::WindowRestore,
330 Self::Close { .. } => IconName::WindowClose,
331 })
332 }
333
334 fn area(&self) -> WindowControlArea {
335 match self {
336 Self::Minimize => WindowControlArea::Min,
337 Self::Maximize | Self::Restore => WindowControlArea::Max,
338 Self::Close { .. } => WindowControlArea::Close,
339 }
340 }
341
342 fn is_close(&self) -> bool {
343 matches!(self, Self::Close { .. })
344 }
345}
346
347impl RenderOnce for Control {
348 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
349 let theme = cx.theme();
350 let (hover_bg, hover_fg): (Hsla, Hsla) = if self.is_close() {
351 (theme.destructive(), theme.destructive_foreground())
352 } else {
353 (theme.secondary(), theme.foreground())
354 };
355 let active_bg = if self.is_close() {
356 theme.destructive().opacity(0.8)
357 } else {
358 theme.selected
359 };
360 let control = self.clone();
361 let on_close_window = match &self {
362 Self::Close { on_close_window } => on_close_window.clone(),
363 _ => None,
364 };
365 div()
366 .id(self.id())
367 .h_flex()
368 .justify_center()
369 .flex_shrink_0()
370 .w(theme.metrics.window_control_width)
371 .h_full()
372 .text_color(theme.muted_foreground())
373 .hover(|style| style.bg(hover_bg).text_color(hover_fg))
374 .active(|style| style.bg(active_bg).text_color(hover_fg))
375 .when(cfg!(target_os = "windows"), |this| {
376 this.window_control_area(self.area())
377 })
378 .when(cfg!(target_os = "linux"), |this| {
379 this.on_mouse_down(MouseButton::Left, |_, window, cx| {
380 window.prevent_default();
381 cx.stop_propagation();
382 })
383 .on_click(move |_, window, cx| {
384 cx.stop_propagation();
385 match &control {
386 Control::Minimize => window.minimize_window(),
387 Control::Maximize | Control::Restore => window.zoom_window(),
388 Control::Close { .. } => match &on_close_window {
389 Some(handler) => handler(&ClickEvent::default(), window, cx),
390 None => window.remove_window(),
391 },
392 }
393 })
394 })
395 .child(self.icon().size_3p5())
396 }
397}