1use presentar_core::{
10 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
11 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
12};
13use std::any::Any;
14use std::time::Duration;
15
16use super::flex_cell::{Alignment, FlexCell, Overflow};
17use super::proportional_bar::{BarSegment, ProportionalBar};
18use super::semantic_label::SemanticStatus;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum LabeledBarLayout {
23 #[default]
25 Horizontal,
26 Stacked,
28 Overlay,
30}
31
32#[derive(Debug, Clone)]
42pub struct LabeledBar {
43 label: String,
45 value: String,
47 segments: Vec<BarSegment>,
49 label_color: Color,
51 value_status: Option<SemanticStatus>,
53 value_color: Color,
55 bar_background: Color,
57 layout_mode: LabeledBarLayout,
59 label_width: usize,
61 value_width: usize,
63 bounds: Rect,
65}
66
67impl Default for LabeledBar {
68 fn default() -> Self {
69 Self::new("Label", 0.0)
70 }
71}
72
73impl LabeledBar {
74 #[must_use]
76 pub fn new(label: impl Into<String>, value: f64) -> Self {
77 let clamped = value.clamp(0.0, 1.0);
78 Self {
79 label: label.into(),
80 value: format!("{:.0}%", clamped * 100.0),
81 segments: vec![BarSegment {
82 value: clamped,
83 color: Color::new(0.3, 0.8, 1.0, 1.0), }],
85 label_color: Color::new(0.8, 0.8, 0.8, 1.0),
86 value_status: None,
87 value_color: Color::new(0.9, 0.9, 0.9, 1.0),
88 bar_background: Color::new(0.15, 0.15, 0.15, 1.0),
89 layout_mode: LabeledBarLayout::Horizontal,
90 label_width: 8,
91 value_width: 6,
92 bounds: Rect::default(),
93 }
94 }
95
96 #[must_use]
98 pub fn memory(label: impl Into<String>, used: u64, total: u64) -> Self {
99 let ratio = if total > 0 {
100 (used as f64 / total as f64).clamp(0.0, 1.0)
101 } else {
102 0.0
103 };
104
105 let used_str = format_bytes(used);
106 let total_str = format_bytes(total);
107
108 Self::new(label, ratio)
109 .with_value(format!("{used_str} / {total_str}"))
110 .with_value_width(14)
111 .with_semantic_value(SemanticStatus::from_usage(ratio * 100.0))
112 }
113
114 #[must_use]
116 pub fn percentage(label: impl Into<String>, pct: f64) -> Self {
117 let clamped = pct.clamp(0.0, 100.0);
118 Self::new(label, clamped / 100.0)
119 .with_value(format!("{clamped:.1}%"))
120 .with_semantic_value(SemanticStatus::from_usage(clamped))
121 }
122
123 #[must_use]
125 pub fn temperature(label: impl Into<String>, temp_c: f64, max_temp: f64) -> Self {
126 let ratio = if max_temp > 0.0 {
127 (temp_c / max_temp).clamp(0.0, 1.0)
128 } else {
129 0.0
130 };
131
132 Self::new(label, ratio)
133 .with_value(format!("{temp_c:.0}°C"))
134 .with_semantic_value(SemanticStatus::from_temperature(temp_c))
135 .with_value_width(5)
136 }
137
138 #[must_use]
140 pub fn with_label(mut self, label: impl Into<String>) -> Self {
141 self.label = label.into();
142 self
143 }
144
145 #[must_use]
147 pub fn with_value(mut self, value: impl Into<String>) -> Self {
148 self.value = value.into();
149 self
150 }
151
152 #[must_use]
154 pub fn with_semantic_value(mut self, status: SemanticStatus) -> Self {
155 self.value_status = Some(status);
156 self
157 }
158
159 #[must_use]
161 pub fn with_label_color(mut self, color: Color) -> Self {
162 self.label_color = color;
163 self
164 }
165
166 #[must_use]
168 pub fn with_value_color(mut self, color: Color) -> Self {
169 self.value_color = color;
170 self.value_status = None;
171 self
172 }
173
174 #[must_use]
176 pub fn with_bar_color(mut self, color: Color) -> Self {
177 if let Some(seg) = self.segments.first_mut() {
178 seg.color = color;
179 }
180 self
181 }
182
183 #[must_use]
185 pub fn with_bar_background(mut self, color: Color) -> Self {
186 self.bar_background = color;
187 self
188 }
189
190 #[must_use]
192 pub fn with_segment(mut self, value: f64, color: Color) -> Self {
193 self.segments.push(BarSegment {
194 value: value.clamp(0.0, 1.0),
195 color,
196 });
197 self
198 }
199
200 #[must_use]
202 pub fn with_segments(mut self, segments: Vec<BarSegment>) -> Self {
203 self.segments = segments;
204 self
205 }
206
207 #[must_use]
209 pub fn with_layout(mut self, mode: LabeledBarLayout) -> Self {
210 self.layout_mode = mode;
211 self
212 }
213
214 #[must_use]
216 pub fn with_label_width(mut self, width: usize) -> Self {
217 self.label_width = width;
218 self
219 }
220
221 #[must_use]
223 pub fn with_value_width(mut self, width: usize) -> Self {
224 self.value_width = width;
225 self
226 }
227
228 fn effective_value_color(&self) -> Color {
230 self.value_status.map_or(self.value_color, |s| s.color())
231 }
232}
233
234impl Widget for LabeledBar {
235 fn type_id(&self) -> TypeId {
236 TypeId::of::<Self>()
237 }
238
239 fn measure(&self, constraints: Constraints) -> Size {
240 let height = match self.layout_mode {
241 LabeledBarLayout::Horizontal | LabeledBarLayout::Overlay => 1.0,
242 LabeledBarLayout::Stacked => 2.0,
243 };
244 constraints.constrain(Size::new(constraints.max_width, height))
245 }
246
247 fn layout(&mut self, bounds: Rect) -> LayoutResult {
248 self.bounds = bounds;
249 LayoutResult {
250 size: Size::new(bounds.width, bounds.height),
251 }
252 }
253
254 fn paint(&self, canvas: &mut dyn Canvas) {
255 if self.bounds.width < 3.0 || self.bounds.height < 1.0 {
256 return;
257 }
258
259 match self.layout_mode {
260 LabeledBarLayout::Horizontal => self.paint_horizontal(canvas),
261 LabeledBarLayout::Stacked => self.paint_stacked(canvas),
262 LabeledBarLayout::Overlay => self.paint_overlay(canvas),
263 }
264 }
265
266 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
267 None
268 }
269
270 fn children(&self) -> &[Box<dyn Widget>] {
271 &[]
272 }
273
274 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
275 &mut []
276 }
277}
278
279impl LabeledBar {
280 fn paint_horizontal(&self, canvas: &mut dyn Canvas) {
281 let total_width = self.bounds.width as usize;
282 let bar_width = total_width.saturating_sub(self.label_width + self.value_width + 2);
283
284 if bar_width < 1 {
285 return;
286 }
287
288 let y = self.bounds.y;
289 let mut x = self.bounds.x;
290
291 let mut label_cell = FlexCell::new(&self.label)
293 .with_color(self.label_color)
294 .with_overflow(Overflow::Ellipsis)
295 .with_alignment(Alignment::Left);
296 label_cell.layout(Rect::new(x, y, self.label_width as f32, 1.0));
297 label_cell.paint(canvas);
298 x += self.label_width as f32 + 1.0;
299
300 let mut bar = ProportionalBar::new().with_background(self.bar_background);
302 for seg in &self.segments {
303 bar = bar.with_segment(seg.value, seg.color);
304 }
305 bar.layout(Rect::new(x, y, bar_width as f32, 1.0));
306 bar.paint(canvas);
307 x += bar_width as f32 + 1.0;
308
309 let mut value_cell = FlexCell::new(&self.value)
311 .with_color(self.effective_value_color())
312 .with_overflow(Overflow::Ellipsis)
313 .with_alignment(Alignment::Right);
314 value_cell.layout(Rect::new(x, y, self.value_width as f32, 1.0));
315 value_cell.paint(canvas);
316 }
317
318 fn paint_stacked(&self, canvas: &mut dyn Canvas) {
319 if self.bounds.height < 2.0 {
320 return;
321 }
322
323 let x = self.bounds.x;
324 let y = self.bounds.y;
325 let width = self.bounds.width;
326
327 let mut label_cell = FlexCell::new(&self.label)
329 .with_color(self.label_color)
330 .with_overflow(Overflow::Ellipsis);
331 label_cell.layout(Rect::new(x, y, width, 1.0));
332 label_cell.paint(canvas);
333
334 let bar_width = (width as usize).saturating_sub(self.value_width + 1);
336 if bar_width < 1 {
337 return;
338 }
339
340 let mut bar = ProportionalBar::new().with_background(self.bar_background);
341 for seg in &self.segments {
342 bar = bar.with_segment(seg.value, seg.color);
343 }
344 bar.layout(Rect::new(x, y + 1.0, bar_width as f32, 1.0));
345 bar.paint(canvas);
346
347 let mut value_cell = FlexCell::new(&self.value)
349 .with_color(self.effective_value_color())
350 .with_overflow(Overflow::Ellipsis)
351 .with_alignment(Alignment::Right);
352 value_cell.layout(Rect::new(
353 x + bar_width as f32 + 1.0,
354 y + 1.0,
355 self.value_width as f32,
356 1.0,
357 ));
358 value_cell.paint(canvas);
359 }
360
361 fn paint_overlay(&self, canvas: &mut dyn Canvas) {
362 let x = self.bounds.x;
363 let y = self.bounds.y;
364 let width = self.bounds.width;
365
366 let mut bar = ProportionalBar::new().with_background(self.bar_background);
368 for seg in &self.segments {
369 bar = bar.with_segment(seg.value, seg.color);
370 }
371 bar.layout(Rect::new(x, y, width, 1.0));
372 bar.paint(canvas);
373
374 let text = format!("{} {}", self.label, self.value);
376 let text_len = text.chars().count();
377 let text_x = x + ((width as usize).saturating_sub(text_len) / 2) as f32;
378
379 canvas.draw_text(
380 &text,
381 Point::new(text_x, y),
382 &TextStyle {
383 color: Color::new(1.0, 1.0, 1.0, 1.0), ..Default::default()
385 },
386 );
387 }
388}
389
390impl Brick for LabeledBar {
391 fn brick_name(&self) -> &'static str {
392 "labeled_bar"
393 }
394
395 fn assertions(&self) -> &[BrickAssertion] {
396 static ASSERTIONS: &[BrickAssertion] = &[
397 BrickAssertion::max_latency_ms(2), ];
399 ASSERTIONS
400 }
401
402 fn budget(&self) -> BrickBudget {
403 BrickBudget::uniform(2)
404 }
405
406 fn verify(&self) -> BrickVerification {
407 let total: f64 = self.segments.iter().map(|s| s.value).sum();
409 let valid = total <= 1.0 + f64::EPSILON && !total.is_nan();
410
411 if valid {
412 BrickVerification {
413 passed: self.assertions().to_vec(),
414 failed: vec![],
415 verification_time: Duration::from_micros(1),
416 }
417 } else {
418 BrickVerification {
419 passed: vec![],
420 failed: self
421 .assertions()
422 .iter()
423 .map(|a| (a.clone(), "Segment total exceeds 1.0".to_string()))
424 .collect(),
425 verification_time: Duration::from_micros(1),
426 }
427 }
428 }
429
430 fn to_html(&self) -> String {
431 format!(
432 r#"<div class="labeled-bar">
433 <span class="label">{}</span>
434 <div class="bar"></div>
435 <span class="value">{}</span>
436 </div>"#,
437 self.label, self.value
438 )
439 }
440
441 fn to_css(&self) -> String {
442 String::new()
443 }
444}
445
446fn format_bytes(bytes: u64) -> String {
448 const KB: u64 = 1024;
449 const MB: u64 = KB * 1024;
450 const GB: u64 = MB * 1024;
451 const TB: u64 = GB * 1024;
452
453 if bytes >= TB {
454 format!("{:.1}T", bytes as f64 / TB as f64)
455 } else if bytes >= GB {
456 format!("{:.1}G", bytes as f64 / GB as f64)
457 } else if bytes >= MB {
458 format!("{:.1}M", bytes as f64 / MB as f64)
459 } else if bytes >= KB {
460 format!("{:.1}K", bytes as f64 / KB as f64)
461 } else {
462 format!("{bytes}B")
463 }
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469 use crate::direct::{CellBuffer, DirectTerminalCanvas};
470
471 #[test]
476 fn test_basic_creation() {
477 let bar = LabeledBar::new("CPU", 0.5);
478 assert_eq!(bar.label, "CPU");
479 assert_eq!(bar.value, "50%");
480 }
481
482 #[test]
483 fn test_default() {
484 let bar = LabeledBar::default();
485 assert_eq!(bar.label, "Label");
486 assert_eq!(bar.value, "0%");
487 }
488
489 #[test]
490 fn test_layout_mode_default() {
491 assert_eq!(LabeledBarLayout::default(), LabeledBarLayout::Horizontal);
492 }
493
494 #[test]
495 fn test_percentage_bar() {
496 let bar = LabeledBar::percentage("Memory", 75.0);
497 assert_eq!(bar.value, "75.0%");
498 assert_eq!(bar.value_status, Some(SemanticStatus::High));
499 }
500
501 #[test]
502 fn test_memory_bar() {
503 let bar = LabeledBar::memory("RAM", 8 * 1024 * 1024 * 1024, 16 * 1024 * 1024 * 1024);
504 assert!(bar.value.contains("8.0G"));
505 assert!(bar.value.contains("16.0G"));
506 }
507
508 #[test]
509 fn test_memory_bar_zero_total() {
510 let bar = LabeledBar::memory("RAM", 100, 0);
511 assert!(bar.segments[0].value >= 0.0);
512 }
513
514 #[test]
515 fn test_temperature_bar() {
516 let bar = LabeledBar::temperature("Core 0", 75.0, 100.0);
517 assert_eq!(bar.value, "75°C");
518 assert_eq!(bar.value_status, Some(SemanticStatus::Warning));
519 }
520
521 #[test]
522 fn test_temperature_bar_zero_max() {
523 let bar = LabeledBar::temperature("Core 0", 50.0, 0.0);
524 assert_eq!(bar.segments[0].value, 0.0);
525 }
526
527 #[test]
532 fn test_with_label() {
533 let bar = LabeledBar::new("Old", 0.5).with_label("New");
534 assert_eq!(bar.label, "New");
535 }
536
537 #[test]
538 fn test_with_value() {
539 let bar = LabeledBar::new("CPU", 0.5).with_value("Custom Value");
540 assert_eq!(bar.value, "Custom Value");
541 }
542
543 #[test]
544 fn test_with_semantic_value() {
545 let bar = LabeledBar::new("CPU", 0.5).with_semantic_value(SemanticStatus::Warning);
546 assert_eq!(bar.value_status, Some(SemanticStatus::Warning));
547 }
548
549 #[test]
550 fn test_with_label_color() {
551 let color = Color::new(1.0, 0.0, 0.0, 1.0);
552 let bar = LabeledBar::new("CPU", 0.5).with_label_color(color);
553 assert_eq!(bar.label_color, color);
554 }
555
556 #[test]
557 fn test_with_value_color() {
558 let color = Color::new(0.0, 1.0, 0.0, 1.0);
559 let bar = LabeledBar::new("CPU", 0.5).with_value_color(color);
560 assert_eq!(bar.value_color, color);
561 assert!(bar.value_status.is_none()); }
563
564 #[test]
565 fn test_with_bar_color() {
566 let color = Color::new(0.0, 0.0, 1.0, 1.0);
567 let bar = LabeledBar::new("CPU", 0.5).with_bar_color(color);
568 assert_eq!(bar.segments[0].color, color);
569 }
570
571 #[test]
572 fn test_with_bar_background() {
573 let color = Color::new(0.1, 0.1, 0.1, 1.0);
574 let bar = LabeledBar::new("CPU", 0.5).with_bar_background(color);
575 assert_eq!(bar.bar_background, color);
576 }
577
578 #[test]
579 fn test_with_segment() {
580 let bar = LabeledBar::new("Disk", 0.3).with_segment(0.2, Color::GREEN);
581 assert_eq!(bar.segments.len(), 2);
582 }
583
584 #[test]
585 fn test_with_segments() {
586 let segments = vec![
587 BarSegment {
588 value: 0.3,
589 color: Color::BLUE,
590 },
591 BarSegment {
592 value: 0.2,
593 color: Color::GREEN,
594 },
595 ];
596 let bar = LabeledBar::new("Disk", 0.0).with_segments(segments);
597 assert_eq!(bar.segments.len(), 2);
598 }
599
600 #[test]
601 fn test_with_layout() {
602 let bar = LabeledBar::new("CPU", 0.5).with_layout(LabeledBarLayout::Stacked);
603 assert_eq!(bar.layout_mode, LabeledBarLayout::Stacked);
604 }
605
606 #[test]
607 fn test_with_label_width() {
608 let bar = LabeledBar::new("CPU", 0.5).with_label_width(15);
609 assert_eq!(bar.label_width, 15);
610 }
611
612 #[test]
613 fn test_with_value_width() {
614 let bar = LabeledBar::new("CPU", 0.5).with_value_width(10);
615 assert_eq!(bar.value_width, 10);
616 }
617
618 #[test]
623 fn test_type_id() {
624 let bar = LabeledBar::new("CPU", 0.5);
625 let id = Widget::type_id(&bar);
626 assert_eq!(id, TypeId::of::<LabeledBar>());
627 }
628
629 #[test]
630 fn test_measure_horizontal() {
631 let bar = LabeledBar::new("CPU", 0.5);
632 let constraints = Constraints::loose(Size::new(100.0, 50.0));
633 let size = bar.measure(constraints);
634 assert_eq!(size.height, 1.0);
635 }
636
637 #[test]
638 fn test_measure_stacked() {
639 let bar = LabeledBar::new("CPU", 0.5).with_layout(LabeledBarLayout::Stacked);
640 let constraints = Constraints::loose(Size::new(100.0, 50.0));
641 let size = bar.measure(constraints);
642 assert_eq!(size.height, 2.0);
643 }
644
645 #[test]
646 fn test_measure_overlay() {
647 let bar = LabeledBar::new("CPU", 0.5).with_layout(LabeledBarLayout::Overlay);
648 let constraints = Constraints::loose(Size::new(100.0, 50.0));
649 let size = bar.measure(constraints);
650 assert_eq!(size.height, 1.0);
651 }
652
653 #[test]
654 fn test_layout() {
655 let mut bar = LabeledBar::new("CPU", 0.5);
656 let bounds = Rect::new(0.0, 0.0, 50.0, 1.0);
657 let result = bar.layout(bounds);
658 assert_eq!(result.size.width, 50.0);
659 assert_eq!(bar.bounds, bounds);
660 }
661
662 #[test]
663 fn test_horizontal_paint() {
664 let mut bar = LabeledBar::new("Test", 0.5);
665 bar.layout(Rect::new(0.0, 0.0, 40.0, 1.0));
666
667 let mut buffer = CellBuffer::new(40, 1);
668 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
669 bar.paint(&mut canvas);
670 }
671
672 #[test]
673 fn test_horizontal_paint_narrow() {
674 let mut bar = LabeledBar::new("Test", 0.5);
675 bar.layout(Rect::new(0.0, 0.0, 10.0, 1.0));
676
677 let mut buffer = CellBuffer::new(10, 1);
678 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
679 bar.paint(&mut canvas);
680 }
681
682 #[test]
683 fn test_horizontal_paint_too_narrow() {
684 let mut bar = LabeledBar::new("Test", 0.5);
685 bar.layout(Rect::new(0.0, 0.0, 2.0, 1.0));
686
687 let mut buffer = CellBuffer::new(2, 1);
688 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
689 bar.paint(&mut canvas); }
691
692 #[test]
693 fn test_stacked_paint() {
694 let mut bar = LabeledBar::new("Test", 0.5).with_layout(LabeledBarLayout::Stacked);
695 bar.layout(Rect::new(0.0, 0.0, 40.0, 2.0));
696
697 let mut buffer = CellBuffer::new(40, 2);
698 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
699 bar.paint(&mut canvas);
700 }
701
702 #[test]
703 fn test_stacked_paint_too_short() {
704 let mut bar = LabeledBar::new("Test", 0.5).with_layout(LabeledBarLayout::Stacked);
705 bar.layout(Rect::new(0.0, 0.0, 40.0, 1.0));
706
707 let mut buffer = CellBuffer::new(40, 1);
708 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
709 bar.paint(&mut canvas); }
711
712 #[test]
713 fn test_overlay_paint() {
714 let mut bar = LabeledBar::new("Test", 0.5).with_layout(LabeledBarLayout::Overlay);
715 bar.layout(Rect::new(0.0, 0.0, 40.0, 1.0));
716
717 let mut buffer = CellBuffer::new(40, 1);
718 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
719 bar.paint(&mut canvas);
720 }
721
722 #[test]
723 fn test_event() {
724 let mut bar = LabeledBar::new("Test", 0.5);
725 let event = Event::key_down(presentar_core::Key::Enter);
726 let result = bar.event(&event);
727 assert!(result.is_none());
728 }
729
730 #[test]
731 fn test_children() {
732 let bar = LabeledBar::new("Test", 0.5);
733 assert!(bar.children().is_empty());
734 }
735
736 #[test]
737 fn test_children_mut() {
738 let mut bar = LabeledBar::new("Test", 0.5);
739 assert!(bar.children_mut().is_empty());
740 }
741
742 #[test]
747 fn test_brick_name() {
748 let bar = LabeledBar::new("Test", 0.5);
749 assert_eq!(bar.brick_name(), "labeled_bar");
750 }
751
752 #[test]
753 fn test_assertions() {
754 let bar = LabeledBar::new("Test", 0.5);
755 let assertions = bar.assertions();
756 assert!(!assertions.is_empty());
757 }
758
759 #[test]
760 fn test_budget() {
761 let bar = LabeledBar::new("Test", 0.5);
762 let budget = bar.budget();
763 assert!(budget.total_ms > 0);
764 }
765
766 #[test]
767 fn test_multi_segment() {
768 let bar = LabeledBar::new("Disk", 0.0).with_segments(vec![
769 BarSegment {
770 value: 0.3,
771 color: Color::BLUE,
772 },
773 BarSegment {
774 value: 0.2,
775 color: Color::GREEN,
776 },
777 ]);
778
779 let v = bar.verify();
780 assert!(v.failed.is_empty());
781 }
782
783 #[test]
784 fn test_verify_exceeds_one() {
785 let bar = LabeledBar::new("Disk", 0.0).with_segments(vec![
786 BarSegment {
787 value: 0.7,
788 color: Color::BLUE,
789 },
790 BarSegment {
791 value: 0.5,
792 color: Color::GREEN,
793 },
794 ]);
795
796 let v = bar.verify();
797 assert!(!v.failed.is_empty());
798 }
799
800 #[test]
801 fn test_to_html() {
802 let bar = LabeledBar::new("CPU", 0.5);
803 let html = bar.to_html();
804 assert!(html.contains("CPU"));
805 assert!(html.contains("50%"));
806 assert!(html.contains("labeled-bar"));
807 }
808
809 #[test]
810 fn test_to_css() {
811 let bar = LabeledBar::new("CPU", 0.5);
812 let css = bar.to_css();
813 assert!(css.is_empty());
814 }
815
816 #[test]
821 fn test_format_bytes() {
822 assert_eq!(format_bytes(500), "500B");
823 assert_eq!(format_bytes(1024), "1.0K");
824 assert_eq!(format_bytes(1024 * 1024), "1.0M");
825 assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0G");
826 assert_eq!(format_bytes(1024u64 * 1024 * 1024 * 1024), "1.0T");
827 }
828
829 #[test]
830 fn test_brick_verification() {
831 let bar = LabeledBar::new("Valid", 0.5);
832 let v = bar.verify();
833 assert!(v.failed.is_empty());
834 }
835
836 #[test]
837 fn test_semantic_coloring() {
838 let critical = LabeledBar::percentage("High", 95.0);
839 assert_eq!(critical.value_status, Some(SemanticStatus::Critical));
840
841 let normal = LabeledBar::percentage("Low", 10.0);
842 assert_eq!(normal.value_status, Some(SemanticStatus::Normal));
843 }
844
845 #[test]
846 fn test_effective_value_color_with_semantic() {
847 let bar = LabeledBar::new("CPU", 0.5).with_semantic_value(SemanticStatus::Critical);
848 let color = bar.effective_value_color();
849 assert!(color.r > 0.5);
851 }
852
853 #[test]
854 fn test_effective_value_color_without_semantic() {
855 let bar = LabeledBar::new("CPU", 0.5);
856 let color = bar.effective_value_color();
857 assert!(color.r > 0.8);
859 }
860}