1use std::rc::Rc;
2
3use crate::{h_flex, ActiveTheme, Icon, IconName, InteractiveElementExt as _, Sizable as _};
4use gpui::{
5 div, prelude::FluentBuilder as _, px, relative, AnyElement, App, ClickEvent, Div, Element,
6 Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce,
7 Stateful, StatefulInteractiveElement as _, Style, Styled, TitlebarOptions, Window,
8 WindowControlArea,
9};
10
11pub const TITLE_BAR_HEIGHT: Pixels = px(34.);
12#[cfg(target_os = "macos")]
13const TITLE_BAR_LEFT_PADDING: Pixels = px(80.);
14#[cfg(not(target_os = "macos"))]
15const TITLE_BAR_LEFT_PADDING: Pixels = px(12.);
16
17#[derive(IntoElement)]
21pub struct TitleBar {
22 base: Stateful<Div>,
23 children: Vec<AnyElement>,
24 on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
25}
26
27impl TitleBar {
28 pub fn new() -> Self {
30 Self {
31 base: div().id("title-bar").pl(TITLE_BAR_LEFT_PADDING),
32 children: Vec::new(),
33 on_close_window: None,
34 }
35 }
36
37 pub fn title_bar_options() -> TitlebarOptions {
39 TitlebarOptions {
40 title: None,
41 appears_transparent: true,
42 traffic_light_position: Some(gpui::point(px(9.0), px(9.0))),
43 }
44 }
45
46 pub fn on_close_window(
49 mut self,
50 f: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
51 ) -> Self {
52 if cfg!(target_os = "linux") {
53 self.on_close_window = Some(Rc::new(Box::new(f)));
54 }
55 self
56 }
57}
58
59#[derive(IntoElement, Clone)]
64enum ControlIcon {
65 Minimize,
66 Restore,
67 Maximize,
68 Close {
69 on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
70 },
71}
72
73impl ControlIcon {
74 fn minimize() -> Self {
75 Self::Minimize
76 }
77
78 fn restore() -> Self {
79 Self::Restore
80 }
81
82 fn maximize() -> Self {
83 Self::Maximize
84 }
85
86 fn close(on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>) -> Self {
87 Self::Close { on_close_window }
88 }
89
90 fn id(&self) -> &'static str {
91 match self {
92 Self::Minimize => "minimize",
93 Self::Restore => "restore",
94 Self::Maximize => "maximize",
95 Self::Close { .. } => "close",
96 }
97 }
98
99 fn icon(&self) -> IconName {
100 match self {
101 Self::Minimize => IconName::WindowMinimize,
102 Self::Restore => IconName::WindowRestore,
103 Self::Maximize => IconName::WindowMaximize,
104 Self::Close { .. } => IconName::WindowClose,
105 }
106 }
107
108 fn window_control_area(&self) -> WindowControlArea {
109 match self {
110 Self::Minimize => WindowControlArea::Min,
111 Self::Restore | Self::Maximize => WindowControlArea::Max,
112 Self::Close { .. } => WindowControlArea::Close,
113 }
114 }
115
116 fn is_close(&self) -> bool {
117 matches!(self, Self::Close { .. })
118 }
119
120 fn fg(&self, cx: &App) -> Hsla {
121 if cx.theme().mode.is_dark() {
122 crate::white()
123 } else {
124 crate::black()
125 }
126 }
127
128 fn hover_fg(&self, cx: &App) -> Hsla {
129 if self.is_close() || cx.theme().mode.is_dark() {
130 crate::white()
131 } else {
132 crate::black()
133 }
134 }
135
136 fn hover_bg(&self, cx: &App) -> Hsla {
137 if self.is_close() {
138 if cx.theme().mode.is_dark() {
139 crate::red_800()
140 } else {
141 crate::red_600()
142 }
143 } else if cx.theme().mode.is_dark() {
144 crate::stone_700()
145 } else {
146 crate::stone_200()
147 }
148 }
149}
150
151impl RenderOnce for ControlIcon {
152 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
153 let is_linux = cfg!(target_os = "linux");
154 let is_windows = cfg!(target_os = "windows");
155 let fg = self.fg(cx);
156 let hover_fg = self.hover_fg(cx);
157 let hover_bg = self.hover_bg(cx);
158 let icon = self.clone();
159 let on_close_window = match &self {
160 ControlIcon::Close { on_close_window } => on_close_window.clone(),
161 _ => None,
162 };
163
164 div()
165 .id(self.id())
166 .flex()
167 .w(TITLE_BAR_HEIGHT)
168 .h_full()
169 .justify_center()
170 .content_center()
171 .items_center()
172 .text_color(fg)
173 .when(is_windows, |this| {
174 this.window_control_area(self.window_control_area())
175 })
176 .when(is_linux, |this| {
177 this.on_mouse_down(MouseButton::Left, move |_, window, cx| {
178 window.prevent_default();
179 cx.stop_propagation();
180 })
181 .on_click(move |_, window, cx| {
182 cx.stop_propagation();
183 match icon {
184 Self::Minimize => window.minimize_window(),
185 Self::Restore | Self::Maximize => window.zoom_window(),
186 Self::Close { .. } => {
187 if let Some(f) = on_close_window.clone() {
188 f(&ClickEvent::default(), window, cx);
189 } else {
190 window.remove_window();
191 }
192 }
193 }
194 })
195 })
196 .hover(|style| style.bg(hover_bg).text_color(hover_fg))
197 .active(|style| style.bg(hover_bg.opacity(0.7)))
198 .child(Icon::new(self.icon()).small())
199 }
200}
201
202#[derive(IntoElement)]
203struct WindowControls {
204 on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
205}
206
207impl RenderOnce for WindowControls {
208 fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
209 if cfg!(target_os = "macos") {
210 return div().id("window-controls");
211 }
212
213 h_flex()
214 .id("window-controls")
215 .items_center()
216 .flex_shrink_0()
217 .h_full()
218 .child(
219 h_flex()
220 .justify_center()
221 .content_stretch()
222 .h_full()
223 .child(ControlIcon::minimize())
224 .child(if window.is_maximized() {
225 ControlIcon::restore()
226 } else {
227 ControlIcon::maximize()
228 }),
229 )
230 .child(ControlIcon::close(self.on_close_window))
231 }
232}
233
234impl Styled for TitleBar {
235 fn style(&mut self) -> &mut gpui::StyleRefinement {
236 self.base.style()
237 }
238}
239
240impl ParentElement for TitleBar {
241 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
242 self.children.extend(elements);
243 }
244}
245
246impl RenderOnce for TitleBar {
247 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
248 let is_linux = cfg!(target_os = "linux");
249 let is_macos = cfg!(target_os = "macos");
250
251 let paddings = self.base.style().padding.clone();
252 self.base.style().padding.left = None;
253 let left_padding = paddings.left.unwrap_or(TITLE_BAR_LEFT_PADDING.into());
254
255 div().flex_shrink_0().child(
256 self.base
257 .flex()
258 .flex_row()
259 .items_center()
260 .justify_between()
261 .h(TITLE_BAR_HEIGHT)
262 .border_b_1()
263 .border_color(cx.theme().title_bar_border)
264 .bg(cx.theme().title_bar)
265 .when(is_linux, |this| {
266 this.on_double_click(|_, window, _| window.zoom_window())
267 })
268 .when(is_macos, |this| {
269 this.on_double_click(|_, window, _| window.titlebar_double_click())
270 })
271 .child(
272 h_flex()
273 .id("bar")
274 .pl(left_padding)
275 .when(window.is_fullscreen(), |this| this.pl_3())
276 .window_control_area(WindowControlArea::Drag)
277 .h_full()
278 .justify_between()
279 .flex_shrink_0()
280 .flex_1()
281 .when(is_linux, |this| {
282 this.child(
283 div()
284 .top_0()
285 .left_0()
286 .absolute()
287 .size_full()
288 .h_full()
289 .child(TitleBarElement {}),
290 )
291 })
292 .children(self.children),
293 )
294 .child(WindowControls {
295 on_close_window: self.on_close_window,
296 }),
297 )
298 }
299}
300
301pub struct TitleBarElement {}
303
304impl IntoElement for TitleBarElement {
305 type Element = Self;
306
307 fn into_element(self) -> Self::Element {
308 self
309 }
310}
311
312impl Element for TitleBarElement {
313 type RequestLayoutState = ();
314
315 type PrepaintState = ();
316
317 fn id(&self) -> Option<gpui::ElementId> {
318 None
319 }
320
321 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
322 None
323 }
324
325 fn request_layout(
326 &mut self,
327 _: Option<&gpui::GlobalElementId>,
328 _: Option<&gpui::InspectorElementId>,
329 window: &mut Window,
330 cx: &mut App,
331 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
332 let mut style = Style::default();
333 style.flex_grow = 1.0;
334 style.flex_shrink = 1.0;
335 style.size.width = relative(1.).into();
336 style.size.height = relative(1.).into();
337
338 let id = window.request_layout(style, [], cx);
339 (id, ())
340 }
341
342 fn prepaint(
343 &mut self,
344 _: Option<&gpui::GlobalElementId>,
345 _: Option<&gpui::InspectorElementId>,
346 _: gpui::Bounds<Pixels>,
347 _: &mut Self::RequestLayoutState,
348 _window: &mut Window,
349 _cx: &mut App,
350 ) -> Self::PrepaintState {
351 }
352
353 #[allow(unused_variables)]
354 fn paint(
355 &mut self,
356 _: Option<&gpui::GlobalElementId>,
357 _: Option<&gpui::InspectorElementId>,
358 bounds: gpui::Bounds<Pixels>,
359 _: &mut Self::RequestLayoutState,
360 _: &mut Self::PrepaintState,
361 window: &mut Window,
362 cx: &mut App,
363 ) {
364 use gpui::{MouseButton, MouseMoveEvent, MouseUpEvent};
365 window.on_mouse_event(
366 move |ev: &MouseMoveEvent, _, window: &mut Window, cx: &mut App| {
367 if bounds.contains(&ev.position) && ev.pressed_button == Some(MouseButton::Left) {
368 window.start_window_move();
369 }
370 },
371 );
372
373 window.on_mouse_event(
374 move |ev: &MouseUpEvent, _, window: &mut Window, cx: &mut App| {
375 if bounds.contains(&ev.position) && ev.button == MouseButton::Right {
376 window.show_window_menu(ev.position);
377 }
378 },
379 );
380 }
381}