bezel-theme 0.2.26

Design tokens for gpui — an environment-driven theme with designed light and dark palettes
Documentation
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
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
//! The app theme: one token set, two concrete instances.

use gpui::{Global, Hsla, SharedString};

use crate::{Appearance, paint};

mod family;
mod glass;
pub(crate) mod install;
mod layout;
mod palettes;
mod syntax;
pub(crate) mod typography;

pub use family::{Seed, ThemeFamily, Variant};
pub use layout::{ControlSize, Sizing};
pub use syntax::{HighlightKind, SyntaxPalette};
pub use typography::{Metrics, TextStyle, Typeset, base_text_size};

/// The two shipped glasses — SwiftUI's `Glass.regular` and `Glass.clear`. A
/// closed variant rather than knobs: Apple exposes no numbers on glass either,
/// only the variant and a tint.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Glass {
    /// The everyday material: it blurs what it covers and dims it hard.
    Regular,
    /// Near-transparent — the backdrop reads through, bent only at the rim.
    Clear,
}

/// SwiftUI's frost scale. Measured 2026-08-31: the five thicknesses are ONE
/// material at five opacities — the tone implied by `tint / (1 - gain)` holds
/// to within 9% across the scale, and the sigma does not move at all
/// (23.1/20.2/21.0pt). So this is a knob, not five looks.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Material {
    UltraThin,
    Thin,
    Regular,
    Thick,
    UltraThick,
}

impl Material {
    /// How much of the backdrop the material covers. Measured off SwiftUI in
    /// dark; the steps come out even to within a point.
    pub const fn opacity(self) -> f32 {
        match self {
            Material::UltraThin => 0.440,
            Material::Thin => 0.543,
            Material::Regular => 0.638,
            Material::Thick => 0.737,
            Material::UltraThick => 0.825,
        }
    }
}

/// Which surface a caller names. Material and glass are different things with
/// different vocabularies — a material has thickness, a glass has a variant —
/// and they meet only at the numbers they resolve to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SurfaceStyle {
    Material(Material),
    Glass(Glass),
}

/// The frost material, before a thickness picks its opacity.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MaterialSpec {
    /// The material's own tone, at full coverage.
    pub tone: Hsla,
    /// Its chroma push, as [`SurfaceSpec::saturation`].
    pub saturation: f32,
    /// Its sigma, which does not move with thickness.
    pub blur: f32,
    /// SwiftUI's frost has no lit rim at all — measured +0 at the boundary.
    /// What is here is bezel's own hairline, the one the popover card used to
    /// draw, moved to the surface that owes it.
    pub edge: f32,
    pub edge_width: f32,
    pub edge_aa: f32,
}

impl MaterialSpec {
    /// This material at one thickness.
    pub fn at(&self, thickness: Material) -> SurfaceSpec {
        let opacity = thickness.opacity();
        SurfaceSpec {
            gain: 1.0 - opacity,
            saturation: self.saturation,
            tint: self.tone.opacity(opacity),
            blur: self.blur,
            rim: 0.0,
            reach: 0.0,
            edge: self.edge,
            edge_width: self.edge_width,
            edge_aa: self.edge_aa,
            // A wash has nothing to bend, so it needs one to lift off the page.
            shadow: true,
        }
    }
}

