1use std::collections::BTreeMap;
5
6use serde_json::Value;
7
8pub const MAX_STARS_PER_PROVIDER: usize = 2;
10pub const MAX_BARS: usize = 4;
12pub const BARS_POINT_SIDE: u32 = 18;
14pub const BARS_PIXEL_SIDE: u32 = BARS_POINT_SIDE * 2;
17#[cfg_attr(not(test), allow(dead_code))]
20pub const BARS_SCALES: [u32; 3] = [1, 2, 3];
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum StripStyle {
25 Text,
26 Bars,
27}
28
29impl StripStyle {
30 pub fn parse(name: &str) -> Self {
31 if name.eq_ignore_ascii_case("text") {
32 Self::Text
33 } else {
34 Self::Bars
35 }
36 }
37
38 pub fn as_str(self) -> &'static str {
39 match self {
40 Self::Text => "text",
41 Self::Bars => "bars",
42 }
43 }
44}
45
46pub type Stars = BTreeMap<String, Vec<String>>;
48
49#[derive(Debug, Clone, PartialEq)]
51pub struct StripMetric {
52 pub provider_id: String,
53 pub provider_name: String,
54 pub key: String,
55 pub label: String,
56 pub value: String,
57 pub fraction: f64,
59 pub bounded: bool,
60}
61
62#[derive(Debug, Clone, PartialEq)]
64pub struct StripContent {
65 pub groups: Vec<(String, String, Vec<StripMetric>)>,
66 pub bars: Vec<StripMetric>,
67}
68
69impl StripContent {
70 pub fn is_empty(&self) -> bool {
71 self.groups.is_empty()
72 }
73
74 pub fn title_line(&self) -> String {
77 let mut parts = Vec::new();
78 for (_, name, metrics) in &self.groups {
79 let values: Vec<&str> = metrics.iter().map(|m| m.value.as_str()).collect();
80 if values.is_empty() {
81 continue;
82 }
83 parts.push(format!("{name} {}", values.join(" ")));
84 }
85 parts.join(" ")
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq)]
92pub struct BarFill {
93 pub fill_w: f64,
94 pub remainder_w: f64,
95 pub divider_x: Option<f64>,
96}
97
98pub fn visual_fraction(fraction: f64) -> f64 {
101 if !fraction.is_finite() {
102 return 0.0;
103 }
104 let clamped = fraction.clamp(0.0, 1.0);
105 if clamped > 0.7 && clamped < 1.0 {
106 let remainder = 1.0 - clamped;
107 let quantized = ((remainder / 0.15).ceil() * 0.15).min(1.0);
108 (1.0 - quantized).max(0.0)
109 } else {
110 clamped
111 }
112}
113
114pub fn bar_fill(track_w: f64, fraction: f64) -> BarFill {
115 if !fraction.is_finite() || fraction <= 0.0 {
116 return BarFill {
117 fill_w: 0.0,
118 remainder_w: 0.0,
119 divider_x: None,
120 };
121 }
122 let visual = visual_fraction(fraction);
123 if visual >= 1.0 {
124 return BarFill {
125 fill_w: track_w,
126 remainder_w: 0.0,
127 divider_x: None,
128 };
129 }
130 let min_visible = (track_w * 0.2).round().max(4.0);
131 let max_fill_w = (track_w - min_visible).max(1.0);
132 let fill_w = (track_w * visual).round().clamp(1.0, max_fill_w);
133 let true_remainder = track_w - fill_w;
134 let remainder_w = true_remainder.max(min_visible).min(track_w - 1.0);
135 BarFill {
136 fill_w,
137 remainder_w,
138 divider_x: Some(track_w - remainder_w),
139 }
140}
141
142pub fn parse_strip_ipc(value: &Value) -> (StripStyle, Stars, Vec<String>) {
145 let style = StripStyle::Bars;
146 let mut stars = Stars::new();
147 if let Some(map) = value.get("stars").and_then(Value::as_object) {
148 for (id, keys) in map {
149 let id = id.trim();
150 if id.is_empty() {
151 continue;
152 }
153 let mut list = Vec::new();
154 if let Some(arr) = keys.as_array() {
155 for key in arr {
156 let Some(key) = key.as_str() else { continue };
157 let key = key.trim();
158 if key.is_empty() || list.iter().any(|k| k == key) {
159 continue;
160 }
161 list.push(key.to_string());
162 if list.len() == MAX_STARS_PER_PROVIDER {
163 break;
164 }
165 }
166 }
167 if !list.is_empty() {
168 stars.insert(id.to_string(), list);
169 }
170 }
171 }
172 let mut order = Vec::new();
173 if let Some(arr) = value.get("order").and_then(Value::as_array) {
174 for id in arr {
175 let Some(id) = id.as_str() else { continue };
176 let id = id.trim();
177 if id.is_empty() || order.iter().any(|existing| existing == id) {
178 continue;
179 }
180 order.push(id.to_string());
181 }
182 }
183 (style, stars, order)
184}
185
186pub fn content_from_payload(payload: &Value, stars: &Stars, order: &[String]) -> StripContent {
191 let Some(entries) = payload.get("entries").and_then(Value::as_array) else {
192 return StripContent {
193 groups: Vec::new(),
194 bars: Vec::new(),
195 };
196 };
197 let mut by_id = std::collections::HashMap::new();
198 let mut payload_ids = Vec::new();
199 for entry in entries {
200 let Some(id) = entry.get("id").and_then(Value::as_str) else {
201 continue;
202 };
203 payload_ids.push(id.to_string());
204 by_id.insert(id.to_string(), entry);
205 }
206 let mut walk = Vec::new();
207 let mut seen = std::collections::HashSet::new();
208 for id in order {
209 if by_id.contains_key(id) && seen.insert(id.clone()) {
210 walk.push(id.clone());
211 }
212 }
213 for id in payload_ids {
214 if seen.insert(id.clone()) {
215 walk.push(id);
216 }
217 }
218 let mut groups = Vec::new();
219 for id in walk {
220 let Some(entry) = by_id.get(&id) else {
221 continue;
222 };
223 if entry.get("status").and_then(Value::as_str) == Some("error") {
224 continue;
225 }
226 let name = entry
227 .get("display_name")
228 .or_else(|| entry.get("name"))
229 .and_then(Value::as_str)
230 .unwrap_or(id.as_str())
231 .to_string();
232 let metrics = metrics_for_entry(entry, &id, &name);
233 let wanted = if stars.is_empty() {
234 metrics
235 .iter()
236 .filter(|m| m.bounded)
237 .take(MAX_STARS_PER_PROVIDER)
238 .map(|m| m.key.clone())
239 .collect()
240 } else {
241 stars.get(&id).cloned().unwrap_or_default()
242 };
243 if wanted.is_empty() {
244 continue;
245 }
246 let mut picked = Vec::new();
247 for key in wanted {
248 if let Some(metric) = metrics
249 .iter()
250 .find(|m| m.key == key && !m.value.trim().is_empty())
251 {
252 picked.push(metric.clone());
253 }
254 }
255 if picked.is_empty() {
256 continue;
257 }
258 groups.push((id, name, picked));
259 }
260 let bars: Vec<StripMetric> = groups
261 .iter()
262 .flat_map(|(_, _, metrics)| metrics.iter().cloned())
263 .filter(|m| m.bounded)
264 .take(MAX_BARS)
265 .collect();
266 StripContent { groups, bars }
267}
268
269fn metrics_for_entry(entry: &Value, id: &str, name: &str) -> Vec<StripMetric> {
270 let Some(sections) = entry.get("sections").and_then(Value::as_array) else {
271 return Vec::new();
272 };
273 let mut group = String::new();
274 let mut rows = Vec::new();
275 let mut seen: BTreeMap<String, u32> = BTreeMap::new();
276 for section in sections {
277 let kind = section.get("type").and_then(Value::as_str).unwrap_or("");
278 if kind == "text" {
279 let label = section.get("label").and_then(Value::as_str).unwrap_or("");
280 let value = section.get("value").and_then(Value::as_str).unwrap_or("");
281 if !label.is_empty() && value.is_empty() {
282 group = label.to_string();
283 }
284 continue;
285 }
286 if kind != "metric" {
287 continue;
288 }
289 let raw_label = section.get("label").and_then(Value::as_str).unwrap_or("");
290 let field_group = section.get("group").and_then(Value::as_str).unwrap_or("");
295 let effective_group: &str = if field_group.is_empty() {
296 group.as_str()
297 } else {
298 field_group
299 };
300 let label = metric_label(id, raw_label);
301 let label = if effective_group.is_empty() {
302 label
303 } else {
304 format!("{label} ({effective_group})")
305 };
306 let percent = section.get("percent").and_then(Value::as_f64);
307 let value = section
308 .get("value")
309 .and_then(Value::as_str)
310 .filter(|value| !value.trim().is_empty())
311 .map(str::to_string)
312 .or_else(|| percent.map(|percent| format!("{}%", percent.round() as i64)))
313 .unwrap_or_default();
314 let mut key = metric_key(id, raw_label, effective_group);
315 let count = seen.entry(key.clone()).or_insert(0);
316 *count += 1;
317 if *count > 1 {
318 key = format!("{key} #{count}");
319 }
320 rows.push(StripMetric {
321 provider_id: id.to_string(),
322 provider_name: name.to_string(),
323 key,
324 label,
325 value,
326 fraction: (percent.unwrap_or(0.0) / 100.0).clamp(0.0, 1.0),
327 bounded: true,
328 });
329 }
330 rows
331}
332
333fn metric_key(entry_id: &str, raw_label: &str, group: &str) -> String {
338 let label = metric_label(entry_id, raw_label);
339 if group.is_empty() {
340 format!("metric:{label}")
341 } else {
342 format!("metric:{label} ({group})")
343 }
344}
345
346fn metric_label(entry_id: &str, label: &str) -> String {
350 let slug = entry_id
351 .split('@')
352 .next()
353 .unwrap_or(entry_id)
354 .to_ascii_lowercase();
355 if slug != "supergrok" {
356 return label.to_string();
357 }
358 let label = strip_trailing_word(label, "build credits");
359 strip_trailing_word(&label, "usage")
360}
361
362fn strip_trailing_word(label: &str, word: &str) -> String {
366 if label.len() > word.len() && label.to_ascii_lowercase().ends_with(word) {
367 let head = &label[..label.len() - word.len()];
368 let trimmed = head.trim_end();
369 if trimmed.len() < head.len() {
370 return trimmed.to_string();
371 }
372 }
373 label.to_string()
374}
375
376#[derive(Debug, Clone, Copy)]
378pub struct BarsLayout {
379 pub n: usize,
380 pub track_x: f64,
381 pub track_w: f64,
382 pub track_h: f64,
383 pub rx: f64,
384 pub y_offset: f64,
385 pub gap: f64,
386}
387
388pub fn bars_layout(count: usize, side: f64) -> BarsLayout {
389 let n = count.clamp(1, MAX_BARS);
390 let pad = (side * 0.08).round().max(1.0);
391 let gap = (side * 0.03).round().max(1.0);
392 let track_x = pad;
393 let track_w = side - 2.0 * pad;
394 let layout_n = n.max(2) as f64;
395 let track_h = ((side - 2.0 * pad - (layout_n - 1.0) * gap) / layout_n)
396 .floor()
397 .max(1.0);
398 let rx = (track_h / 3.0).floor().max(1.0);
399 let total_h = n as f64 * track_h + (n as f64 - 1.0) * gap;
400 let y_offset = pad + ((side - 2.0 * pad - total_h) / 2.0).floor();
401 BarsLayout {
402 n,
403 track_x,
404 track_w,
405 track_h,
406 rx,
407 y_offset,
408 gap,
409 }
410}
411
412pub fn bars_rgba(fractions: &[f64], side: u32) -> Vec<u8> {
415 let side = side.max(8);
416 let mut buf = vec![0u8; side as usize * side as usize * 4];
417 if fractions.is_empty() {
418 return buf;
419 }
420 let size = f64::from(side);
421 let layout = bars_layout(fractions.len(), size);
422 let n = layout.n;
423
424 for (i, fraction) in fractions.iter().copied().take(n).enumerate() {
425 let y = layout.y_offset + i as f64 * (layout.track_h + layout.gap) + 1.0;
426 stamp_round_rect(
427 &mut buf,
428 side,
429 RoundBar {
430 x: layout.track_x,
431 y,
432 w: layout.track_w,
433 h: layout.track_h,
434 r_left: layout.rx,
435 r_right: layout.rx,
436 },
437 41, );
439 let fill = bar_fill(layout.track_w, fraction);
440 if fill.fill_w > 0.0 {
441 let trailing = if fill.fill_w >= layout.track_w {
442 layout.rx
443 } else {
444 (layout.rx * 0.35).floor().max(0.0)
445 };
446 stamp_round_rect(
447 &mut buf,
448 side,
449 RoundBar {
450 x: layout.track_x,
451 y,
452 w: fill.fill_w,
453 h: layout.track_h,
454 r_left: layout.rx,
455 r_right: trailing,
456 },
457 255,
458 );
459 }
460 if fill.fill_w > 0.0
461 && fill.remainder_w > 0.0
462 && let Some(divider_x) = fill.divider_x
463 {
464 stamp_round_rect(
465 &mut buf,
466 side,
467 RoundBar {
468 x: layout.track_x + divider_x,
469 y,
470 w: fill.remainder_w,
471 h: layout.track_h,
472 r_left: (layout.rx * 0.2).floor().max(0.0),
473 r_right: layout.rx,
474 },
475 61, );
477 }
478 }
479 buf
480}
481
482struct RoundBar {
483 x: f64,
484 y: f64,
485 w: f64,
486 h: f64,
487 r_left: f64,
488 r_right: f64,
489}
490
491fn stamp_round_rect(buf: &mut [u8], side: u32, bar: RoundBar, alpha: u8) {
492 if bar.w <= 0.0 || bar.h <= 0.0 {
493 return;
494 }
495 let side_i = side as i32;
496 let min_x = bar.x.floor().max(0.0) as i32;
497 let min_y = bar.y.floor().max(0.0) as i32;
498 let max_x = (bar.x + bar.w).ceil().min(f64::from(side)) as i32;
499 let max_y = (bar.y + bar.h).ceil().min(f64::from(side)) as i32;
500 let target = f64::from(alpha);
501 for py in min_y..max_y {
502 for px in min_x..max_x {
503 if px < 0 || py < 0 || px >= side_i || py >= side_i {
504 continue;
505 }
506 let cover = sample_round_rect(f64::from(px) + 0.5, f64::from(py) + 0.5, &bar);
507 if cover <= 0.0 {
508 continue;
509 }
510 let idx = ((py as u32 * side + px as u32) * 4) as usize;
511 let stamped = (target * cover).round().clamp(0.0, 255.0) as u8;
512 if stamped > buf[idx + 3] {
513 buf[idx] = 0;
514 buf[idx + 1] = 0;
515 buf[idx + 2] = 0;
516 buf[idx + 3] = stamped;
517 }
518 }
519 }
520}
521
522fn sample_round_rect(px: f64, py: f64, bar: &RoundBar) -> f64 {
523 (0.5 - sd_round_box(px, py, bar)).clamp(0.0, 1.0)
524}
525
526fn sd_round_box(px: f64, py: f64, bar: &RoundBar) -> f64 {
527 let r = if px < bar.x + bar.w * 0.5 {
528 bar.r_left
529 } else {
530 bar.r_right
531 };
532 let r = r.max(0.0).min(bar.h * 0.5).min(bar.w * 0.5);
533 let cx = bar.x + bar.w * 0.5;
534 let cy = bar.y + bar.h * 0.5;
535 let dx = (px - cx).abs() - (bar.w * 0.5 - r);
536 let dy = (py - cy).abs() - (bar.h * 0.5 - r);
537 let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
538 outside + dx.min(0.0).max(dy.min(0.0)) - r
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544 use serde_json::json;
545
546 #[test]
547 fn zero_or_negative_fraction_draws_nothing() {
548 assert_eq!(bar_fill(100.0, 0.0).fill_w, 0.0);
549 assert_eq!(bar_fill(100.0, -0.5).fill_w, 0.0);
550 }
551
552 #[test]
553 fn full_fraction_fills_track_with_no_remainder() {
554 let fill = bar_fill(100.0, 1.0);
555 assert_eq!(fill.fill_w, 100.0);
556 assert_eq!(fill.remainder_w, 0.0);
557 assert_eq!(fill.divider_x, None);
558 }
559
560 #[test]
561 fn near_full_keeps_a_visible_tail() {
562 let fill = bar_fill(100.0, 0.97);
563 assert!(fill.fill_w < 100.0);
564 assert!(fill.remainder_w >= 20.0);
565 assert_eq!(fill.divider_x, Some(fill.fill_w));
566 }
567
568 #[test]
569 fn visual_fraction_quantizes_near_full_in_fifteen_percent_steps() {
570 assert!((visual_fraction(0.97) - 0.85).abs() < 0.0001);
571 assert_eq!(visual_fraction(1.0), 1.0);
572 assert_eq!(visual_fraction(0.0), 0.0);
573 assert!((visual_fraction(0.5) - 0.5).abs() < 0.0001);
574 }
575
576 fn sample_payload() -> Value {
577 json!({
578 "entries": [
579 {
580 "id": "anthropic",
581 "display_name": "Claude",
582 "status": "ready",
583 "sections": [
584 {"type": "metric", "label": "Weekly", "percent": 19, "value": "19%"},
585 {"type": "metric", "label": "Session", "percent": 41, "value": "41%"}
586 ]
587 },
588 {
589 "id": "openai",
590 "display_name": "Codex",
591 "status": "ready",
592 "sections": [
593 {"type": "metric", "label": "Codex weekly", "percent": 2, "value": "2%"}
594 ]
595 },
596 {
597 "id": "cursor",
598 "display_name": "Cursor",
599 "status": "error",
600 "sections": [
601 {"type": "metric", "label": "Cursor Models", "percent": 90, "value": "90%"}
602 ]
603 }
604 ]
605 })
606 }
607
608 #[test]
611 fn metric_keys_match_the_popover_fixture() {
612 let fixture: Value =
613 serde_json::from_str(include_str!("../../tests/fixtures/strip_metric_keys.json"))
614 .expect("fixture is JSON");
615 let cases = fixture["cases"].as_array().expect("cases");
616 assert!(!cases.is_empty());
617 for case in cases {
618 let id = case["id"].as_str().unwrap();
619 let label = case["label"].as_str().unwrap();
620 let group = case["group"].as_str().unwrap_or("");
621 assert_eq!(
622 metric_key(id, label, group),
623 case["key"].as_str().unwrap(),
624 "{case}"
625 );
626 }
627 }
628
629 #[test]
633 fn a_starred_supergrok_meter_is_painted() {
634 let payload = serde_json::json!({ "entries": [
635 { "id": "supergrok", "status": "ready", "sections": [
636 { "type": "metric", "label": "Weekly usage", "percent": 3 }
637 ]}
638 ]});
639 let mut stars = Stars::new();
640 stars.insert("supergrok".into(), vec!["metric:Weekly".into()]);
641 let content = content_from_payload(&payload, &stars, &["supergrok".into()]);
642 assert_eq!(content.bars.len(), 1);
643 assert_eq!(content.bars[0].key, "metric:Weekly");
644 }
645
646 #[test]
647 fn empty_stars_fall_back_to_first_two_bounded_metrics() {
648 let content = content_from_payload(&sample_payload(), &Stars::new(), &[]);
649 assert_eq!(content.groups.len(), 2);
650 assert_eq!(content.groups[0].2.len(), 2);
651 assert_eq!(content.groups[0].2[0].key, "metric:Weekly");
652 assert_eq!(content.groups[0].2[1].key, "metric:Session");
653 assert_eq!(content.groups[1].2[0].key, "metric:Codex weekly");
654 assert_eq!(content.bars.len(), 3);
655 assert!(!content.title_line().is_empty());
656 }
657
658 #[test]
659 fn stars_select_named_metrics_and_skip_errors() {
660 let mut stars = Stars::new();
661 stars.insert("anthropic".into(), vec!["metric:Session".into()]);
662 stars.insert("cursor".into(), vec!["metric:Cursor Models".into()]);
663 let content = content_from_payload(&sample_payload(), &stars, &[]);
664 assert_eq!(content.groups.len(), 1);
665 assert_eq!(content.groups[0].2[0].key, "metric:Session");
666 assert_eq!(content.groups[0].2[0].fraction, 0.41);
667 assert_eq!(content.bars.len(), 1);
668 }
669
670 #[test]
671 fn grouped_metric_keys_match_the_popover() {
672 let payload = json!({
673 "entries": [{
674 "id": "antigravity",
675 "display_name": "Antigravity",
676 "status": "ready",
677 "sections": [
678 {"type": "text", "label": "Session", "value": ""},
679 {"type": "metric", "label": "Gemini", "percent": 4, "value": "4%"},
680 {"type": "text", "label": "Weekly", "value": ""},
681 {"type": "metric", "label": "Gemini", "percent": 11, "value": "11%"}
682 ]
683 }]
684 });
685 let content = content_from_payload(&payload, &Stars::new(), &[]);
686 assert_eq!(content.groups[0].2[0].key, "metric:Gemini (Session)");
687 assert_eq!(content.groups[0].2[1].key, "metric:Gemini (Weekly)");
688 }
689
690 #[test]
695 fn metric_level_groups_key_like_positional_ones() {
696 let payload = json!({
697 "entries": [{
698 "id": "anthropic",
699 "display_name": "Claude",
700 "status": "ready",
701 "sections": [
702 {"type": "metric", "label": "Weekly (7d)", "percent": 32, "value": "32%"},
703 {"type": "metric", "label": "ship the release", "percent": 91,
704 "value": "91%", "group": "Sessions"},
705 {"type": "metric", "label": "Grok Build", "percent": 94,
706 "value": "94%", "group": "Breakdown"}
707 ]
708 }]
709 });
710 let mut stars = Stars::new();
711 stars.insert(
712 "anthropic".into(),
713 vec![
714 "metric:ship the release (Sessions)".into(),
715 "metric:Grok Build (Breakdown)".into(),
716 ],
717 );
718 let content = content_from_payload(&payload, &stars, &[]);
719 let bars = &content.groups[0].2;
720 assert_eq!(bars.len(), 2);
721 assert_eq!(bars[0].key, "metric:ship the release (Sessions)");
722 assert_eq!(bars[0].label, "ship the release (Sessions)");
723 assert_eq!(bars[1].key, "metric:Grok Build (Breakdown)");
724 assert_eq!(bars[1].label, "Grok Build (Breakdown)");
725 }
726
727 #[test]
728 fn parse_strip_ipc_caps_stars_at_two_per_provider() {
729 let value = json!({
730 "style": "text",
731 "stars": {
732 "anthropic": ["metric:Weekly", "metric:Session", "metric:Extra"],
733 "": ["x"],
734 "openai": ["metric:Codex weekly"]
735 }
736 });
737 let (style, stars, order) = parse_strip_ipc(&value);
738 assert_eq!(style, StripStyle::Bars);
739 assert_eq!(stars["anthropic"].len(), 2);
740 assert_eq!(stars["openai"], vec!["metric:Codex weekly".to_string()]);
741 assert!(!stars.contains_key(""));
742 assert!(order.is_empty());
743 }
744
745 #[test]
746 fn strip_follows_popover_card_order() {
747 let mut stars = Stars::new();
748 stars.insert("anthropic".into(), vec!["metric:Weekly".into()]);
749 stars.insert("openai".into(), vec!["metric:Codex weekly".into()]);
750 let content = content_from_payload(
751 &sample_payload(),
752 &stars,
753 &["openai".into(), "anthropic".into()],
754 );
755 assert_eq!(content.groups[0].0, "openai");
756 assert_eq!(content.groups[1].0, "anthropic");
757 assert_eq!(content.bars[0].provider_id, "openai");
758 assert_eq!(content.bars[1].provider_id, "anthropic");
759 }
760
761 #[test]
762 fn bars_rgba_is_square_black_and_anti_aliased() {
763 let bytes = bars_rgba(&[0.4, 0.97], BARS_PIXEL_SIDE);
764 assert_eq!(
765 bytes.len(),
766 (BARS_PIXEL_SIDE * BARS_PIXEL_SIDE * 4) as usize
767 );
768 assert_eq!(bytes[3], 0, "top-left stays transparent");
769 let last = bytes.len() - 1;
770 assert_eq!(bytes[last], 0, "bottom-right stays transparent");
771 let mut inked = 0usize;
772 let mut soft = 0usize;
773 for pixel in bytes.as_chunks::<4>().0 {
774 if pixel[3] > 0 {
775 inked += 1;
776 assert_eq!(&pixel[..3], &[0, 0, 0]);
777 }
778 if pixel[3] > 0 && pixel[3] < 255 {
779 soft += 1;
780 }
781 }
782 assert!(inked > 40, "glyph too sparse: {inked}");
783 assert!(soft > 0, "expected anti-aliased edges");
784 }
785
786 #[test]
787 fn empty_fractions_are_fully_transparent() {
788 let bytes = bars_rgba(&[], BARS_PIXEL_SIDE);
789 assert!(bytes.as_chunks::<4>().0.iter().all(|p| p[3] == 0));
790 }
791
792 #[test]
793 fn bars_rgba_at_each_status_item_scale() {
794 for scale in BARS_SCALES {
795 let side = BARS_POINT_SIDE * scale;
796 let bytes = bars_rgba(&[0.4, 0.97], side);
797 assert_eq!(bytes.len(), (side * side * 4) as usize, "scale {scale}");
798 assert!(
799 bytes.as_chunks::<4>().0.iter().any(|p| p[3] > 0),
800 "scale {scale} produced an empty glyph"
801 );
802 }
803 }
804}