1use fluent_core::{Theme, ThemeProvider as _};
2use gpui::{
3 div, prelude::*, px, svg, ClickEvent, Context, IntoElement, MouseButton, MouseDownEvent,
4 Render, SharedString, Window,
5};
6
7use crate::window_title::WindowTitle;
8
9const DOUBLE_CLICK_WINDOW_MS: u64 = 350;
12
13pub struct TitleBar {
19 pub title: SharedString,
20 pub icon: Option<SharedString>,
21 pub show_controls: bool,
22 follow_window_title: bool,
23 last_press_ms: u64,
24}
25
26impl TitleBar {
27 pub fn new(cx: &mut Context<Self>, title: impl Into<SharedString>) -> Self {
28 cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
29 cx.observe_global::<WindowTitle>(|_, cx| cx.notify())
30 .detach();
31 Self {
32 title: title.into(),
33 icon: None,
34 show_controls: true,
35 follow_window_title: false,
36 last_press_ms: 0,
37 }
38 }
39
40 pub fn from_window_title(cx: &mut Context<Self>) -> Self {
41 let title = cx
42 .try_global::<WindowTitle>()
43 .map(|title| title.title().clone())
44 .unwrap_or_default();
45
46 Self::new(cx, title).follow_window_title(true)
47 }
48
49 pub fn set_title(&mut self, title: impl Into<SharedString>, cx: &mut Context<Self>) {
50 self.title = title.into();
51 self.follow_window_title = false;
52 cx.notify();
53 }
54
55 pub fn show_controls(mut self, show: bool) -> Self {
56 self.show_controls = show;
57 self
58 }
59
60 pub fn follow_window_title(mut self, follow: bool) -> Self {
61 self.follow_window_title = follow;
62 self
63 }
64
65 pub fn icon(mut self, path: impl Into<SharedString>) -> Self {
68 self.icon = Some(path.into());
69 self
70 }
71}
72
73impl Render for TitleBar {
74 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
75 let theme = cx.theme();
76 let colors = theme.colors.clone();
77 let spacing = theme.spacing;
78 let typography = theme.typography;
79
80 let (title, status) = if self.follow_window_title {
81 cx.try_global::<WindowTitle>()
82 .map(|window_title| (window_title.title().clone(), window_title.status().cloned()))
83 .unwrap_or_else(|| (self.title.clone(), None))
84 } else {
85 (self.title.clone(), None)
86 };
87 let show_controls = self.show_controls;
88
89 let bar_bg = colors.surface_dim;
90 let fg = colors.on_neutral;
91 let ctrl_hover = colors.subtle_hover;
92 let close_hover: gpui::Hsla = gpui::rgb(0xC42B1C).into();
93
94 let drag_handler = cx.listener(
97 |this: &mut TitleBar, _: &MouseDownEvent, window: &mut Window, _| {
98 let now = std::time::SystemTime::now()
99 .duration_since(std::time::UNIX_EPOCH)
100 .map(|d| d.as_millis() as u64)
101 .unwrap_or(0);
102 if now.saturating_sub(this.last_press_ms) < DOUBLE_CLICK_WINDOW_MS {
103 this.last_press_ms = 0;
104 window.zoom_window();
105 return;
106 }
107 this.last_press_ms = now;
108 window.start_window_move();
109 },
110 );
111
112 let min_handler =
113 cx.listener(|_: &mut TitleBar, _: &ClickEvent, window: &mut Window, _| {
114 window.minimize_window();
115 });
116 let max_handler =
117 cx.listener(|_: &mut TitleBar, _: &ClickEvent, window: &mut Window, _| {
118 window.zoom_window();
119 });
120 let close_handler =
121 cx.listener(|_: &mut TitleBar, _: &ClickEvent, window: &mut Window, _| {
122 window.remove_window();
123 });
124
125 let icon = self.icon.clone();
128 let accent_fg = colors.on_neutral_accent;
129 let mut title_area = div()
130 .flex_1()
131 .h_full()
132 .flex()
133 .items_center()
134 .gap(px(spacing.sm))
135 .pl(px(spacing.md))
136 .text_size(px(typography.caption.size))
137 .text_color(fg)
138 .on_mouse_down(MouseButton::Left, drag_handler);
139 if let Some(path) = icon {
140 title_area = title_area.child(svg().path(path).size(px(16.0)).text_color(accent_fg));
141 }
142 let mut title_area = title_area.child(title);
143 if let Some(status) = status {
144 title_area = title_area.child(
145 div()
146 .text_size(px(typography.caption.size))
147 .text_color(colors.on_subtle)
148 .child(status),
149 );
150 }
151
152 let bar = div()
153 .flex()
154 .flex_row()
155 .h(px(36.0))
156 .bg(bar_bg)
157 .child(title_area);
158
159 if !show_controls {
160 return bar;
161 }
162
163 bar.child(
165 div()
166 .flex()
167 .flex_row()
168 .h_full()
169 .child(
170 div()
171 .id("titlebar-min")
172 .w(px(46.0))
173 .h_full()
174 .flex()
175 .items_center()
176 .justify_center()
177 .cursor_pointer()
178 .hover(move |s| s.bg(ctrl_hover))
179 .on_click(min_handler)
180 .child(
181 svg()
182 .path("icons/minimize.svg")
183 .size(px(10.0))
184 .text_color(fg),
185 ),
186 )
187 .child(
188 div()
189 .id("titlebar-max")
190 .w(px(46.0))
191 .h_full()
192 .flex()
193 .items_center()
194 .justify_center()
195 .cursor_pointer()
196 .hover(move |s| s.bg(ctrl_hover))
197 .on_click(max_handler)
198 .child(
199 svg()
200 .path("icons/maximize.svg")
201 .size(px(10.0))
202 .text_color(fg),
203 ),
204 )
205 .child(
206 div()
207 .id("titlebar-close")
208 .w(px(46.0))
209 .h_full()
210 .flex()
211 .items_center()
212 .justify_center()
213 .cursor_pointer()
214 .hover(move |s| s.bg(close_hover))
215 .on_click(close_handler)
216 .child(
217 svg()
218 .path("icons/dismiss.svg")
219 .size(px(10.0))
220 .text_color(fg),
221 ),
222 ),
223 )
224 }
225}