1use crate::{ActiveTheme, Button, ButtonVariant, IconName, button, icon};
7use gpui_kit::{
8 App, Div, ElementId, FontWeight, IntoElement, RenderOnce, SharedString, Stateful, Window,
9 WindowControlArea, div, prelude::*, px, rems,
10};
11use std::time::Duration;
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub enum Material {
16 Sidebar,
17 #[default]
18 Toolbar,
19 Popover,
20 Hud,
21}
22
23pub fn material(kind: Material, cx: &App) -> Div {
26 let t = cx.macos();
27 div()
28 .bg(match kind {
29 Material::Sidebar | Material::Hud => {
30 t.inset
31 .opacity(if t.reduce_transparency { 1. } else { 0.94 })
32 }
33 Material::Toolbar | Material::Popover => t.glass_fill(),
34 })
35 .border_color(t.glass_border())
36 .font_family(t.font.clone())
37 .text_size(rems(0.8125))
38 .text_color(t.foreground)
39}
40
41pub fn native_material(
45 id: impl Into<ElementId>,
46 kind: Material,
47 cx: &App,
48) -> crate::native::NativeGlass {
49 crate::native::native_glass(id, cx)
50 .corner_radius(if kind == Material::Sidebar {
51 px(0.)
52 } else {
53 px(12.)
54 })
55 .clear(kind == Material::Hud)
56}
57
58pub fn toolbar(id: impl Into<ElementId>, cx: &App) -> Stateful<Div> {
60 let t = cx.macos();
61 div()
62 .id(id)
63 .flex()
64 .items_center()
65 .w_full()
66 .min_h(rems(3.25))
67 .gap(rems(0.625))
68 .px(rems(0.875))
69 .py(rems(0.25))
70 .font_family(t.font.clone())
71 .text_size(rems(0.8125))
72 .text_color(t.foreground)
73 .flex_shrink_0()
74}
75
76pub fn toolbar_group(cx: &App) -> Div {
78 material(Material::Toolbar, cx)
79 .flex()
80 .items_center()
81 .gap(rems(0.125))
82 .p(rems(0.125))
83 .rounded_full()
84 .border_1()
85 .shadow(cx.macos().control_shadow())
86}
87
88pub fn native_toolbar_group(id: impl Into<ElementId>, cx: &App) -> crate::native::NativeGlass {
91 native_material(id, Material::Toolbar, cx)
92 .corner_radius(rems(1.125).to_pixels(px(16.)))
93 .flex()
94 .items_center()
95 .gap(rems(0.125))
96 .p(rems(0.125))
97 .border_1()
98 .shadow(cx.macos().control_shadow())
99}
100
101pub fn toolbar_button(
104 id: impl Into<ElementId>,
105 name: IconName,
106 label: impl Into<SharedString>,
107 cx: &App,
108) -> Button {
109 let t = cx.macos();
110 button(id, "", ButtonVariant::Secondary, cx)
111 .accessibility_label(label)
112 .fill_duration(Duration::ZERO)
113 .size(rems(2.))
114 .p(rems(0.))
115 .rounded_full()
116 .border_0()
117 .shadow_none()
118 .bg(t.foreground.opacity(0.))
119 .hover(|s| s.bg(t.hover_fill()))
120 .active(|s| s.bg(t.pressed_fill()))
121 .focus_visible(|s| s.bg(t.hover_fill()).shadow(t.focus_shadow()))
122 .child(icon(name).size(rems(1.)))
123}
124
125pub fn sidebar(id: impl Into<ElementId>, cx: &App) -> Stateful<Div> {
127 let id = id.into();
128 div()
129 .id(id.clone())
130 .relative()
131 .font_family(cx.macos().font.clone())
132 .text_size(rems(0.8125))
133 .text_color(cx.macos().foreground)
134 .flex()
135 .flex_col()
136 .w(rems(13.75))
137 .h_full()
138 .flex_shrink_0()
139 .p(rems(0.5))
140 .child(
141 crate::native::native_glass((id, "sidebar-material"), cx)
142 .absolute()
143 .inset_0()
144 .corner_radius(px(0.)),
145 )
146}
147
148#[derive(IntoElement)]
149struct SidebarSymbol {
150 name: IconName,
151 selected: bool,
152}
153
154impl RenderOnce for SidebarSymbol {
155 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
156 let t = cx.macos();
157 let accent =
158 if !window.is_window_active() && !t.reduce_transparency && !t.increased_contrast {
159 t.accent.blend(if t.is_dark() {
162 gpui_kit::Hsla::black().opacity(0.4)
163 } else {
164 gpui_kit::Hsla::white().opacity(0.4)
165 })
166 } else {
167 t.accent
168 };
169 icon(self.name)
170 .size(rems(
171 crate::native::sidebar_metrics().icon_column_width.as_f32() / 16.,
172 ))
173 .font_weight(FontWeight::NORMAL)
174 .text_color(if self.selected && window.is_window_active() {
175 t.on_accent
176 } else {
177 accent
178 })
179 }
180}
181
182pub fn sidebar_item(
185 id: impl Into<ElementId>,
186 label: impl Into<SharedString>,
187 name: IconName,
188 selected: bool,
189 cx: &App,
190) -> Button {
191 let t = cx.macos();
192 let metrics = crate::native::sidebar_metrics();
193 let label = label.into();
194 button(id, "", ButtonVariant::Secondary, cx)
195 .accessibility_label(label.clone())
196 .selected(selected)
197 .aria_selected(selected)
198 .selected_fill(t.accent)
199 .inactive_style(
200 if selected {
201 crate::native::sidebar_inactive_selection(cx)
202 } else {
203 t.foreground.opacity(0.)
204 },
205 if selected {
206 if t.is_dark() && !t.increased_contrast {
207 t.accent.blend(gpui_kit::Hsla::white().opacity(0.2))
208 } else {
209 t.accent
210 }
211 } else {
212 t.foreground
213 },
214 )
215 .fill_duration(Duration::ZERO)
218 .bg(if selected {
219 t.accent
220 } else {
221 t.foreground.opacity(0.)
222 })
223 .w_full()
224 .h(rems(metrics.row_height.as_f32() / 16.))
225 .text_size(rems(metrics.label_size.as_f32() / 16.))
226 .px(rems(0.5))
227 .py(rems(0.))
228 .rounded(rems(0.5))
229 .border_0()
230 .border_color(t.foreground.opacity(0.))
231 .shadow_none()
232 .justify_start()
233 .gap(rems(0.25))
234 .text_color(if selected { t.on_accent } else { t.foreground })
235 .font_weight(if selected {
236 FontWeight::SEMIBOLD
237 } else {
238 FontWeight::NORMAL
239 })
240 .hover(|s| {
241 s.bg(if selected { t.accent } else { t.hover_fill() })
242 .border_color(t.foreground.opacity(0.))
243 })
244 .active(|s| s.bg(if selected { t.accent } else { t.pressed_fill() }))
245 .focus_visible(|s| {
246 s.bg(if selected { t.accent } else { t.hover_fill() })
247 .border_color(t.accent)
248 .shadow(t.focus_shadow())
249 })
250 .child(SidebarSymbol { name, selected })
251 .child(div().min_w_0().truncate().child(label))
252}
253
254pub fn sidebar_section(title: impl Into<SharedString>, cx: &App) -> Div {
256 div().flex().flex_col().pt(rems(0.8125)).child(
257 div()
258 .flex()
259 .items_center()
260 .h(rems(1.1875))
261 .px(rems(0.375))
262 .text_size(rems(0.6875))
263 .font_weight(FontWeight::SEMIBOLD)
264 .text_color(cx.macos().secondary)
265 .child(title.into()),
266 )
267}
268
269pub fn title_bar(title: impl Into<SharedString>, cx: &App) -> Div {
273 let t = cx.macos();
274 div()
275 .flex()
276 .items_center()
277 .w_full()
278 .h(rems(3.25))
279 .pl(rems(5.))
280 .pr(rems(0.875))
281 .gap(rems(0.625))
282 .flex_shrink_0()
283 .font_family(t.font.clone())
284 .text_size(rems(0.8125))
285 .text_color(t.foreground)
286 .font_weight(FontWeight::SEMIBOLD)
287 .window_control_area(WindowControlArea::Drag)
288 .child(div().min_w_0().truncate().child(title.into()))
289}
290
291pub fn status_bar(cx: &App) -> Div {
293 let t = cx.macos();
294 div()
295 .flex()
296 .items_center()
297 .justify_between()
298 .w_full()
299 .min_h(rems(1.75))
300 .px(rems(0.875))
301 .gap(rems(0.625))
302 .border_t_1()
303 .border_color(t.divider())
304 .flex_shrink_0()
305 .bg(t.background)
306 .font_family(t.font.clone())
307 .text_size(rems(0.6875))
308 .text_color(t.secondary)
309}
310
311pub fn inspector(title: impl Into<SharedString>, cx: &App) -> Div {
313 material(Material::Sidebar, cx)
314 .flex()
315 .flex_col()
316 .w(rems(17.5))
317 .h_full()
318 .flex_shrink_0()
319 .gap(rems(0.75))
320 .p(rems(0.875))
321 .border_l_1()
322 .border_color(cx.macos().divider())
323 .child(div().font_weight(FontWeight::SEMIBOLD).child(title.into()))
324}
325
326pub fn settings_group(title: impl Into<SharedString>, cx: &App) -> Div {
328 let t = cx.macos();
329 let title = title.into();
330 div()
331 .flex()
332 .flex_col()
333 .w_full()
334 .rounded(rems(0.75))
335 .overflow_hidden()
336 .bg(t.normal_fill())
337 .font_family(t.font.clone())
338 .text_size(rems(0.8125))
339 .text_color(t.foreground)
340 .when(!title.is_empty(), |group| {
341 group.child(
342 div()
343 .px(rems(0.875))
344 .py(rems(0.75))
345 .font_weight(FontWeight::SEMIBOLD)
346 .child(title),
347 )
348 })
349}
350
351pub fn settings_row(
353 label: impl Into<SharedString>,
354 description: impl Into<SharedString>,
355 cx: &App,
356) -> Div {
357 let description = description.into();
358 div()
359 .relative()
360 .flex()
361 .items_center()
362 .justify_between()
363 .min_h(rems(2.875))
364 .gap(rems(1.))
365 .px(rems(0.875))
366 .py(rems(0.625))
367 .child(
368 div()
369 .flex()
370 .flex_col()
371 .flex_1()
372 .min_w_0()
373 .gap(rems(0.1875))
374 .child(label.into())
375 .when(!description.is_empty(), |labels| {
376 labels.child(
377 div()
378 .text_size(rems(0.6875))
379 .text_color(cx.macos().secondary)
380 .child(description),
381 )
382 }),
383 )
384 .child(
385 div()
386 .absolute()
387 .top_0()
388 .left(rems(0.875))
389 .right(rems(0.875))
390 .h(rems(0.0625))
391 .bg(cx.macos().divider()),
392 )
393}