Skip to main content

gpui_macos/
chrome.rs

1//! Composable macOS window chrome, sidebars, inspectors and grouped settings.
2//!
3//! Sidebars host AppKit glass on macOS 26 and newer. Other compositions can
4//! opt in using [`native_material`]. [`material`] supplies a deterministic fill
5//! for canvas overlays and other platforms.
6use 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/// Semantic material roles with light and dark theme support.
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub enum Material {
16    Sidebar,
17    #[default]
18    Toolbar,
19    Popover,
20    Hud,
21}
22
23/// A composable material surface. Add geometry, content and elevation as needed.
24/// The translucent fill does not blur or refract the content behind it.
25pub 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
41/// Real AppKit glass for navigation and command surfaces, with a themed fallback.
42/// Keep ancestor fills transparent; this underlay refracts the window backdrop.
43/// GPUI content above the native view remains on its own rendering plane.
44pub 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
58/// A clear toolbar layout. Compose glass command groups, an unbacked title and search.
59pub 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
76/// Shared glass for related toolbar commands. Compose flat `toolbar_button` children.
77pub 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
88/// A command group backed by actual system glass on supported macOS windows.
89/// Supply a stable, sibling-unique ID and compose `toolbar_button` children.
90pub 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
101/// A flat, icon-only command inside a shared toolbar glass group.
102/// The label names the action for assistive technology; add a tooltip if useful.
103pub 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
125/// A navigation sidebar. Scrollable content can be supplied inside this shell.
126pub 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                // Source-list vibrancy deemphasizes symbols in an inactive window.
160                // Blend the app accent so a custom or system accent keeps its hue.
161                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
182/// A keyboard-accessible sidebar command with a blue selected row.
183/// The application owns selection and supplies its click handler.
184pub 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        // Native row selection is immediate. A transparent resting row should
216        // not interpolate through a tinted opaque color when highlighted.
217        .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
254/// A sidebar section with a subdued heading and caller-supplied row children.
255pub 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
269/// A draggable title row that leaves room for the system window controls.
270/// Configure `WindowOptions::titlebar` with native traffic lights; this builder
271/// reserves their leading space and never draws replacement window controls.
272pub 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
291/// A quiet bottom row for selection counts, activity and auxiliary commands.
292pub 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
311/// A trailing inspector pane with a section title and caller-supplied controls.
312pub 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
326/// A rounded System Settings group. Compose `settings_row` children inside it.
327pub 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
351/// A settings label and optional description; append the control as a child.
352pub 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}