Skip to main content

theme/theme/
palettes.rs

1//! The two concrete palettes: dark and light.
2
3use gpui::hsla;
4
5use crate::{
6    Appearance, color, paint,
7    theme::{Theme, syntax::SyntaxPalette},
8};
9
10impl Theme {
11    /// Build the dark theme. The surface tones are sampled straight from the
12    /// reference screenshots of the original app (docs/reference): main panel
13    /// `#060606`, shell/sidebar `#0d0d0d`.
14    pub fn dark() -> Self {
15        Self {
16            appearance: Appearance::Dark,
17            bg: color::grey(6),       // main panel — sampled #060606
18            surface: color::grey(13), // shell / sidebar — sampled #0d0d0d
19            surface_raised: color::neutral(0.235),
20            surface_card: color::grey(0x0e),
21            surface_dialog: color::grey(0x10),
22            surface_overlay: color::grey(0x16),
23            element_hover: hsla(0.0, 0.0, 0.92, 0.11),
24            element_active: hsla(0.0, 0.0, 0.92, 0.16),
25            border: hsla(0.0, 0.0, 1.0, 0.08),
26            border_strong: hsla(0.0, 0.0, 1.0, 0.14),
27            text: color::neutral(0.922),       // ~neutral-200
28            text_muted: color::neutral(0.708), // ~neutral-400
29            text_faint: color::neutral(0.556), // ~neutral-500
30            text_dim: color::grey(0x98),
31            solid: color::neutral(0.922),         // near-white plate
32            on_solid: color::grey(0x0e),          // near-black label
33            accent: color::neutral(0.673),        // indigo-400's lightness, no chroma
34            accent_strong: color::neutral(0.922), // the solid plate
35            on_accent: color::grey(0x0e),         // its inverse label
36            danger: color::oklch(0.704, 0.191, 22.216), // red-400
37            danger_muted: color::oklch(0.808, 0.114, 19.571), // red-300
38            warning: color::oklch(0.828, 0.189, 84.429), // amber-400
39            warning_muted: color::oklch(0.924, 0.12, 95.746), // amber-200
40            success: color::oklch(0.765, 0.177, 163.223), // emerald-400
41            busy: color::oklch(0.718, 0.202, 349.761), // pink-400
42            success_muted: color::oklch(0.845, 0.143, 164.978), // emerald-300
43            surface_raised_hover: color::neutral(0.29),
44            band: paint::band_for(Appearance::Dark),
45            input_bg: hsla(0.0, 0.0, 1.0, 0.03),
46            selection: hsla(0.66, 0.6, 0.55, 0.35),
47            cursor: hsla(0.0, 0.0, 1.0, 0.35),
48            caret: color::neutral(0.922), // the body text colour
49            ring: paint::hairline_for(Appearance::Dark, 0.35), // ~2.5× border_strong
50            danger_strong: color::oklch(0.58, 0.16, 25.0),
51            code_text: color::neutral(0.94), // near-white, a shade above body text
52            code_wash: hsla(0.0, 0.0, 1.0, 0.08), // white/8
53            syntax: SyntaxPalette::dark(
54                color::neutral(0.922),
55                color::neutral(0.60),
56                color::oklch(0.704, 0.191, 22.216),
57            ),
58            diff_add: color::oklch(0.765, 0.177, 163.223), // emerald-400
59            diff_del: color::oklch(0.704, 0.191, 22.216),  // red-400
60            diff_hunk_bg: hsla(0.6, 0.35, 0.6, 0.05),
61            font_sans: "Geist".into(),
62            font_mono: "Geist Mono".into(),
63            font_sans_fallback: system_sans().into(),
64            font_mono_fallback: system_mono().into(),
65        }
66    }
67
68    /// Build the light theme.
69    ///
70    /// Neutrals are the same oklch scale read from the other end, but the *roles*
71    /// are reassigned rather than mirrored (see the module docs): content plane
72    /// white, chrome grey, raised surfaces white-plus-shadow. Text tones are
73    /// picked to reproduce the dark theme's contrast ratios, and accents drop
74    /// from the 400 to the 600 step at identical hue so they clear WCAG AA on
75    /// white instead of glowing.
76    pub fn light() -> Self {
77        Self {
78            appearance: Appearance::Light,
79            bg: color::grey(0xff), // main panel — clean white
80            // Deeper than ~neutral-100 looks on paper: the content card is pure
81            // white and sits *inside* this surface, so too small a step leaves the
82            // whole window one flat sheet with a hairline drawn on it.
83            surface: color::neutral(0.968),
84            // A real grey, NOT white. This is the opaque-plate tone — user
85            // message bubbles, the jump-to-bottom pill — and those sit directly
86            // on the white content plane with no border or shadow to save them.
87            // White here made the user's own messages vanish into the page.
88            // Popovers do not use this; they have their own ladder below.
89            surface_raised: color::neutral(0.940),
90            surface_card: color::grey(0xff),
91            surface_dialog: color::grey(0xff),
92            surface_overlay: color::grey(0xff),
93            element_hover: hsla(0.0, 0.0, 0.10, 0.06),
94            element_active: hsla(0.0, 0.0, 0.10, 0.10),
95            border: hsla(0.0, 0.0, 0.0, 0.10),
96            border_strong: hsla(0.0, 0.0, 0.0, 0.17),
97            // ~neutral-850. Pure neutral-900 measures 17.9:1 on white — *more*
98            // contrast than dark mode's 16.1:1, which reads as harsh rather than
99            // crisp. Backing off to 0.25 lands at ~16:1: the same perceived
100            // weight as the dark theme, not the maximum available.
101            text: color::neutral(0.25),
102            text_muted: color::neutral(0.439), // ~neutral-600 → ~7.7:1
103            // A touch darker than dark mode's neutral-500 counterpart: the light
104            // sidebar is a real grey, and faint text has to clear its floor there
105            // too, not just on the white content plane.
106            text_faint: color::neutral(0.535),
107            text_dim: color::neutral(0.50),
108            solid: color::neutral(0.205), // near-black plate, deeper than body text
109            on_solid: color::neutral(0.985), // near-white label
110            accent: color::neutral(0.511), // indigo-600's lightness, no chroma
111            accent_strong: color::neutral(0.205), // the solid plate
112            on_accent: color::neutral(0.985), // its inverse label
113            danger: color::oklch(0.577, 0.245, 27.325), // red-600
114            danger_muted: color::oklch(0.505, 0.213, 27.518), // red-700
115            warning: color::oklch(0.555, 0.163, 48.998), // amber-700 — carries 12px text
116            warning_muted: color::oklch(0.473, 0.137, 46.201), // amber-800
117            success: color::oklch(0.596, 0.145, 163.225), // emerald-600
118            busy: color::oklch(0.592, 0.249, 0.584), // pink-600
119            success_muted: color::oklch(0.508, 0.118, 165.612), // emerald-700
120            // Opaque pills darken on hover here rather than brighten — same
121            // "brighten the plate, don't wash it out" rule, read the other way.
122            surface_raised_hover: color::neutral(0.900),
123            // A recessed strip on white needs far less ink than on near-black;
124            // the dark 16% would read as a bruise.
125            band: paint::band_for(Appearance::Light),
126            input_bg: color::grey(0xff),
127            selection: hsla(0.66, 0.75, 0.62, 0.28),
128            cursor: hsla(0.0, 0.0, 0.0, 0.55),
129            caret: color::neutral(0.25), // the body text colour
130            ring: paint::hairline_for(Appearance::Light, 0.35),
131            danger_strong: color::oklch(0.51, 0.20, 25.0),
132            code_text: color::neutral(0.18), // near-black, a shade under body text
133            code_wash: hsla(0.0, 0.0, 0.0, 0.06), // black/6
134            syntax: SyntaxPalette::light(
135                color::neutral(0.25),
136                color::neutral(0.48),
137                color::oklch(0.505, 0.213, 27.518),
138            ),
139            diff_add: color::oklch(0.596, 0.145, 163.225), // emerald-600
140            diff_del: color::oklch(0.577, 0.245, 27.325),  // red-600
141            diff_hunk_bg: hsla(0.6, 0.35, 0.35, 0.07),
142            font_sans: "Geist".into(),
143            font_mono: "Geist Mono".into(),
144            font_sans_fallback: system_sans().into(),
145            font_mono_fallback: system_mono().into(),
146        }
147    }
148
149    /// Build the theme for an appearance.
150    pub fn for_appearance(appearance: Appearance) -> Self {
151        match appearance {
152            Appearance::Dark => Self::dark(),
153            Appearance::Light => Self::light(),
154        }
155    }
156}
157
158fn system_sans() -> &'static str {
159    if cfg!(target_os = "macos") {
160        "Helvetica"
161    } else if cfg!(target_os = "windows") {
162        "Segoe UI"
163    } else {
164        "DejaVu Sans"
165    }
166}
167
168fn system_mono() -> &'static str {
169    if cfg!(target_os = "macos") {
170        "Menlo"
171    } else if cfg!(target_os = "windows") {
172        "Consolas"
173    } else {
174        "DejaVu Sans Mono"
175    }
176}