1use eframe::egui::{self, Color32, Rounding, Stroke, Vec2};
4
5pub struct Theme {
7 pub background: Color32,
9 pub panel_bg: Color32,
11 pub accent: Color32,
13 pub accent_secondary: Color32,
15 pub text: Color32,
17 pub text_muted: Color32,
19 pub success: Color32,
21 pub warning: Color32,
23 pub error: Color32,
25 pub node_default: Color32,
27 pub node_start: Color32,
29 pub node_end: Color32,
31 pub edge_default: Color32,
33 pub edge_fast: Color32,
35 pub edge_slow: Color32,
37 pub token: Color32,
39 pub bottleneck: Color32,
41 pub loop_highlight: Color32,
43}
44
45impl Default for Theme {
46 fn default() -> Self {
47 Self::dark()
48 }
49}
50
51impl Theme {
52 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), accent_secondary: Color32::from_rgb(139, 92, 246), 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), node_start: Color32::from_rgb(34, 197, 94), node_end: Color32::from_rgb(239, 68, 68), 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), bottleneck: Color32::from_rgb(239, 68, 68),
72 loop_highlight: Color32::from_rgb(59, 130, 246),
73 }
74 }
75
76 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 pub fn apply(&self, ctx: &egui::Context) {
102 let mut style = (*ctx.style()).clone();
103
104 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 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 style.visuals.selection.bg_fill = self.accent.linear_multiply(0.3);
119 style.visuals.selection.stroke = Stroke::new(1.0, self.accent);
120
121 style.visuals.window_rounding = Rounding::same(8.0);
123 style.visuals.menu_rounding = Rounding::same(6.0);
124
125 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 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) } else if fitness >= 0.50 {
139 self.warning
140 } else {
141 self.error
142 }
143 }
144
145 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 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
167pub 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
179pub 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}