1use gpui::{App, Global, Hsla, SharedString};
4use std::collections::HashMap;
5use std::sync::Arc;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ThemeVariant {
10 Dark,
11 Light,
12}
13
14impl Default for ThemeVariant {
15 fn default() -> Self {
16 ThemeVariant::Dark
17 }
18}
19
20#[derive(Debug, Clone)]
22pub struct Theme {
23 pub name: SharedString,
24 pub variant: ThemeVariant,
25
26 pub fg: Hsla,
28
29 pub bg: Hsla,
31
32 pub surface: Hsla,
34
35 pub border: Hsla,
37
38 pub outline: Hsla,
40}
41
42impl Theme {
43 pub fn gruvbox_dark() -> Self {
45 Theme {
46 name: SharedString::from("Gruvbox Dark"),
47 variant: ThemeVariant::Dark,
48 fg: parse_hex("#ebdbb2"),
49 bg: parse_hex("#282828"),
50 surface: parse_hex("#3c3836"),
51 border: parse_hex("#504945"),
52 outline: parse_hex("#458588"),
53 }
54 }
55
56 pub fn gruvbox_light() -> Self {
58 Theme {
59 name: SharedString::from("Gruvbox Light"),
60 variant: ThemeVariant::Light,
61 fg: parse_hex("#3c3836"),
62 bg: parse_hex("#fbf1c7"),
63 surface: parse_hex("#ebdbb2"),
64 border: parse_hex("#d5c4a1"),
65 outline: parse_hex("#076678"),
66 }
67 }
68
69 pub fn get_global(cx: &App) -> &Arc<Theme> {
71 &cx.global::<GlobalTheme>().0
72 }
73}
74
75impl Default for Theme {
76 fn default() -> Self {
77 Theme::gruvbox_dark()
78 }
79}
80
81#[derive(Clone, Debug)]
83pub struct GlobalTheme(pub Arc<Theme>);
84
85impl Global for GlobalTheme {}
86
87impl Default for GlobalTheme {
88 fn default() -> Self {
89 GlobalTheme(Arc::new(Theme::default()))
90 }
91}
92
93pub trait ActiveTheme {
95 fn theme(&self) -> &Arc<Theme>;
97}
98
99impl ActiveTheme for App {
100 fn theme(&self) -> &Arc<Theme> {
101 &self.global::<GlobalTheme>().0
102 }
103}
104
105#[derive(Debug, Default)]
107pub struct Themes {
108 themes: HashMap<SharedString, Arc<Theme>>,
109 active: Option<SharedString>,
110}
111
112impl Themes {
113 pub fn new() -> Self {
115 let mut themes = HashMap::new();
116
117 let gruvbox_dark = Theme::gruvbox_dark();
119 let gruvbox_light = Theme::gruvbox_light();
120
121 themes.insert(gruvbox_dark.name.clone(), Arc::new(gruvbox_dark));
122 themes.insert(gruvbox_light.name.clone(), Arc::new(gruvbox_light));
123
124 Self {
125 themes,
126 active: Some(SharedString::from("Gruvbox Dark")),
127 }
128 }
129
130 pub fn add(&mut self, theme: Theme) {
132 self.themes.insert(theme.name.clone(), Arc::new(theme));
133 }
134
135 pub fn get(&self, name: &str) -> Option<Arc<Theme>> {
137 self.themes.get(name).cloned()
138 }
139
140 pub fn set_active(&mut self, name: impl Into<SharedString>) -> Option<Arc<Theme>> {
142 let name = name.into();
143 if let Some(theme) = self.themes.get(&name) {
144 self.active = Some(name);
145 Some(theme.clone())
146 } else {
147 None
148 }
149 }
150
151 pub fn active(&self) -> Option<Arc<Theme>> {
153 self.active
154 .as_ref()
155 .and_then(|name| self.themes.get(name).cloned())
156 }
157
158 pub fn list(&self) -> Vec<SharedString> {
160 self.themes.keys().cloned().collect()
161 }
162
163 pub fn apply_global(&self, cx: &mut App) -> Option<Arc<Theme>> {
165 if let Some(theme) = self.active() {
166 cx.set_global(GlobalTheme(theme.clone()));
167 Some(theme)
168 } else {
169 None
170 }
171 }
172}
173
174fn parse_hex(hex: &str) -> Hsla {
176 let hex = hex.trim_start_matches('#');
177
178 let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
179 let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
180 let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
181
182 let r = r as f32 / 255.0;
184 let g = g as f32 / 255.0;
185 let b = b as f32 / 255.0;
186
187 let max = r.max(g).max(b);
188 let min = r.min(g).min(b);
189 let delta = max - min;
190
191 let lightness = (max + min) / 2.0;
192
193 let saturation = if delta == 0.0 {
194 0.0
195 } else {
196 delta / (1.0 - (2.0 * lightness - 1.0).abs())
197 };
198
199 let hue = if delta == 0.0 {
200 0.0
201 } else if max == r {
202 60.0 * (((g - b) / delta) % 6.0)
203 } else if max == g {
204 60.0 * ((b - r) / delta + 2.0)
205 } else {
206 60.0 * ((r - g) / delta + 4.0)
207 };
208
209 let hue = if hue < 0.0 { hue + 360.0 } else { hue };
210
211 gpui::hsla(hue / 360.0, saturation, lightness, 1.0)
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_default_theme() {
220 let theme = Theme::default();
221 assert_eq!(theme.name, SharedString::from("Gruvbox Dark"));
222 assert_eq!(theme.variant, ThemeVariant::Dark);
223 }
224
225 #[test]
226 fn test_themes_manager() {
227 let mut themes = Themes::new();
228
229 assert!(themes.get("Gruvbox Dark").is_some());
231 assert!(themes.get("Gruvbox Light").is_some());
232
233 assert!(themes.active().is_some());
235
236 let light_theme = themes.set_active("Gruvbox Light");
238 assert!(light_theme.is_some());
239 assert_eq!(themes.active().unwrap().variant, ThemeVariant::Light);
240
241 let custom = Theme {
243 name: SharedString::from("Custom"),
244 variant: ThemeVariant::Dark,
245 fg: parse_hex("#ffffff"),
246 bg: parse_hex("#000000"),
247 surface: parse_hex("#111111"),
248 border: parse_hex("#222222"),
249 outline: parse_hex("#0066cc"),
250 };
251
252 themes.add(custom);
253 assert!(themes.get("Custom").is_some());
254 }
255
256 #[test]
257 fn test_hex_parsing() {
258 let color = parse_hex("#ffffff");
259 assert_eq!(color.l, 1.0); let color = parse_hex("#000000");
262 assert_eq!(color.l, 0.0); }
264}