1use super::Theme;
4use eframe::egui::{self, Color32, Pos2, Rect, Rounding, Stroke, Vec2};
5
6pub fn kpi_card(ui: &mut egui::Ui, theme: &Theme, label: &str, value: &str, trend: Option<f32>) {
8 egui::Frame::none()
9 .fill(Color32::from_rgb(38, 38, 48))
10 .rounding(Rounding::same(6.0))
11 .inner_margin(egui::Margin::same(10.0))
12 .show(ui, |ui| {
13 ui.vertical(|ui| {
14 ui.label(
15 egui::RichText::new(label)
16 .size(11.0)
17 .color(theme.text_muted),
18 );
19 ui.horizontal(|ui| {
20 ui.label(egui::RichText::new(value).size(20.0).strong());
21 if let Some(trend) = trend {
22 let (icon, color) = if trend > 0.0 {
23 ("↑", theme.success)
24 } else if trend < 0.0 {
25 ("↓", theme.error)
26 } else {
27 ("→", theme.text_muted)
28 };
29 ui.label(egui::RichText::new(icon).size(14.0).color(color));
30 }
31 });
32 });
33 });
34}
35
36pub fn labeled_progress(
38 ui: &mut egui::Ui,
39 theme: &Theme,
40 label: &str,
41 progress: f32,
42 color: Option<Color32>,
43) {
44 ui.horizontal(|ui| {
45 ui.label(egui::RichText::new(label).size(12.0));
46 ui.add_space(8.0);
47 let bar_width = ui.available_width() - 50.0;
48 let (rect, _) = ui.allocate_exact_size(Vec2::new(bar_width, 16.0), egui::Sense::hover());
49
50 ui.painter()
52 .rect_filled(rect, Rounding::same(4.0), Color32::from_rgb(38, 38, 48));
53
54 let fill_width = rect.width() * progress.clamp(0.0, 1.0);
56 let fill_rect = Rect::from_min_size(rect.min, Vec2::new(fill_width, rect.height()));
57 ui.painter().rect_filled(
58 fill_rect,
59 Rounding::same(4.0),
60 color.unwrap_or(theme.accent),
61 );
62
63 ui.add_space(4.0);
64 ui.label(egui::RichText::new(format!("{:.0}%", progress * 100.0)).size(11.0));
65 });
66}
67
68pub fn status_dot(ui: &mut egui::Ui, color: Color32, pulsing: bool) {
70 let (rect, _) = ui.allocate_exact_size(Vec2::splat(10.0), egui::Sense::hover());
71 let center = rect.center();
72 let radius = 4.0;
73
74 if pulsing {
75 let time = ui.ctx().input(|i| i.time);
76 let alpha = ((time * 3.0).sin() * 0.5 + 0.5) as f32;
77 let pulse_color = color.linear_multiply(0.3 + alpha * 0.7);
78 ui.painter()
79 .circle_filled(center, radius + 2.0, pulse_color.linear_multiply(0.3));
80 }
81
82 ui.painter().circle_filled(center, radius, color);
83}
84
85pub fn sparkline(ui: &mut egui::Ui, theme: &Theme, values: &[f32], width: f32, height: f32) {
87 if values.is_empty() {
88 return;
89 }
90
91 let (rect, _) = ui.allocate_exact_size(Vec2::new(width, height), egui::Sense::hover());
92
93 let max_val = values.iter().copied().fold(f32::MIN, f32::max).max(1.0);
94 let min_val = values.iter().copied().fold(f32::MAX, f32::min);
95 let range = (max_val - min_val).max(1.0);
96
97 let points: Vec<Pos2> = values
98 .iter()
99 .enumerate()
100 .map(|(i, &v)| {
101 let x = rect.left() + (i as f32 / values.len().max(1) as f32) * rect.width();
102 let y = rect.bottom() - ((v - min_val) / range) * rect.height();
103 Pos2::new(x, y)
104 })
105 .collect();
106
107 if points.len() > 1 {
109 for window in points.windows(2) {
110 ui.painter()
111 .line_segment([window[0], window[1]], Stroke::new(1.5, theme.accent));
112 }
113 }
114
115 if let Some(first) = points.first() {
117 ui.painter().circle_filled(*first, 2.0, theme.accent);
118 }
119 if let Some(last) = points.last() {
120 ui.painter().circle_filled(*last, 3.0, theme.accent);
121 }
122}
123
124pub fn pattern_badge(ui: &mut egui::Ui, pattern_type: &str, severity_color: Color32, count: u64) {
126 egui::Frame::none()
127 .fill(severity_color.linear_multiply(0.2))
128 .rounding(Rounding::same(4.0))
129 .inner_margin(egui::Margin::symmetric(8.0, 4.0))
130 .stroke(Stroke::new(1.0, severity_color))
131 .show(ui, |ui| {
132 ui.horizontal(|ui| {
133 ui.label(
134 egui::RichText::new(pattern_type)
135 .size(11.0)
136 .color(severity_color),
137 );
138 if count > 1 {
139 ui.label(
140 egui::RichText::new(format!("×{}", count))
141 .size(10.0)
142 .color(severity_color.linear_multiply(0.7)),
143 );
144 }
145 });
146 });
147}
148
149pub fn sector_selector(ui: &mut egui::Ui, current: &mut crate::fabric::SectorTemplate) -> bool {
151 use crate::fabric::{
152 FinanceConfig, HealthcareConfig, IncidentConfig, ManufacturingConfig, SectorTemplate,
153 };
154
155 let sectors = [
156 SectorTemplate::Healthcare(HealthcareConfig::default()),
157 SectorTemplate::Manufacturing(ManufacturingConfig::default()),
158 SectorTemplate::Finance(FinanceConfig::default()),
159 SectorTemplate::IncidentManagement(IncidentConfig::default()),
160 ];
161
162 let mut changed = false;
163
164 egui::ComboBox::from_label("Sector")
165 .selected_text(current.name())
166 .show_ui(ui, |ui| {
167 for sector in §ors {
168 if ui
169 .selectable_value(current, sector.clone(), sector.name())
170 .clicked()
171 {
172 changed = true;
173 }
174 }
175 });
176
177 changed
178}
179
180pub fn conformance_gauge(ui: &mut egui::Ui, theme: &Theme, fitness: f32, size: f32) {
182 let (rect, _) = ui.allocate_exact_size(Vec2::splat(size), egui::Sense::hover());
183 let center = rect.center();
184 let radius = size / 2.0 - 4.0;
185
186 let stroke_width = 6.0;
188 ui.painter().circle_stroke(
189 center,
190 radius,
191 Stroke::new(stroke_width, Color32::from_rgb(38, 38, 48)),
192 );
193
194 let color = theme.fitness_color(fitness);
196
197 let segments = 32;
199 let start_angle = -std::f32::consts::FRAC_PI_2;
200 for i in 0..segments {
201 let t = i as f32 / segments as f32;
202 if t > fitness {
203 break;
204 }
205 let a1 = start_angle + t * std::f32::consts::TAU;
206 let a2 = start_angle + (i + 1) as f32 / segments as f32 * std::f32::consts::TAU;
207 let p1 = center + Vec2::new(a1.cos(), a1.sin()) * radius;
208 let p2 = center + Vec2::new(a2.cos(), a2.sin()) * radius;
209 ui.painter()
210 .line_segment([p1, p2], Stroke::new(stroke_width, color));
211 }
212
213 let text = format!("{:.0}%", fitness * 100.0);
215 ui.painter().text(
216 center,
217 egui::Align2::CENTER_CENTER,
218 text,
219 egui::FontId::proportional(size / 4.0),
220 theme.text,
221 );
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn test_theme() {
230 let theme = Theme::dark();
231 assert!(theme.fitness_color(0.95) == theme.success);
232 }
233}