Skip to main content

fluent_app/
title_bar.rs

1use fluent_core::{Theme, ThemeProvider as _};
2use gpui::{
3    div, prelude::*, px, svg, ClickEvent, Context, IntoElement, MouseButton, MouseDownEvent,
4    Render, SharedString, Window,
5};
6
7/// A custom frameless title bar that replaces the OS-provided one.
8///
9/// Draggable via `window.start_window_move()`. Renders the app title and
10/// window controls (min/max/close) on the right. Enable frameless mode with
11/// `TitlebarOptions { appears_transparent: true }` + `WindowDecorations::Client`.
12pub struct TitleBar {
13    pub title: SharedString,
14    pub show_controls: bool,
15}
16
17impl TitleBar {
18    pub fn new(cx: &mut Context<Self>, title: impl Into<SharedString>) -> Self {
19        cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
20        Self {
21            title: title.into(),
22            show_controls: true,
23        }
24    }
25
26    pub fn show_controls(mut self, show: bool) -> Self {
27        self.show_controls = show;
28        self
29    }
30}
31
32impl Render for TitleBar {
33    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
34        let theme = cx.theme();
35        let colors = theme.colors.clone();
36        let spacing = theme.spacing;
37        let typography = theme.typography;
38
39        let title = self.title.clone();
40        let show_controls = self.show_controls;
41
42        let bar_bg = colors.surface_dim;
43        let fg = colors.on_neutral;
44        let ctrl_hover = colors.subtle_hover;
45        let close_hover: gpui::Hsla = gpui::rgb(0xC42B1C).into();
46
47        // Drag: left-button press starts window move
48        let drag_handler = cx.listener(
49            |_: &mut TitleBar, _: &MouseDownEvent, window: &mut Window, _| {
50                window.start_window_move();
51            },
52        );
53
54        let min_handler =
55            cx.listener(|_: &mut TitleBar, _: &ClickEvent, window: &mut Window, _| {
56                window.minimize_window();
57            });
58        let max_handler =
59            cx.listener(|_: &mut TitleBar, _: &ClickEvent, window: &mut Window, _| {
60                window.zoom_window();
61            });
62        let close_handler =
63            cx.listener(|_: &mut TitleBar, _: &ClickEvent, window: &mut Window, _| {
64                window.remove_window();
65            });
66
67        // The drag area is only the title text, NOT the window control buttons.
68        // Putting on_mouse_down on the full bar would intercept clicks on min/max/close.
69        let title_area = div()
70            .flex_1()
71            .h_full()
72            .flex()
73            .items_center()
74            .pl(px(spacing.md))
75            .text_size(px(typography.caption.size))
76            .text_color(fg)
77            .on_mouse_down(MouseButton::Left, drag_handler)
78            .child(title);
79
80        let bar = div()
81            .flex()
82            .flex_row()
83            .h(px(36.0))
84            .bg(bar_bg)
85            .child(title_area);
86
87        if !show_controls {
88            return bar;
89        }
90
91        // Controls container fills full bar height so hover fills the entire button box.
92        bar.child(
93            div()
94                .flex()
95                .flex_row()
96                .h_full()
97                .child(
98                    div()
99                        .id("titlebar-min")
100                        .w(px(46.0))
101                        .h_full()
102                        .flex()
103                        .items_center()
104                        .justify_center()
105                        .cursor_pointer()
106                        .hover(move |s| s.bg(ctrl_hover))
107                        .on_click(min_handler)
108                        .child(
109                            svg()
110                                .path("icons/minimize.svg")
111                                .size(px(10.0))
112                                .text_color(fg),
113                        ),
114                )
115                .child(
116                    div()
117                        .id("titlebar-max")
118                        .w(px(46.0))
119                        .h_full()
120                        .flex()
121                        .items_center()
122                        .justify_center()
123                        .cursor_pointer()
124                        .hover(move |s| s.bg(ctrl_hover))
125                        .on_click(max_handler)
126                        .child(
127                            svg()
128                                .path("icons/maximize.svg")
129                                .size(px(10.0))
130                                .text_color(fg),
131                        ),
132                )
133                .child(
134                    div()
135                        .id("titlebar-close")
136                        .w(px(46.0))
137                        .h_full()
138                        .flex()
139                        .items_center()
140                        .justify_center()
141                        .cursor_pointer()
142                        .hover(move |s| s.bg(close_hover))
143                        .on_click(close_handler)
144                        .child(
145                            svg()
146                                .path("icons/dismiss.svg")
147                                .size(px(10.0))
148                                .text_color(fg),
149                        ),
150                ),
151        )
152    }
153}