diff --git a/apps/desktop-gpui/assets/icons/chevrons-up-down.svg b/apps/desktop-gpui/assets/icons/chevrons-up-down.svg
new file mode 100644
index 0000000000..af575371a4
--- /dev/null
+++ b/apps/desktop-gpui/assets/icons/chevrons-up-down.svg
@@ -0,0 +1 @@
+
diff --git a/apps/desktop-gpui/src/assets.rs b/apps/desktop-gpui/src/assets.rs
index 746fccfef4..28fe176178 100644
--- a/apps/desktop-gpui/src/assets.rs
+++ b/apps/desktop-gpui/src/assets.rs
@@ -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",
@@ -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"),
diff --git a/apps/desktop-gpui/src/main_window.rs b/apps/desktop-gpui/src/main_window.rs
index d20a331252..c2d663122d 100644
--- a/apps/desktop-gpui/src/main_window.rs
+++ b/apps/desktop-gpui/src/main_window.rs
@@ -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(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 {
@@ -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)), |_, _, _, _| {})
@@ -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;
@@ -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))
@@ -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();
}));
diff --git a/apps/desktop-gpui/src/ui/button.rs b/apps/desktop-gpui/src/ui/button.rs
index fc57412450..37dda34ff3 100644
--- a/apps/desktop-gpui/src/ui/button.rs
+++ b/apps/desktop-gpui/src/ui/button.rs
@@ -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,
@@ -222,7 +239,7 @@ impl Button {
};
Self {
- radius: Some(px(8.)),
+ radius: Some(px(999.)),
..Self::plain(theme, id, variant, size)
}
.paint(paint)
diff --git a/apps/desktop-gpui/src/ui/glass.rs b/apps/desktop-gpui/src/ui/glass.rs
index 6155b71bcd..1b7910e6c8 100644
--- a/apps/desktop-gpui/src/ui/glass.rs
+++ b/apps/desktop-gpui/src/ui/glass.rs
@@ -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(
@@ -142,9 +144,11 @@ fn pane_with(
radius: f32,
backdrop: Option,
tint: Option,
+ 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, _| {
@@ -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
diff --git a/apps/desktop-gpui/src/ui/select.rs b/apps/desktop-gpui/src/ui/select.rs
index 3b0498493f..b2ba6c2218 100644
--- a/apps/desktop-gpui/src/ui/select.rs
+++ b/apps/desktop-gpui/src/ui/select.rs
@@ -37,15 +37,16 @@ pub struct Select {
/// `gap-1.5` on the settings row, `gap-2` on the editor's trigger.
gap: Pixels,
stretch: bool,
+ chip: Option,
disabled: bool,
on_click: Option,
on_open: Option,
}
impl Select {
- /// `SelectSettingItem`'s button: `flex flex-row gap-1.5 text-xs items-center
- /// px-2.5 py-1.5 rounded-lg border bg-gray-3 text-gray-12 border-gray-4`,
- /// radius 8 under the settings material.
+ /// A settings row's pop-up, as macOS draws one in a grouped form
+ /// (`scripts/glass-reference/gallery.swift`): the value as plain text and
+ /// a 20 pt round chip holding an up-down chevron, with no border or fill.
pub fn settings(
theme: &Theme,
id: impl Into,
@@ -54,20 +55,24 @@ impl Select {
Self {
id: id.into(),
label: label.into(),
- padding_x: px(10.),
- padding_y: px(6.),
- height: None,
- radius: px(8.),
- text_size: px(12.),
- // No `font-*` class on the button, so it keeps `body`'s Medium.
- weight: FontWeight::MEDIUM,
- bg: Some(theme.settings_fill()),
- border: Some(theme.settings_border()),
+ padding_x: px(2.),
+ padding_y: px(0.),
+ height: Some(px(22.)),
+ radius: px(11.),
+ text_size: px(13.),
+ weight: FontWeight::NORMAL,
+ bg: None,
+ border: None,
text: theme.settings_text(),
- chevron: theme.settings_muted(),
- chevron_size: px(14.),
- gap: px(6.),
+ chevron: theme.settings_text(),
+ chevron_size: px(11.),
+ gap: px(7.),
stretch: false,
+ chip: Some(if theme.is_dark() {
+ gpui::hsla(0., 0., 1., 0.12)
+ } else {
+ gpui::hsla(0., 0., 0., 0.07)
+ }),
disabled: false,
on_click: None,
on_open: None,
@@ -92,6 +97,7 @@ impl Select {
chevron: Hsla::from(theme.editor.text_3),
chevron_size: px(12.),
gap: px(8.),
+ chip: None,
..Self::settings(theme, id, label)
}
}
@@ -143,6 +149,7 @@ impl RenderOnce for Select {
chevron_size,
gap,
stretch,
+ chip,
disabled,
on_click,
on_open,
@@ -178,13 +185,31 @@ impl RenderOnce for Select {
this.child(label)
}
})
- .child(
- svg()
- .path("icons/chevron-down.svg")
- .size(chevron_size)
- .flex_shrink_0()
- .text_color(chevron),
- )
+ .map(|this| match chip {
+ Some(fill) => this.child(
+ div()
+ .size(px(20.))
+ .flex_shrink_0()
+ .flex()
+ .items_center()
+ .justify_center()
+ .rounded_full()
+ .bg(fill)
+ .child(
+ svg()
+ .path("icons/chevrons-up-down.svg")
+ .size(chevron_size)
+ .text_color(chevron),
+ ),
+ ),
+ None => this.child(
+ svg()
+ .path("icons/chevron-down.svg")
+ .size(chevron_size)
+ .flex_shrink_0()
+ .text_color(chevron),
+ ),
+ })
.when_some(on_click.filter(|_| !disabled), |this, handler| {
this.on_click(move |event, window, cx| handler(event, window, cx))
})
diff --git a/apps/desktop-gpui/src/ui/toggle.rs b/apps/desktop-gpui/src/ui/toggle.rs
index 4a6bf7fdf1..fa5a1a2a4d 100644
--- a/apps/desktop-gpui/src/ui/toggle.rs
+++ b/apps/desktop-gpui/src/ui/toggle.rs
@@ -24,6 +24,10 @@ pub enum ToggleSize {
Md,
/// `w-14 h-7 p-0.75`, `size-6` thumb.
Lg,
+ /// The system's mini switch, measured from macOS's own in a grouped
+ /// settings form (`scripts/glass-reference/gallery.swift`): a 36 x 16
+ /// track and a 22 x 14 capsule knob.
+ Mini,
}
impl ToggleSize {
@@ -32,14 +36,16 @@ impl ToggleSize {
ToggleSize::Sm => (px(34.), px(20.), px(2.)),
ToggleSize::Md => (px(44.), px(24.), px(2.)),
ToggleSize::Lg => (px(56.), px(28.), px(3.)),
+ ToggleSize::Mini => (px(36.), px(16.), px(1.)),
}
}
- fn thumb(self) -> Pixels {
+ fn thumb(self) -> (Pixels, Pixels) {
match self {
- ToggleSize::Sm => px(16.),
- ToggleSize::Md => px(20.),
- ToggleSize::Lg => px(24.),
+ ToggleSize::Sm => (px(16.), px(16.)),
+ ToggleSize::Md => (px(20.), px(20.)),
+ ToggleSize::Lg => (px(24.), px(24.)),
+ ToggleSize::Mini => (px(22.), px(14.)),
}
}
}
@@ -82,7 +88,7 @@ impl Toggle {
/// bevel the same rule adds has no gpui equivalent (README deviation).
pub fn settings(theme: &Theme, id: impl Into, checked: bool) -> Self {
Self {
- size: ToggleSize::Sm,
+ size: ToggleSize::Mini,
// The material's control fill is near-white in light mode, so an
// off toggle read as a blank pill on the white settings cards.
off_fill: if theme.is_dark() {
@@ -173,7 +179,21 @@ impl RenderOnce for Toggle {
.when(checked, |this| this.justify_end())
.when(!disabled, |this| this.cursor_pointer())
.bg(if checked { on_fill } else { off_fill })
- .child(div().size(size.thumb()).rounded_full().bg(thumb))
+ .child({
+ let (thumb_width, thumb_height) = size.thumb();
+ div()
+ .w(thumb_width)
+ .h(thumb_height)
+ .rounded_full()
+ .bg(thumb)
+ .shadow(vec![gpui::BoxShadow {
+ color: gpui::hsla(0., 0., 0., 0.18),
+ offset: gpui::point(px(0.), px(0.5)),
+ blur_radius: px(1.5),
+ spread_radius: px(0.),
+ inset: false,
+ }])
+ })
.when_some(on_click.filter(|_| !disabled), |this, handler| {
this.on_click(move |event, window, cx| handler(event, window, cx))
})