gpui_component/
title_bar.rs

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/// TitleBar used to customize the appearance of the title bar.
18///
19/// We can put some elements inside the title bar.
20#[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    /// Returns the default title bar options for compatible with the [`crate::TitleBar`].
37    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    /// Add custom for close window event, default is None, then click X button will call `window.remove_window()`.
46    /// Linux only, this will do nothing on other platforms.
47    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// The Windows control buttons have a fixed width of 35px.
59//
60// We don't need implementation the click event for the control buttons.
61// If user clicked in the bounds, the window event will be triggered.
62#[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
300/// A TitleBar Element that can be move the window.
301pub 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}