/// One surface's numbers. `out = gain * saturated(backdrop) + tint`, over a
/// backdrop blurred at `blur`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SurfaceSpec {
    /// Slope of the transfer line. Below 1 compresses contrast toward
    /// `tint`; above 1 it brightens and slightly expands, which is what light
    /// `Clear` measures. Named for what it is, not for one of its directions.
    pub gain: f32,
    /// How far the backdrop's chroma is pushed from its own grey, before the
    /// gain drops the level. A gain alone moves level and colour together, so
    /// this is the only way a surface goes dark and keeps its colours. 1 is
    /// the pass-through `Clear` measures.
    pub saturation: f32,
    /// Its offset — the look's own tone.
    pub tint: Hsla,
    /// Gaussian sigma under the lens, in logical pixels. It stands in for the
    /// average attenuation of fine detail, which the real material gets from
    /// snapshotting its backdrop coarsely.
    pub blur: f32,
    /// How far in from the rim the lens bends the backdrop, in logical pixels.
    /// A length rather than a share of the box: the displacement curve measured
    /// 2026-08-30 is one curve, the same from a 96pt box to a 320pt one and
    /// from r24 to r84. Past it the backdrop is untouched.
    pub rim: f32,
    /// How far the outermost pixel drags what it samples, in logical pixels —
    /// the far end of that same curve, measured at ~47pt.
    pub reach: f32,
    /// How much white the lit rim adds at the edge itself, 0..1.
    pub edge: f32,
    /// How far in that light falls off to nothing, in logical pixels. Even the
    /// whole way round: measured 2026-08-30, the real rim reads the same on all
    /// four sides.
    pub edge_width: f32,
    /// The coverage ramp at the shape's own boundary, in logical pixels. 0.5
    /// is one device pixel at 2x — the plain rasteriser's answer; 0 is the hard
    /// edge it replaced.
    pub edge_aa: f32,
    /// Whether the surface casts [`paint::surface_shadows`], painted outside
    /// its shape.
    pub shadow: bool,
}

impl SurfaceSpec {
    /// The one backdrop this surface returns unchanged — `tint / (1 - gain)`,
    /// opaque. What a renderer with no backdrop blur paints in its place: the
    /// tint alone is a coverage over a blur, and with no blur under it the
    /// backdrop reads through as itself.
    ///
    /// `None` where the line has no such point — a `gain` of 1 or above
    /// brightens whatever it covers, and a `tint` carrying more light than the
    /// surface covers exceeds white.
    ///
    /// `tint` is [`Self::tint`] unless a caller substituted its own.
    pub fn flat(&self, tint: Hsla) -> Option<Hsla> {
        let coverage = 1.0 - self.gain;
        let l = tint.l * tint.a / coverage;
        (coverage > 0.0 && l <= 1.0).then_some(Hsla { l, a: 1.0, ..tint })
    }
}

impl SurfaceStyle {
    /// The numbers this style resolves to against a theme.
    pub fn spec(self, theme: &Theme) -> SurfaceSpec {
        match self {
            SurfaceStyle::Material(thickness) => theme.material.at(thickness),
            SurfaceStyle::Glass(Glass::Regular) => theme.glass_regular,
            SurfaceStyle::Glass(Glass::Clear) => theme.glass_clear,
        }
    }
}

/// The app theme. Two concrete instances — [`Theme::dark`] and [`Theme::light`].
#[derive(Debug, Clone)]
pub struct Theme {
    /// Which appearance these tokens were built for.
    pub appearance: Appearance,
    /// The [`ThemeFamily`] these tokens came from, by name. `None` is the
    /// shipped palette, the only one a [`Brand`](crate::Brand)'s tint rotates.
    pub family: Option<SharedString>,

    // ---- paint: neutral surfaces ----
    /// Main content panel. Dark: the deepest plane (#060606). Light: pure white —
    /// long-form content reads best on an unbroken white field.
    pub bg: Hsla,
    /// Shell / sidebar surface. Dark: one step *up* from `bg`. Light: one step
    /// *down* (grey) — chrome recedes from the content plane in both, which is
    /// the direction a naive invert gets backwards.
    pub surface: Hsla,
    /// Raised surface: opaque pills and chips that sit proud of the panel.
    /// Dark: lighter than `surface`. Light: white, separated by `border` +
    /// shadow rather than by lightness.
    pub surface_raised: Hsla,

