Skip to main content

ringkernel_procint/gui/
theme.rs

1//! Theme and styling for the process intelligence GUI.
2
3use eframe::egui::{self, Color32, Rounding, Stroke, Vec2};
4
5/// Application theme colors.
6pub struct Theme {
7    /// Background color.
8    pub background: Color32,
9    /// Panel background.
10    pub panel_bg: Color32,
11    /// Primary accent color.
12    pub accent: Color32,
13    /// Secondary accent.
14    pub accent_secondary: Color32,
15    /// Text color.
16    pub text: Color32,
17    /// Muted text.
18    pub text_muted: Color32,
19    /// Success color.
20    pub success: Color32,
21    /// Warning color.
22    pub warning: Color32,
23    /// Error color.
24    pub error: Color32,
25    /// Node default color.
26    pub node_default: Color32,
27    /// Node start color.
28    pub node_start: Color32,
29    /// Node end color.
30    pub node_end: Color32,
31    /// Edge default color.
32    pub edge_default: Color32,
33    /// Edge fast color.
34    pub edge_fast: Color32,
35    /// Edge slow color.
36    pub edge_slow: Color32,
37    /// Token color.
38    pub token: Color32,
39    /// Bottleneck highlight.
40    pub bottleneck: Color32,
41    /// Loop highlight.
42    pub loop_highlight: Color32,
43}
44
45impl Default for Theme {
46    fn default() -> Self {
47        Self::dark()
48    }
49}
50
51impl Theme {
52    /// Dark theme (default).
53    pub fn dark() -> Self {
54        Self {
55            background: Color32::from_rgb(18, 18, 24),
56            panel_bg: Color32::from_rgb(28, 28, 36),
57            accent: Color32::from_rgb(99, 102, 241), // Indigo
58            accent_secondary: Color32::from_rgb(139, 92, 246), // Purple
59            text: Color32::from_rgb(229, 231, 235),
60            text_muted: Color32::from_rgb(156, 163, 175),
61            success: Color32::from_rgb(34, 197, 94),
62            warning: Color32::from_rgb(234, 179, 8),
63            error: Color32::from_rgb(239, 68, 68),
64            node_default: Color32::from_rgb(59, 130, 246), // Blue
65            node_start: Color32::from_rgb(34, 197, 94),    // Green
66            node_end: Color32::from_rgb(239, 68, 68),      // Red
67            edge_default: Color32::from_rgb(100, 116, 139),
68            edge_fast: Color32::from_rgb(34, 197, 94),
69            edge_slow: Color32::from_rgb(239, 68, 68),
70            token: Color32::from_rgb(250, 204, 21), // Yellow
71            bottleneck: Color32::from_rgb(239, 68, 68),
72            loop_highlight: Color32::from_rgb(59, 130, 246),
73        }
74    }
75
76    /// Light theme.
77    pub fn light() -> Self {
78        Self {
79            background: Color32::from_rgb(249, 250, 251),
80            panel_bg: Color32::from_rgb(255, 255, 255),
81            accent: Color32::from_rgb(79, 70, 229),
82            accent_secondary: Color32::from_rgb(124, 58, 237),
83            text: Color32::from_rgb(17, 24, 39),
84            text_muted: Color32::from_rgb(107, 114, 128),
85            success: Color32::from_rgb(22, 163, 74),
86            warning: Color32::from_rgb(202, 138, 4),
87            error: Color32::from_rgb(220, 38, 38),
88            node_default: Color32::from_rgb(37, 99, 235),
89            node_start: Color32::from_rgb(22, 163, 74),
90            node_end: Color32::from_rgb(220, 38, 38),
91            edge_default: Color32::from_rgb(148, 163, 184),
92            edge_fast: Color32::from_rgb(22, 163, 74),
93            edge_slow: Color32::from_rgb(220, 38, 38),
94            token: Color32::from_rgb(234, 179, 8),
95            bottleneck: Color32::from_rgb(220, 38, 38),
96            loop_highlight: Color32::from_rgb(37, 99, 235),
97        }
98    }
99
100    /// Apply theme to egui context.
101    pub fn apply(&self, ctx: &egui::Context) {
102        let mut style = (*ctx.style()).clone();
103
104        // Set colors
105        style.visuals.dark_mode = self.background.r() < 128;
106        style.visuals.override_text_color = Some(self.text);
107        style.visuals.panel_fill = self.panel_bg;
108        style.visuals.window_fill = self.panel_bg;
109        style.visuals.extreme_bg_color = self.background;
110
111        // Widgets
112        style.visuals.widgets.noninteractive.bg_fill = self.panel_bg;
113        style.visuals.widgets.inactive.bg_fill = Color32::from_rgb(38, 38, 48);
114        style.visuals.widgets.hovered.bg_fill = Color32::from_rgb(48, 48, 58);
115        style.visuals.widgets.active.bg_fill = self.accent;
116
117        // Selection
118        style.visuals.selection.bg_fill = self.accent.linear_multiply(0.3);
119        style.visuals.selection.stroke = Stroke::new(1.0, self.accent);
120
121        // Rounding
122        style.visuals.window_rounding = Rounding::same(8.0);
123        style.visuals.menu_rounding = Rounding::same(6.0);
124
125        // Spacing
126        style.spacing.item_spacing = Vec2::new(8.0, 6.0);
127        style.spacing.button_padding = Vec2::new(12.0, 6.0);
128
129        ctx.set_style(style);
130    }
131
132    /// Get color for fitness value.
133    pub fn fitness_color(&self, fitness: f32) -> Color32 {
134        if fitness >= 0.95 {
135            self.success
136        } else if fitness >= 0.80 {
137            Color32::from_rgb(34, 211, 238) // Cyan
138        } else if fitness >= 0.50 {
139            self.warning
140        } else {
141            self.error
142        }
143    }
144
145    /// Get color for edge duration.
146    pub fn edge_duration_color(&self, duration_ms: f32, avg_duration_ms: f32) -> Color32 {
147        let ratio = duration_ms / avg_duration_ms.max(1.0);
148        if ratio < 0.5 {
149            self.edge_fast
150        } else if ratio > 2.0 {
151            self.edge_slow
152        } else {
153            self.edge_default
154        }
155    }
156
157    /// Get pattern severity color.
158    pub fn severity_color(&self, severity: crate::models::PatternSeverity) -> Color32 {
159        match severity {
160            crate::models::PatternSeverity::Info => self.accent,
161            crate::models::PatternSeverity::Warning => self.warning,
162            crate::models::PatternSeverity::Critical => self.error,
163        }
164    }
165}
166
167/// Styled panel with rounded corners.
168pub fn styled_panel(ui: &mut egui::Ui, theme: &Theme, add_contents: impl FnOnce(&mut egui::Ui)) {
169    egui::Frame::none()
170        .fill(theme.panel_bg)
171        .rounding(Rounding::same(8.0))
172        .inner_margin(egui::Margin::same(12.0))
173        .stroke(Stroke::new(1.0, Color32::from_rgb(38, 38, 48)))
174        .show(ui, |ui| {
175            add_contents(ui);
176        });
177}
178
179/// Section header with accent underline.
180pub fn section_header(ui: &mut egui::Ui, theme: &Theme, title: &str) {
181    ui.horizontal(|ui| {
182        ui.label(egui::RichText::new(title).strong().size(14.0));
183    });
184    ui.add_space(2.0);
185    let rect = ui.available_rect_before_wrap();
186    ui.painter().line_segment(
187        [
188            egui::pos2(rect.left(), rect.top()),
189            egui::pos2(rect.left() + 60.0, rect.top()),
190        ],
191        Stroke::new(2.0, theme.accent),
192    );
193    ui.add_space(8.0);
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_theme_creation() {
202        let dark = Theme::dark();
203        assert!(dark.background.r() < 50);
204
205        let light = Theme::light();
206        assert!(light.background.r() > 200);
207    }
208
209    #[test]
210    fn test_fitness_color() {
211        let theme = Theme::dark();
212        assert_eq!(theme.fitness_color(0.98), theme.success);
213        assert_eq!(theme.fitness_color(0.30), theme.error);
214    }
215}