1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
//! Glass recipes: translucent chrome over the blurred window background, and
//! the modal scrim.
use gpui::{Hsla, WindowBackgroundAppearance};
use crate::{
Appearance, color, paint,
theme::{Theme, layout},
};
impl Theme {
/// The frost tint painted over the blurred window background (macOS glass),
/// at [`Brand::glass`](crate::Brand::glass)'s alpha. Dark: darker than
/// `surface`, matched to the reference vibrancy scrim `hsl(0 0% 3%)`.
/// Light: a near-white frost. Opaque, this IS the surface tone.
pub fn glass(&self) -> Hsla {
let alpha = layout::frost();
if alpha >= 1.0 {
return self.surface;
}
match self.appearance {
Appearance::Dark => color::grey(8).opacity(alpha),
// 0xfa, not the surface's 0xf4-ish grey: at 90% coverage the tint
// IS the sidebar tone, and the darker grey read as a dingy pane
// next to the white content card.
Appearance::Light => color::grey(0xfa).opacity(alpha),
}
}
/// The app's root fill: the frost where glass is on, the opaque panel
/// where it is not. What a root element paints instead of
/// [`Self::bg`](Self#structfield.bg), so a window that opens blurred is not
/// then covered over by the paint that made the blur pointless.
pub fn window_bg(&self) -> Hsla {
if self.is_glass() {
self.glass()
} else {
self.bg
}
}
/// Whether this appearance paints translucent chrome over the blurred
/// desktop. Glass-only recipes — backdrop blurs, translucent popover
/// tints, per-glyph edge fades — must gate on this rather than reading the
/// brand's alpha, because an opaque appearance can still be asked for one.
pub fn is_glass(&self) -> bool {
self.glass().a < 1.0
}
/// Hover wash for chrome that sits ON GLASS (sidebar rows, tabs, titlebar
/// buttons). One recipe, both appearances: the 11% [`wash`](crate::paint::wash),
/// tone-flipped by the palette convention (soft-white on dark, soft-black
/// on light).
///
/// Hover and selection share the SAME fill (selection adds only the ring).
/// Light previously ran heavy white washes here (hover 0.55, selection
/// 0.92) after a black-hover-next-to-white-selection mismatch report; now
/// hover and selection are *both* the tone-flipped wash, so they lift the
/// same way again. Light's alpha sits under dark's: dark's 11% at the
/// light tone read too dark over the bright frost (user report).
pub fn glass_hover(&self) -> Hsla {
match self.appearance {
Appearance::Dark => paint::wash_for(Appearance::Dark, 0.11),
Appearance::Light => paint::wash_for(Appearance::Light, 0.06),
}
}
/// The translucent tint floating cards paint over their backdrop blur
/// (see `bezel::material`). Dark: the reference
/// `.glass-surface` menu tint verbatim — `oklch(0.33 0 0 / 34%)`. The
/// previous `surface_overlay` at 65% was tuned back when the tint had to
/// *approximate* the composited recipe without a real blur; kept over the
/// blur it buried the backdrop's colour and menus read as flat grey slabs
/// next to the hue-inheriting chrome (user report). At 34% the blurred
/// backdrop carries the card and the mid-grey only lifts it off the
/// plane. Light: heavier — a translucent white tint left menu text
/// ghosting over whatever sat behind the popover, so light coverage
/// steps up to keep rows on a known background.
pub fn glass_overlay(&self) -> Hsla {
match self.appearance {
Appearance::Dark => color::oklch(0.33, 0.0, 0.0).opacity(0.34),
Appearance::Light => self.surface_overlay.opacity(0.85),
}
}
/// The composer pill / question panel fill. Light's `input_bg` is opaque
/// white (the elevation ladder on an opaque page) — over glass it read as
/// a solid slab in front of the frosted blur, so it thins to a
/// translucent tint there (0.6 and then 0.45 both still read too bright
/// over the 0.80 frost — lowered on user request). Dark's 3% white wash
/// is already glass-native.
pub fn input_glass_bg(&self) -> Hsla {
if self.is_glass() && matches!(self.appearance, Appearance::Light) {
self.input_bg.opacity(0.30)
} else {
self.input_bg
}
}
/// Section-card fill (settings cards and similar in-panel cards). The
/// opaque `surface` tone read as a harsh solid slab floating on the
/// frosted blur (user report), so glass thins it to a translucent tint;
/// opaque platforms keep the true card tone.
pub fn card_glass_bg(&self) -> Hsla {
if self.is_glass() {
self.surface.opacity(0.40)
} else {
self.surface
}
}
/// The lift behind liquid glass (`bezel::ui::material::Glass::glass_effect`).
///
/// White, and composited *additively*: measured off `NSGlassEffectView`
/// over flat fields at five luminances (2026-08), its interior comes out
/// at `1.042 * backdrop + 19/255` — a veil that brightens whatever is
/// behind it, at every level, rather than a scrim that dims it.
pub fn glass_clear(&self) -> Hsla {
gpui::hsla(0.0, 0.0, 1.0, 0.075)
}
/// How far in from the rim the refracting profile reaches, given `extent`
/// — the shape's smaller side.
///
/// The interior is a straight pass-through: measurement of Apple's own
/// output shows the backdrop unperturbed to within a hundredth of a point
/// below the band, with every displacement inside it.
pub fn glass_bevel(extent: f32) -> f32 {
extent * layout::bevel()
}
/// Displacement amplitude of the glass lens, signed: positive magnifies
/// the interior and compresses what it displaces into the rim, negative
/// inverts it. 0.34 is what a single glass surface at eta 1.52 bends by,
/// which is where the physical model this replaced happened to land;
/// the number is a knob now rather than an index.
pub fn glass_magnify() -> f32 {
layout::magnify()
}
/// Per-channel spread of the displacement — the chromatic fringe at the
/// rim. Sub-percent: the fringe scales with the displacement, and at the
/// lens amplitude a wider spread reads as rainbow rather than glass.
pub fn glass_dispersion() -> f32 {
0.005
}
/// The standard modal backdrop — see [`scrim`](crate::paint::scrim).
pub fn scrim(&self) -> Hsla {
paint::scrim_for(self.appearance, paint::SCRIM_ALPHA_DARK)
}
/// How the platform should composite the window behind our paint. The mode
/// falls out of the alpha: frost under 1.0 wants the blurred desktop,
/// opaque wants opaque compositing (subpixel-friendly, no vibrancy cost for
/// a blur nothing shows).
///
/// This is a method rather than a constant because it has to be *re-applied* after
/// every theme swap: gpui's macOS backend tears the `NSVisualEffectView`
/// out of the hierarchy whenever the value is anything but `Blurred`, and
/// the re-apply in `appearance::apply` is what restores vibrancy when the
/// user switches back to dark. See zed's `crates/zed/src/main.rs`, which
/// runs the same loop on every settings change.
pub fn window_background_appearance(&self) -> WindowBackgroundAppearance {
if self.is_glass() {
WindowBackgroundAppearance::Blurred
} else {
WindowBackgroundAppearance::Opaque
}
}
}