    // ---- paint: elevation ladder ----
    //
    // Dark mode distinguishes floating planes by lightness, and the steps are
    // *small* (#0e → #10 → #16 → #1e). They are not interchangeable: collapsing
    // them onto one token visibly lifts popovers off their intended plane.
    //
    // Light mode cannot use the same trick, because the content plane is already
    // white and there is nothing lighter to climb to. All three land on white and
    // let `border` + shadow carry the separation instead — the standard light-UI
    // answer, and the reason this is a ladder of tokens rather than an arithmetic
    // offset applied to one.
    /// Inline card resting on the main panel (auth gate, empty-state cards).
    pub surface_card: Hsla,
    /// Modal dialog, floating over a [`Theme::scrim`].
    pub surface_dialog: Hsla,
    /// Popover, menu and command-palette surface — the highest plane.
    pub surface_overlay: Hsla,
    /// Hover wash for interactive rows and buttons, on glass and off it alike:
    /// `../desktop`'s `--color-hover`.
    pub element_hover: Hsla,
    /// Active/selected wash, one rung over the hover — `--color-active`.
    pub element_active: Hsla,
    /// Faintest hairline: dividers inside a card, nested frames.
    pub border_faint: Hsla,
    /// Hairline border.
    pub border: Hsla,
    /// Stronger border for focused/raised edges.
    pub border_strong: Hsla,
    /// Canvas workspace background.
    pub canvas_bg: Hsla,
    /// Canvas background grid dots.
    pub canvas_grid: Hsla,

    // ---- paint: text ----
    /// Primary text. Translucent, like `text_muted` and `text_faint`: the
    /// appearance's ink at the alpha [`Brand::ink`](crate::Brand::ink) sets.
    pub text: Hsla,
    /// Muted text: timestamps, secondary labels.
    pub text_muted: Hsla,
    /// Faint text: placeholders, disabled.
    pub text_faint: Hsla,
    /// One notch below `text_muted` — the diff file-path tone. It exists as its
    /// own token rather than being folded into `text_muted` because the dark
    /// value was sampled (#989898) and folding it would shift that label, which
    /// is a palette change dressed up as a refactor.
    pub text_dim: Hsla,

    // ---- paint: high-contrast solid (primary buttons) ----
    /// The maximum-contrast solid fill: near-white on dark, near-black on light.
    /// This is the primary button plate.
    pub solid: Hsla,
    /// Label/icon color on top of [`Self::solid`] — its inverse.
    pub on_solid: Hsla,

    // ---- paint: accents ----
    /// Accent — the emphasis weight for text and icons.
    ///
    /// **Neutral by default, and deliberately.** A component library that ships
    /// a hue puts that hue in every app that installs it, and bezel had an
    /// indigo running through spinners, pagination, date selection and list
    /// markers before anyone chose it. The default is now the same lightness
    /// with the chroma at zero.
    ///
    /// This is the token to brand:
    ///
    /// ```ignore
    /// let mut theme = Theme::for_appearance(appearance);
    /// theme.accent = my_brand_accent(appearance);
    /// ```
    ///
    /// See [`crate::AppExt::set_palette`](crate::AppExt::set_palette), which is what
    /// makes an override survive an appearance switch.
    pub accent: Hsla,
    /// Stronger accent for fills that carry [`Self::on_accent`] text. Neutral by
    /// default, it is the maximum-contrast plate — a mid grey would not carry a
    /// label the way the indigo it replaced did.
    pub accent_strong: Hsla,
    /// Label color on top of [`Self::accent_strong`].
    pub on_accent: Hsla,
    /// Danger — red (errors, stop button).
    pub danger: Hsla,
    /// Softer danger for secondary/inline error copy.
    pub danger_muted: Hsla,
    /// Warning — amber (offline notices, awaiting-input).
    pub warning: Hsla,
    /// Softer warning for secondary copy.
    pub warning_muted: Hsla,
    /// Success / online — emerald.
    pub success: Hsla,
    /// Working / streaming indicator — pink.
    pub busy: Hsla,
    /// Softer success for text on a success-tinted chip.
    pub success_muted: Hsla,

