Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
51bf072
feat(desktop-gpui): macOS control styling for settings switches, pop-…
richiemcilroy Oct 27, 2026
9a074bd
Merge feat/gpui-picker-glass into feat/gpui-glass-app
richiemcilroy Oct 27, 2026
fb4d5e0
test(desktop-gpui): scan the select module for the icons it draws
richiemcilroy Oct 27, 2026
118c279
Merge feat/gpui-picker-glass into feat/gpui-glass-app
richiemcilroy Oct 27, 2026
7de7ea9
feat(desktop-gpui): edge hairlines on picker glass and flat controls;…
richiemcilroy Oct 27, 2026
4b589fc
Merge feat/gpui-picker-glass into feat/gpui-glass-app
richiemcilroy Oct 27, 2026
f704a6c
Merge feat/gpui-picker-glass into feat/gpui-glass-app
richiemcilroy Oct 27, 2026
4dbb8d8
Merge feat/gpui-picker-glass into feat/gpui-glass-app
richiemcilroy Oct 27, 2026
2e0acbd
Merge feat/gpui-picker-glass into feat/gpui-glass-app
richiemcilroy Oct 27, 2026
cbbb7a2
Merge feat/gpui-picker-glass into feat/gpui-glass-app
richiemcilroy Oct 27, 2026
4f3cf5f
Merge feat/gpui-picker-glass into feat/gpui-glass-app
richiemcilroy Oct 27, 2026
a834ac0
Merge commit 'ad5f7662b7ac2d34b6779c9f0bc6919eafc03fff' into HEAD
richiemcilroy Oct 27, 2026
efe53a7
Merge feat/gpui-picker-glass into feat/gpui-glass-app
richiemcilroy Oct 27, 2026
f71feed
chore(desktop-gpui): drop comments that restate the select chip and t…
richiemcilroy Oct 27, 2026
ea13917
Merge feat/gpui-picker-glass into feat/gpui-glass-app
richiemcilroy Oct 28, 2026
5d0155d
Merge feat/gpui-picker-glass into feat/gpui-glass-app
richiemcilroy Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop-gpui/assets/icons/chevrons-up-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions apps/desktop-gpui/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const ICONS: &[(&str, &[u8])] = assets!("icons":
"check.svg",
"clapperboard.svg",
"chevron-down.svg",
"chevrons-up-down.svg",
"crop.svg",
"cursor.svg",
"chevron-left.svg",
Expand Down Expand Up @@ -349,6 +350,7 @@ mod tests {
include_str!("ui/radio_cards.rs"),
include_str!("ui/windows_caption.rs"),
include_str!("ui/hint.rs"),
include_str!("ui/select.rs"),
// The onboarding window's welcome cards and permissions surface; the
// per-permission row glyphs are named on `OSPermission::icon`.
include_str!("onboarding_window.rs"),
Expand Down
102 changes: 40 additions & 62 deletions apps/desktop-gpui/src/main_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,50 +232,25 @@ fn shadow(color: Hsla, y: f32, blur: f32, spread: f32, inset: bool) -> gpui::Box
}
}

