Skip to main content

gpui_component/
title_bar.rs

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/// TitleBar used to customize the appearance of the title bar.
21///
22/// We can put some elements inside the title bar.
23#[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    /// Create a new TitleBar.
32    pub fn new() -> Self {
33        Self {
34            style: StyleRefinement::default(),
35            children: SmallVec::new(),
36            on_close_window: None,
37        }
38    }
39
40    /// Returns the default title bar options for compatible with the [`crate::TitleBar`].
41    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    /// Add custom for close window event, default is None, then click X button will call `window.remove_window()`.
50    /// Linux only, this will do nothing on other platforms.
51    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// The Windows control buttons have a fixed width of 35px.
63//
64// We don't need implementation the click event for the control buttons.
65// If user clicked in the bounds, the window event will be triggered.
66#[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
245// TODO: Remove this when GPUI has released v0.2.3
246impl 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}