    // ---- paint: components ----
    /// Hover tone for an *opaque* raised pill. Hover must brighten the plate in
    /// dark mode, never swap it for a translucent wash (that made pills go
    /// see-through — user-reported); in light mode it darkens instead, same idea.
    pub surface_raised_hover: Hsla,
    /// Recessed band behind a palette/picker header or footer strip. Translucent
    /// so the glass still reads through.
    pub band: Hsla,
    /// The composer pill and other input plates.
    ///
    /// Its own token because "lifted" inverts between appearances. On dark, a
    /// faint *white* wash over near-black reads as raised. The literal light
    /// translation — a faint *black* wash on white — reads as **recessed**, a dent
    /// rather than a plate, which is why the prompt looked like bare text on a
    /// smudge. Light mode lifts the way light UIs actually do: pure white, with
    /// the border and shadow carrying the elevation.
    pub input_bg: Hsla,
    /// Text-selection highlight in the composer and inputs.
    ///
    /// Opaque, painted under the text. Measured macOS 26, 2026-09-27, default
    /// accent: `NSColor.selectedTextBackgroundColor` is `#B3D7FF` light and
    /// `#3F638B` dark.
    pub selection: Hsla,
    /// Terminal block cursor. Opaque: the glyph under it is drawn in the
    /// cell's background colour.
    pub cursor: Hsla,
    /// Text caret — [`Self::accent`]'s lightness.
    ///
    /// Measured macOS 26, 2026-08-31: `NSColor.textInsertionPointColor` is the
    /// accent in both appearances, where `NSColor.textColor` is white and
    /// black. So a caret is not the next glyph before you type it, which is
    /// what this used to carry; the platform gives it its own role, and a brand
    /// tints it here the way the system accent tints it there.
    pub caret: Hsla,
    /// Keyboard focus ring — a hairline, so it marks the control without
    /// restating the label inside it.
    pub ring: Hsla,
    /// Where a dragged block would land in the block editor.
    pub drop_line: Hsla,
    /// Fill of the region a dragged pane would dock into, where no material
    /// paints it. Translucent: the pane under it stays visible.
    pub drop_target: Hsla,
    /// Destructive-action button fill (danger plate, carries [`Self::on_accent`]).
    pub danger_strong: Hsla,

    // ---- paint: code & diff ----
    /// Inline-code text. Neutral: code is already set apart by the mono face
    /// and its wash, and a hue on top reads as a link rather than as code.
    pub code_text: Hsla,
    /// Inline-code wash behind [`Self::code_text`].
    pub code_wash: Hsla,
    /// Shared paint-only syntax palette.
    pub syntax: SyntaxPalette,
    /// Diff: added lines.
    pub diff_add: Hsla,
    /// Diff: deleted lines.
    pub diff_del: Hsla,
    /// Diff: hunk-header wash (bluish grey).
    pub diff_hunk_bg: Hsla,

    // ---- paint: terminal ----
    /// The terminal grid's background, behind cells that paint none.
    pub terminal_bg: Hsla,
    /// The 16 named ANSI colours: 0-7 normal, 8-15 bright. Indexes 16-255 —
    /// the xterm cube and grey ramp — are not part of a palette.
    pub terminal_ansi: [Hsla; 16],

