1use std::rc::Rc;
2
3use crate::{
4 ActiveTheme, Icon, IconName, InteractiveElementExt as _, Sizable as _, StyledExt, h_flex,
5};
6use gpui::{
7 AnyElement, App, ClickEvent, Context, Decorations, Hsla, InteractiveElement, IntoElement,
8 MouseButton, ParentElement, Pixels, Render, RenderOnce, StatefulInteractiveElement as _,
9 StyleRefinement, Styled, TitlebarOptions, Window, WindowControlArea, div,
10 prelude::FluentBuilder as _, px,
11};
12use smallvec::SmallVec;
13
14pub const TITLE_BAR_HEIGHT: Pixels = px(34.);
15#[cfg(target_os = "macos")]
16const TITLE_BAR_LEFT_PADDING: Pixels = px(80.);
17#[cfg(not(target_os = "macos"))]
18const TITLE_BAR_LEFT_PADDING: Pixels = px(12.);
19
20#[derive(IntoElement)]
24pub struct TitleBar {
25 style: StyleRefinement,
26 children: SmallVec<[AnyElement; 1]>,
27 on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
28}
29
30impl TitleBar {
31 pub fn new() -> Self {
33 Self {
34 style: StyleRefinement::default(),
35 children: SmallVec::new(),
36 on_close_window: None,
37 }
38 }
39
40 pub fn title_bar_options() -> TitlebarOptions {
42 TitlebarOptions {
43 title: None,
44 appears_transparent: true,
45 traffic_light_position: Some(gpui::point(px(9.0), px(9.0))),
46 }
47 }
48
49 pub fn on_close_window(
52 mut self,
53 f: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
54 ) -> Self {
55 if cfg!(target_os = "linux") {
56 self.on_close_window = Some(Rc::new(Box::new(f)));
57 }
58 self
59 }
60}
61
62#[derive(IntoElement, Clone)]
67enum ControlIcon {
68 Minimize,
69 Restore,
70 Maximize,
71 Close {
72 on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
73 },
74}
75
76impl ControlIcon {
77 fn minimize() -> Self {
78 Self::Minimize
79 }
80
81 fn restore() -> Self {
82 Self::Restore
83 }
84
85 fn maximize() -> Self {
86 Self::Maximize
87 }
88
89 fn close(on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>) -> Self {
90 Self::Close { on_close_window }
91 }
92
93 fn id(&self) -> &'static str {
94 match self {
95 Self::Minimize => "minimize",
96 Self::Restore => "restore",
97 Self::Maximize => "maximize",
98 Self::Close { .. } => "close",
99 }
100 }
101
102 fn icon(&self) -> IconName {
103 match self {
104 Self::Minimize => IconName::WindowMinimize,
105 Self::Restore => IconName::WindowRestore,
106 Self::Maximize => IconName::WindowMaximize,
107 Self::Close { .. } => IconName::WindowClose,
108 }
109 }
110
111 fn window_control_area(&self) -> WindowControlArea {
112 match self {
113 Self::Minimize => WindowControlArea::Min,
114 Self::Restore | Self::Maximize => WindowControlArea::Max,
115 Self::Close { .. } => WindowControlArea::Close,
116 }
117 }
118
119 fn is_close(&self) -> bool {
120 matches!(self, Self::Close { .. })
121 }
122
123 #[inline]
124 fn hover_fg(&self, cx: &App) -> Hsla {
125 if self.is_close() {
126 cx.theme().danger_foreground
127 } else {
128 cx.theme().secondary_foreground
129 }
130 }
131
132 #[inline]
133 fn hover_bg(&self, cx: &App) -> Hsla {
134 if self.is_close() {
135 cx.theme().danger
136 } else {
137 cx.theme().secondary_hover
138 }
139 }
140
141 #[inline]
142 fn active_bg(&self, cx: &mut App) -> Hsla {
143 if self.is_close() {
144 cx.theme().danger_active
145 } else {
146 cx.theme().secondary_active
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 hover_fg = self.hover_fg(cx);
156 let hover_bg = self.hover_bg(cx);
157 let active_bg = self.active_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 .flex_shrink_0()
170 .justify_center()
171 .content_center()
172 .items_center()
173 .text_color(cx.theme().foreground)
174 .hover(|style| style.bg(hover_bg).text_color(hover_fg))
175 .active(|style| style.bg(active_bg).text_color(hover_fg))
176 .when(is_windows, |this| {
177 this.window_control_area(self.window_control_area())
178 })
179 .when(is_linux, |this| {
180 this.on_mouse_down(MouseButton::Left, move |_, window, cx| {
181 window.prevent_default();
182 cx.stop_propagation();
183 })
184 .on_click(move |_, window, cx| {
185 cx.stop_propagation();
186 match icon {
187 Self::Minimize => window.minimize_window(),
188 Self::Restore | Self::Maximize => window.zoom_window(),
189 Self::Close { .. } => {
190 if let Some(f) = on_close_window.clone() {
191 f(&ClickEvent::default(), window, cx);
192 } else {
193 window.remove_window();
194 }
195 }
196 }
197 })
198 })
199 .child(Icon::new(self.icon()).small())
200 }
201}
202
203#[derive(IntoElement)]
204struct WindowControls {
205 on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
206}
207
208impl RenderOnce for WindowControls {
209 fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
210 if cfg!(target_os = "macos") {
211 return div().id("window-controls");
212 }
213
214 h_flex()
215 .id("window-controls")
216 .items_center()
217 .flex_shrink_0()
218 .h_full()
219 .child(ControlIcon::minimize())
220 .child(if window.is_maximized() {
221 ControlIcon::restore()
222 } else {
223 ControlIcon::maximize()
224 })
225 .child(ControlIcon::close(self.on_close_window))
226 }
227}
228
229impl Styled for TitleBar {
230 fn style(&mut self) -> &mut gpui::StyleRefinement {
231 &mut self.style
232 }
233}
234
235impl ParentElement for TitleBar {
236 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
237 self.children.extend(elements);
238 }
239}
240
241struct TitleBarState {
242 should_move: bool,
243}
244
245impl Render for TitleBarState {
247 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
248 div()
249 }
250}
251
252impl RenderOnce for TitleBar {
253 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
254 let is_client_decorated = matches!(window.window_decorations(), Decorations::Client { .. });
255 let is_linux = cfg!(target_os = "linux");
256 let is_macos = cfg!(target_os = "macos");
257
258 let state = window.use_state(cx, |_, _| TitleBarState { should_move: false });
259
260 div().flex_shrink_0().child(
261 div()
262 .id("title-bar")
263 .flex()
264 .flex_row()
265 .items_center()
266 .justify_between()
267 .h(TITLE_BAR_HEIGHT)
268 .pl(TITLE_BAR_LEFT_PADDING)
269 .border_b_1()
270 .border_color(cx.theme().title_bar_border)
271 .bg(cx.theme().title_bar)
272 .refine_style(&self.style)
273 .when(is_linux, |this| {
274 this.on_double_click(|_, window, _| window.zoom_window())
275 })
276 .when(is_macos, |this| {
277 this.on_double_click(|_, window, _| window.titlebar_double_click())
278 })
279 .on_mouse_down_out(window.listener_for(&state, |state, _, _, _| {
280 state.should_move = false;
281 }))
282 .on_mouse_down(
283 MouseButton::Left,
284 window.listener_for(&state, |state, _, _, _| {
285 state.should_move = true;
286 }),
287 )
288 .on_mouse_up(
289 MouseButton::Left,
290 window.listener_for(&state, |state, _, _, _| {
291 state.should_move = false;
292 }),
293 )
294 .on_mouse_move(window.listener_for(&state, |state, _, window, _| {
295 if state.should_move {
296 state.should_move = false;
297 window.start_window_move();
298 }
299 }))
300 .child(
301 h_flex()
302 .id("bar")
303 .window_control_area(WindowControlArea::Drag)
304 .when(window.is_fullscreen(), |this| this.pl_3())
305 .h_full()
306 .justify_between()
307 .flex_shrink_0()
308 .flex_1()
309 .when(is_linux && is_client_decorated, |this| {
310 this.child(
311 div()
312 .top_0()
313 .left_0()
314 .absolute()
315 .size_full()
316 .h_full()
317 .on_mouse_down(MouseButton::Right, move |ev, window, _| {
318 window.show_window_menu(ev.position)
319 }),
320 )
321 })
322 .children(self.children),
323 )
324 .child(WindowControls {
325 on_close_window: self.on_close_window,
326 }),
327 )
328 }
329}