1mod color;
9mod css;
10mod json;
11mod palette;
12mod presets;
13mod tokens;
14
15pub use color::Color;
16pub use css::{css, hsl, hsla, rgb, rgba, CssColorError};
17pub use json::ThemeJsonError;
18pub use palette::{open_color, ColorName, Palette, Shades};
19pub use presets::PRESET_NAMES;
20pub use tokens::{Scale, Size};
21
22use gpui::{App, Global, SharedString};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ColorScheme {
27 Light,
28 Dark,
29}
30
31impl ColorScheme {
32 pub fn is_dark(self) -> bool {
33 matches!(self, ColorScheme::Dark)
34 }
35
36 pub fn toggled(self) -> Self {
38 match self {
39 ColorScheme::Light => ColorScheme::Dark,
40 ColorScheme::Dark => ColorScheme::Light,
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct Theme {
48 pub scheme: ColorScheme,
49 pub palette: Palette,
50 pub primary_color: ColorName,
52 pub primary_shade_light: usize,
54 pub primary_shade_dark: usize,
55 pub white: Color,
56 pub black: Color,
57 pub spacing: Scale,
58 pub radius: Scale,
59 pub font_size: Scale,
60 pub default_radius: Size,
62 pub line_height: f32,
63 pub font_family: SharedString,
64 pub overrides: Overrides,
68}
69
70#[derive(Debug, Clone, Copy, Default)]
73pub struct Overrides {
74 pub primary: Option<Color>,
75 pub body: Option<Color>,
76 pub surface: Option<Color>,
77 pub surface_hover: Option<Color>,
78 pub text: Option<Color>,
79 pub dimmed: Option<Color>,
80 pub border: Option<Color>,
81 pub success: Option<Color>,
82 pub warning: Option<Color>,
83 pub danger: Option<Color>,
84 pub info: Option<Color>,
85}
86
87impl Global for Theme {}
88
89impl Default for Theme {
90 fn default() -> Self {
91 Theme::light()
92 }
93}
94
95impl Theme {
96 pub fn light() -> Self {
97 Theme {
98 scheme: ColorScheme::Light,
99 palette: open_color(),
100 primary_color: ColorName::Blue,
101 primary_shade_light: 6,
102 primary_shade_dark: 8,
103 white: Color::hex("#ffffff"),
104 black: Color::hex("#000000"),
105 spacing: Scale::spacing(),
106 radius: Scale::radius(),
107 font_size: Scale::font_size(),
108 default_radius: Size::Sm,
109 line_height: 1.55,
110 font_family: SharedString::new_static("Helvetica"),
111 overrides: Overrides::default(),
112 }
113 }
114
115 pub fn dark() -> Self {
116 Theme {
117 scheme: ColorScheme::Dark,
118 ..Theme::light()
119 }
120 }
121
122 pub fn init(self, cx: &mut App) {
124 cx.set_global(self);
125 }
126
127 pub fn with_primary(mut self, color: impl Into<gpui::Hsla>) -> Self {
136 self.overrides.primary = Some(Color::from_hsla(color.into()));
137 self
138 }
139
140 pub fn with_body(mut self, color: impl Into<gpui::Hsla>) -> Self {
142 self.overrides.body = Some(Color::from_hsla(color.into()));
143 self
144 }
145
146 pub fn with_surface(mut self, color: impl Into<gpui::Hsla>) -> Self {
148 self.overrides.surface = Some(Color::from_hsla(color.into()));
149 self
150 }
151
152 pub fn with_surface_hover(mut self, color: impl Into<gpui::Hsla>) -> Self {
154 self.overrides.surface_hover = Some(Color::from_hsla(color.into()));
155 self
156 }
157
158 pub fn with_text(mut self, color: impl Into<gpui::Hsla>) -> Self {
160 self.overrides.text = Some(Color::from_hsla(color.into()));
161 self
162 }
163
164 pub fn with_dimmed(mut self, color: impl Into<gpui::Hsla>) -> Self {
166 self.overrides.dimmed = Some(Color::from_hsla(color.into()));
167 self
168 }
169
170 pub fn with_border(mut self, color: impl Into<gpui::Hsla>) -> Self {
172 self.overrides.border = Some(Color::from_hsla(color.into()));
173 self
174 }
175
176 pub fn with_success(mut self, color: impl Into<gpui::Hsla>) -> Self {
178 self.overrides.success = Some(Color::from_hsla(color.into()));
179 self
180 }
181
182 pub fn with_warning(mut self, color: impl Into<gpui::Hsla>) -> Self {
184 self.overrides.warning = Some(Color::from_hsla(color.into()));
185 self
186 }
187
188 pub fn with_danger(mut self, color: impl Into<gpui::Hsla>) -> Self {
190 self.overrides.danger = Some(Color::from_hsla(color.into()));
191 self
192 }
193
194 pub fn with_info(mut self, color: impl Into<gpui::Hsla>) -> Self {
196 self.overrides.info = Some(Color::from_hsla(color.into()));
197 self
198 }
199
200 pub fn spacing(&self, size: Size) -> f32 {
203 self.spacing.get(size)
204 }
205
206 pub fn radius(&self, size: Size) -> f32 {
207 self.radius.get(size)
208 }
209
210 pub fn font_size(&self, size: Size) -> f32 {
211 self.font_size.get(size)
212 }
213
214 pub fn color(&self, name: ColorName, shade: usize) -> Color {
218 self.palette.get(name, shade)
219 }
220
221 pub fn primary_shade(&self) -> usize {
223 match self.scheme {
224 ColorScheme::Light => self.primary_shade_light,
225 ColorScheme::Dark => self.primary_shade_dark,
226 }
227 }
228
229 pub fn primary(&self) -> Color {
231 self
232 .overrides
233 .primary
234 .unwrap_or_else(|| self.color(self.primary_color, self.primary_shade()))
235 }
236
237 pub fn body(&self) -> Color {
241 self.overrides.body.unwrap_or_else(|| match self.scheme {
242 ColorScheme::Light => self.white,
243 ColorScheme::Dark => self.color(ColorName::Dark, 7),
244 })
245 }
246
247 pub fn surface(&self) -> Color {
249 self.overrides.surface.unwrap_or_else(|| match self.scheme {
250 ColorScheme::Light => self.white,
251 ColorScheme::Dark => self.color(ColorName::Dark, 6),
252 })
253 }
254
255 pub fn surface_hover(&self) -> Color {
257 self
258 .overrides
259 .surface_hover
260 .unwrap_or_else(|| match self.scheme {
261 ColorScheme::Light => self.color(ColorName::Gray, 0),
262 ColorScheme::Dark => self.color(ColorName::Dark, 5),
263 })
264 }
265
266 pub fn text(&self) -> Color {
268 self.overrides.text.unwrap_or_else(|| match self.scheme {
269 ColorScheme::Light => self.color(ColorName::Dark, 9),
270 ColorScheme::Dark => self.color(ColorName::Dark, 0),
271 })
272 }
273
274 pub fn dimmed(&self) -> Color {
276 self.overrides.dimmed.unwrap_or_else(|| match self.scheme {
277 ColorScheme::Light => self.color(ColorName::Gray, 6),
278 ColorScheme::Dark => self.color(ColorName::Dark, 2),
279 })
280 }
281
282 pub fn border(&self) -> Color {
284 self.overrides.border.unwrap_or_else(|| match self.scheme {
285 ColorScheme::Light => self.color(ColorName::Gray, 3),
286 ColorScheme::Dark => self.color(ColorName::Dark, 4),
287 })
288 }
289
290 pub fn selection(&self) -> gpui::Hsla {
295 self.primary().alpha(0.30)
296 }
297
298 pub fn success(&self) -> Color {
302 self
303 .overrides
304 .success
305 .unwrap_or_else(|| self.color(ColorName::Green, self.primary_shade()))
306 }
307
308 pub fn warning(&self) -> Color {
310 self
311 .overrides
312 .warning
313 .unwrap_or_else(|| self.color(ColorName::Yellow, self.primary_shade()))
314 }
315
316 pub fn danger(&self) -> Color {
318 self
319 .overrides
320 .danger
321 .unwrap_or_else(|| self.color(ColorName::Red, self.primary_shade()))
322 }
323
324 pub fn info(&self) -> Color {
326 self
327 .overrides
328 .info
329 .unwrap_or_else(|| self.color(ColorName::Cyan, self.primary_shade()))
330 }
331}
332
333pub fn theme(cx: &App) -> &Theme {
335 cx.global::<Theme>()
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn semantic_override_replaces_scheme_default() {
344 let base = Theme::light();
345 let themed = Theme::light()
346 .with_primary(css::rgb(200, 10, 10))
347 .with_body(css::css("#0b0b0f").unwrap());
348
349 assert!(themed.overrides.primary.is_some());
350 assert_ne!(themed.primary(), base.primary());
351 assert_ne!(themed.body(), base.body());
352 assert_eq!(themed.text(), base.text());
354 }
355}