Skip to main content

jolt_theme/
types.rs

1//! Core theme types.
2//!
3//! This module defines the fundamental types for the theme system:
4//! - `Color` - RGB color representation
5//! - `ThemeColors` - The 14 semantic colors used in the UI
6//! - `ThemeVariants` - Dark and light variants of a theme
7//! - `NamedTheme` - A complete theme with metadata
8
9use serde::{Deserialize, Serialize};
10
11/// RGB color representation.
12///
13/// Each component is a value from 0-255.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15pub struct Color {
16    pub r: u8,
17    pub g: u8,
18    pub b: u8,
19}
20
21impl Color {
22    /// Create a new color from RGB components.
23    pub const fn new(r: u8, g: u8, b: u8) -> Self {
24        Self { r, g, b }
25    }
26
27    /// Parse a hex color string (e.g., "#ffffff" or "ffffff").
28    pub fn from_hex(hex: &str) -> Option<Self> {
29        let hex = hex.trim_start_matches('#');
30        if hex.len() != 6 {
31            return None;
32        }
33        let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
34        let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
35        let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
36        Some(Self { r, g, b })
37    }
38
39    /// Convert to hex string (e.g., "#ffffff").
40    pub fn to_hex(&self) -> String {
41        format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
42    }
43
44    /// Convert sRGB channel to linear for luminance calculation.
45    fn linearize(val: u8) -> f64 {
46        let v = val as f64 / 255.0;
47        if v <= 0.03928 {
48            v / 12.92
49        } else {
50            ((v + 0.055) / 1.055).powf(2.4)
51        }
52    }
53
54    /// Calculate relative luminance (0.0 = black, 1.0 = white).
55    pub fn luminance(&self) -> f64 {
56        0.2126 * Self::linearize(self.r)
57            + 0.7152 * Self::linearize(self.g)
58            + 0.0722 * Self::linearize(self.b)
59    }
60
61    /// Calculate WCAG contrast ratio between two colors (1:1 to 21:1).
62    pub fn contrast_ratio(&self, other: &Color) -> f64 {
63        let l1 = self.luminance();
64        let l2 = other.luminance();
65        let lighter = l1.max(l2);
66        let darker = l1.min(l2);
67        (lighter + 0.05) / (darker + 0.05)
68    }
69}
70
71impl Default for Color {
72    fn default() -> Self {
73        Self::new(128, 128, 128)
74    }
75}
76
77/// The 14 semantic colors used in the UI.
78///
79/// These colors define the visual appearance of all UI elements.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct ThemeColors {
82    /// Main background color
83    pub bg: Color,
84    /// Dialog/modal background color
85    pub dialog_bg: Color,
86    /// Primary foreground/text color
87    pub fg: Color,
88    /// Primary accent color (links, focus)
89    pub accent: Color,
90    /// Secondary accent color
91    pub accent_secondary: Color,
92    /// Highlight color (warnings, emphasis)
93    pub highlight: Color,
94    /// Muted/dimmed text color
95    pub muted: Color,
96    /// Success state color
97    pub success: Color,
98    /// Warning state color
99    pub warning: Color,
100    /// Danger/error state color
101    pub danger: Color,
102    /// Border color
103    pub border: Color,
104    /// Selection background color
105    pub selection_bg: Color,
106    /// Selection foreground color
107    pub selection_fg: Color,
108    /// Graph line color
109    pub graph_line: Color,
110}
111
112impl Default for ThemeColors {
113    fn default() -> Self {
114        // Default dark theme
115        Self {
116            bg: Color::from_hex("#16161e").unwrap(),
117            dialog_bg: Color::from_hex("#23232d").unwrap(),
118            fg: Color::from_hex("#e6e6f0").unwrap(),
119            accent: Color::from_hex("#8ab4f8").unwrap(),
120            accent_secondary: Color::from_hex("#bb86fc").unwrap(),
121            highlight: Color::from_hex("#ffcb6b").unwrap(),
122            muted: Color::from_hex("#80808c").unwrap(),
123            success: Color::from_hex("#81c784").unwrap(),
124            warning: Color::from_hex("#ffb74d").unwrap(),
125            danger: Color::from_hex("#ef5350").unwrap(),
126            border: Color::from_hex("#3c3c50").unwrap(),
127            selection_bg: Color::from_hex("#323246").unwrap(),
128            selection_fg: Color::from_hex("#ffffff").unwrap(),
129            graph_line: Color::from_hex("#8ab4f8").unwrap(),
130        }
131    }
132}
133
134/// Dark and light variants of a theme.
135#[derive(Debug, Clone, Default)]
136pub struct ThemeVariants {
137    pub dark: Option<ThemeColors>,
138    pub light: Option<ThemeColors>,
139}
140
141/// A complete theme with metadata.
142#[derive(Debug, Clone)]
143pub struct NamedTheme {
144    /// Unique identifier (filename without extension)
145    pub id: String,
146    /// Display name
147    pub name: String,
148    /// Theme color variants
149    pub variants: ThemeVariants,
150    /// Whether this is a built-in theme
151    pub is_builtin: bool,
152}
153
154impl NamedTheme {
155    /// Get the appropriate colors for the current appearance mode.
156    pub fn get_colors(&self, is_dark: bool) -> ThemeColors {
157        if is_dark {
158            self.variants
159                .dark
160                .or(self.variants.light)
161                .expect("Theme must have at least one variant")
162        } else {
163            self.variants
164                .light
165                .or(self.variants.dark)
166                .expect("Theme must have at least one variant")
167        }
168    }
169
170    /// Check if the theme has a dark variant.
171    pub fn has_dark(&self) -> bool {
172        self.variants.dark.is_some()
173    }
174
175    /// Check if the theme has a light variant.
176    pub fn has_light(&self) -> bool {
177        self.variants.light.is_some()
178    }
179
180    /// Get a label describing available variants.
181    pub fn variants_label(&self) -> &'static str {
182        match (self.has_dark(), self.has_light()) {
183            (true, true) => "dark + light",
184            (true, false) => "dark only",
185            (false, true) => "light only",
186            _ => "unknown",
187        }
188    }
189}