Skip to main content

presentar_terminal/widgets/
gauge.rs

1//! Arc/circular gauge widget.
2//!
3//! Displays a value as an arc gauge using Unicode box-drawing characters.
4//! Useful for compact metric displays like CPU temperature or utilization.
5
6use presentar_core::{
7    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
8    LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
9};
10use std::any::Any;
11use std::time::Duration;
12
13/// Gauge display mode.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum GaugeMode {
16    /// Full arc (360° - ╭───╮).
17    #[default]
18    Arc,
19    /// Quarter arc (90° - bottom-right corner).
20    Quarter,
21    /// Half arc (180° - bottom semicircle ╰───╯).
22    Half,
23    /// Three-quarter arc (270° - U-shape open at top-left).
24    ThreeQuarter,
25    /// Vertical bar with ticks.
26    Vertical,
27    /// Compact single-line.
28    Compact,
29}
30
31/// Arc gauge widget using Unicode characters.
32#[derive(Debug, Clone)]
33pub struct Gauge {
34    /// Current value.
35    value: f64,
36    /// Maximum value.
37    max: f64,
38    /// Gauge label.
39    label: Option<String>,
40    /// Display mode.
41    mode: GaugeMode,
42    /// Gauge color.
43    color: Color,
44    /// Warning threshold (percentage).
45    warn_threshold: f64,
46    /// Critical threshold (percentage).
47    critical_threshold: f64,
48    /// Show value text.
49    show_value: bool,
50    /// Unit suffix (e.g., "°C", "%").
51    unit: Option<String>,
52    /// Cached bounds.
53    bounds: Rect,
54}
55
56impl Default for Gauge {
57    fn default() -> Self {
58        Self::new(0.0, 100.0)
59    }
60}
61
62impl Gauge {
63    /// Create a new gauge with value and max.
64    #[must_use]
65    pub fn new(value: f64, max: f64) -> Self {
66        Self {
67            value: value.clamp(0.0, max),
68            max,
69            label: None,
70            mode: GaugeMode::default(),
71            color: Color::new(0.3, 0.7, 1.0, 1.0),
72            warn_threshold: 70.0,
73            critical_threshold: 90.0,
74            show_value: true,
75            unit: None,
76            bounds: Rect::default(),
77        }
78    }
79
80    /// Create a percentage gauge (0-100).
81    #[must_use]
82    pub fn percentage(value: f64) -> Self {
83        Self::new(value, 100.0).with_unit("%")
84    }
85
86    /// Create a temperature gauge.
87    #[must_use]
88    pub fn temperature(value: f64, max: f64) -> Self {
89        Self::new(value, max)
90            .with_unit("°C")
91            .with_thresholds(60.0, 80.0)
92    }
93
94    /// Set the label.
95    #[must_use]
96    pub fn with_label(mut self, label: impl Into<String>) -> Self {
97        self.label = Some(label.into());
98        self
99    }
100
101    /// Set the display mode.
102    #[must_use]
103    pub fn with_mode(mut self, mode: GaugeMode) -> Self {
104        self.mode = mode;
105        self
106    }
107
108    /// Set the color.
109    #[must_use]
110    pub fn with_color(mut self, color: Color) -> Self {
111        self.color = color;
112        self
113    }
114
115    /// Set warning and critical thresholds.
116    #[must_use]
117    pub fn with_thresholds(mut self, warn: f64, critical: f64) -> Self {
118        self.warn_threshold = warn;
119        self.critical_threshold = critical;
120        self
121    }
122
123    /// Set whether to show value text.
124    #[must_use]
125    pub fn with_value_display(mut self, show: bool) -> Self {
126        self.show_value = show;
127        self
128    }
129
130    /// Set unit suffix.
131    #[must_use]
132    pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
133        self.unit = Some(unit.into());
134        self
135    }
136
137    /// Update the value.
138    pub fn set_value(&mut self, value: f64) {
139        self.value = value.clamp(0.0, self.max);
140    }
141
142    /// Get current value.
143    #[must_use]
144    pub fn value(&self) -> f64 {
145        self.value
146    }
147
148    /// Get percentage (0-100).
149    #[must_use]
150    pub fn percent(&self) -> f64 {
151        if self.max == 0.0 {
152            0.0
153        } else {
154            (self.value / self.max * 100.0).clamp(0.0, 100.0)
155        }
156    }
157
158    /// Get current color based on thresholds.
159    #[must_use]
160    pub fn current_color(&self) -> Color {
161        let pct = self.percent();
162        if pct >= self.critical_threshold {
163            Color::new(1.0, 0.3, 0.3, 1.0) // Red
164        } else if pct >= self.warn_threshold {
165            Color::new(1.0, 0.7, 0.2, 1.0) // Orange
166        } else {
167            self.color
168        }
169    }
170
171    fn render_arc(&self, canvas: &mut dyn Canvas) {
172        let width = self.bounds.width as usize;
173        let height = self.bounds.height as usize;
174
175        if width < 5 || height < 3 {
176            // Fall back to compact mode
177            self.render_compact(canvas);
178            return;
179        }
180
181        let color = self.current_color();
182        let style = TextStyle {
183            color,
184            ..Default::default()
185        };
186        let dim_style = TextStyle {
187            color: Color::new(0.3, 0.3, 0.3, 1.0),
188            ..Default::default()
189        };
190
191        // Arc characters
192        let pct = self.percent() / 100.0;
193        let arc_width = width.saturating_sub(2);
194        let filled = ((pct * arc_width as f64).round() as usize).min(arc_width);
195
196        // Top of arc: ╭───╮
197        let mut top = String::with_capacity(width);
198        top.push('╭');
199        for i in 0..arc_width {
200            if i < filled {
201                top.push('━');
202            } else {
203                top.push('─');
204            }
205        }
206        top.push('╮');
207        canvas.draw_text(&top, Point::new(self.bounds.x, self.bounds.y), &style);
208
209        // Middle: │ value │
210        if height > 2 {
211            let value_text = if self.show_value {
212                let unit = self.unit.as_deref().unwrap_or("");
213                format!("{:.0}{}", self.value, unit)
214            } else {
215                String::new()
216            };
217            let padding = (arc_width.saturating_sub(value_text.len())) / 2;
218            let mut middle = String::with_capacity(width);
219            middle.push('│');
220            for _ in 0..padding {
221                middle.push(' ');
222            }
223            middle.push_str(&value_text);
224            for _ in 0..(arc_width - padding - value_text.len()) {
225                middle.push(' ');
226            }
227            middle.push('│');
228            canvas.draw_text(
229                &middle,
230                Point::new(self.bounds.x, self.bounds.y + 1.0),
231                &dim_style,
232            );
233        }
234
235        // Bottom of arc: ╰───╯
236        if height > 1 {
237            let mut bottom = String::with_capacity(width);
238            bottom.push('╰');
239            for _ in 0..arc_width {
240                bottom.push('─');
241            }
242            bottom.push('╯');
243            let y = if height > 2 {
244                self.bounds.y + 2.0
245            } else {
246                self.bounds.y + 1.0
247            };
248            canvas.draw_text(&bottom, Point::new(self.bounds.x, y), &dim_style);
249        }
250
251        // Label below
252        if let Some(ref label) = self.label {
253            let label_y = self.bounds.y + height.min(3) as f32;
254            if label_y < self.bounds.y + self.bounds.height {
255                let label_style = TextStyle {
256                    color: Color::new(0.6, 0.6, 0.6, 1.0),
257                    ..Default::default()
258                };
259                canvas.draw_text(label, Point::new(self.bounds.x, label_y), &label_style);
260            }
261        }
262    }
263
264    fn render_vertical(&self, canvas: &mut dyn Canvas) {
265        let height = (self.bounds.height as usize).saturating_sub(1);
266        if height == 0 {
267            return;
268        }
269
270        let color = self.current_color();
271        let style = TextStyle {
272            color,
273            ..Default::default()
274        };
275        let dim_style = TextStyle {
276            color: Color::new(0.3, 0.3, 0.3, 1.0),
277            ..Default::default()
278        };
279
280        let pct = self.percent() / 100.0;
281        let filled = ((pct * height as f64).round() as usize).min(height);
282
283        // Draw vertical bar from bottom to top
284        for i in 0..height {
285            let y = self.bounds.y + (height - 1 - i) as f32;
286            if i < filled {
287                canvas.draw_text("█", Point::new(self.bounds.x, y), &style);
288            } else {
289                canvas.draw_text("░", Point::new(self.bounds.x, y), &dim_style);
290            }
291        }
292
293        // Value at bottom
294        if self.show_value {
295            let unit = self.unit.as_deref().unwrap_or("");
296            let value_text = format!("{:.0}{}", self.value, unit);
297            canvas.draw_text(
298                &value_text,
299                Point::new(self.bounds.x + 2.0, self.bounds.y + height as f32),
300                &style,
301            );
302        }
303    }
304
305    fn render_compact(&self, canvas: &mut dyn Canvas) {
306        let color = self.current_color();
307        let style = TextStyle {
308            color,
309            ..Default::default()
310        };
311
312        let unit = self.unit.as_deref().unwrap_or("");
313        let label = self.label.as_deref().unwrap_or("");
314        let text = if label.is_empty() {
315            format!("{:.0}{}", self.value, unit)
316        } else {
317            format!("{}: {:.0}{}", label, self.value, unit)
318        };
319
320        canvas.draw_text(&text, Point::new(self.bounds.x, self.bounds.y), &style);
321    }
322
323    /// Render quarter arc (90° - bottom-right corner style).
324    fn render_quarter(&self, canvas: &mut dyn Canvas) {
325        let width = self.bounds.width as usize;
326        let height = self.bounds.height as usize;
327
328        if width < 3 || height < 2 {
329            self.render_compact(canvas);
330            return;
331        }
332
333        let color = self.current_color();
334        let style = TextStyle {
335            color,
336            ..Default::default()
337        };
338        let dim_style = TextStyle {
339            color: Color::new(0.3, 0.3, 0.3, 1.0),
340            ..Default::default()
341        };
342
343        let pct = self.percent() / 100.0;
344        let arc_width = width.saturating_sub(1);
345        let filled = ((pct * arc_width as f64).round() as usize).min(arc_width);
346
347        // Quarter arc: ───╮
348        //                 │
349        let mut top = String::with_capacity(width);
350        for i in 0..arc_width {
351            if i < filled {
352                top.push('━');
353            } else {
354                top.push('─');
355            }
356        }
357        top.push('╮');
358        canvas.draw_text(&top, Point::new(self.bounds.x, self.bounds.y), &style);
359
360        // Vertical part
361        if height > 1 {
362            let x_pos = self.bounds.x + arc_width as f32;
363            canvas.draw_text("│", Point::new(x_pos, self.bounds.y + 1.0), &dim_style);
364        }
365
366        // Value display
367        if self.show_value && height > 1 {
368            let unit = self.unit.as_deref().unwrap_or("");
369            let value_text = format!("{:.0}{}", self.value, unit);
370            canvas.draw_text(
371                &value_text,
372                Point::new(self.bounds.x, self.bounds.y + 1.0),
373                &style,
374            );
375        }
376
377        // Label
378        if let Some(ref label) = self.label {
379            if height > 2 {
380                let label_style = TextStyle {
381                    color: Color::new(0.6, 0.6, 0.6, 1.0),
382                    ..Default::default()
383                };
384                canvas.draw_text(
385                    label,
386                    Point::new(self.bounds.x, self.bounds.y + 2.0),
387                    &label_style,
388                );
389            }
390        }
391    }
392
393    /// Render half arc (180° - bottom semicircle).
394    fn render_half(&self, canvas: &mut dyn Canvas) {
395        let width = self.bounds.width as usize;
396        let height = self.bounds.height as usize;
397
398        if width < 4 || height < 2 {
399            self.render_compact(canvas);
400            return;
401        }
402
403        let color = self.current_color();
404        let style = TextStyle {
405            color,
406            ..Default::default()
407        };
408
409        let pct = self.percent() / 100.0;
410        let arc_width = width.saturating_sub(2);
411        let filled = ((pct * arc_width as f64).round() as usize).min(arc_width);
412
413        // Half arc (bottom semicircle): ╰───╯
414        let mut arc = String::with_capacity(width);
415        arc.push('╰');
416        for i in 0..arc_width {
417            if i < filled {
418                arc.push('━');
419            } else {
420                arc.push('─');
421            }
422        }
423        arc.push('╯');
424        canvas.draw_text(&arc, Point::new(self.bounds.x, self.bounds.y), &style);
425
426        // Value above arc
427        if self.show_value && height > 1 {
428            let unit = self.unit.as_deref().unwrap_or("");
429            let value_text = format!("{:.0}{}", self.value, unit);
430            let padding = (arc_width.saturating_sub(value_text.len())) / 2;
431            canvas.draw_text(
432                &value_text,
433                Point::new(self.bounds.x + 1.0 + padding as f32, self.bounds.y + 1.0),
434                &style,
435            );
436        }
437
438        // Label
439        if let Some(ref label) = self.label {
440            if height > 2 {
441                let label_style = TextStyle {
442                    color: Color::new(0.6, 0.6, 0.6, 1.0),
443                    ..Default::default()
444                };
445                canvas.draw_text(
446                    label,
447                    Point::new(self.bounds.x, self.bounds.y + 2.0),
448                    &label_style,
449                );
450            }
451        }
452    }
453
454    /// Render three-quarter arc (270° - U-shape open at top-left).
455    fn render_three_quarter(&self, canvas: &mut dyn Canvas) {
456        let width = self.bounds.width as usize;
457        let height = self.bounds.height as usize;
458
459        if width < 4 || height < 3 {
460            self.render_compact(canvas);
461            return;
462        }
463
464        let color = self.current_color();
465        let style = TextStyle {
466            color,
467            ..Default::default()
468        };
469        let dim_style = TextStyle {
470            color: Color::new(0.3, 0.3, 0.3, 1.0),
471            ..Default::default()
472        };
473
474        let pct = self.percent() / 100.0;
475        let arc_width = width.saturating_sub(1);
476
477        // Three-quarter arc: open at top-left
478        // ────╮
479        //     │
480        // ────╯
481
482        // Top section (horizontal part with right corner)
483        let top_filled = ((pct * arc_width as f64).round() as usize).min(arc_width);
484        let mut top = String::with_capacity(width);
485        for i in 0..arc_width {
486            if i < top_filled {
487                top.push('━');
488            } else {
489                top.push('─');
490            }
491        }
492        top.push('╮');
493        canvas.draw_text(&top, Point::new(self.bounds.x, self.bounds.y), &style);
494
495        // Middle section (vertical bar on right)
496        if height > 2 {
497            let x_pos = self.bounds.x + arc_width as f32;
498            canvas.draw_text("│", Point::new(x_pos, self.bounds.y + 1.0), &dim_style);
499
500            // Value in middle
501            if self.show_value {
502                let unit = self.unit.as_deref().unwrap_or("");
503                let value_text = format!("{:.0}{}", self.value, unit);
504                canvas.draw_text(
505                    &value_text,
506                    Point::new(self.bounds.x, self.bounds.y + 1.0),
507                    &style,
508                );
509            }
510        }
511
512        // Bottom section (horizontal with right corner)
513        let bottom_y = if height > 2 {
514            self.bounds.y + 2.0
515        } else {
516            self.bounds.y + 1.0
517        };
518        let mut bottom = String::with_capacity(width);
519        for _ in 0..arc_width {
520            bottom.push('─');
521        }
522        bottom.push('╯');
523        canvas.draw_text(&bottom, Point::new(self.bounds.x, bottom_y), &dim_style);
524
525        // Label
526        if let Some(ref label) = self.label {
527            if height > 3 {
528                let label_style = TextStyle {
529                    color: Color::new(0.6, 0.6, 0.6, 1.0),
530                    ..Default::default()
531                };
532                canvas.draw_text(
533                    label,
534                    Point::new(self.bounds.x, self.bounds.y + 3.0),
535                    &label_style,
536                );
537            }
538        }
539    }
540}
541
542impl Brick for Gauge {
543    fn brick_name(&self) -> &'static str {
544        "gauge"
545    }
546
547    fn assertions(&self) -> &[BrickAssertion] {
548        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
549        ASSERTIONS
550    }
551
552    fn budget(&self) -> BrickBudget {
553        BrickBudget::uniform(16)
554    }
555
556    fn verify(&self) -> BrickVerification {
557        let mut passed = Vec::new();
558        let mut failed = Vec::new();
559
560        if self.value >= 0.0 && self.value <= self.max {
561            passed.push(BrickAssertion::max_latency_ms(16));
562        } else {
563            failed.push((
564                BrickAssertion::max_latency_ms(16),
565                format!("Value {} outside range [0, {}]", self.value, self.max),
566            ));
567        }
568
569        BrickVerification {
570            passed,
571            failed,
572            verification_time: Duration::from_micros(5),
573        }
574    }
575
576    fn to_html(&self) -> String {
577        String::new()
578    }
579
580    fn to_css(&self) -> String {
581        String::new()
582    }
583}
584
585impl Widget for Gauge {
586    fn type_id(&self) -> TypeId {
587        TypeId::of::<Self>()
588    }
589
590    fn measure(&self, constraints: Constraints) -> Size {
591        match self.mode {
592            GaugeMode::Arc | GaugeMode::ThreeQuarter => {
593                let width = 10.0_f32.min(constraints.max_width);
594                let height = 4.0_f32.min(constraints.max_height);
595                constraints.constrain(Size::new(width, height))
596            }
597            GaugeMode::Quarter => {
598                let width = 8.0_f32.min(constraints.max_width);
599                let height = 3.0_f32.min(constraints.max_height);
600                constraints.constrain(Size::new(width, height))
601            }
602            GaugeMode::Half => {
603                let width = 10.0_f32.min(constraints.max_width);
604                let height = 3.0_f32.min(constraints.max_height);
605                constraints.constrain(Size::new(width, height))
606            }
607            GaugeMode::Vertical => {
608                let height = 8.0_f32.min(constraints.max_height);
609                constraints.constrain(Size::new(6.0, height))
610            }
611            GaugeMode::Compact => constraints.constrain(Size::new(12.0, 1.0)),
612        }
613    }
614
615    fn layout(&mut self, bounds: Rect) -> LayoutResult {
616        self.bounds = bounds;
617        LayoutResult {
618            size: Size::new(bounds.width, bounds.height),
619        }
620    }
621
622    fn paint(&self, canvas: &mut dyn Canvas) {
623        match self.mode {
624            GaugeMode::Arc => self.render_arc(canvas),
625            GaugeMode::Quarter => self.render_quarter(canvas),
626            GaugeMode::Half => self.render_half(canvas),
627            GaugeMode::ThreeQuarter => self.render_three_quarter(canvas),
628            GaugeMode::Vertical => self.render_vertical(canvas),
629            GaugeMode::Compact => self.render_compact(canvas),
630        }
631    }
632
633    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
634        None
635    }
636
637    fn children(&self) -> &[Box<dyn Widget>] {
638        &[]
639    }
640
641    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
642        &mut []
643    }
644}
645
646#[cfg(test)]
647mod tests {
648    use super::*;
649
650    struct MockCanvas {
651        texts: Vec<(String, Point)>,
652    }
653
654    impl MockCanvas {
655        fn new() -> Self {
656            Self { texts: vec![] }
657        }
658    }
659
660    impl Canvas for MockCanvas {
661        fn fill_rect(&mut self, _rect: Rect, _color: Color) {}
662        fn stroke_rect(&mut self, _rect: Rect, _color: Color, _width: f32) {}
663        fn draw_text(&mut self, text: &str, position: Point, _style: &TextStyle) {
664            self.texts.push((text.to_string(), position));
665        }
666        fn draw_line(&mut self, _from: Point, _to: Point, _color: Color, _width: f32) {}
667        fn fill_circle(&mut self, _center: Point, _radius: f32, _color: Color) {}
668        fn stroke_circle(&mut self, _center: Point, _radius: f32, _color: Color, _width: f32) {}
669        fn fill_arc(&mut self, _c: Point, _r: f32, _s: f32, _e: f32, _color: Color) {}
670        fn draw_path(&mut self, _points: &[Point], _color: Color, _width: f32) {}
671        fn fill_polygon(&mut self, _points: &[Point], _color: Color) {}
672        fn push_clip(&mut self, _rect: Rect) {}
673        fn pop_clip(&mut self) {}
674        fn push_transform(&mut self, _transform: presentar_core::Transform2D) {}
675        fn pop_transform(&mut self) {}
676    }
677
678    #[test]
679    fn test_gauge_creation() {
680        let gauge = Gauge::new(50.0, 100.0);
681        assert_eq!(gauge.value(), 50.0);
682        assert_eq!(gauge.max, 100.0);
683    }
684
685    #[test]
686    fn test_gauge_percentage_constructor() {
687        let gauge = Gauge::percentage(75.0);
688        assert_eq!(gauge.percent(), 75.0);
689        assert_eq!(gauge.unit, Some("%".to_string()));
690    }
691
692    #[test]
693    fn test_gauge_temperature() {
694        let gauge = Gauge::temperature(65.0, 100.0);
695        assert_eq!(gauge.value(), 65.0);
696        assert_eq!(gauge.unit, Some("°C".to_string()));
697    }
698
699    #[test]
700    fn test_gauge_assertions() {
701        let gauge = Gauge::default();
702        assert!(!gauge.assertions().is_empty());
703    }
704
705    #[test]
706    fn test_gauge_verify() {
707        let gauge = Gauge::new(50.0, 100.0);
708        assert!(gauge.verify().is_valid());
709    }
710
711    #[test]
712    fn test_gauge_with_label() {
713        let gauge = Gauge::new(50.0, 100.0).with_label("CPU");
714        assert_eq!(gauge.label, Some("CPU".to_string()));
715    }
716
717    #[test]
718    fn test_gauge_with_mode() {
719        let gauge = Gauge::default().with_mode(GaugeMode::Vertical);
720        assert_eq!(gauge.mode, GaugeMode::Vertical);
721    }
722
723    #[test]
724    fn test_gauge_with_color() {
725        let gauge = Gauge::default().with_color(Color::RED);
726        assert_eq!(gauge.color, Color::RED);
727    }
728
729    #[test]
730    fn test_gauge_with_thresholds() {
731        let gauge = Gauge::default().with_thresholds(50.0, 80.0);
732        assert_eq!(gauge.warn_threshold, 50.0);
733        assert_eq!(gauge.critical_threshold, 80.0);
734    }
735
736    #[test]
737    fn test_gauge_with_value_display() {
738        let gauge = Gauge::default().with_value_display(false);
739        assert!(!gauge.show_value);
740    }
741
742    #[test]
743    fn test_gauge_with_unit() {
744        let gauge = Gauge::default().with_unit("MB");
745        assert_eq!(gauge.unit, Some("MB".to_string()));
746    }
747
748    #[test]
749    fn test_gauge_set_value() {
750        let mut gauge = Gauge::new(50.0, 100.0);
751        gauge.set_value(75.0);
752        assert_eq!(gauge.value(), 75.0);
753    }
754
755    #[test]
756    fn test_gauge_set_value_clamped() {
757        let mut gauge = Gauge::new(50.0, 100.0);
758        gauge.set_value(150.0);
759        assert_eq!(gauge.value(), 100.0);
760
761        gauge.set_value(-10.0);
762        assert_eq!(gauge.value(), 0.0);
763    }
764
765    #[test]
766    fn test_gauge_current_color_normal() {
767        let gauge = Gauge::new(50.0, 100.0);
768        let color = gauge.current_color();
769        assert_eq!(color, gauge.color);
770    }
771
772    #[test]
773    fn test_gauge_current_color_warning() {
774        let gauge = Gauge::new(75.0, 100.0);
775        let color = gauge.current_color();
776        assert!(color.r > 0.9);
777    }
778
779    #[test]
780    fn test_gauge_current_color_critical() {
781        let gauge = Gauge::new(95.0, 100.0);
782        let color = gauge.current_color();
783        assert!(color.r > 0.9);
784        assert!(color.g < 0.5);
785    }
786
787    #[test]
788    fn test_gauge_paint_arc() {
789        let mut gauge = Gauge::default().with_mode(GaugeMode::Arc);
790        gauge.bounds = Rect::new(0.0, 0.0, 10.0, 4.0);
791        let mut canvas = MockCanvas::new();
792        gauge.paint(&mut canvas);
793        assert!(!canvas.texts.is_empty());
794    }
795
796    #[test]
797    fn test_gauge_paint_vertical() {
798        let mut gauge = Gauge::percentage(50.0).with_mode(GaugeMode::Vertical);
799        gauge.bounds = Rect::new(0.0, 0.0, 6.0, 8.0);
800        let mut canvas = MockCanvas::new();
801        gauge.paint(&mut canvas);
802        assert!(!canvas.texts.is_empty());
803    }
804
805    #[test]
806    fn test_gauge_paint_compact() {
807        let mut gauge = Gauge::percentage(50.0).with_mode(GaugeMode::Compact);
808        gauge.bounds = Rect::new(0.0, 0.0, 20.0, 1.0);
809        let mut canvas = MockCanvas::new();
810        gauge.paint(&mut canvas);
811        assert!(!canvas.texts.is_empty());
812    }
813
814    #[test]
815    fn test_gauge_paint_compact_with_label() {
816        let mut gauge = Gauge::percentage(50.0)
817            .with_mode(GaugeMode::Compact)
818            .with_label("CPU");
819        gauge.bounds = Rect::new(0.0, 0.0, 20.0, 1.0);
820        let mut canvas = MockCanvas::new();
821        gauge.paint(&mut canvas);
822        assert!(canvas.texts.iter().any(|(t, _)| t.contains("CPU")));
823    }
824
825    #[test]
826    fn test_gauge_measure_arc() {
827        let gauge = Gauge::default().with_mode(GaugeMode::Arc);
828        let size = gauge.measure(Constraints::loose(Size::new(100.0, 100.0)));
829        assert!(size.width > 0.0);
830        assert!(size.height > 0.0);
831    }
832
833    #[test]
834    fn test_gauge_measure_vertical() {
835        let gauge = Gauge::default().with_mode(GaugeMode::Vertical);
836        let size = gauge.measure(Constraints::loose(Size::new(100.0, 100.0)));
837        assert!(size.height > size.width);
838    }
839
840    #[test]
841    fn test_gauge_measure_compact() {
842        let gauge = Gauge::default().with_mode(GaugeMode::Compact);
843        let size = gauge.measure(Constraints::loose(Size::new(100.0, 100.0)));
844        assert_eq!(size.height, 1.0);
845    }
846
847    #[test]
848    fn test_gauge_layout() {
849        let mut gauge = Gauge::default();
850        let bounds = Rect::new(5.0, 10.0, 20.0, 10.0);
851        let result = gauge.layout(bounds);
852        assert_eq!(result.size.width, 20.0);
853        assert_eq!(gauge.bounds, bounds);
854    }
855
856    #[test]
857    fn test_gauge_brick_name() {
858        let gauge = Gauge::default();
859        assert_eq!(gauge.brick_name(), "gauge");
860    }
861
862    #[test]
863    fn test_gauge_type_id() {
864        let gauge = Gauge::default();
865        assert_eq!(Widget::type_id(&gauge), TypeId::of::<Gauge>());
866    }
867
868    #[test]
869    fn test_gauge_children() {
870        let gauge = Gauge::default();
871        assert!(gauge.children().is_empty());
872    }
873
874    #[test]
875    fn test_gauge_event() {
876        let mut gauge = Gauge::default();
877        let event = Event::key_down(presentar_core::Key::Enter);
878        assert!(gauge.event(&event).is_none());
879    }
880
881    #[test]
882    fn test_gauge_default() {
883        let gauge = Gauge::default();
884        assert_eq!(gauge.value(), 0.0);
885        assert_eq!(gauge.max, 100.0);
886    }
887
888    #[test]
889    fn test_gauge_percentage_zero_max() {
890        let gauge = Gauge::new(50.0, 0.0);
891        assert_eq!(gauge.percent(), 0.0);
892    }
893
894    #[test]
895    fn test_gauge_arc_fallback_to_compact() {
896        // Arc mode with small bounds should fall back to compact
897        let mut gauge = Gauge::percentage(50.0).with_mode(GaugeMode::Arc);
898        gauge.bounds = Rect::new(0.0, 0.0, 3.0, 2.0); // Too small for arc
899        let mut canvas = MockCanvas::new();
900        gauge.paint(&mut canvas);
901        // Should have rendered compact mode instead
902        assert!(!canvas.texts.is_empty());
903    }
904
905    #[test]
906    fn test_gauge_arc_no_value_display() {
907        let mut gauge = Gauge::percentage(50.0)
908            .with_mode(GaugeMode::Arc)
909            .with_value_display(false);
910        gauge.bounds = Rect::new(0.0, 0.0, 10.0, 4.0);
911        let mut canvas = MockCanvas::new();
912        gauge.paint(&mut canvas);
913        // Should render without value text
914        assert!(!canvas.texts.is_empty());
915    }
916
917    #[test]
918    fn test_gauge_arc_with_label() {
919        let mut gauge = Gauge::percentage(50.0)
920            .with_mode(GaugeMode::Arc)
921            .with_label("CPU");
922        gauge.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
923        let mut canvas = MockCanvas::new();
924        gauge.paint(&mut canvas);
925        // Should render with label below arc
926        assert!(canvas.texts.iter().any(|(t, _)| t.contains("CPU")));
927    }
928
929    #[test]
930    fn test_gauge_arc_height_two() {
931        // Arc with only 2 lines height - falls back to compact (height < 3)
932        let mut gauge = Gauge::percentage(50.0).with_mode(GaugeMode::Arc);
933        gauge.bounds = Rect::new(0.0, 0.0, 10.0, 2.0);
934        let mut canvas = MockCanvas::new();
935        gauge.paint(&mut canvas);
936        // Falls back to compact mode
937        assert!(!canvas.texts.is_empty());
938    }
939
940    #[test]
941    fn test_gauge_arc_height_three_no_middle() {
942        // Arc with exactly 3 lines height - draws top and bottom
943        let mut gauge = Gauge::percentage(50.0).with_mode(GaugeMode::Arc);
944        gauge.bounds = Rect::new(0.0, 0.0, 10.0, 3.0);
945        let mut canvas = MockCanvas::new();
946        gauge.paint(&mut canvas);
947        // Should draw top and bottom (height > 2 is false, so no middle)
948        assert!(canvas.texts.len() >= 2);
949    }
950
951    #[test]
952    fn test_gauge_arc_height_four() {
953        // Arc with 4 lines - draws all sections
954        let mut gauge = Gauge::percentage(50.0).with_mode(GaugeMode::Arc);
955        gauge.bounds = Rect::new(0.0, 0.0, 10.0, 4.0);
956        let mut canvas = MockCanvas::new();
957        gauge.paint(&mut canvas);
958        // Should draw top, middle, and bottom (height > 2)
959        assert!(canvas.texts.len() >= 3);
960    }
961
962    #[test]
963    fn test_gauge_vertical_zero_height() {
964        let mut gauge = Gauge::percentage(50.0).with_mode(GaugeMode::Vertical);
965        gauge.bounds = Rect::new(0.0, 0.0, 6.0, 1.0); // height - 1 = 0
966        let mut canvas = MockCanvas::new();
967        gauge.paint(&mut canvas);
968        // Early return when height is 0
969        assert!(canvas.texts.is_empty());
970    }
971
972    #[test]
973    fn test_gauge_vertical_no_value_display() {
974        let mut gauge = Gauge::percentage(50.0)
975            .with_mode(GaugeMode::Vertical)
976            .with_value_display(false);
977        gauge.bounds = Rect::new(0.0, 0.0, 6.0, 8.0);
978        let mut canvas = MockCanvas::new();
979        gauge.paint(&mut canvas);
980        // Should not have value text
981        assert!(!canvas.texts.is_empty());
982    }
983
984    #[test]
985    fn test_gauge_verify_invalid() {
986        // Create a gauge and manually set invalid value
987        let mut gauge = Gauge::new(50.0, 100.0);
988        gauge.value = -10.0; // Invalid: outside range
989        let result = gauge.verify();
990        assert!(!result.is_valid());
991        assert!(!result.failed.is_empty());
992    }
993
994    #[test]
995    fn test_gauge_verify_above_max() {
996        let mut gauge = Gauge::new(50.0, 100.0);
997        gauge.value = 150.0; // Invalid: above max
998        let result = gauge.verify();
999        assert!(!result.is_valid());
1000    }
1001
1002    #[test]
1003    fn test_gauge_children_mut() {
1004        let mut gauge = Gauge::default();
1005        assert!(gauge.children_mut().is_empty());
1006    }
1007
1008    #[test]
1009    fn test_gauge_budget() {
1010        let gauge = Gauge::default();
1011        let budget = gauge.budget();
1012        assert_eq!(budget.total_ms, 16);
1013    }
1014
1015    #[test]
1016    fn test_gauge_to_html() {
1017        let gauge = Gauge::default();
1018        assert!(gauge.to_html().is_empty());
1019    }
1020
1021    #[test]
1022    fn test_gauge_to_css() {
1023        let gauge = Gauge::default();
1024        assert!(gauge.to_css().is_empty());
1025    }
1026
1027    #[test]
1028    fn test_gauge_compact_no_label() {
1029        // Compact mode without label
1030        let mut gauge = Gauge::percentage(75.0).with_mode(GaugeMode::Compact);
1031        gauge.bounds = Rect::new(0.0, 0.0, 20.0, 1.0);
1032        let mut canvas = MockCanvas::new();
1033        gauge.paint(&mut canvas);
1034        // Should show value without label prefix
1035        assert!(canvas.texts.iter().any(|(t, _)| t.contains("75")));
1036    }
1037
1038    #[test]
1039    fn test_gauge_quarter_mode() {
1040        let gauge = Gauge::default().with_mode(GaugeMode::Quarter);
1041        assert_eq!(gauge.mode, GaugeMode::Quarter);
1042    }
1043
1044    #[test]
1045    fn test_gauge_half_mode() {
1046        let gauge = Gauge::default().with_mode(GaugeMode::Half);
1047        assert_eq!(gauge.mode, GaugeMode::Half);
1048    }
1049
1050    #[test]
1051    fn test_gauge_three_quarter_mode() {
1052        let gauge = Gauge::default().with_mode(GaugeMode::ThreeQuarter);
1053        assert_eq!(gauge.mode, GaugeMode::ThreeQuarter);
1054    }
1055
1056    #[test]
1057    fn test_gauge_quarter_paint() {
1058        let mut gauge = Gauge::percentage(50.0).with_mode(GaugeMode::Quarter);
1059        gauge.bounds = Rect::new(0.0, 0.0, 8.0, 3.0);
1060        let mut canvas = MockCanvas::new();
1061        gauge.paint(&mut canvas);
1062        assert!(!canvas.texts.is_empty());
1063        // Quarter arc should have ╮ character
1064        assert!(canvas.texts.iter().any(|(t, _)| t.contains('╮')));
1065    }
1066
1067    #[test]
1068    fn test_gauge_half_paint() {
1069        let mut gauge = Gauge::percentage(50.0).with_mode(GaugeMode::Half);
1070        gauge.bounds = Rect::new(0.0, 0.0, 10.0, 3.0);
1071        let mut canvas = MockCanvas::new();
1072        gauge.paint(&mut canvas);
1073        assert!(!canvas.texts.is_empty());
1074        // Half arc should have ╰ and ╯ characters
1075        assert!(canvas.texts.iter().any(|(t, _)| t.contains('╰')));
1076    }
1077
1078    #[test]
1079    fn test_gauge_three_quarter_paint() {
1080        let mut gauge = Gauge::percentage(50.0).with_mode(GaugeMode::ThreeQuarter);
1081        gauge.bounds = Rect::new(0.0, 0.0, 10.0, 4.0);
1082        let mut canvas = MockCanvas::new();
1083        gauge.paint(&mut canvas);
1084        assert!(!canvas.texts.is_empty());
1085        // Three-quarter arc should have ╮ and ╯ characters
1086        assert!(canvas.texts.iter().any(|(t, _)| t.contains('╮')));
1087        assert!(canvas.texts.iter().any(|(t, _)| t.contains('╯')));
1088    }
1089
1090    #[test]
1091    fn test_gauge_quarter_fallback() {
1092        // Quarter mode with too small bounds falls back to compact
1093        let mut gauge = Gauge::percentage(50.0).with_mode(GaugeMode::Quarter);
1094        gauge.bounds = Rect::new(0.0, 0.0, 2.0, 1.0);
1095        let mut canvas = MockCanvas::new();
1096        gauge.paint(&mut canvas);
1097        assert!(!canvas.texts.is_empty());
1098    }
1099
1100    #[test]
1101    fn test_gauge_half_fallback() {
1102        // Half mode with too small bounds falls back to compact
1103        let mut gauge = Gauge::percentage(50.0).with_mode(GaugeMode::Half);
1104        gauge.bounds = Rect::new(0.0, 0.0, 2.0, 1.0);
1105        let mut canvas = MockCanvas::new();
1106        gauge.paint(&mut canvas);
1107        assert!(!canvas.texts.is_empty());
1108    }
1109
1110    #[test]
1111    fn test_gauge_three_quarter_fallback() {
1112        // Three-quarter mode with too small bounds falls back to compact
1113        let mut gauge = Gauge::percentage(50.0).with_mode(GaugeMode::ThreeQuarter);
1114        gauge.bounds = Rect::new(0.0, 0.0, 2.0, 2.0);
1115        let mut canvas = MockCanvas::new();
1116        gauge.paint(&mut canvas);
1117        assert!(!canvas.texts.is_empty());
1118    }
1119
1120    #[test]
1121    fn test_gauge_measure_quarter() {
1122        let gauge = Gauge::default().with_mode(GaugeMode::Quarter);
1123        let size = gauge.measure(Constraints::loose(Size::new(100.0, 100.0)));
1124        assert_eq!(size.width, 8.0);
1125        assert_eq!(size.height, 3.0);
1126    }
1127
1128    #[test]
1129    fn test_gauge_measure_half() {
1130        let gauge = Gauge::default().with_mode(GaugeMode::Half);
1131        let size = gauge.measure(Constraints::loose(Size::new(100.0, 100.0)));
1132        assert_eq!(size.width, 10.0);
1133        assert_eq!(size.height, 3.0);
1134    }
1135
1136    #[test]
1137    fn test_gauge_measure_three_quarter() {
1138        let gauge = Gauge::default().with_mode(GaugeMode::ThreeQuarter);
1139        let size = gauge.measure(Constraints::loose(Size::new(100.0, 100.0)));
1140        assert_eq!(size.width, 10.0);
1141        assert_eq!(size.height, 4.0);
1142    }
1143
1144    #[test]
1145    fn test_gauge_quarter_with_label() {
1146        let mut gauge = Gauge::percentage(50.0)
1147            .with_mode(GaugeMode::Quarter)
1148            .with_label("Temp");
1149        gauge.bounds = Rect::new(0.0, 0.0, 8.0, 4.0);
1150        let mut canvas = MockCanvas::new();
1151        gauge.paint(&mut canvas);
1152        assert!(canvas.texts.iter().any(|(t, _)| t.contains("Temp")));
1153    }
1154
1155    #[test]
1156    fn test_gauge_half_with_label() {
1157        let mut gauge = Gauge::percentage(50.0)
1158            .with_mode(GaugeMode::Half)
1159            .with_label("Load");
1160        gauge.bounds = Rect::new(0.0, 0.0, 10.0, 4.0);
1161        let mut canvas = MockCanvas::new();
1162        gauge.paint(&mut canvas);
1163        assert!(canvas.texts.iter().any(|(t, _)| t.contains("Load")));
1164    }
1165
1166    #[test]
1167    fn test_gauge_three_quarter_with_label() {
1168        let mut gauge = Gauge::percentage(50.0)
1169            .with_mode(GaugeMode::ThreeQuarter)
1170            .with_label("CPU");
1171        gauge.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
1172        let mut canvas = MockCanvas::new();
1173        gauge.paint(&mut canvas);
1174        assert!(canvas.texts.iter().any(|(t, _)| t.contains("CPU")));
1175    }
1176}