1mod color;
9mod css;
10mod palette;
11mod tokens;
12
13pub use color::Color;
14pub use css::{css, hsl, hsla, rgb, rgba, CssColorError};
15pub use palette::{mantine, ColorName, Palette, Shades};
16pub use tokens::{Scale, Size};
17
18use gpui::{App, Global, SharedString};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ColorScheme {
23 Light,
24 Dark,
25}
26
27impl ColorScheme {
28 pub fn is_dark(self) -> bool {
29 matches!(self, ColorScheme::Dark)
30 }
31
32 pub fn toggled(self) -> Self {
34 match self {
35 ColorScheme::Light => ColorScheme::Dark,
36 ColorScheme::Dark => ColorScheme::Light,
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct Theme {
44 pub scheme: ColorScheme,
45 pub palette: Palette,
46 pub primary_color: ColorName,
48 pub primary_shade_light: usize,
50 pub primary_shade_dark: usize,
51 pub white: Color,
52 pub black: Color,
53 pub spacing: Scale,
54 pub radius: Scale,
55 pub font_size: Scale,
56 pub default_radius: Size,
58 pub line_height: f32,
59 pub font_family: SharedString,
60 pub overrides: Overrides,
64}
65
66#[derive(Debug, Clone, Copy, Default)]
69pub struct Overrides {
70 pub primary: Option<Color>,
71 pub body: Option<Color>,
72 pub surface: Option<Color>,
73 pub surface_hover: Option<Color>,
74 pub text: Option<Color>,
75 pub dimmed: Option<Color>,
76 pub border: Option<Color>,
77}
78
79impl Global for Theme {}
80
81impl Default for Theme {
82 fn default() -> Self {
83 Theme::light()
84 }
85}
86
87impl Theme {
88 pub fn light() -> Self {
89 Theme {
90 scheme: ColorScheme::Light,
91 palette: mantine(),
92 primary_color: ColorName::Blue,
93 primary_shade_light: 6,
94 primary_shade_dark: 8,
95 white: Color::hex("#ffffff"),
96 black: Color::hex("#000000"),
97 spacing: Scale::spacing(),
98 radius: Scale::radius(),
99 font_size: Scale::font_size(),
100 default_radius: Size::Sm,
101 line_height: 1.55,
102 font_family: SharedString::new_static("Helvetica"),
103 overrides: Overrides::default(),
104 }
105 }
106
107 pub fn dark() -> Self {
108 Theme {
109 scheme: ColorScheme::Dark,
110 ..Theme::light()
111 }
112 }
113
114 pub fn init(self, cx: &mut App) {
116 cx.set_global(self);
117 }
118
119 pub fn with_primary(mut self, color: impl Into<gpui::Hsla>) -> Self {
128 self.overrides.primary = Some(Color::from_hsla(color.into()));
129 self
130 }
131
132 pub fn with_body(mut self, color: impl Into<gpui::Hsla>) -> Self {
134 self.overrides.body = Some(Color::from_hsla(color.into()));
135 self
136 }
137
138 pub fn with_surface(mut self, color: impl Into<gpui::Hsla>) -> Self {
140 self.overrides.surface = Some(Color::from_hsla(color.into()));
141 self
142 }
143
144 pub fn with_surface_hover(mut self, color: impl Into<gpui::Hsla>) -> Self {
146 self.overrides.surface_hover = Some(Color::from_hsla(color.into()));
147 self
148 }
149
150 pub fn with_text(mut self, color: impl Into<gpui::Hsla>) -> Self {
152 self.overrides.text = Some(Color::from_hsla(color.into()));
153 self
154 }
155
156 pub fn with_dimmed(mut self, color: impl Into<gpui::Hsla>) -> Self {
158 self.overrides.dimmed = Some(Color::from_hsla(color.into()));
159 self
160 }
161
162 pub fn with_border(mut self, color: impl Into<gpui::Hsla>) -> Self {
164 self.overrides.border = Some(Color::from_hsla(color.into()));
165 self
166 }
167
168 pub fn spacing(&self, size: Size) -> f32 {
171 self.spacing.get(size)
172 }
173
174 pub fn radius(&self, size: Size) -> f32 {
175 self.radius.get(size)
176 }
177
178 pub fn font_size(&self, size: Size) -> f32 {
179 self.font_size.get(size)
180 }
181
182 pub fn color(&self, name: ColorName, shade: usize) -> Color {
186 self.palette.get(name, shade)
187 }
188
189 pub fn primary_shade(&self) -> usize {
191 match self.scheme {
192 ColorScheme::Light => self.primary_shade_light,
193 ColorScheme::Dark => self.primary_shade_dark,
194 }
195 }
196
197 pub fn primary(&self) -> Color {
199 self.overrides
200 .primary
201 .unwrap_or_else(|| self.color(self.primary_color, self.primary_shade()))
202 }
203
204 pub fn body(&self) -> Color {
208 self.overrides.body.unwrap_or_else(|| match self.scheme {
209 ColorScheme::Light => self.white,
210 ColorScheme::Dark => self.color(ColorName::Dark, 7),
211 })
212 }
213
214 pub fn surface(&self) -> Color {
216 self.overrides.surface.unwrap_or_else(|| match self.scheme {
217 ColorScheme::Light => self.white,
218 ColorScheme::Dark => self.color(ColorName::Dark, 6),
219 })
220 }
221
222 pub fn surface_hover(&self) -> Color {
224 self.overrides
225 .surface_hover
226 .unwrap_or_else(|| match self.scheme {
227 ColorScheme::Light => self.color(ColorName::Gray, 0),
228 ColorScheme::Dark => self.color(ColorName::Dark, 5),
229 })
230 }
231
232 pub fn text(&self) -> Color {
234 self.overrides.text.unwrap_or_else(|| match self.scheme {
235 ColorScheme::Light => self.color(ColorName::Dark, 9),
236 ColorScheme::Dark => self.color(ColorName::Dark, 0),
237 })
238 }
239
240 pub fn dimmed(&self) -> Color {
242 self.overrides.dimmed.unwrap_or_else(|| match self.scheme {
243 ColorScheme::Light => self.color(ColorName::Gray, 6),
244 ColorScheme::Dark => self.color(ColorName::Dark, 2),
245 })
246 }
247
248 pub fn border(&self) -> Color {
250 self.overrides.border.unwrap_or_else(|| match self.scheme {
251 ColorScheme::Light => self.color(ColorName::Gray, 3),
252 ColorScheme::Dark => self.color(ColorName::Dark, 4),
253 })
254 }
255}
256
257pub fn theme(cx: &App) -> &Theme {
259 cx.global::<Theme>()
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn semantic_override_replaces_scheme_default() {
268 let base = Theme::light();
269 let themed = Theme::light()
270 .with_primary(css::rgb(200, 10, 10))
271 .with_body(css::css("#0b0b0f").unwrap());
272
273 assert!(themed.overrides.primary.is_some());
274 assert_ne!(themed.primary(), base.primary());
275 assert_ne!(themed.body(), base.body());
276 assert_eq!(themed.text(), base.text());
278 }
279}