gpuikit_theme/
lib.rs

1//! A simple theme system for gpui-kit
2
3use gpui::{App, Global, Hsla, SharedString};
4use std::collections::HashMap;
5use std::sync::Arc;
6
7/// Available theme variants
8#[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/// Core theme structure with essential color tokens
21#[derive(Debug, Clone)]
22pub struct Theme {
23    pub name: SharedString,
24    pub variant: ThemeVariant,
25
26    /// Foreground color for text
27    pub fg: Hsla,
28
29    /// Background color for the main application
30    pub bg: Hsla,
31
32    /// Surface color for cards, panels, and elevated surfaces
33    pub surface: Hsla,
34
35    /// Border color for dividers and component boundaries
36    pub border: Hsla,
37
38    /// Outline color for focus states
39    pub outline: Hsla,
40}
41
42impl Theme {
43    /// Create a Gruvbox Dark theme
44    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    /// Create a Gruvbox Light theme
57    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    /// Get the global theme instance
70    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/// Global container for the application-wide theme
82#[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
93/// Trait for accessing the current theme from an App context
94pub trait ActiveTheme {
95    /// Returns a reference to the currently active theme
96    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/// Simple theme collection manager
106#[derive(Debug, Default)]
107pub struct Themes {
108    themes: HashMap<SharedString, Arc<Theme>>,
109    active: Option<SharedString>,
110}
111
112impl Themes {
113    /// Create a new theme collection with built-in themes
114    pub fn new() -> Self {
115        let mut themes = HashMap::new();
116
117        // Add built-in themes
118        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    /// Add a custom theme
131    pub fn add(&mut self, theme: Theme) {
132        self.themes.insert(theme.name.clone(), Arc::new(theme));
133    }
134
135    /// Get a theme by name
136    pub fn get(&self, name: &str) -> Option<Arc<Theme>> {
137        self.themes.get(name).cloned()
138    }
139
140    /// Set the active theme by name
141    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    /// Get the active theme
152    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    /// List all available theme names
159    pub fn list(&self) -> Vec<SharedString> {
160        self.themes.keys().cloned().collect()
161    }
162
163    /// Apply the active theme globally
164    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
174/// Helper function to parse hex color strings
175fn 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    // Convert RGB to HSLA
183    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        // Should have built-in themes
230        assert!(themes.get("Gruvbox Dark").is_some());
231        assert!(themes.get("Gruvbox Light").is_some());
232
233        // Active theme should be set
234        assert!(themes.active().is_some());
235
236        // Should be able to switch themes
237        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        // Can add custom themes
242        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); // White should have lightness of 1.0
260
261        let color = parse_hex("#000000");
262        assert_eq!(color.l, 0.0); // Black should have lightness of 0.0
263    }
264}