1mod color;
9mod css;
10mod json;
11mod manager;
12mod palette;
13mod presets;
14mod tokens;
15
16pub use color::Color;
17pub use css::{css, hsl, hsla, rgb, rgba, CssColorError};
18pub use json::ThemeJsonError;
19pub use manager::{
20 manager, scheme_of, ThemeChoice, ThemeEntry, ThemeLoadError, ThemeManager, ThemeSource,
21};
22pub use palette::{open_color, ColorName, Palette, Shades};
23pub use presets::PRESET_NAMES;
24pub use tokens::{Scale, Size};
25
26use gpui::{App, Global, SharedString};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ColorScheme {
31 Light,
32 Dark,
33}
34
35impl ColorScheme {
36 pub fn is_dark(self) -> bool {
37 matches!(self, ColorScheme::Dark)
38 }
39
40 pub fn toggled(self) -> Self {
42 match self {
43 ColorScheme::Light => ColorScheme::Dark,
44 ColorScheme::Dark => ColorScheme::Light,
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct Theme {
52 pub scheme: ColorScheme,
53 pub palette: Palette,
54 pub primary_color: ColorName,
56 pub primary_shade_light: usize,
58 pub primary_shade_dark: usize,
59 pub white: Color,
60 pub black: Color,
61 pub spacing: Scale,
62 pub radius: Scale,
63 pub font_size: Scale,
64 pub default_radius: Size,
66 pub line_height: f32,
67 pub font_family: SharedString,
68 pub overrides: Overrides,
72}
73
74#[derive(Debug, Clone, Copy, Default)]
77pub struct Overrides {
78 pub primary: Option<Color>,
79 pub body: Option<Color>,
80 pub surface: Option<Color>,
81 pub surface_hover: Option<Color>,
82 pub text: Option<Color>,
83 pub dimmed: Option<Color>,
84 pub border: Option<Color>,
85 pub success: Option<Color>,
86 pub warning: Option<Color>,
87 pub danger: Option<Color>,
88 pub info: Option<Color>,
89}
90
91impl Global for Theme {}
92
93impl Default for Theme {
94 fn default() -> Self {
95 Theme::light()
96 }
97}
98
99impl Theme {
100 pub fn light() -> Self {
101 Theme {
102 scheme: ColorScheme::Light,
103 palette: open_color(),
104 primary_color: ColorName::Blue,
105 primary_shade_light: 6,
106 primary_shade_dark: 8,
107 white: Color::hex("#ffffff"),
108 black: Color::hex("#000000"),
109 spacing: Scale::spacing(),
110 radius: Scale::radius(),
111 font_size: Scale::font_size(),
112 default_radius: Size::Sm,
113 line_height: 1.55,
114 font_family: SharedString::new_static("Helvetica"),
115 overrides: Overrides::default(),
116 }
117 }
118
119 pub fn dark() -> Self {
120 Theme {
121 scheme: ColorScheme::Dark,
122 ..Theme::light()
123 }
124 }
125
126 pub fn init(self, cx: &mut App) {
128 cx.set_global(self);
129 }
130
131 pub fn with_primary(mut self, color: impl Into<gpui::Hsla>) -> Self {
140 self.overrides.primary = Some(Color::from_hsla(color.into()));
141 self
142 }
143
144 pub fn with_body(mut self, color: impl Into<gpui::Hsla>) -> Self {
146 self.overrides.body = Some(Color::from_hsla(color.into()));
147 self
148 }
149
150 pub fn with_surface(mut self, color: impl Into<gpui::Hsla>) -> Self {
152 self.overrides.surface = Some(Color::from_hsla(color.into()));
153 self
154 }
155
156 pub fn with_surface_hover(mut self, color: impl Into<gpui::Hsla>) -> Self {
158 self.overrides.surface_hover = Some(Color::from_hsla(color.into()));
159 self
160 }
161
162 pub fn with_text(mut self, color: impl Into<gpui::Hsla>) -> Self {
164 self.overrides.text = Some(Color::from_hsla(color.into()));
165 self
166 }
167
168 pub fn with_dimmed(mut self, color: impl Into<gpui::Hsla>) -> Self {
170 self.overrides.dimmed = Some(Color::from_hsla(color.into()));
171 self
172 }
173
174 pub fn with_border(mut self, color: impl Into<gpui::Hsla>) -> Self {
176 self.overrides.border = Some(Color::from_hsla(color.into()));
177 self
178 }
179
180 pub fn with_success(mut self, color: impl Into<gpui::Hsla>) -> Self {
182 self.overrides.success = Some(Color::from_hsla(color.into()));
183 self
184 }
185
186 pub fn with_warning(mut self, color: impl Into<gpui::Hsla>) -> Self {
188 self.overrides.warning = Some(Color::from_hsla(color.into()));
189 self
190 }
191
192 pub fn with_danger(mut self, color: impl Into<gpui::Hsla>) -> Self {
194 self.overrides.danger = Some(Color::from_hsla(color.into()));
195 self
196 }
197
198 pub fn with_info(mut self, color: impl Into<gpui::Hsla>) -> Self {
200 self.overrides.info = Some(Color::from_hsla(color.into()));
201 self
202 }
203
204 pub fn spacing(&self, size: Size) -> f32 {
207 self.spacing.get(size)
208 }
209
210 pub fn radius(&self, size: Size) -> f32 {
211 self.radius.get(size)
212 }
213
214 pub fn font_size(&self, size: Size) -> f32 {
215 self.font_size.get(size)
216 }
217
218 pub fn color(&self, name: ColorName, shade: usize) -> Color {
222 self.palette.get(name, shade)
223 }
224
225 pub fn primary_shade(&self) -> usize {
227 match self.scheme {
228 ColorScheme::Light => self.primary_shade_light,
229 ColorScheme::Dark => self.primary_shade_dark,
230 }
231 }
232
233 pub fn primary(&self) -> Color {
235 self
236 .overrides
237 .primary
238 .unwrap_or_else(|| self.color(self.primary_color, self.primary_shade()))
239 }
240
241 pub fn body(&self) -> Color {
245 self.overrides.body.unwrap_or_else(|| match self.scheme {
246 ColorScheme::Light => self.white,
247 ColorScheme::Dark => self.color(ColorName::Dark, 7),
248 })
249 }
250
251 pub fn surface(&self) -> Color {
253 self.overrides.surface.unwrap_or_else(|| match self.scheme {
254 ColorScheme::Light => self.white,
255 ColorScheme::Dark => self.color(ColorName::Dark, 6),
256 })
257 }
258
259 pub fn surface_hover(&self) -> Color {
261 self
262 .overrides
263 .surface_hover
264 .unwrap_or_else(|| match self.scheme {
265 ColorScheme::Light => self.color(ColorName::Gray, 0),
266 ColorScheme::Dark => self.color(ColorName::Dark, 5),
267 })
268 }
269
270 pub fn text(&self) -> Color {
272 self.overrides.text.unwrap_or_else(|| match self.scheme {
273 ColorScheme::Light => self.color(ColorName::Dark, 9),
274 ColorScheme::Dark => self.color(ColorName::Dark, 0),
275 })
276 }
277
278 pub fn dimmed(&self) -> Color {
280 self.overrides.dimmed.unwrap_or_else(|| match self.scheme {
281 ColorScheme::Light => self.color(ColorName::Gray, 6),
282 ColorScheme::Dark => self.color(ColorName::Dark, 2),
283 })
284 }
285
286 pub fn border(&self) -> Color {
288 self.overrides.border.unwrap_or_else(|| match self.scheme {
289 ColorScheme::Light => self.color(ColorName::Gray, 3),
290 ColorScheme::Dark => self.color(ColorName::Dark, 4),
291 })
292 }
293
294 pub fn selection(&self) -> gpui::Hsla {
299 self.primary().alpha(0.30)
300 }
301
302 pub fn success(&self) -> Color {
306 self
307 .overrides
308 .success
309 .unwrap_or_else(|| self.color(ColorName::Green, self.primary_shade()))
310 }
311
312 pub fn warning(&self) -> Color {
314 self
315 .overrides
316 .warning
317 .unwrap_or_else(|| self.color(ColorName::Yellow, self.primary_shade()))
318 }
319
320 pub fn danger(&self) -> Color {
322 self
323 .overrides
324 .danger
325 .unwrap_or_else(|| self.color(ColorName::Red, self.primary_shade()))
326 }
327
328 pub fn info(&self) -> Color {
330 self
331 .overrides
332 .info
333 .unwrap_or_else(|| self.color(ColorName::Cyan, self.primary_shade()))
334 }
335}
336
337pub fn theme(cx: &App) -> &Theme {
339 cx.global::<Theme>()
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn semantic_override_replaces_scheme_default() {
348 let base = Theme::light();
349 let themed = Theme::light()
350 .with_primary(css::rgb(200, 10, 10))
351 .with_body(css::css("#0b0b0f").unwrap());
352
353 assert!(themed.overrides.primary.is_some());
354 assert_ne!(themed.primary(), base.primary());
355 assert_ne!(themed.body(), base.body());
356 assert_eq!(themed.text(), base.text());
358 }
359}