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