Skip to main content

presentar_terminal/widgets/
labeled_bar.rs

1//! `LabeledBar` molecule widget.
2//!
3//! Composition of `FlexCell` (label) + `ProportionalBar` (bar) + `FlexCell` (value).
4//! Reference: SPEC-024 Appendix I (Atomic Widget Mandate - Molecules).
5//!
6//! This is the fundamental building block for memory bars, CPU meters,
7//! disk usage, GPU utilization, etc.
8
9use 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/// Layout mode for the labeled bar.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum LabeledBarLayout {
23    /// Label | Bar | Value (horizontal)
24    #[default]
25    Horizontal,
26    /// Label on top, Bar below
27    Stacked,
28    /// Bar only with label overlay
29    Overlay,
30}
31
32/// `LabeledBar` - composition of label + bar + value.
33///
34/// Example renders:
35/// ```text
36/// Horizontal: CPU   ████████░░░░░░░░░░  45%
37/// Stacked:    Memory
38///             ████████████░░░░░░░░  8.2G / 16G
39/// Overlay:    ███45%████░░░░░░░░░░
40/// ```
41#[derive(Debug, Clone)]
42pub struct LabeledBar {
43    /// Label text (e.g., "CPU", "Memory").
44    label: String,
45    /// Value text (e.g., "45%", "8.2G / 16G").
46    value: String,
47    /// Bar segments.
48    segments: Vec<BarSegment>,
49    /// Label color.
50    label_color: Color,
51    /// Value color (or semantic).
52    value_status: Option<SemanticStatus>,
53    /// Value color override (if not using semantic).
54    value_color: Color,
55    /// Bar background color.
56    bar_background: Color,
57    /// Layout mode.
58    layout_mode: LabeledBarLayout,
59    /// Label width (fixed characters).
60    label_width: usize,
61    /// Value width (fixed characters).
62    value_width: usize,
63    /// Cached bounds.
64    bounds: Rect,
65}
66
67impl Default for LabeledBar {
68    fn default() -> Self {
69        Self::new("Label", 0.0)
70    }
71}
72
73impl LabeledBar {
74    /// Create a new labeled bar with a single value.
75    #[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), // Default cyan
84            }],
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    /// Create a memory-style bar (used/total format).
97    #[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    /// Create a percentage bar with semantic coloring.
115    #[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    /// Create a temperature bar.
124    #[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    /// Set label text.
139    #[must_use]
140    pub fn with_label(mut self, label: impl Into<String>) -> Self {
141        self.label = label.into();
142        self
143    }
144
145    /// Set value text.
146    #[must_use]
147    pub fn with_value(mut self, value: impl Into<String>) -> Self {
148        self.value = value.into();
149        self
150    }
151
152    /// Set semantic coloring for value.
153    #[must_use]
154    pub fn with_semantic_value(mut self, status: SemanticStatus) -> Self {
155        self.value_status = Some(status);
156        self
157    }
158
159    /// Set label color.
160    #[must_use]
161    pub fn with_label_color(mut self, color: Color) -> Self {
162        self.label_color = color;
163        self
164    }
165
166    /// Set value color (overrides semantic).
167    #[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    /// Set bar color.
175    #[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    /// Set bar background color.
184    #[must_use]
185    pub fn with_bar_background(mut self, color: Color) -> Self {
186        self.bar_background = color;
187        self
188    }
189
190    /// Add a segment to the bar.
191    #[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    /// Set multiple segments (replaces existing).
201    #[must_use]
202    pub fn with_segments(mut self, segments: Vec<BarSegment>) -> Self {
203        self.segments = segments;
204        self
205    }
206
207    /// Set layout mode.
208    #[must_use]
209    pub fn with_layout(mut self, mode: LabeledBarLayout) -> Self {
210        self.layout_mode = mode;
211        self
212    }
213
214    /// Set label width.
215    #[must_use]
216    pub fn with_label_width(mut self, width: usize) -> Self {
217        self.label_width = width;
218        self
219    }
220
221    /// Set value width.
222    #[must_use]
223    pub fn with_value_width(mut self, width: usize) -> Self {
224        self.value_width = width;
225        self
226    }
227
228    /// Get effective value color.
229    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        // Label (left-aligned, fixed width)
292        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        // Bar
301        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        // Value (right-aligned, fixed width)
310        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        // Label on first line
328        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        // Bar on second line (with value at end)
335        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        // Value at end of bar line
348        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        // Draw bar first
367        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        // Overlay value text in center
375        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), // White overlay
384                ..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), // Composition of 3 widgets
398        ];
399        ASSERTIONS
400    }
401
402    fn budget(&self) -> BrickBudget {
403        BrickBudget::uniform(2)
404    }
405
406    fn verify(&self) -> BrickVerification {
407        // Verify segments don't exceed 1.0 total
408        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
446/// Format bytes to human-readable string.
447fn 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    // =========================================================================
472    // CREATION TESTS
473    // =========================================================================
474
475    #[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    // =========================================================================
528    // BUILDER TESTS
529    // =========================================================================
530
531    #[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()); // Clears semantic
562    }
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    // =========================================================================
619    // WIDGET TESTS
620    // =========================================================================
621
622    #[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); // Should return early
690    }
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); // Should return early
710    }
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    // =========================================================================
743    // BRICK TESTS
744    // =========================================================================
745
746    #[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    // =========================================================================
817    // HELPER FUNCTION TESTS
818    // =========================================================================
819
820    #[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        // Critical should be red-ish
850        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        // Default value color is grayish
858        assert!(color.r > 0.8);
859    }
860}