Skip to main content

enya_plugin/
theme.rs

1//! Custom theme definitions for plugins.
2//!
3//! This module provides types for plugins to define custom color themes
4//! that integrate with the editor's theme system.
5
6/// A custom theme definition from a plugin.
7#[derive(Debug, Clone)]
8pub struct ThemeDefinition {
9    /// Unique identifier (e.g., "tokyo-night")
10    pub name: String,
11    /// Display name for UI (e.g., "Tokyo Night")
12    pub display_name: String,
13    /// Base theme to inherit from ("dark" or "light")
14    pub base: ThemeBase,
15    /// Color palette
16    pub colors: ThemeColors,
17}
18
19impl ThemeDefinition {
20    /// Create a new theme definition with default colors.
21    pub fn new(name: impl Into<String>, display_name: impl Into<String>, base: ThemeBase) -> Self {
22        Self {
23            name: name.into(),
24            display_name: display_name.into(),
25            base,
26            colors: ThemeColors::default(),
27        }
28    }
29
30    /// Set background colors.
31    pub fn with_backgrounds(
32        mut self,
33        base: Option<u32>,
34        surface: Option<u32>,
35        elevated: Option<u32>,
36    ) -> Self {
37        self.colors.bg_base = base;
38        self.colors.bg_surface = surface;
39        self.colors.bg_elevated = elevated;
40        self
41    }
42
43    /// Set text colors.
44    pub fn with_text(
45        mut self,
46        primary: Option<u32>,
47        secondary: Option<u32>,
48        muted: Option<u32>,
49    ) -> Self {
50        self.colors.text_primary = primary;
51        self.colors.text_secondary = secondary;
52        self.colors.text_muted = muted;
53        self
54    }
55
56    /// Set accent colors.
57    pub fn with_accents(
58        mut self,
59        primary: Option<u32>,
60        hover: Option<u32>,
61        muted: Option<u32>,
62    ) -> Self {
63        self.colors.accent_primary = primary;
64        self.colors.accent_hover = hover;
65        self.colors.accent_muted = muted;
66        self
67    }
68
69    /// Set border colors.
70    pub fn with_borders(mut self, subtle: Option<u32>, strong: Option<u32>) -> Self {
71        self.colors.border_subtle = subtle;
72        self.colors.border_strong = strong;
73        self
74    }
75
76    /// Set semantic colors.
77    pub fn with_semantic(
78        mut self,
79        success: Option<u32>,
80        warning: Option<u32>,
81        error: Option<u32>,
82        info: Option<u32>,
83    ) -> Self {
84        self.colors.success = success;
85        self.colors.warning = warning;
86        self.colors.error = error;
87        self.colors.info = info;
88        self
89    }
90
91    /// Set chart palette.
92    pub fn with_chart_palette(mut self, palette: Vec<u32>) -> Self {
93        self.colors.chart_palette = palette;
94        self
95    }
96}
97
98/// Base theme to inherit missing colors from.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
100pub enum ThemeBase {
101    /// Inherit from the dark theme
102    #[default]
103    Dark,
104    /// Inherit from the light theme
105    Light,
106}
107
108impl ThemeBase {
109    /// Parse from string.
110    pub fn parse(s: &str) -> Self {
111        match s.to_lowercase().as_str() {
112            "light" | "l" => Self::Light,
113            _ => Self::Dark,
114        }
115    }
116}
117
118/// Color palette for a custom theme.
119///
120/// All colors are optional - missing colors are inherited from the base theme.
121/// Colors are stored as RGB hex values (e.g., 0x1a1b26 for #1a1b26).
122#[derive(Debug, Clone, Default)]
123pub struct ThemeColors {
124    // Backgrounds
125    /// Main canvas background (e.g., 0x1a1b26)
126    pub bg_base: Option<u32>,
127    /// Surface/panel background
128    pub bg_surface: Option<u32>,
129    /// Elevated elements (cards, dropdowns)
130    pub bg_elevated: Option<u32>,
131
132    // Text
133    /// Primary text color
134    pub text_primary: Option<u32>,
135    /// Secondary text color
136    pub text_secondary: Option<u32>,
137    /// Muted/disabled text color
138    pub text_muted: Option<u32>,
139
140    // Accents
141    /// Primary accent color
142    pub accent_primary: Option<u32>,
143    /// Hover accent color (brighter)
144    pub accent_hover: Option<u32>,
145    /// Muted accent color (for subtle backgrounds)
146    pub accent_muted: Option<u32>,
147
148    // Borders
149    /// Subtle border color
150    pub border_subtle: Option<u32>,
151    /// Strong border color
152    pub border_strong: Option<u32>,
153
154    // Semantic colors
155    /// Success color (green-ish)
156    pub success: Option<u32>,
157    /// Warning color (yellow/orange-ish)
158    pub warning: Option<u32>,
159    /// Error color (red-ish)
160    pub error: Option<u32>,
161    /// Info color (blue-ish)
162    pub info: Option<u32>,
163
164    // Chart palette
165    /// Colors for chart series (up to 8)
166    pub chart_palette: Vec<u32>,
167}
168
169impl ThemeColors {
170    /// Parse a hex color string (e.g., "#1a1b26" or "1a1b26") to u32.
171    pub fn parse_hex(s: &str) -> Option<u32> {
172        let hex = s.trim().trim_start_matches('#');
173        if hex.len() != 6 {
174            return None;
175        }
176        u32::from_str_radix(hex, 16).ok()
177    }
178
179    /// Convert u32 RGB to (r, g, b) tuple.
180    pub fn to_rgb(color: u32) -> (u8, u8, u8) {
181        let r = ((color >> 16) & 0xFF) as u8;
182        let g = ((color >> 8) & 0xFF) as u8;
183        let b = (color & 0xFF) as u8;
184        (r, g, b)
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_theme_base_parse() {
194        assert_eq!(ThemeBase::parse("dark"), ThemeBase::Dark);
195        assert_eq!(ThemeBase::parse("light"), ThemeBase::Light);
196        assert_eq!(ThemeBase::parse("l"), ThemeBase::Light);
197        assert_eq!(ThemeBase::parse("LIGHT"), ThemeBase::Light);
198        assert_eq!(ThemeBase::parse("unknown"), ThemeBase::Dark); // default
199    }
200
201    #[test]
202    fn test_parse_hex() {
203        assert_eq!(ThemeColors::parse_hex("#1a1b26"), Some(0x1a1b26));
204        assert_eq!(ThemeColors::parse_hex("1a1b26"), Some(0x1a1b26));
205        assert_eq!(ThemeColors::parse_hex("#FFFFFF"), Some(0xFFFFFF));
206        assert_eq!(ThemeColors::parse_hex("invalid"), None);
207        assert_eq!(ThemeColors::parse_hex("#12345"), None); // too short
208    }
209
210    #[test]
211    fn test_to_rgb() {
212        assert_eq!(ThemeColors::to_rgb(0xFF0000), (255, 0, 0));
213        assert_eq!(ThemeColors::to_rgb(0x00FF00), (0, 255, 0));
214        assert_eq!(ThemeColors::to_rgb(0x0000FF), (0, 0, 255));
215        assert_eq!(ThemeColors::to_rgb(0x1a1b26), (26, 27, 38));
216    }
217
218    #[test]
219    fn test_theme_definition_builder() {
220        let theme = ThemeDefinition::new("tokyo-night", "Tokyo Night", ThemeBase::Dark)
221            .with_backgrounds(Some(0x1a1b26), Some(0x24283b), Some(0x414868))
222            .with_accents(Some(0x7aa2f7), Some(0x89b4fa), None)
223            .with_chart_palette(vec![0x7aa2f7, 0x9ece6a, 0xe0af68]);
224
225        assert_eq!(theme.name, "tokyo-night");
226        assert_eq!(theme.display_name, "Tokyo Night");
227        assert_eq!(theme.base, ThemeBase::Dark);
228        assert_eq!(theme.colors.bg_base, Some(0x1a1b26));
229        assert_eq!(theme.colors.accent_primary, Some(0x7aa2f7));
230        assert_eq!(theme.colors.chart_palette.len(), 3);
231    }
232
233    #[test]
234    fn test_theme_default() {
235        assert_eq!(ThemeBase::default(), ThemeBase::Dark);
236    }
237}