/// Drawn in gpui rather than with an `NSGlassEffectView` per button, which
/// would have to track gpui's layout under the Metal layer. Two shadows, one
/// unblurred, keep it cheap on a window that repaints with the mic meter.
/// A control on the main window's glass, as macOS draws one inside a glass
/// window (`scripts/glass-reference/gallery.swift`): a flat, soft fill with
/// no outline, gradient or lift, since glass does not stack on glass. The
/// selected card takes the accent as a tint.
fn glass_button<E: Styled>(element: E, theme: Theme, selected: bool) -> E {
let dark = theme.is_dark();
let (top, bottom, rim) = match (selected, dark) {
(false, false) => (
gpui::hsla(0., 0., 1., 0.7),
gpui::hsla(0., 0., 1., 0.58),
gpui::hsla(0., 0., 0., 0.08),
),
(false, true) => (
gpui::hsla(0., 0., 1., 0.085),
gpui::hsla(0., 0., 1., 0.06),
gpui::hsla(0., 0., 1., 0.07),
),
(true, false) => (
Theme::with_alpha(theme.blue_3, 0.9),
Theme::with_alpha(theme.blue_3, 0.75),
Theme::with_alpha(theme.blue_8, 0.4),
),
(true, true) => (
Theme::with_alpha(theme.blue_9, 0.24),
Theme::with_alpha(theme.blue_9, 0.18),
Theme::with_alpha(theme.blue_9, 0.3),
),
let fill = match (selected, theme.is_dark()) {
(false, false) => gpui::hsla(0., 0., 0., 0.05),
(false, true) => gpui::hsla(0., 0., 1., 0.08),
(true, false) => Theme::with_alpha(theme.blue_9, 0.14),
(true, true) => Theme::with_alpha(theme.blue_9, 0.28),
};
let (highlight, lift) = if dark {
(gpui::hsla(0., 0., 1., 0.07), gpui::hsla(0., 0., 0., 0.2))
// A hairline so the control holds its shape over whatever shows through
// the window's glass, light or dark.
let edge = if theme.is_dark() {
gpui::hsla(0., 0., 1., 0.1)
} else {
(gpui::hsla(0., 0., 1., 0.8), gpui::hsla(0., 0., 0., 0.04))
gpui::hsla(0., 0., 0., 0.08)
};
element
.bg(gpui::linear_gradient(
180.,
gpui::linear_color_stop(top, 0.),
gpui::linear_color_stop(bottom, 1.),
))
.border_1()
.border_color(rim)
.shadow(vec![
shadow(highlight, 1., 0., 0., true),
shadow(lift, 1., 2., 0., false),
])
element.bg(fill).border_1().border_color(edge)
}

fn capture_hover_fill(theme: Theme, selected: bool) -> Hsla {
Expand Down Expand Up @@ -6343,24 +6318,21 @@ impl MainWindow {
.flex()
.items_center()
.justify_center()
.size(px(28.))
.w(px(34.))
.h(px(28.))
.rounded_full()
.bg(if selected {
Hsla::from(theme.gray_7)
} else {
theme.body_fill(3)
})
// A segmented control's selected segment, as macOS raises
// it: a lifted capsule over the track.
.when(selected, |this| {
// `ring-2 ring-blue-500 ring-offset-1 ring-offset-gray-1`.
this.border_2()
.border_color(theme.blue_500)
.shadow(vec![gpui::BoxShadow {
color: theme.ring_offset(),
offset: gpui::point(px(0.), px(0.)),
blur_radius: px(0.),
spread_radius: px(1.),
inset: false,
}])
this.bg(if theme.is_dark() {
gpui::hsla(0., 0., 1., 0.24)
} else {
gpui::white()
})
.shadow(vec![
shadow(gpui::hsla(0., 0., 0., 0.12), 1., 3., 0., false),
shadow(gpui::hsla(0., 0., 0., 0.06), 0., 0., 0.5, false),
])
})
.child(
gpui::canvas(move |value, _, _| bounds.set(Some(value)), |_, _, _, _| {})
Expand All @@ -6373,7 +6345,9 @@ impl MainWindow {
.size(px(mode.icon_size()))
.text_color(theme.gray_12),
)
.hover(|style| style.bg(theme.body_hover_fill(7)))
.when(!selected, |this| {
this.hover(move |style| style.bg(glass_tint(theme, 0.05, 0.07)))
})
.on_click(cx.listener(move |this, _, _window, cx| {
if this.session.read(cx).editor_recording_target().is_some() {
return;
Expand All @@ -6388,12 +6362,10 @@ impl MainWindow {
.flex()
.flex_row()
.items_center()
.gap(px(8.))
.p(px(6.))
.gap(px(2.))
.p(px(3.))
.rounded_full()
.border_1()
.border_color(theme.body_border(5))
.bg(theme.body_fill(3))
.bg(glass_tint(theme, 0.06, 0.09))
.child(button(Mode::Instant, "mode-instant", 0))
.child(button(Mode::Studio, "mode-studio", 1))
.child(button(Mode::Screenshot, "mode-screenshot", 2))
Expand Down Expand Up @@ -6609,6 +6581,12 @@ impl MainWindow {
} else {
Some(target)
};
// The tile picks by hovering: a window chosen from its
// dropdown earlier would otherwise stay locked in, and the
// highlight would ignore the window under the pointer.
if target == TargetType::Window {
this.selected_window = None;
}
this.sync_overlays(cx);
cx.notify();
}));
Expand Down
47 changes: 32 additions & 15 deletions apps/desktop-gpui/src/ui/button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,26 +194,43 @@ impl Button {
variant: ButtonVariant,
size: ButtonSize,
) -> Self {
let control_fill = theme
.material
.map(|material| Hsla::from(material.control_fill))
.unwrap_or_else(|| Hsla::from(theme.gray_5));

// No `:hover` arm: the settings block's hover rules land on the nav and
// profile rows, not on `button[data-variant]`, and the window has never
// drawn one.
// macOS's bordered buttons in a grouped form
// (`scripts/glass-reference/gallery.swift`): a capsule with a soft grey
// fill and no outline, or the accent for the prominent family.
let (fill, hover) = if theme.is_dark() {
(gpui::hsla(0., 0., 1., 0.12), gpui::hsla(0., 0., 1., 0.18))
} else {
(gpui::hsla(0., 0., 0., 0.06), gpui::hsla(0., 0., 0., 0.1))
};
let accent: Hsla = gpui::rgb(Theme::SETTINGS_ACCENT).into();
let edge = if theme.is_dark() {
gpui::hsla(0., 0., 1., 0.08)
} else {
gpui::hsla(0., 0., 0., 0.07)
};
let paint = match variant {
ButtonVariant::Gray | ButtonVariant::White | ButtonVariant::Outline => ButtonPaint {
bg: Some(control_fill),
hover_bg: None,
ButtonVariant::Gray
| ButtonVariant::White
| ButtonVariant::Outline
| ButtonVariant::Ghost => ButtonPaint {
bg: Some(fill),
hover_bg: Some(hover),
text: theme.settings_text(),
hover_text: None,
border: Some(theme.settings_border()),
border: Some(edge),
hover_border: None,
},
ButtonVariant::Destructive => ButtonPaint {
bg: Some(fill),
hover_bg: Some(hover),
text: gpui::rgb(0xff3b30).into(),
hover_text: None,
border: None,
hover_border: None,
},
_ => ButtonPaint {
bg: Some(gpui::rgb(Theme::SETTINGS_ACCENT).into()),
hover_bg: None,
bg: Some(accent),
hover_bg: Some(accent.blend(gpui::hsla(0., 0., 0., 0.1))),
text: gpui::white(),
hover_text: None,
border: None,
Expand All @@ -222,7 +239,7 @@ impl Button {
};

Self {
radius: Some(px(8.)),
radius: Some(px(999.)),
..Self::plain(theme, id, variant, size)
}
.paint(paint)
Expand Down
51 changes: 46 additions & 5 deletions apps/desktop-gpui/src/ui/glass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,20 @@ fn sample_reach(style: &PaneStyle) -> f32 {
/// everything added after it paints on the glass. `radius` is clamped to the
/// pane like CSS clamps `border-radius`.
pub fn pane(kind: GlassKind, dark: bool, radius: f32) -> Div {
pane_with(kind, dark, radius, None, None)
pane_with(kind, dark, radius, None, None, false)
}

/// [`pane`] with a tint.
pub fn pane_tinted(kind: GlassKind, dark: bool, radius: f32, tint: Tint) -> Div {
pane_with(kind, dark, radius, None, Some(tint))
pane_with(kind, dark, radius, None, Some(tint), false)
}

/// [`pane`] on a transparent window, reading `backdrop` instead of the
/// window's own (mostly empty) pixels.
/// window's own (mostly empty) pixels, with an edge so a control holds its
/// shape over whatever the desktop shows. [`pane`] alone stays exactly the
/// system material, which the glass probe measures against.
pub fn pane_over(kind: GlassKind, dark: bool, radius: f32, backdrop: &Backdrop) -> Div {
pane_with(kind, dark, radius, Some(backdrop.clone()), None)
pane_with(kind, dark, radius, Some(backdrop.clone()), None, true)
}

fn pane_with(
Expand All @@ -142,9 +144,11 @@ fn pane_with(
radius: f32,
backdrop: Option<Backdrop>,
tint: Option<Tint>,
edge: bool,
) -> Div {
let material = *material(kind, dark);
div().relative().rounded(px(radius)).child(
let (outer, inner) = edge_lines(dark);
let pane = div().relative().rounded(px(radius)).child(
canvas(
|_, _, _| {},
move |bounds, _, window, _| {
Expand All @@ -158,9 +162,46 @@ fn pane_with(
.top_0()
.left_0()
.size_full(),
);
if !edge {
return pane;
}
// The pane's edge, so it holds its shape on any backdrop: a faint dark
// line that shows on light content and a faint light one just inside it
// that shows on dark. Drawn over the glass rather than as a border so the
// glass still reaches the pane's edge.
pane.child(
div()
.absolute()
.top_0()
.left_0()
.size_full()
.rounded(px(radius))
.border_1()
.border_color(outer),
)
.child(
div()
.absolute()
.top(px(1.))
.left(px(1.))
.right(px(1.))
.bottom(px(1.))
.rounded(px((radius - 1.).max(0.)))
.border_1()
.border_color(inner),
)
}

/// The two hairlines around a pane's edge: dark outside, light inside.
fn edge_lines(dark: bool) -> (Hsla, Hsla) {
if dark {
(gpui::hsla(0., 0., 0., 0.28), gpui::hsla(0., 0., 1., 0.14))
} else {
(gpui::hsla(0., 0., 0., 0.1), gpui::hsla(0., 0., 1., 0.45))
}
}

/// What a transparent window's glass panes show through them: a capture of
/// the desktop beneath the window, painted under the panes before anything
/// else the window draws, so whatever the window paints over the desktop
Expand Down
Loading
Loading