Skip to main content

cbtop/bricks/panels/
cpu.rs

1//! CPU panel brick (Layer 3)
2
3use crate::brick::{Brick, BrickAssertion, BrickBudget, BrickVerification};
4use presentar_core::{Canvas, Point, Rect, TextStyle, Widget};
5use presentar_terminal::{BrailleGraph, GraphMode, Meter, Theme};
6use std::any::Any;
7
8pub struct CpuPanelBrick {
9    pub cpu_data: Vec<f64>,
10    pub intensity: f64,
11    pub theme: Theme,
12}
13
14impl CpuPanelBrick {
15    pub fn new() -> Self {
16        Self {
17            cpu_data: Vec::new(),
18            intensity: 0.0,
19            theme: Theme::tokyo_night(),
20        }
21    }
22
23    pub fn paint(&self, canvas: &mut dyn Canvas, width: f32, _height: f32) {
24        let label_style = TextStyle {
25            color: self.theme.foreground,
26            ..Default::default()
27        };
28        let dim_style = TextStyle {
29            color: self.theme.dim,
30            ..Default::default()
31        };
32
33        canvas.draw_text("CPU Monitor", Point::new(2.0, 2.0), &label_style);
34
35        // Main graph
36        if !self.cpu_data.is_empty() {
37            let cpu_usage = self.cpu_data.last().copied().unwrap_or(0.0);
38            let mut graph = BrailleGraph::new(self.cpu_data.clone())
39                .with_color(self.theme.cpu_color(cpu_usage))
40                .with_range(0.0, 100.0)
41                .with_mode(GraphMode::Braille);
42            graph.layout(Rect::new(2.0, 3.0, width - 4.0, 8.0));
43            graph.paint(canvas);
44        }
45
46        // Simulated per-core meters
47        canvas.draw_text(
48            "Per-Core Usage (simulated)",
49            Point::new(2.0, 12.0),
50            &label_style,
51        );
52        for i in 0..8 {
53            let y = 13.0 + i as f32;
54            let usage = 30.0 + (i as f64 * 7.0) + (self.intensity * 50.0);
55            let usage = usage.min(100.0);
56
57            canvas.draw_text(&format!("Core {}: ", i), Point::new(2.0, y), &dim_style);
58
59            // Use theme gradient for per-core meter color
60            let mut meter = Meter::new(usage, 100.0).with_color(self.theme.cpu_color(usage));
61            meter.layout(Rect::new(12.0, y, 20.0, 1.0));
62            meter.paint(canvas);
63
64            // Color the percentage based on usage
65            let pct_style = TextStyle {
66                color: self.theme.cpu_color(usage),
67                ..Default::default()
68            };
69            canvas.draw_text(&format!("{:5.1}%", usage), Point::new(34.0, y), &pct_style);
70        }
71    }
72}
73
74impl Brick for CpuPanelBrick {
75    fn brick_name(&self) -> &'static str {
76        "cpu_panel"
77    }
78
79    fn assertions(&self) -> Vec<BrickAssertion> {
80        vec![
81            BrickAssertion::MinWidth(40),
82            BrickAssertion::MinHeight(15),
83            BrickAssertion::max_latency_ms(8),
84        ]
85    }
86
87    fn budget(&self) -> BrickBudget {
88        BrickBudget::FRAME_60FPS
89    }
90
91    fn verify(&self) -> BrickVerification {
92        let mut v = BrickVerification::new();
93        for assertion in self.assertions() {
94            v.check(&assertion);
95        }
96        v
97    }
98
99    fn as_any(&self) -> &dyn Any {
100        self
101    }
102}
103
104impl Default for CpuPanelBrick {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use presentar_core::RecordingCanvas;
114
115    #[test]
116    fn test_cpu_panel_brick_name() {
117        let panel = CpuPanelBrick::new();
118        assert_eq!(panel.brick_name(), "cpu_panel");
119    }
120
121    #[test]
122    fn test_cpu_panel_has_assertions() {
123        let panel = CpuPanelBrick::new();
124        assert!(!panel.assertions().is_empty());
125    }
126
127    #[test]
128    fn test_cpu_panel_paint_empty() {
129        let panel = CpuPanelBrick::new();
130        let mut canvas = RecordingCanvas::new();
131
132        panel.paint(&mut canvas, 80.0, 24.0);
133
134        // Should draw header and per-core meters even with no graph data
135        assert!(!canvas.is_empty());
136        // Header + "Per-Core Usage" + 8 cores * (label + meter + percent) = many commands
137        assert!(canvas.command_count() >= 10);
138    }
139
140    #[test]
141    fn test_cpu_panel_paint_with_data() {
142        let mut panel = CpuPanelBrick::new();
143        panel.cpu_data = vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0];
144
145        let mut canvas = RecordingCanvas::new();
146        panel.paint(&mut canvas, 80.0, 24.0);
147
148        // Should draw CPU graph + meters
149        assert!(canvas.command_count() >= 15);
150    }
151
152    #[test]
153    fn test_cpu_panel_paint_with_intensity() {
154        let mut panel = CpuPanelBrick::new();
155        panel.intensity = 0.8; // 80% intensity
156
157        let mut canvas = RecordingCanvas::new();
158        panel.paint(&mut canvas, 80.0, 24.0);
159
160        // Intensity affects per-core usage display
161        assert!(!canvas.is_empty());
162    }
163
164    #[test]
165    fn test_cpu_panel_paint_high_usage() {
166        let mut panel = CpuPanelBrick::new();
167        // High usage data
168        panel.cpu_data = vec![90.0, 95.0, 92.0, 88.0, 91.0];
169        panel.intensity = 1.0; // Max intensity
170
171        let mut canvas = RecordingCanvas::new();
172        panel.paint(&mut canvas, 80.0, 24.0);
173
174        // Should handle high usage values without crashing
175        assert!(canvas.command_count() >= 10);
176    }
177
178    #[test]
179    fn test_cpu_panel_default() {
180        let panel = CpuPanelBrick::default();
181        assert!(panel.cpu_data.is_empty());
182        assert_eq!(panel.intensity, 0.0);
183    }
184
185    #[test]
186    fn test_cpu_panel_verify() {
187        let panel = CpuPanelBrick::new();
188        let verification = panel.verify();
189        assert!(verification.is_valid());
190    }
191
192    #[test]
193    fn test_cpu_panel_budget() {
194        let panel = CpuPanelBrick::new();
195        let budget = panel.budget();
196        assert_eq!(budget.total_ms(), 16); // FRAME_60FPS
197    }
198}