Skip to main content

presentar_terminal/widgets/
segmented_meter.rs

1//! Segmented meter widget for showing multiple values in a single bar.
2//!
3//! Useful for displaying memory breakdown (used/cached/free), disk usage, etc.
4
5use presentar_core::{
6    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
7    LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
8};
9use std::any::Any;
10use std::time::Duration;
11
12/// A single segment in a segmented meter.
13#[derive(Debug, Clone)]
14pub struct Segment {
15    /// Value of this segment (will be normalized against total).
16    pub value: f64,
17    /// Color of this segment.
18    pub color: Color,
19    /// Optional label.
20    pub label: Option<String>,
21}
22
23impl Segment {
24    /// Create a new segment.
25    #[must_use]
26    pub fn new(value: f64, color: Color) -> Self {
27        Self {
28            value,
29            color,
30            label: None,
31        }
32    }
33
34    /// Add a label to the segment.
35    #[must_use]
36    pub fn with_label(mut self, label: impl Into<String>) -> Self {
37        self.label = Some(label.into());
38        self
39    }
40}
41
42/// A segmented meter showing multiple values in a single horizontal bar.
43#[derive(Debug, Clone)]
44pub struct SegmentedMeter {
45    /// Segments to display.
46    segments: Vec<Segment>,
47    /// Maximum value (segments are normalized to this).
48    max: f64,
49    /// Background color for unfilled portion.
50    background: Color,
51    /// Whether to show percentage text.
52    show_percentages: bool,
53    /// Layout bounds.
54    bounds: Rect,
55}
56
57impl SegmentedMeter {
58    /// Create a new segmented meter with the given segments and max value.
59    #[must_use]
60    pub fn new(segments: Vec<Segment>, max: f64) -> Self {
61        Self {
62            segments,
63            max,
64            background: Color::rgb(0.2, 0.2, 0.2),
65            show_percentages: false,
66            bounds: Rect::new(0.0, 0.0, 0.0, 0.0),
67        }
68    }
69
70    /// Create a memory-style meter with used, cached, and free segments.
71    #[must_use]
72    pub fn memory(used: f64, cached: f64, total: f64) -> Self {
73        let free = (total - used - cached).max(0.0);
74        Self::new(
75            vec![
76                Segment::new(used, Color::rgb(1.0, 0.7, 0.2)).with_label("Used"),
77                Segment::new(cached, Color::rgb(0.2, 0.6, 1.0)).with_label("Cached"),
78                Segment::new(free, Color::rgb(0.3, 0.3, 0.3)).with_label("Free"),
79            ],
80            total,
81        )
82    }
83
84    /// Set the background color.
85    #[must_use]
86    pub fn with_background(mut self, color: Color) -> Self {
87        self.background = color;
88        self
89    }
90
91    /// Set whether to show percentage text.
92    #[must_use]
93    pub fn with_percentages(mut self, show: bool) -> Self {
94        self.show_percentages = show;
95        self
96    }
97
98    /// Update segments.
99    pub fn set_segments(&mut self, segments: Vec<Segment>) {
100        self.segments = segments;
101    }
102
103    /// Update max value.
104    pub fn set_max(&mut self, max: f64) {
105        self.max = max;
106    }
107
108    fn render(&self, canvas: &mut dyn Canvas) {
109        let width = self.bounds.width as usize;
110        let height = self.bounds.height as usize;
111        if width == 0 || height == 0 {
112            return;
113        }
114
115        // Calculate total and normalize
116        let total: f64 = self.segments.iter().map(|s| s.value).sum();
117        let scale = if self.max > 0.0 {
118            width as f64 / self.max
119        } else if total > 0.0 {
120            width as f64 / total
121        } else {
122            0.0
123        };
124
125        let mut x_offset = 0usize;
126
127        // Draw each segment
128        for segment in &self.segments {
129            let segment_width = (segment.value * scale).round() as usize;
130            if segment_width == 0 {
131                continue;
132            }
133
134            let style = TextStyle {
135                color: segment.color,
136                ..Default::default()
137            };
138
139            // Draw filled portion
140            for row in 0..height {
141                for col in 0..segment_width {
142                    let x = x_offset + col;
143                    if x >= width {
144                        break;
145                    }
146                    canvas.draw_text(
147                        "█",
148                        Point::new(self.bounds.x + x as f32, self.bounds.y + row as f32),
149                        &style,
150                    );
151                }
152            }
153
154            x_offset += segment_width;
155        }
156
157        // Fill remaining with background
158        if x_offset < width {
159            let bg_style = TextStyle {
160                color: self.background,
161                ..Default::default()
162            };
163
164            for row in 0..height {
165                for col in x_offset..width {
166                    canvas.draw_text(
167                        "░",
168                        Point::new(self.bounds.x + col as f32, self.bounds.y + row as f32),
169                        &bg_style,
170                    );
171                }
172            }
173        }
174    }
175}
176
177impl Brick for SegmentedMeter {
178    fn brick_name(&self) -> &'static str {
179        "segmented_meter"
180    }
181
182    fn assertions(&self) -> &[BrickAssertion] {
183        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(8)];
184        ASSERTIONS
185    }
186
187    fn budget(&self) -> BrickBudget {
188        BrickBudget::uniform(8)
189    }
190
191    fn verify(&self) -> BrickVerification {
192        BrickVerification {
193            passed: vec![BrickAssertion::max_latency_ms(8)],
194            failed: vec![],
195            verification_time: Duration::from_micros(5),
196        }
197    }
198
199    fn to_html(&self) -> String {
200        String::new()
201    }
202
203    fn to_css(&self) -> String {
204        String::new()
205    }
206}
207
208impl Widget for SegmentedMeter {
209    fn type_id(&self) -> TypeId {
210        TypeId::of::<Self>()
211    }
212
213    fn measure(&self, constraints: Constraints) -> Size {
214        let width = constraints.max_width.max(10.0);
215        let height = constraints.max_height.clamp(1.0, 2.0);
216        constraints.constrain(Size::new(width, height))
217    }
218
219    fn layout(&mut self, bounds: Rect) -> LayoutResult {
220        self.bounds = bounds;
221        LayoutResult {
222            size: Size::new(bounds.width, bounds.height),
223        }
224    }
225
226    fn paint(&self, canvas: &mut dyn Canvas) {
227        self.render(canvas);
228    }
229
230    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
231        None
232    }
233
234    fn children(&self) -> &[Box<dyn Widget>] {
235        &[]
236    }
237
238    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
239        &mut []
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    struct MockCanvas {
248        texts: Vec<(String, Point, Color)>,
249    }
250
251    impl MockCanvas {
252        fn new() -> Self {
253            Self { texts: vec![] }
254        }
255    }
256
257    impl Canvas for MockCanvas {
258        fn fill_rect(&mut self, _rect: Rect, _color: Color) {}
259        fn stroke_rect(&mut self, _rect: Rect, _color: Color, _width: f32) {}
260        fn draw_text(&mut self, text: &str, position: Point, style: &TextStyle) {
261            self.texts.push((text.to_string(), position, style.color));
262        }
263        fn draw_line(&mut self, _from: Point, _to: Point, _color: Color, _width: f32) {}
264        fn fill_circle(&mut self, _center: Point, _radius: f32, _color: Color) {}
265        fn stroke_circle(&mut self, _center: Point, _radius: f32, _color: Color, _width: f32) {}
266        fn fill_arc(
267            &mut self,
268            _center: Point,
269            _radius: f32,
270            _start: f32,
271            _end: f32,
272            _color: Color,
273        ) {
274        }
275        fn draw_path(&mut self, _points: &[Point], _color: Color, _width: f32) {}
276        fn fill_polygon(&mut self, _points: &[Point], _color: Color) {}
277        fn push_clip(&mut self, _rect: Rect) {}
278        fn pop_clip(&mut self) {}
279        fn push_transform(&mut self, _transform: presentar_core::Transform2D) {}
280        fn pop_transform(&mut self) {}
281    }
282
283    #[test]
284    fn test_segment_creation() {
285        let segment = Segment::new(50.0, Color::RED);
286        assert_eq!(segment.value, 50.0);
287        assert_eq!(segment.color, Color::RED);
288        assert!(segment.label.is_none());
289    }
290
291    #[test]
292    fn test_segment_with_label() {
293        let segment = Segment::new(50.0, Color::RED).with_label("Used");
294        assert_eq!(segment.label, Some("Used".to_string()));
295    }
296
297    #[test]
298    fn test_segmented_meter_creation() {
299        let meter = SegmentedMeter::new(
300            vec![
301                Segment::new(30.0, Color::RED),
302                Segment::new(20.0, Color::BLUE),
303            ],
304            100.0,
305        );
306        assert_eq!(meter.segments.len(), 2);
307        assert_eq!(meter.max, 100.0);
308    }
309
310    #[test]
311    fn test_segmented_meter_memory() {
312        let meter = SegmentedMeter::memory(60.0, 20.0, 100.0);
313        assert_eq!(meter.segments.len(), 3);
314        assert_eq!(meter.max, 100.0);
315    }
316
317    #[test]
318    fn test_segmented_meter_with_background() {
319        let meter = SegmentedMeter::new(vec![], 100.0).with_background(Color::BLACK);
320        assert_eq!(meter.background, Color::BLACK);
321    }
322
323    #[test]
324    fn test_segmented_meter_with_percentages() {
325        let meter = SegmentedMeter::new(vec![], 100.0).with_percentages(true);
326        assert!(meter.show_percentages);
327    }
328
329    #[test]
330    fn test_segmented_meter_set_segments() {
331        let mut meter = SegmentedMeter::new(vec![], 100.0);
332        meter.set_segments(vec![Segment::new(50.0, Color::GREEN)]);
333        assert_eq!(meter.segments.len(), 1);
334    }
335
336    #[test]
337    fn test_segmented_meter_set_max() {
338        let mut meter = SegmentedMeter::new(vec![], 100.0);
339        meter.set_max(200.0);
340        assert_eq!(meter.max, 200.0);
341    }
342
343    #[test]
344    fn test_segmented_meter_paint() {
345        let mut meter = SegmentedMeter::new(
346            vec![
347                Segment::new(50.0, Color::RED),
348                Segment::new(30.0, Color::BLUE),
349            ],
350            100.0,
351        );
352        meter.bounds = Rect::new(0.0, 0.0, 20.0, 1.0);
353        let mut canvas = MockCanvas::new();
354        meter.paint(&mut canvas);
355        assert!(!canvas.texts.is_empty());
356    }
357
358    #[test]
359    fn test_segmented_meter_paint_empty() {
360        let mut meter = SegmentedMeter::new(vec![], 100.0);
361        meter.bounds = Rect::new(0.0, 0.0, 20.0, 1.0);
362        let mut canvas = MockCanvas::new();
363        meter.paint(&mut canvas);
364        // Should still render background
365        assert!(!canvas.texts.is_empty());
366    }
367
368    #[test]
369    fn test_segmented_meter_paint_zero_bounds() {
370        let mut meter = SegmentedMeter::new(vec![Segment::new(50.0, Color::RED)], 100.0);
371        meter.bounds = Rect::new(0.0, 0.0, 0.0, 0.0);
372        let mut canvas = MockCanvas::new();
373        meter.paint(&mut canvas);
374        assert!(canvas.texts.is_empty());
375    }
376
377    #[test]
378    fn test_segmented_meter_brick_name() {
379        let meter = SegmentedMeter::new(vec![], 100.0);
380        assert_eq!(meter.brick_name(), "segmented_meter");
381    }
382
383    #[test]
384    fn test_segmented_meter_assertions_not_empty() {
385        let meter = SegmentedMeter::new(vec![], 100.0);
386        assert!(!meter.assertions().is_empty());
387    }
388
389    #[test]
390    fn test_segmented_meter_verify() {
391        let meter = SegmentedMeter::new(vec![], 100.0);
392        assert!(meter.verify().is_valid());
393    }
394
395    #[test]
396    fn test_segmented_meter_measure() {
397        let meter = SegmentedMeter::new(vec![], 100.0);
398        let constraints = Constraints::new(0.0, 50.0, 0.0, 10.0);
399        let size = meter.measure(constraints);
400        assert!(size.width >= 10.0);
401        assert!(size.height >= 1.0);
402    }
403
404    #[test]
405    fn test_segmented_meter_colors() {
406        let mut meter = SegmentedMeter::new(
407            vec![
408                Segment::new(25.0, Color::RED),
409                Segment::new(25.0, Color::GREEN),
410                Segment::new(25.0, Color::BLUE),
411            ],
412            100.0,
413        );
414        meter.bounds = Rect::new(0.0, 0.0, 12.0, 1.0);
415        let mut canvas = MockCanvas::new();
416        meter.paint(&mut canvas);
417
418        // Should have multiple colors
419        let colors: std::collections::HashSet<_> = canvas
420            .texts
421            .iter()
422            .map(|(_, _, c)| format!("{:?}", c))
423            .collect();
424        assert!(colors.len() >= 3); // At least 3 different colors (red, green, blue, maybe background)
425    }
426
427    #[test]
428    fn test_segmented_meter_layout() {
429        let mut meter = SegmentedMeter::new(vec![Segment::new(50.0, Color::RED)], 100.0);
430        let result = meter.layout(Rect::new(0.0, 0.0, 80.0, 2.0));
431        assert_eq!(result.size.width, 80.0);
432        assert_eq!(result.size.height, 2.0);
433    }
434
435    #[test]
436    fn test_segmented_meter_event() {
437        let mut meter = SegmentedMeter::new(vec![], 100.0);
438        let event = Event::Resize {
439            width: 80.0,
440            height: 24.0,
441        };
442        assert!(meter.event(&event).is_none());
443    }
444
445    #[test]
446    fn test_segmented_meter_children() {
447        let meter = SegmentedMeter::new(vec![], 100.0);
448        assert!(meter.children().is_empty());
449    }
450
451    #[test]
452    fn test_segmented_meter_children_mut() {
453        let mut meter = SegmentedMeter::new(vec![], 100.0);
454        assert!(meter.children_mut().is_empty());
455    }
456
457    #[test]
458    fn test_segmented_meter_type_id() {
459        let meter = SegmentedMeter::new(vec![], 100.0);
460        let tid = Widget::type_id(&meter);
461        assert_eq!(tid, TypeId::of::<SegmentedMeter>());
462    }
463
464    #[test]
465    fn test_segmented_meter_budget() {
466        let meter = SegmentedMeter::new(vec![], 100.0);
467        let budget = meter.budget();
468        assert!(budget.layout_ms > 0);
469    }
470
471    #[test]
472    fn test_segmented_meter_to_html() {
473        let meter = SegmentedMeter::new(vec![], 100.0);
474        assert!(meter.to_html().is_empty());
475    }
476
477    #[test]
478    fn test_segmented_meter_to_css() {
479        let meter = SegmentedMeter::new(vec![], 100.0);
480        assert!(meter.to_css().is_empty());
481    }
482
483    #[test]
484    fn test_segment_clone() {
485        let seg = Segment::new(50.0, Color::RED).with_label("Test");
486        let cloned = seg.clone();
487        assert_eq!(cloned.value, seg.value);
488        assert_eq!(cloned.label, seg.label);
489    }
490
491    #[test]
492    fn test_segmented_meter_clone() {
493        let meter = SegmentedMeter::new(vec![Segment::new(50.0, Color::RED)], 100.0);
494        let cloned = meter.clone();
495        assert_eq!(cloned.max, meter.max);
496        assert_eq!(cloned.segments.len(), meter.segments.len());
497    }
498
499    #[test]
500    fn test_segment_debug() {
501        let seg = Segment::new(50.0, Color::RED);
502        let debug = format!("{seg:?}");
503        assert!(debug.contains("50"));
504    }
505
506    #[test]
507    fn test_segmented_meter_debug() {
508        let meter = SegmentedMeter::new(vec![], 100.0);
509        let debug = format!("{meter:?}");
510        assert!(debug.contains("100"));
511    }
512
513    #[test]
514    fn test_segmented_meter_paint_zero_max() {
515        let mut meter = SegmentedMeter::new(vec![Segment::new(50.0, Color::RED)], 0.0);
516        meter.bounds = Rect::new(0.0, 0.0, 20.0, 1.0);
517        let mut canvas = MockCanvas::new();
518        meter.paint(&mut canvas);
519        // With max=0, uses total of segments as denominator
520    }
521
522    #[test]
523    fn test_segmented_meter_paint_multi_row() {
524        let mut meter = SegmentedMeter::new(
525            vec![
526                Segment::new(50.0, Color::RED),
527                Segment::new(30.0, Color::BLUE),
528            ],
529            100.0,
530        );
531        meter.bounds = Rect::new(0.0, 0.0, 20.0, 3.0);
532        let mut canvas = MockCanvas::new();
533        meter.paint(&mut canvas);
534        // Should have drawn on multiple rows
535        assert!(!canvas.texts.is_empty());
536    }
537
538    #[test]
539    fn test_segmented_meter_paint_zero_segment() {
540        let mut meter = SegmentedMeter::new(
541            vec![
542                Segment::new(50.0, Color::RED),
543                Segment::new(0.0, Color::BLUE), // Zero-value segment
544                Segment::new(30.0, Color::GREEN),
545            ],
546            100.0,
547        );
548        meter.bounds = Rect::new(0.0, 0.0, 20.0, 1.0);
549        let mut canvas = MockCanvas::new();
550        meter.paint(&mut canvas);
551        // Zero segments should be skipped
552        assert!(!canvas.texts.is_empty());
553    }
554
555    #[test]
556    fn test_segmented_meter_paint_overflow() {
557        let mut meter = SegmentedMeter::new(
558            vec![
559                Segment::new(100.0, Color::RED),
560                Segment::new(100.0, Color::BLUE),
561            ],
562            100.0, // Total > max, will overflow width
563        );
564        meter.bounds = Rect::new(0.0, 0.0, 20.0, 1.0);
565        let mut canvas = MockCanvas::new();
566        meter.paint(&mut canvas);
567        // Should still handle gracefully
568        assert!(!canvas.texts.is_empty());
569    }
570
571    #[test]
572    fn test_segmented_meter_memory_overflow() {
573        // Test when used + cached > total
574        let meter = SegmentedMeter::memory(80.0, 30.0, 100.0);
575        assert_eq!(meter.segments.len(), 3);
576        // Free should be clamped to 0
577        assert_eq!(meter.segments[2].value, 0.0);
578    }
579}