1use crate::state::Level;
8use tiny_skia::{Color, FillRule, Paint, PathBuilder, Pixmap, Stroke, Transform};
9
10const MARK: &str = include_str!("../../../assets/identity/marks/planarian-mark-micro-currentcolor.svg");
11pub const SIZES: [u32; 6] = [16, 22, 24, 32, 48, 64];
13
14fn mark_path() -> Option<tiny_skia::Path> {
16 let start = MARK.find(" d=\"")? + 4;
17 let d = &MARK[start..start + MARK[start..].find('"')?];
18 let mut b = PathBuilder::new();
19 let mut nums: Vec<f32> = vec![];
20 let mut cmd = ' ';
21 let flush = |cmd: char, nums: &mut Vec<f32>, b: &mut PathBuilder| -> Option<()> {
22 match cmd {
23 'M' if nums.len() == 2 => b.move_to(nums[0], nums[1]),
24 'C' if nums.len().is_multiple_of(6) => {
25 for c in nums.chunks(6) {
26 b.cubic_to(c[0], c[1], c[2], c[3], c[4], c[5]);
27 }
28 }
29 'Z' | ' ' if nums.is_empty() => {}
30 _ => return None,
31 }
32 nums.clear();
33 Some(())
34 };
35 for tok in d.split(|c: char| c.is_whitespace() || c == ',').filter(|t| !t.is_empty()) {
36 let first = tok.chars().next()?;
37 if first.is_ascii_alphabetic() {
38 flush(cmd, &mut nums, &mut b)?;
39 if first == 'Z' {
40 b.close();
41 }
42 cmd = first;
43 if tok.len() > 1 {
44 nums.push(tok[1..].parse().ok()?);
45 }
46 } else {
47 nums.push(tok.parse().ok()?);
48 }
49 }
50 flush(cmd, &mut nums, &mut b)?;
51 b.finish()
52}
53
54fn dot_color(level: Level) -> Option<Color> {
55 match level {
56 Level::Ok => None,
57 Level::Working => Some(Color::from_rgba8(0x8f, 0xb0, 0xd8, 0xff)),
58 Level::Attention => Some(Color::from_rgba8(0xe0, 0xa4, 0x3a, 0xff)),
59 Level::Fault => Some(Color::from_rgba8(0xe0, 0x52, 0x4f, 0xff)),
60 }
61}
62
63pub fn render(level: Level, size: u32) -> Option<(u32, u32, Vec<u8>)> {
66 let mut px = Pixmap::new(size, size)?;
67 let s = size as f32;
68 let mut paint = Paint { anti_alias: true, ..Paint::default() };
69 paint.set_color(Color::WHITE);
70 let path = mark_path()?;
71 px.fill_path(&path, &paint, FillRule::EvenOdd, Transform::from_scale(s / 1024.0, s / 1024.0), None);
72 if let Some(c) = dot_color(level) {
73 let r = (s * 0.2).max(3.0);
75 let (cx, cy) = (s - r - s * 0.02, s - r - s * 0.02);
76 let dot = PathBuilder::from_circle(cx, cy, r)?;
77 paint.set_color(c);
78 px.fill_path(&dot, &paint, FillRule::Winding, Transform::identity(), None);
79 paint.set_color(Color::from_rgba8(0x15, 0x17, 0x19, 0xff));
80 let ring = Stroke { width: (s * 0.06).max(1.0), ..Stroke::default() };
81 px.stroke_path(&dot, &paint, &ring, Transform::identity(), None);
82 }
83 let mut argb = Vec::with_capacity((size * size * 4) as usize);
84 for p in px.pixels() {
85 let c = p.demultiply();
86 argb.extend_from_slice(&[c.alpha(), c.red(), c.green(), c.blue()]);
87 }
88 Some((size, size, argb))
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 fn at(icon: &(u32, u32, Vec<u8>), x: u32, y: u32) -> [u8; 4] {
96 let i = ((y * icon.0 + x) * 4) as usize;
97 icon.2[i..i + 4].try_into().unwrap()
98 }
99
100 #[test]
101 fn the_mark_parses_and_draws_white_with_transparent_corners() {
102 assert!(mark_path().is_some());
103 let ok = render(Level::Ok, 64).unwrap();
104 assert_eq!(ok.2.len(), 64 * 64 * 4);
105 assert_eq!(at(&ok, 0, 0)[0], 0, "corner is transparent");
106 let white = ok.2.chunks(4).filter(|p| p[0] == 255 && p[1] == 255 && p[2] == 255 && p[3] == 255).count();
108 assert!(white > 64 * 64 / 10, "{white} white pixels");
109 assert!(ok.2.chunks(4).all(|p| p[0] == 0 || (p[1] == p[2] && p[2] == p[3])), "monochrome when ok");
110 }
111
112 #[test]
115 fn dump_for_inspection() {
116 let Ok(dir) = std::env::var("PLANARIAN_TRAY_DUMP") else { return };
117 for l in [Level::Ok, Level::Working, Level::Attention, Level::Fault] {
118 let (_, _, argb) = render(l, 64).unwrap();
119 let rgba: Vec<u8> = argb.chunks(4).flat_map(|p| [p[1], p[2], p[3], p[0]]).collect();
120 std::fs::write(format!("{dir}/{l:?}.rgba"), rgba).unwrap();
121 }
122 }
123
124 #[test]
125 fn the_dot_shows_the_state() {
126 let r = 64.0 * 0.2;
127 let c = (64.0 - r - 64.0 * 0.02) as u32;
128 assert_eq!(at(&render(Level::Fault, 64).unwrap(), c, c), [0xff, 0xe0, 0x52, 0x4f]);
129 assert_eq!(at(&render(Level::Attention, 64).unwrap(), c, c), [0xff, 0xe0, 0xa4, 0x3a]);
130 assert_ne!(at(&render(Level::Ok, 64).unwrap(), c, c), [0xff, 0xe0, 0x52, 0x4f]);
131 for s in SIZES {
132 assert!(render(Level::Working, s).is_some());
133 }
134 }
135}