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.overrides
232 .primary
233 .unwrap_or_else(|| self.color(self.primary_color, self.primary_shade()))
234 }
235
236 pub fn body(&self) -> Color {
240 self.overrides.body.unwrap_or_else(|| match self.scheme {
241 ColorScheme::Light => self.white,
242 ColorScheme::Dark => self.color(ColorName::Dark, 7),
243 })
244 }
245
246 pub fn surface(&self) -> Color {
248 self.overrides.surface.unwrap_or_else(|| match self.scheme {
249 ColorScheme::Light => self.white,
250 ColorScheme::Dark => self.color(ColorName::Dark, 6),
251 })
252 }
253
254 pub fn surface_hover(&self) -> Color {
256 self.overrides
257 .surface_hover
258 .unwrap_or_else(|| match self.scheme {
259 ColorScheme::Light => self.color(ColorName::Gray, 0),
260 ColorScheme::Dark => self.color(ColorName::Dark, 5),
261 })
262 }
263
264 pub fn text(&self) -> Color {
266 self.overrides.text.unwrap_or_else(|| match self.scheme {
267 ColorScheme::Light => self.color(ColorName::Dark, 9),
268 ColorScheme::Dark => self.color(ColorName::Dark, 0),
269 })
270 }
271
272 pub fn dimmed(&self) -> Color {
274 self.overrides.dimmed.unwrap_or_else(|| match self.scheme {
275 ColorScheme::Light => self.color(ColorName::Gray, 6),
276 ColorScheme::Dark => self.color(ColorName::Dark, 2),
277 })
278 }
279
280 pub fn border(&self) -> Color {
282 self.overrides.border.unwrap_or_else(|| match self.scheme {
283 ColorScheme::Light => self.color(ColorName::Gray, 3),
284 ColorScheme::Dark => self.color(ColorName::Dark, 4),
285 })
286 }
287
288 pub fn selection(&self) -> gpui::Hsla {
293 self.primary().alpha(0.30)
294 }
295
296 pub fn success(&self) -> Color {
300 self.overrides
301 .success
302 .unwrap_or_else(|| self.color(ColorName::Green, self.primary_shade()))
303 }
304
305 pub fn warning(&self) -> Color {
307 self.overrides
308 .warning
309 .unwrap_or_else(|| self.color(ColorName::Yellow, self.primary_shade()))
310 }
311
312 pub fn danger(&self) -> Color {
314 self.overrides
315 .danger
316 .unwrap_or_else(|| self.color(ColorName::Red, self.primary_shade()))
317 }
318
319 pub fn info(&self) -> Color {
321 self.overrides
322 .info
323 .unwrap_or_else(|| self.color(ColorName::Cyan, self.primary_shade()))
324 }
325}
326
327pub fn theme(cx: &App) -> &Theme {
329 cx.global::<Theme>()
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn semantic_override_replaces_scheme_default() {
338 let base = Theme::light();
339 let themed = Theme::light()
340 .with_primary(css::rgb(200, 10, 10))
341 .with_body(css::css("#0b0b0f").unwrap());
342
343 assert!(themed.overrides.primary.is_some());
344 assert_ne!(themed.primary(), base.primary());
345 assert_ne!(themed.body(), base.body());
346 assert_eq!(themed.text(), base.text());
348 }
349}