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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
//! The two concrete palettes: dark and light.
use gpui::hsla;
use crate::{
Appearance, color, paint,
theme::{Glass, MaterialSpec, SurfaceSpec, SurfaceStyle, Theme, syntax::SyntaxPalette},
};
impl Theme {
/// Build the dark theme. The surface tones are sampled straight from the
/// reference screenshots of the original app (docs/reference): main panel
/// `#060606`, shell/sidebar `#0d0d0d`.
pub fn dark() -> Self {
Self {
appearance: Appearance::Dark,
bg: color::grey(6), // main panel — sampled #060606
surface: color::grey(13), // shell / sidebar — sampled #0d0d0d
surface_raised: color::neutral(0.235),
surface_card: color::grey(0x0e),
surface_dialog: color::grey(0x10),
surface_overlay: color::grey(0x16),
// `../desktop`'s `--color-hover`, pure ink: white at 8%.
element_hover: hsla(0.0, 0.0, 1.0, 0.08),
// `--color-active`, a rung above the hover: white at 12%.
element_active: hsla(0.0, 0.0, 1.0, 0.12),
border: hsla(0.0, 0.0, 1.0, 0.08),
border_strong: hsla(0.0, 0.0, 1.0, 0.14),
text: color::neutral(0.922), // ~neutral-200
text_muted: color::neutral(0.708), // ~neutral-400
text_faint: color::neutral(0.556), // ~neutral-500
text_dim: color::grey(0x98),
solid: color::neutral(0.922), // near-white plate
on_solid: color::grey(0x0e), // near-black label
accent: color::neutral(0.673), // indigo-400's lightness, no chroma
accent_strong: color::neutral(0.922), // the solid plate
on_accent: color::grey(0x0e), // its inverse label
danger: color::oklch(0.704, 0.191, 22.216), // red-400
danger_muted: color::oklch(0.808, 0.114, 19.571), // red-300
warning: color::oklch(0.828, 0.189, 84.429), // amber-400
warning_muted: color::oklch(0.924, 0.12, 95.746), // amber-200
success: color::oklch(0.765, 0.177, 163.223), // emerald-400
busy: color::oklch(0.718, 0.202, 349.761), // pink-400
success_muted: color::oklch(0.845, 0.143, 164.978), // emerald-300
surface_raised_hover: color::neutral(0.29),
band: paint::band_for(Appearance::Dark),
input_bg: hsla(0.0, 0.0, 1.0, 0.03),
selection: hsla(0.66, 0.6, 0.55, 0.35),
cursor: hsla(0.0, 0.0, 1.0, 0.35),
caret: color::neutral(0.673), // the accent
ring: paint::hairline_for(Appearance::Dark, 0.35), // ~2.5× border_strong
danger_strong: color::oklch(0.58, 0.16, 25.0),
code_text: color::neutral(0.94), // near-white, a shade above body text
code_wash: hsla(0.0, 0.0, 1.0, 0.08), // white/8
syntax: SyntaxPalette::dark(
color::neutral(0.922),
color::neutral(0.60),
color::oklch(0.704, 0.191, 22.216),
),
diff_add: color::oklch(0.765, 0.177, 163.223), // emerald-400
diff_del: color::oklch(0.704, 0.191, 22.216), // red-400
diff_hunk_bg: hsla(0.6, 0.35, 0.6, 0.05),
vibrancy_alpha: Self::VIBRANCY_ALPHA,
vibrancy: Self::VIBRANCY_ALPHA < 1.0,
glass: Self::VIBRANCY_ALPHA < 1.0,
// SwiftUI's frost, measured 2026-08-31: `tint / (1 - gain)` implies
// one tone across all five thicknesses (49.8 down to 45.3), and the
// sigma does not move with them. The rim is bezel's, not Apple's —
// SwiftUI's material has no lit edge at all, and the popover card
// used to draw this hairline itself.
material: MaterialSpec {
tone: color::grey(47),
saturation: 2.1,
blur: 21.0,
edge: 0.10,
edge_width: 1.0,
edge_aa: 0.5,
},
// Measured 2026-08-31 off a real NSGlassEffectView, one
// whole-canvas tone at a time: a fill the size of the probe cannot
// be contaminated by a 10pt blur, which is what every earlier
// reading of this look got wrong. Six greys give the line at rms
// 0.9 levels, four saturated tones agree on the saturation to 0.1,
// and the sigma is the gaussian that best fits a 70pt bar edge at
// the centre of a 320pt glass, rms 0.4 levels. Over a backdrop that
// is NOT locally flat the real material saturates less than these
// numbers reproduce, so its saturation is not the per-pixel one
// this models. Rim and lit edge are `Clear`'s.
glass_regular: SurfaceSpec {
gain: 0.311,
saturation: 2.55,
tint: gpui::hsla(0.0, 0.0, 1.0, 11.0 / 255.0),
// An on-screen choice, 2026-09-01. The measurement above reads
// 10.8, at which the interior is a flat wash and every seam in
// the lens shows as a step in it.
blur: 4.0,
rim: 18.75,
reach: 47.0,
// Measured 2026-08-31 over a black canvas, where the coverage
// blend can only pull down, so anything above the interior is
// rim light: +25 levels at the boundary and gone by 1pt, the
// same in both appearances. Clear's 0.26 was read over a bright
// backdrop, where the blend toward a brighter outside inflates
// it — at that value the rim reads as a drawn border. It scales
// with what is behind it (+25 over black, +36 over mid grey)
// where this is a constant, so it is one point on their curve.
edge: 0.10,
edge_width: 1.0,
edge_aa: 0.5,
shadow: false,
},
// `Clear` refit 2026-08-30 over the gallery's own backdrops, rms
// 0.4 levels on backdrop 0..212. A window that is not key carries a
// different material, and this is the key one. The sigma is off a
// 2pt rule, which a 48pt band is too wide to resolve. The rim is
// off the position-coded backdrop, pooled over four shapes from
// 96pt to 320pt and r24 to r84: one curve, rms 1.8pt.
glass_clear: SurfaceSpec {
gain: 1.029,
saturation: 1.0,
tint: gpui::hsla(0.0, 0.0, 1.0, 16.0 / 255.0),
blur: 1.2,
rim: 18.75,
reach: 47.0,
edge: 0.26,
edge_width: 1.4,
edge_aa: 0.5,
shadow: false,
},
// Matched on screen against a real NSGlassEffectView, which is a
// higher bar than the dome's algebra: fitting the formula to their
// measured curve lands ~15% short of what the shader then renders.
popover_surface: SurfaceStyle::Glass(Glass::Regular),
glass_magnify: 1.1,
glass_dispersion: 0.005,
font_sans: SYSTEM_SANS.into(),
font_mono: system_mono().into(),
}
}
/// Build the light theme.
///
/// Neutrals are the same oklch scale read from the other end, but the *roles*
/// are reassigned rather than mirrored (see the module docs): content plane
/// white, chrome grey, raised surfaces white-plus-shadow. Text tones are
/// picked to reproduce the dark theme's contrast ratios, and accents drop
/// from the 400 to the 600 step at identical hue so they clear WCAG AA on
/// white instead of glowing.
pub fn light() -> Self {
Self {
appearance: Appearance::Light,
bg: color::grey(0xff), // main panel — clean white
// Deeper than ~neutral-100 looks on paper: the content card is pure
// white and sits *inside* this surface, so too small a step leaves the
// whole window one flat sheet with a hairline drawn on it.
surface: color::neutral(0.968),
// A real grey, NOT white. This is the opaque-plate tone — user
// message bubbles, the jump-to-bottom pill — and those sit directly
// on the white content plane with no border or shadow to save them.
// White here made the user's own messages vanish into the page.
// Popovers do not use this; they have their own ladder below.
surface_raised: color::neutral(0.940),
surface_card: color::grey(0xff),
surface_dialog: color::grey(0xff),
surface_overlay: color::grey(0xff),
// The same token in light: black at 4%.
element_hover: hsla(0.0, 0.0, 0.0, 0.04),
// The same rung in light: black at 6%.
element_active: hsla(0.0, 0.0, 0.0, 0.06),
border: hsla(0.0, 0.0, 0.0, 0.10),
border_strong: hsla(0.0, 0.0, 0.0, 0.17),
// ~neutral-850. Pure neutral-900 measures 17.9:1 on white — *more*
// contrast than dark mode's 16.1:1, which reads as harsh rather than
// crisp. Backing off to 0.25 lands at ~16:1: the same perceived
// weight as the dark theme, not the maximum available.
text: color::neutral(0.25),
text_muted: color::neutral(0.439), // ~neutral-600 → ~7.7:1
// A touch darker than dark mode's neutral-500 counterpart: the light
// sidebar is a real grey, and faint text has to clear its floor there
// too, not just on the white content plane.
text_faint: color::neutral(0.535),
text_dim: color::neutral(0.50),
solid: color::neutral(0.205), // near-black plate, deeper than body text
on_solid: color::neutral(0.985), // near-white label
accent: color::neutral(0.511), // indigo-600's lightness, no chroma
accent_strong: color::neutral(0.205), // the solid plate
on_accent: color::neutral(0.985), // its inverse label
danger: color::oklch(0.577, 0.245, 27.325), // red-600
danger_muted: color::oklch(0.505, 0.213, 27.518), // red-700
warning: color::oklch(0.555, 0.163, 48.998), // amber-700 — carries 12px text
warning_muted: color::oklch(0.473, 0.137, 46.201), // amber-800
success: color::oklch(0.596, 0.145, 163.225), // emerald-600
busy: color::oklch(0.592, 0.249, 0.584), // pink-600
success_muted: color::oklch(0.508, 0.118, 165.612), // emerald-700
// Opaque pills darken on hover here rather than brighten — same
// "brighten the plate, don't wash it out" rule, read the other way.
surface_raised_hover: color::neutral(0.900),
// A recessed strip on white needs far less ink than on near-black;
// the dark 16% would read as a bruise.
band: paint::band_for(Appearance::Light),
input_bg: color::grey(0xff),
selection: hsla(0.66, 0.75, 0.62, 0.28),
cursor: hsla(0.0, 0.0, 0.0, 0.55),
caret: color::neutral(0.511), // the accent
ring: paint::hairline_for(Appearance::Light, 0.35),
danger_strong: color::oklch(0.51, 0.20, 25.0),
code_text: color::neutral(0.18), // near-black, a shade under body text
code_wash: hsla(0.0, 0.0, 0.0, 0.06), // black/6
syntax: SyntaxPalette::light(
color::neutral(0.25),
color::neutral(0.48),
color::oklch(0.505, 0.213, 27.518),
),
diff_add: color::oklch(0.596, 0.145, 163.225), // emerald-600
diff_del: color::oklch(0.577, 0.245, 27.325), // red-600
diff_hunk_bg: hsla(0.6, 0.35, 0.35, 0.07),
vibrancy_alpha: Self::VIBRANCY_ALPHA,
vibrancy: Self::VIBRANCY_ALPHA < 1.0,
glass: Self::VIBRANCY_ALPHA < 1.0,
// Measured 2026-08-30, macOS 26.3 LIGHT, same instruments. The
// material is not a tone-flip of dark: `regular` keeps its opacity
// (86%) and swaps a 19% grey base for a 97% white one, which is why
// it reads as ordinary frost here. `clear` stops compressing
// altogether — it is very nearly a pure lift.
// Material: the menu surface before glass, kept as a look of its own
// because it stays readable over content a lens would only bend.
// Light's own tone, same instrument: gain 0.378 at `Regular`, so
// opacity 0.622 against dark's 0.638 — the scale is shared and only
// the tone moves. The sigma is dark's; it is a SwiftUI constant,
// not an appearance choice.
material: MaterialSpec {
tone: color::grey(235),
saturation: 2.1,
blur: 21.0,
edge: 0.10,
edge_width: 1.0,
edge_aa: 0.5,
},
// Measured 2026-08-31, same instrument as dark. Light `regular`
// is very nearly a white sheet — 84% of the output is tint — so
// the little backdrop that survives is pushed much harder to keep
// its colour: saturation 4.27 against dark's 2.55, each within 0.1
// over four hues. Rim and lit edge are dark's, unmeasured here.
glass_regular: SurfaceSpec {
gain: 0.139,
saturation: 4.27,
tint: gpui::hsla(0.0, 0.0, 1.0, 214.0 / 255.0),
blur: 8.9,
rim: 18.75,
reach: 47.0,
edge: 0.10,
edge_width: 1.0,
edge_aa: 0.5,
shadow: false,
},
// Carries dark's rim, sigma and edge: light has not been
// re-measured since the instrument learned to hold the window key.
glass_clear: SurfaceSpec {
gain: 1.041,
saturation: 1.0,
tint: gpui::hsla(0.0, 0.0, 1.0, 18.8 / 255.0),
blur: 1.2,
rim: 18.75,
reach: 47.0,
edge: 0.26,
edge_width: 1.4,
edge_aa: 0.5,
shadow: false,
},
// At 1-3pt inside the rim the real material drags the backdrop 26pt
// or more, and lets go by 5.5pt; on the shader's profile that is 8.
popover_surface: SurfaceStyle::Glass(Glass::Regular),
glass_magnify: 1.1,
glass_dispersion: 0.005,
font_sans: SYSTEM_SANS.into(),
font_mono: system_mono().into(),
}
}
/// Build the theme for an appearance.
pub fn for_appearance(appearance: Appearance) -> Self {
match appearance {
Appearance::Dark => Self::dark(),
Appearance::Light => Self::light(),
}
}
}
/// gpui's alias for whatever the platform calls its UI font, resolved per
/// backend by `font_name_with_fallbacks`.
const SYSTEM_SANS: &str = ".SystemUIFont";
/// The mono face has no alias of its own, so each backend is named here.
/// macOS wants the dot-name — "SF Mono" does not resolve. wasm has only what
/// gpui-web bundles, which `.ZedMono` names.
fn system_mono() -> &'static str {
if cfg!(target_family = "wasm") {
".ZedMono"
} else if cfg!(target_os = "macos") {
".AppleSystemUIFontMonospaced"
} else if cfg!(target_os = "windows") {
"Consolas"
} else {
"DejaVu Sans Mono"
}
}