1use presentar_core::Color;
7
8#[derive(Debug, Clone)]
10pub struct Gradient {
11 stops: Vec<Color>,
13}
14
15impl Gradient {
16 #[must_use]
18 pub fn two(start: Color, end: Color) -> Self {
19 Self {
20 stops: vec![start, end],
21 }
22 }
23
24 #[must_use]
26 pub fn three(start: Color, mid: Color, end: Color) -> Self {
27 Self {
28 stops: vec![start, mid, end],
29 }
30 }
31
32 #[must_use]
34 pub fn from_hex(stops: &[&str]) -> Self {
35 Self {
36 stops: stops.iter().map(|s| parse_hex(s)).collect(),
37 }
38 }
39
40 #[must_use]
42 pub fn sample(&self, t: f64) -> Color {
43 let t = if t.is_finite() {
45 t.clamp(0.0, 1.0)
46 } else {
47 0.0
48 };
49
50 if self.stops.is_empty() {
51 return Color::WHITE;
52 }
53
54 if self.stops.len() == 1 {
55 return self.stops[0];
56 }
57
58 let segment_count = self.stops.len() - 1;
60 let segment_size = 1.0 / segment_count as f64;
61 let segment = ((t / segment_size) as usize).min(segment_count - 1);
62 let local_t = (t - segment as f64 * segment_size) / segment_size;
63
64 let start = self.stops[segment];
65 let end = self.stops[segment + 1];
66
67 interpolate_lab(start, end, local_t)
68 }
69
70 #[must_use]
72 pub fn for_percent(&self, percent: f64) -> Color {
73 self.sample(percent / 100.0)
74 }
75}
76
77impl Default for Gradient {
78 fn default() -> Self {
79 Self::from_hex(&["#00FF00", "#FFFF00", "#FF0000"])
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct Theme {
87 pub name: String,
89 pub background: Color,
91 pub foreground: Color,
93 pub border: Color,
95 pub dim: Color,
97 pub cpu: Gradient,
99 pub memory: Gradient,
101 pub gpu: Gradient,
103 pub temperature: Gradient,
105 pub network: Gradient,
107}
108
109impl Default for Theme {
110 fn default() -> Self {
111 Self::tokyo_night()
112 }
113}
114
115impl Theme {
116 #[must_use]
118 pub fn new() -> Self {
119 Self::default()
120 }
121
122 #[must_use]
124 pub fn tokyo_night() -> Self {
125 Self {
126 name: "tokyo_night".to_string(),
127 background: parse_hex("#1a1b26"),
128 foreground: parse_hex("#c0caf5"),
129 border: parse_hex("#414868"),
130 dim: parse_hex("#565f89"),
131 cpu: Gradient::from_hex(&["#7aa2f7", "#e0af68", "#f7768e"]),
132 memory: Gradient::from_hex(&["#9ece6a", "#e0af68", "#f7768e"]),
133 gpu: Gradient::from_hex(&["#bb9af7", "#7dcfff", "#f7768e"]),
134 temperature: Gradient::from_hex(&["#7dcfff", "#e0af68", "#f7768e"]),
135 network: Gradient::from_hex(&["#7dcfff", "#9ece6a"]),
136 }
137 }
138
139 #[must_use]
141 pub fn dracula() -> Self {
142 Self {
143 name: "dracula".to_string(),
144 background: parse_hex("#282a36"),
145 foreground: parse_hex("#f8f8f2"),
146 border: parse_hex("#6272a4"),
147 dim: parse_hex("#44475a"),
148 cpu: Gradient::from_hex(&["#50fa7b", "#f1fa8c", "#ff5555"]),
149 memory: Gradient::from_hex(&["#8be9fd", "#f1fa8c", "#ff5555"]),
150 gpu: Gradient::from_hex(&["#bd93f9", "#ff79c6", "#ff5555"]),
151 temperature: Gradient::from_hex(&["#8be9fd", "#ffb86c", "#ff5555"]),
152 network: Gradient::from_hex(&["#8be9fd", "#50fa7b"]),
153 }
154 }
155
156 #[must_use]
158 pub fn nord() -> Self {
159 Self {
160 name: "nord".to_string(),
161 background: parse_hex("#2e3440"),
162 foreground: parse_hex("#eceff4"),
163 border: parse_hex("#4c566a"),
164 dim: parse_hex("#3b4252"),
165 cpu: Gradient::from_hex(&["#a3be8c", "#ebcb8b", "#bf616a"]),
166 memory: Gradient::from_hex(&["#88c0d0", "#ebcb8b", "#bf616a"]),
167 gpu: Gradient::from_hex(&["#b48ead", "#81a1c1", "#bf616a"]),
168 temperature: Gradient::from_hex(&["#88c0d0", "#ebcb8b", "#bf616a"]),
169 network: Gradient::from_hex(&["#88c0d0", "#a3be8c"]),
170 }
171 }
172
173 #[must_use]
175 pub fn monokai() -> Self {
176 Self {
177 name: "monokai".to_string(),
178 background: parse_hex("#272822"),
179 foreground: parse_hex("#f8f8f2"),
180 border: parse_hex("#49483e"),
181 dim: parse_hex("#75715e"),
182 cpu: Gradient::from_hex(&["#a6e22e", "#e6db74", "#f92672"]),
183 memory: Gradient::from_hex(&["#66d9ef", "#e6db74", "#f92672"]),
184 gpu: Gradient::from_hex(&["#ae81ff", "#fd971f", "#f92672"]),
185 temperature: Gradient::from_hex(&["#66d9ef", "#fd971f", "#f92672"]),
186 network: Gradient::from_hex(&["#66d9ef", "#a6e22e"]),
187 }
188 }
189
190 #[must_use]
192 pub fn cpu_color(&self, percent: f64) -> Color {
193 self.cpu.for_percent(percent)
194 }
195
196 #[must_use]
198 pub fn memory_color(&self, percent: f64) -> Color {
199 self.memory.for_percent(percent)
200 }
201
202 #[must_use]
204 pub fn gpu_color(&self, percent: f64) -> Color {
205 self.gpu.for_percent(percent)
206 }
207
208 #[must_use]
210 pub fn temp_color(&self, temp_c: f64, max_temp: f64) -> Color {
211 let percent = (temp_c / max_temp * 100.0).clamp(0.0, 100.0);
212 self.temperature.for_percent(percent)
213 }
214}
215
216fn parse_hex(hex: &str) -> Color {
218 let hex = hex.trim_start_matches('#');
219 if hex.len() != 6 {
220 return Color::WHITE;
221 }
222
223 let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(255);
224 let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(255);
225 let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(255);
226
227 Color::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0)
229}
230
231#[allow(clippy::many_single_char_names)]
233fn interpolate_lab(start: Color, end: Color, t: f64) -> Color {
234 let lab1 = rgb_to_lab(start);
236 let lab2 = rgb_to_lab(end);
237
238 let l = lab1.0 + t * (lab2.0 - lab1.0);
240 let a = lab1.1 + t * (lab2.1 - lab1.1);
241 let b = lab1.2 + t * (lab2.2 - lab1.2);
242
243 lab_to_rgb(l, a, b)
245}
246
247#[allow(clippy::many_single_char_names, clippy::unreadable_literal)]
249fn rgb_to_lab(c: Color) -> (f64, f64, f64) {
250 let r = c.r as f64;
252 let g = c.g as f64;
253 let b = c.b as f64;
254
255 let r = if r > 0.04045 {
257 ((r + 0.055) / 1.055).powf(2.4)
258 } else {
259 r / 12.92
260 };
261 let g = if g > 0.04045 {
262 ((g + 0.055) / 1.055).powf(2.4)
263 } else {
264 g / 12.92
265 };
266 let b = if b > 0.04045 {
267 ((b + 0.055) / 1.055).powf(2.4)
268 } else {
269 b / 12.92
270 };
271
272 let x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375;
274 let y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750;
275 let z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041;
276
277 let x = x / 0.95047;
279 let z = z / 1.08883;
280
281 let fx = if x > 0.008856 {
282 x.cbrt()
283 } else {
284 (7.787 * x) + (16.0 / 116.0)
285 };
286 let fy = if y > 0.008856 {
287 y.cbrt()
288 } else {
289 (7.787 * y) + (16.0 / 116.0)
290 };
291 let fz = if z > 0.008856 {
292 z.cbrt()
293 } else {
294 (7.787 * z) + (16.0 / 116.0)
295 };
296
297 let l = (116.0 * fy) - 16.0;
298 let a = 500.0 * (fx - fy);
299 let b_val = 200.0 * (fy - fz);
300
301 (l, a, b_val)
302}
303
304#[allow(clippy::many_single_char_names, clippy::unreadable_literal)]
306fn lab_to_rgb(l: f64, a: f64, b: f64) -> Color {
307 let fy = (l + 16.0) / 116.0;
309 let fx = a / 500.0 + fy;
310 let fz = fy - b / 200.0;
311
312 let x = if fx.powi(3) > 0.008856 {
313 fx.powi(3)
314 } else {
315 (fx - 16.0 / 116.0) / 7.787
316 };
317 let y = if l > 7.9996 { fy.powi(3) } else { l / 903.3 };
318 let z = if fz.powi(3) > 0.008856 {
319 fz.powi(3)
320 } else {
321 (fz - 16.0 / 116.0) / 7.787
322 };
323
324 let x = x * 0.95047;
326 let z = z * 1.08883;
327
328 let r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314;
330 let g = x * -0.9692660 + y * 1.8760108 + z * 0.0415560;
331 let b_val = x * 0.0556434 + y * -0.2040259 + z * 1.0572252;
332
333 let r = if r > 0.0031308 {
335 1.055 * r.powf(1.0 / 2.4) - 0.055
336 } else {
337 12.92 * r
338 };
339 let g = if g > 0.0031308 {
340 1.055 * g.powf(1.0 / 2.4) - 0.055
341 } else {
342 12.92 * g
343 };
344 let b_val = if b_val > 0.0031308 {
345 1.055 * b_val.powf(1.0 / 2.4) - 0.055
346 } else {
347 12.92 * b_val
348 };
349
350 let r = r.clamp(0.0, 1.0) as f32;
352 let g = g.clamp(0.0, 1.0) as f32;
353 let b_val = b_val.clamp(0.0, 1.0) as f32;
354
355 Color::new(r, g, b_val, 1.0)
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 #[test]
363 fn test_parse_hex() {
364 let c = parse_hex("#FF0000");
365 assert!((c.r - 1.0).abs() < 0.01); assert!((c.g - 0.0).abs() < 0.01);
367 assert!((c.b - 0.0).abs() < 0.01);
368 }
369
370 #[test]
371 fn test_parse_hex_green() {
372 let c = parse_hex("#00FF00");
373 assert!((c.r - 0.0).abs() < 0.01);
374 assert!((c.g - 1.0).abs() < 0.01); assert!((c.b - 0.0).abs() < 0.01);
376 }
377
378 #[test]
379 fn test_gradient_two() {
380 let g = Gradient::two(Color::RED, Color::BLUE);
381 let start = g.sample(0.0);
382 let end = g.sample(1.0);
383 assert!((start.r - 1.0).abs() < 0.01); assert!((end.b - 1.0).abs() < 0.01); }
386
387 #[test]
388 fn test_gradient_three() {
389 let g = Gradient::three(Color::RED, Color::GREEN, Color::BLUE);
390 let mid = g.sample(0.5);
391 assert!(mid.g > mid.r);
393 assert!(mid.g > mid.b);
394 }
395
396 #[test]
397 fn test_gradient_from_hex() {
398 let g = Gradient::from_hex(&["#FF0000", "#00FF00"]);
399 let start = g.sample(0.0);
400 assert!((start.r - 1.0).abs() < 0.01); }
402
403 #[test]
404 fn test_gradient_for_percent() {
405 let g = Gradient::default();
406 let _ = g.for_percent(50.0);
407 let _ = g.for_percent(0.0);
408 let _ = g.for_percent(100.0);
409 }
410
411 #[test]
412 fn test_theme_tokyo_night() {
413 let t = Theme::tokyo_night();
414 assert_eq!(t.name, "tokyo_night");
415 }
416
417 #[test]
418 fn test_theme_dracula() {
419 let t = Theme::dracula();
420 assert_eq!(t.name, "dracula");
421 }
422
423 #[test]
424 fn test_theme_nord() {
425 let t = Theme::nord();
426 assert_eq!(t.name, "nord");
427 }
428
429 #[test]
430 fn test_theme_monokai() {
431 let t = Theme::monokai();
432 assert_eq!(t.name, "monokai");
433 }
434
435 #[test]
436 fn test_theme_cpu_color() {
437 let t = Theme::default();
438 let _ = t.cpu_color(0.0);
439 let _ = t.cpu_color(50.0);
440 let _ = t.cpu_color(100.0);
441 }
442
443 #[test]
444 fn test_theme_memory_color() {
445 let t = Theme::default();
446 let _ = t.memory_color(75.0);
447 }
448
449 #[test]
450 fn test_theme_gpu_color() {
451 let t = Theme::default();
452 let _ = t.gpu_color(25.0);
453 }
454
455 #[test]
456 fn test_theme_temp_color() {
457 let t = Theme::default();
458 let _ = t.temp_color(65.0, 100.0);
459 }
460
461 #[test]
462 fn test_gradient_empty() {
463 let g = Gradient { stops: vec![] };
464 let c = g.sample(0.5);
465 assert_eq!(c, Color::WHITE);
466 }
467
468 #[test]
469 fn test_gradient_single() {
470 let g = Gradient {
471 stops: vec![Color::RED],
472 };
473 let c = g.sample(0.5);
474 assert!((c.r - 1.0).abs() < 0.01); }
476
477 #[test]
478 fn test_gradient_clamp() {
479 let g = Gradient::default();
480 let _ = g.sample(-1.0); let _ = g.sample(2.0); }
483
484 #[test]
485 fn test_lab_roundtrip() {
486 let original = Color::new(0.5, 0.25, 0.75, 1.0);
488 let lab = rgb_to_lab(original);
489 let back = lab_to_rgb(lab.0, lab.1, lab.2);
490 assert!((original.r - back.r).abs() < 0.02);
491 assert!((original.g - back.g).abs() < 0.02);
492 assert!((original.b - back.b).abs() < 0.02);
493 }
494
495 #[test]
496 fn test_interpolate_lab_endpoints() {
497 let start = Color::RED;
498 let end = Color::BLUE;
499
500 let at_start = interpolate_lab(start, end, 0.0);
501 let at_end = interpolate_lab(start, end, 1.0);
502
503 assert!((at_start.r - 1.0).abs() < 0.02); assert!((at_end.b - 1.0).abs() < 0.02); }
506
507 #[test]
508 fn test_theme_default() {
509 let t = Theme::default();
510 assert_eq!(t.name, "tokyo_night");
511 }
512
513 #[test]
514 fn test_parse_hex_invalid() {
515 let c = parse_hex("invalid");
516 assert_eq!(c, Color::WHITE);
517 }
518
519 #[test]
520 fn test_parse_hex_no_hash() {
521 let c = parse_hex("FF0000");
522 assert!((c.r - 1.0).abs() < 0.01); }
524
525 #[test]
526 fn test_parse_hex_blue() {
527 let c = parse_hex("#0000FF");
528 assert!((c.r - 0.0).abs() < 0.01);
529 assert!((c.g - 0.0).abs() < 0.01);
530 assert!((c.b - 1.0).abs() < 0.01);
531 }
532
533 #[test]
534 fn test_parse_hex_mixed() {
535 let c = parse_hex("#808080");
536 assert!((c.r - 0.5).abs() < 0.01);
537 assert!((c.g - 0.5).abs() < 0.01);
538 assert!((c.b - 0.5).abs() < 0.01);
539 }
540
541 #[test]
542 fn test_gradient_default() {
543 let g = Gradient::default();
544 let start = g.sample(0.0);
546 assert!(start.g > 0.9); let end = g.sample(1.0);
548 assert!(end.r > 0.9); }
550
551 #[test]
552 fn test_theme_new() {
553 let t = Theme::new();
554 assert_eq!(t.name, "tokyo_night");
555 }
556
557 #[test]
558 fn test_theme_colors_non_panic() {
559 let t = Theme::default();
560 for pct in [0.0, 25.0, 50.0, 75.0, 100.0] {
562 let _ = t.cpu_color(pct);
563 let _ = t.memory_color(pct);
564 let _ = t.gpu_color(pct);
565 }
566 for temp in [0.0, 25.0, 50.0, 75.0, 100.0] {
567 let _ = t.temp_color(temp, 100.0);
568 }
569 }
570
571 #[test]
572 fn test_temp_color_cold() {
573 let t = Theme::default();
574 let cold = t.temp_color(0.0, 100.0);
575 let hot = t.temp_color(100.0, 100.0);
576 assert!((cold.r - hot.r).abs() > 0.1 || (cold.g - hot.g).abs() > 0.1);
578 }
579
580 #[test]
581 fn test_temp_color_clamped() {
582 let t = Theme::default();
583 let over = t.temp_color(150.0, 100.0);
585 let at_max = t.temp_color(100.0, 100.0);
586 assert!((over.r - at_max.r).abs() < 0.01);
587 }
588
589 #[test]
590 fn test_gradient_four_stops() {
591 let g = Gradient {
593 stops: vec![Color::RED, Color::GREEN, Color::BLUE, Color::WHITE],
594 };
595 let _ = g.sample(0.0);
596 let _ = g.sample(0.33);
597 let _ = g.sample(0.66);
598 let _ = g.sample(1.0);
599 }
600
601 #[test]
602 fn test_theme_background_foreground() {
603 let t = Theme::tokyo_night();
604 assert!(t.background.r < 0.2);
606 assert!(t.background.g < 0.2);
607 assert!(t.foreground.r > 0.5);
609 assert!(t.foreground.g > 0.5);
610 }
611
612 #[test]
613 fn test_theme_border_dim() {
614 let t = Theme::dracula();
615 assert!(t.border.r > 0.2 && t.border.r < 0.8);
617 assert!(t.dim.r > 0.1 && t.dim.r < 0.5);
618 }
619
620 #[test]
621 fn test_interpolate_lab_midpoint() {
622 let start = Color::new(0.0, 0.0, 0.0, 1.0); let end = Color::new(1.0, 1.0, 1.0, 1.0); let mid = interpolate_lab(start, end, 0.5);
625 assert!(mid.r > 0.3 && mid.r < 0.7);
627 }
628
629 #[test]
630 fn test_rgb_lab_roundtrip_black() {
631 let black = Color::new(0.0, 0.0, 0.0, 1.0);
632 let lab = rgb_to_lab(black);
633 let back = lab_to_rgb(lab.0, lab.1, lab.2);
634 assert!((back.r - 0.0).abs() < 0.02);
635 }
636
637 #[test]
638 fn test_rgb_lab_roundtrip_white() {
639 let white = Color::new(1.0, 1.0, 1.0, 1.0);
640 let lab = rgb_to_lab(white);
641 let back = lab_to_rgb(lab.0, lab.1, lab.2);
642 assert!((back.r - 1.0).abs() < 0.02);
643 }
644
645 #[test]
646 fn test_gradient_segment_boundary() {
647 let g = Gradient::three(Color::RED, Color::GREEN, Color::BLUE);
648 let at_half = g.sample(0.5);
650 assert!(at_half.g > at_half.r);
651 assert!(at_half.g > at_half.b);
652 }
653
654 #[test]
655 fn test_all_themes_valid() {
656 let themes = [
657 Theme::tokyo_night(),
658 Theme::dracula(),
659 Theme::nord(),
660 Theme::monokai(),
661 ];
662 for t in themes {
663 assert!(!t.name.is_empty());
664 let _ = t.cpu.sample(0.5);
666 let _ = t.memory.sample(0.5);
667 let _ = t.gpu.sample(0.5);
668 let _ = t.temperature.sample(0.5);
669 let _ = t.network.sample(0.5);
670 }
671 }
672
673 #[test]
674 fn test_parse_hex_short() {
675 let c = parse_hex("#FFF");
677 assert_eq!(c, Color::WHITE);
678 }
679
680 #[test]
681 fn test_parse_hex_long() {
682 let c = parse_hex("#FFFFFFFF");
684 assert_eq!(c, Color::WHITE);
685 }
686}