    // ---- glass ----
    //
    // Numbers, so they flow with the appearance the way every other token
    // does. `ui::surface` reads them off the theme it is handed;
    // nothing here is a parameter on a component.
    /// How opaque the tint over the blurred window is.
    pub vibrancy_alpha: f32,
    /// The fill of a floating content card over a translucent window, at full
    /// coverage — see [`Self::content_bg`].
    pub content_background: Hsla,
    /// How much of the chrome [`Self::content_bg`] covers.
    pub content_opacity: f32,
    /// The tint over the blurred window, at full coverage — see
    /// [`Self::vibrancy_tint`].
    pub vibrancy_tone: Hsla,
    /// The blur behind a translucent window, in native filter pixels with no
    /// scale conversion. Zero leaves the window transparent and unblurred.
    /// macOS alone; other platforms ignore it.
    pub window_blur: f32,
    /// Whether the window composites translucent, so the desktop reaches what
    /// is painted over it — AppKit's vibrancy.
    pub vibrancy: bool,
    /// Whether components paint glass — translucent popovers and cards, and
    /// the lens. An opaque window can still carry it.
    pub glass: bool,
    /// The surfaces this theme can paint. Blur belongs to the look, not to
    /// the caller: Apple exposes no blur parameter on either family, only the
    /// thickness or the variant.
    pub material: MaterialSpec,
    pub glass_regular: SurfaceSpec,
    pub glass_clear: SurfaceSpec,
    /// What the popover surfaces — menus, dialogs, sheets, tooltips — mount
    /// on. They take no theme of their own, so this is where the choice
    /// lives; a component that owns its surface names its own style instead.
    pub popover_surface: SurfaceStyle,
    /// What a docking drop preview paints on where [`LENSED`](crate::LENSED)
    /// and [`Theme::glass`] hold; elsewhere it is a [`Theme::drop_target`]
    /// wash.
    pub drop_preview: SurfaceStyle,
    /// What a dragged item's floating copy paints on — see
    /// `ui::drag::Carried::ghost`.
    pub carried_surface: SurfaceStyle,
    /// Lens displacement amplitude, signed; negative inverts it.
    pub glass_magnify: f32,
    /// Per-channel spread of that displacement — the chromatic fringe.
    pub glass_dispersion: f32,

    // ---- fonts ----
    /// UI font family — the name the text system resolves, not the bytes.
    /// Defaults to the platform's own UI face; point it at your own family
    /// once you have registered that font with the text system.
    pub font_sans: SharedString,
    /// The family prose is set in — what `markdown` shapes a document's body
    /// with. Defaults to [`Self::font_sans`]; an app that sets it apart gives
    /// documents a reading face while the chrome keeps the UI one.
    pub font_body: SharedString,
    /// Monospace family for code/terminal.
    pub font_mono: SharedString,
}

impl Theme {
    /// Selection wash resolved against this palette, including local previews.
    pub fn card_selected_bg(&self) -> Hsla {
        paint::card_selected_bg_for(self.appearance)
    }

    /// A list row's text at rest: [`Self::text`] selected, else
    /// [`Self::text_faint`] archived, else [`Self::text_muted`].
    pub fn row_text(&self, selected: bool, archived: bool) -> Hsla {
        match (selected, archived) {
            (true, _) => self.text,
            (false, true) => self.text_faint,
            (false, false) => self.text_muted,
        }
    }

    /// Overlay ink at `alpha` — see [`ink`](crate::paint::ink).
    pub fn ink(&self, alpha: f32) -> Hsla {
        paint::ink_for(self.appearance, alpha)
    }

    /// The colour at `index` of the categorical set — a chart's series, a
    /// label's tint: the accent, then the status colours, then hues between
    /// them. Any index is valid; the set wraps.
    pub fn categorical(&self, index: usize) -> Hsla {
        let hue = |color: Hsla, degrees: f32| Hsla {
            h: degrees / 360.0,
            ..color
        };
        let palette = [
            self.accent,
            self.success,
            self.warning,
            self.danger,
            hue(self.success, 185.0),
            hue(self.accent, 270.0),
            hue(self.danger, 330.0),
            hue(self.warning, 50.0),
        ];
        palette[index % palette.len()]
    }

    /// Hairline ink at `alpha` — see [`hairline`](crate::paint::hairline).
    pub fn hairline(&self, alpha: f32) -> Hsla {
        paint::hairline_for(self.appearance, alpha)
    }

    /// State wash at `alpha` — see [`wash`](crate::paint::wash).
    pub fn wash(&self, alpha: f32) -> Hsla {
        paint::wash_for(self.appearance, alpha)
    }
}

impl Default for Theme {
    fn default() -> Self {
        Self::dark()
    }
}

impl Global for Theme {}