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}