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    /// Create a new TitleBar.
29    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    /// Returns the default title bar options for compatible with the [`crate::TitleBar`].
38    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    /// Add custom for close window event, default is None, then click X button will call `window.remove_window()`.
47    /// Linux only, this will do nothing on other platforms.
48    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// The Windows control buttons have a fixed width of 35px.
60//
61// We don't need implementation the click event for the control buttons.
62// If user clicked in the bounds, the window event will be triggered.
63#[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
301/// A TitleBar Element that can be move the window.
302pub 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}