1use crate::theme::Gradient;
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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
15pub enum MultiBarMode {
16 #[default]
18 Vertical,
19 Horizontal,
21}
22
23#[derive(Debug, Clone)]
27pub struct MultiBarGraph {
28 values: Vec<f64>,
30 color: Color,
32 gradient: Option<Gradient>,
34 mode: MultiBarMode,
36 labels: Option<Vec<String>>,
38 bounds: Rect,
40 gap: u16,
42}
43
44impl MultiBarGraph {
45 #[must_use]
48 pub fn new(values: Vec<f64>) -> Self {
49 Self {
50 values,
51 color: Color::GREEN,
52 gradient: None,
53 mode: MultiBarMode::default(),
54 labels: None,
55 bounds: Rect::new(0.0, 0.0, 0.0, 0.0),
56 gap: 0,
57 }
58 }
59
60 #[must_use]
62 pub fn with_color(mut self, color: Color) -> Self {
63 self.color = color;
64 self
65 }
66
67 #[must_use]
69 pub fn with_gradient(mut self, gradient: Gradient) -> Self {
70 self.gradient = Some(gradient);
71 self
72 }
73
74 #[must_use]
76 pub fn with_mode(mut self, mode: MultiBarMode) -> Self {
77 self.mode = mode;
78 self
79 }
80
81 #[must_use]
83 pub fn with_labels(mut self, labels: Vec<String>) -> Self {
84 self.labels = Some(labels);
85 self
86 }
87
88 #[must_use]
90 pub fn with_gap(mut self, gap: u16) -> Self {
91 self.gap = gap;
92 self
93 }
94
95 pub fn set_values(&mut self, values: Vec<f64>) {
97 self.values = values;
98 }
99
100 fn color_for_value(&self, value: f64) -> Color {
102 match &self.gradient {
103 Some(gradient) => gradient.sample(value.clamp(0.0, 1.0)),
104 None => self.color,
105 }
106 }
107
108 fn render_vertical(&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 || self.values.is_empty() {
112 return;
113 }
114
115 let bar_count = self.values.len();
116 let total_gap = self.gap as usize * bar_count.saturating_sub(1);
117 let available_width = width.saturating_sub(total_gap);
118 let bar_width = (available_width / bar_count).max(1);
119
120 let blocks = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
122
123 for (i, &value) in self.values.iter().enumerate() {
124 let value = value.clamp(0.0, 1.0);
125 let bar_x = i * (bar_width + self.gap as usize);
126 if bar_x >= width {
127 break;
128 }
129
130 let color = self.color_for_value(value);
131 let style = TextStyle {
132 color,
133 ..Default::default()
134 };
135
136 let total_eighths = (value * height as f64 * 8.0).round() as usize;
138 let full_rows = total_eighths / 8;
139 let partial_eighths = total_eighths % 8;
140
141 for row in 0..height {
142 let y = height - 1 - row; let ch = if row < full_rows {
144 '█'
145 } else if row == full_rows && partial_eighths > 0 {
146 blocks[partial_eighths]
147 } else {
148 ' '
149 };
150
151 for bx in 0..bar_width {
153 let x = bar_x + bx;
154 if x < width {
155 canvas.draw_text(
156 &ch.to_string(),
157 Point::new(self.bounds.x + x as f32, self.bounds.y + y as f32),
158 &style,
159 );
160 }
161 }
162 }
163 }
164 }
165
166 fn render_horizontal(&self, canvas: &mut dyn Canvas) {
167 let width = self.bounds.width as usize;
168 let height = self.bounds.height as usize;
169 if width == 0 || height == 0 || self.values.is_empty() {
170 return;
171 }
172
173 let bar_count = self.values.len();
174 let total_gap = self.gap as usize * bar_count.saturating_sub(1);
175 let available_height = height.saturating_sub(total_gap);
176 let bar_height = (available_height / bar_count).max(1);
177
178 for (i, &value) in self.values.iter().enumerate() {
179 let value = value.clamp(0.0, 1.0);
180 let bar_y = i * (bar_height + self.gap as usize);
181 if bar_y >= height {
182 break;
183 }
184
185 let color = self.color_for_value(value);
186 let style = TextStyle {
187 color,
188 ..Default::default()
189 };
190
191 let filled_cols = (value * width as f64).round() as usize;
192
193 for row in 0..bar_height {
194 let y = bar_y + row;
195 if y >= height {
196 break;
197 }
198
199 for col in 0..width {
200 let ch = if col < filled_cols { '█' } else { '░' };
201 canvas.draw_text(
202 &ch.to_string(),
203 Point::new(self.bounds.x + col as f32, self.bounds.y + y as f32),
204 &style,
205 );
206 }
207 }
208 }
209 }
210}
211
212impl Brick for MultiBarGraph {
213 fn brick_name(&self) -> &'static str {
214 "multi_bar_graph"
215 }
216
217 fn assertions(&self) -> &[BrickAssertion] {
218 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
219 ASSERTIONS
220 }
221
222 fn budget(&self) -> BrickBudget {
223 BrickBudget::uniform(16)
224 }
225
226 fn verify(&self) -> BrickVerification {
227 BrickVerification {
228 passed: vec![BrickAssertion::max_latency_ms(16)],
229 failed: vec![],
230 verification_time: Duration::from_micros(10),
231 }
232 }
233
234 fn to_html(&self) -> String {
235 String::new()
236 }
237
238 fn to_css(&self) -> String {
239 String::new()
240 }
241}
242
243impl Widget for MultiBarGraph {
244 fn type_id(&self) -> TypeId {
245 TypeId::of::<Self>()
246 }
247
248 fn measure(&self, constraints: Constraints) -> Size {
249 let width = constraints.max_width.max(self.values.len() as f32);
250 let height = constraints.max_height.max(3.0);
251 constraints.constrain(Size::new(width, height))
252 }
253
254 fn layout(&mut self, bounds: Rect) -> LayoutResult {
255 self.bounds = bounds;
256 LayoutResult {
257 size: Size::new(bounds.width, bounds.height),
258 }
259 }
260
261 fn paint(&self, canvas: &mut dyn Canvas) {
262 match self.mode {
263 MultiBarMode::Vertical => self.render_vertical(canvas),
264 MultiBarMode::Horizontal => self.render_horizontal(canvas),
265 }
266 }
267
268 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
269 None
270 }
271
272 fn children(&self) -> &[Box<dyn Widget>] {
273 &[]
274 }
275
276 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
277 &mut []
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 struct MockCanvas {
286 texts: Vec<(String, Point, Color)>,
287 }
288
289 impl MockCanvas {
290 fn new() -> Self {
291 Self { texts: vec![] }
292 }
293 }
294
295 impl Canvas for MockCanvas {
296 fn fill_rect(&mut self, _rect: Rect, _color: Color) {}
297 fn stroke_rect(&mut self, _rect: Rect, _color: Color, _width: f32) {}
298 fn draw_text(&mut self, text: &str, position: Point, style: &TextStyle) {
299 self.texts.push((text.to_string(), position, style.color));
300 }
301 fn draw_line(&mut self, _from: Point, _to: Point, _color: Color, _width: f32) {}
302 fn fill_circle(&mut self, _center: Point, _radius: f32, _color: Color) {}
303 fn stroke_circle(&mut self, _center: Point, _radius: f32, _color: Color, _width: f32) {}
304 fn fill_arc(
305 &mut self,
306 _center: Point,
307 _radius: f32,
308 _start: f32,
309 _end: f32,
310 _color: Color,
311 ) {
312 }
313 fn draw_path(&mut self, _points: &[Point], _color: Color, _width: f32) {}
314 fn fill_polygon(&mut self, _points: &[Point], _color: Color) {}
315 fn push_clip(&mut self, _rect: Rect) {}
316 fn pop_clip(&mut self) {}
317 fn push_transform(&mut self, _transform: presentar_core::Transform2D) {}
318 fn pop_transform(&mut self) {}
319 }
320
321 #[test]
322 fn test_multi_bar_creation() {
323 let graph = MultiBarGraph::new(vec![0.5, 0.75, 0.25]);
324 assert_eq!(graph.values.len(), 3);
325 }
326
327 #[test]
328 fn test_multi_bar_with_color() {
329 let graph = MultiBarGraph::new(vec![0.5]).with_color(Color::RED);
330 assert_eq!(graph.color, Color::RED);
331 }
332
333 #[test]
334 fn test_multi_bar_with_gradient() {
335 let gradient = Gradient::from_hex(&["#00FF00", "#FF0000"]);
336 let graph = MultiBarGraph::new(vec![0.5]).with_gradient(gradient);
337 assert!(graph.gradient.is_some());
338 }
339
340 #[test]
341 fn test_multi_bar_with_mode() {
342 let graph = MultiBarGraph::new(vec![0.5]).with_mode(MultiBarMode::Horizontal);
343 assert_eq!(graph.mode, MultiBarMode::Horizontal);
344 }
345
346 #[test]
347 fn test_multi_bar_with_gap() {
348 let graph = MultiBarGraph::new(vec![0.5]).with_gap(1);
349 assert_eq!(graph.gap, 1);
350 }
351
352 #[test]
353 fn test_multi_bar_set_values() {
354 let mut graph = MultiBarGraph::new(vec![0.5]);
355 graph.set_values(vec![0.1, 0.2, 0.3]);
356 assert_eq!(graph.values.len(), 3);
357 }
358
359 #[test]
360 fn test_multi_bar_paint_vertical() {
361 let mut graph = MultiBarGraph::new(vec![0.5, 1.0, 0.25]);
362 graph.bounds = Rect::new(0.0, 0.0, 6.0, 4.0);
363 let mut canvas = MockCanvas::new();
364 graph.paint(&mut canvas);
365 assert!(!canvas.texts.is_empty());
366 }
367
368 #[test]
369 fn test_multi_bar_paint_horizontal() {
370 let mut graph =
371 MultiBarGraph::new(vec![0.5, 1.0, 0.25]).with_mode(MultiBarMode::Horizontal);
372 graph.bounds = Rect::new(0.0, 0.0, 10.0, 6.0);
373 let mut canvas = MockCanvas::new();
374 graph.paint(&mut canvas);
375 assert!(!canvas.texts.is_empty());
376 }
377
378 #[test]
379 fn test_multi_bar_gradient_coloring() {
380 let gradient = Gradient::from_hex(&["#00FF00", "#FF0000"]);
381 let mut graph = MultiBarGraph::new(vec![0.0, 0.5, 1.0]).with_gradient(gradient);
382 graph.bounds = Rect::new(0.0, 0.0, 6.0, 4.0);
383 let mut canvas = MockCanvas::new();
384 graph.paint(&mut canvas);
385
386 let colors: Vec<Color> = canvas.texts.iter().map(|(_, _, c)| *c).collect();
388 assert!(!colors.is_empty());
389 }
390
391 #[test]
392 fn test_multi_bar_empty_bounds() {
393 let mut graph = MultiBarGraph::new(vec![0.5]);
394 graph.bounds = Rect::new(0.0, 0.0, 0.0, 0.0);
395 let mut canvas = MockCanvas::new();
396 graph.paint(&mut canvas);
397 assert!(canvas.texts.is_empty());
398 }
399
400 #[test]
401 fn test_multi_bar_empty_values() {
402 let mut graph = MultiBarGraph::new(vec![]);
403 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
404 let mut canvas = MockCanvas::new();
405 graph.paint(&mut canvas);
406 assert!(canvas.texts.is_empty());
407 }
408
409 #[test]
410 fn test_multi_bar_brick_name() {
411 let graph = MultiBarGraph::new(vec![0.5]);
412 assert_eq!(graph.brick_name(), "multi_bar_graph");
413 }
414
415 #[test]
416 fn test_multi_bar_assertions_not_empty() {
417 let graph = MultiBarGraph::new(vec![0.5]);
418 assert!(!graph.assertions().is_empty());
419 }
420
421 #[test]
422 fn test_multi_bar_verify() {
423 let graph = MultiBarGraph::new(vec![0.5]);
424 assert!(graph.verify().is_valid());
425 }
426
427 #[test]
428 fn test_multi_bar_measure() {
429 let graph = MultiBarGraph::new(vec![0.5, 0.5, 0.5]);
430 let constraints = Constraints::new(0.0, 100.0, 0.0, 50.0);
431 let size = graph.measure(constraints);
432 assert!(size.width >= 3.0);
433 assert!(size.height >= 3.0);
434 }
435
436 #[test]
437 fn test_multi_bar_mode_default() {
438 assert_eq!(MultiBarMode::default(), MultiBarMode::Vertical);
439 }
440
441 #[test]
442 fn test_multi_bar_many_values() {
443 let values: Vec<f64> = (0..48).map(|i| i as f64 / 48.0).collect();
445 let mut graph = MultiBarGraph::new(values);
446 graph.bounds = Rect::new(0.0, 0.0, 96.0, 6.0);
447 let mut canvas = MockCanvas::new();
448 graph.paint(&mut canvas);
449 assert!(!canvas.texts.is_empty());
450 }
451
452 #[test]
453 fn test_multi_bar_layout() {
454 let mut graph = MultiBarGraph::new(vec![0.5, 0.75]);
455 let result = graph.layout(Rect::new(0.0, 0.0, 40.0, 10.0));
456 assert_eq!(result.size.width, 40.0);
457 assert_eq!(result.size.height, 10.0);
458 }
459
460 #[test]
461 fn test_multi_bar_event() {
462 let mut graph = MultiBarGraph::new(vec![0.5]);
463 let event = Event::Resize {
464 width: 80.0,
465 height: 24.0,
466 };
467 assert!(graph.event(&event).is_none());
468 }
469
470 #[test]
471 fn test_multi_bar_children() {
472 let graph = MultiBarGraph::new(vec![0.5]);
473 assert!(graph.children().is_empty());
474 }
475
476 #[test]
477 fn test_multi_bar_children_mut() {
478 let mut graph = MultiBarGraph::new(vec![0.5]);
479 assert!(graph.children_mut().is_empty());
480 }
481
482 #[test]
483 fn test_multi_bar_type_id() {
484 let graph = MultiBarGraph::new(vec![0.5]);
485 let tid = Widget::type_id(&graph);
486 assert_eq!(tid, TypeId::of::<MultiBarGraph>());
487 }
488
489 #[test]
490 fn test_multi_bar_budget() {
491 let graph = MultiBarGraph::new(vec![0.5]);
492 let budget = graph.budget();
493 assert!(budget.layout_ms > 0);
494 }
495
496 #[test]
497 fn test_multi_bar_to_html() {
498 let graph = MultiBarGraph::new(vec![0.5]);
499 assert!(graph.to_html().is_empty());
500 }
501
502 #[test]
503 fn test_multi_bar_to_css() {
504 let graph = MultiBarGraph::new(vec![0.5]);
505 assert!(graph.to_css().is_empty());
506 }
507
508 #[test]
509 fn test_multi_bar_clone() {
510 let graph = MultiBarGraph::new(vec![0.5, 0.75]).with_gap(2);
511 let cloned = graph.clone();
512 assert_eq!(cloned.values.len(), graph.values.len());
513 assert_eq!(cloned.gap, graph.gap);
514 }
515
516 #[test]
517 fn test_multi_bar_debug() {
518 let graph = MultiBarGraph::new(vec![0.5]);
519 let debug = format!("{graph:?}");
520 assert!(debug.contains("MultiBarGraph"));
521 }
522
523 #[test]
524 fn test_multi_bar_mode_debug() {
525 let mode = MultiBarMode::Vertical;
526 let debug = format!("{mode:?}");
527 assert!(debug.contains("Vertical"));
528 }
529
530 #[test]
531 fn test_multi_bar_mode_clone() {
532 let mode = MultiBarMode::Horizontal;
533 let cloned = mode;
534 assert_eq!(cloned, MultiBarMode::Horizontal);
535 }
536
537 #[test]
538 fn test_multi_bar_with_labels() {
539 let graph =
540 MultiBarGraph::new(vec![0.5]).with_labels(vec!["CPU0".to_string(), "CPU1".to_string()]);
541 assert!(graph.labels.is_some());
542 assert_eq!(graph.labels.unwrap().len(), 2);
543 }
544
545 #[test]
546 fn test_multi_bar_color_for_value_no_gradient() {
547 let graph = MultiBarGraph::new(vec![0.5]).with_color(Color::BLUE);
548 let color = graph.color_for_value(0.75);
549 assert_eq!(color, Color::BLUE);
550 }
551
552 #[test]
553 fn test_multi_bar_color_for_value_with_gradient() {
554 let gradient = Gradient::from_hex(&["#00FF00", "#FF0000"]);
555 let graph = MultiBarGraph::new(vec![0.5]).with_gradient(gradient);
556 let color = graph.color_for_value(0.5);
557 assert!(color.r > 0.0 || color.g > 0.0);
559 }
560
561 #[test]
562 fn test_multi_bar_vertical_overflow() {
563 let mut graph = MultiBarGraph::new(vec![0.5; 20]);
565 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
566 let mut canvas = MockCanvas::new();
567 graph.paint(&mut canvas);
568 }
570
571 #[test]
572 fn test_multi_bar_horizontal_overflow() {
573 let mut graph = MultiBarGraph::new(vec![0.5; 20]).with_mode(MultiBarMode::Horizontal);
575 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
576 let mut canvas = MockCanvas::new();
577 graph.paint(&mut canvas);
578 }
580
581 #[test]
582 fn test_multi_bar_vertical_with_gap() {
583 let mut graph = MultiBarGraph::new(vec![0.5, 0.75, 1.0]).with_gap(1);
584 graph.bounds = Rect::new(0.0, 0.0, 12.0, 5.0);
585 let mut canvas = MockCanvas::new();
586 graph.paint(&mut canvas);
587 assert!(!canvas.texts.is_empty());
588 }
589
590 #[test]
591 fn test_multi_bar_horizontal_with_gap() {
592 let mut graph = MultiBarGraph::new(vec![0.5, 0.75, 1.0])
593 .with_mode(MultiBarMode::Horizontal)
594 .with_gap(1);
595 graph.bounds = Rect::new(0.0, 0.0, 10.0, 12.0);
596 let mut canvas = MockCanvas::new();
597 graph.paint(&mut canvas);
598 assert!(!canvas.texts.is_empty());
599 }
600
601 #[test]
602 fn test_multi_bar_horizontal_empty() {
603 let mut graph = MultiBarGraph::new(vec![]).with_mode(MultiBarMode::Horizontal);
604 graph.bounds = Rect::new(0.0, 0.0, 10.0, 5.0);
605 let mut canvas = MockCanvas::new();
606 graph.paint(&mut canvas);
607 assert!(canvas.texts.is_empty());
608 }
609
610 #[test]
611 fn test_multi_bar_horizontal_zero_bounds() {
612 let mut graph = MultiBarGraph::new(vec![0.5]).with_mode(MultiBarMode::Horizontal);
613 graph.bounds = Rect::new(0.0, 0.0, 0.0, 0.0);
614 let mut canvas = MockCanvas::new();
615 graph.paint(&mut canvas);
616 assert!(canvas.texts.is_empty());
617 }
618
619 #[test]
620 fn test_multi_bar_clamped_values() {
621 let mut graph = MultiBarGraph::new(vec![-0.5, 1.5, 2.0]);
623 graph.bounds = Rect::new(0.0, 0.0, 9.0, 5.0);
624 let mut canvas = MockCanvas::new();
625 graph.paint(&mut canvas);
626 }
628}