1use 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, PartialEq, Eq, Default)]
15pub enum GaugeMode {
16 #[default]
18 Arc,
19 Quarter,
21 Half,
23 ThreeQuarter,
25 Vertical,
27 Compact,
29}
30
31#[derive(Debug, Clone)]
33pub struct Gauge {
34 value: f64,
36 max: f64,
38 label: Option<String>,
40 mode: GaugeMode,
42 color: Color,
44 warn_threshold: f64,
46 critical_threshold: f64,
48 show_value: bool,
50 unit: Option<String>,
52 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 #[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 #[must_use]
82 pub fn percentage(value: f64) -> Self {
83 Self::new(value, 100.0).with_unit("%")
84 }
85
86 #[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 #[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 #[must_use]
103 pub fn with_mode(mut self, mode: GaugeMode) -> Self {
104 self.mode = mode;
105 self
106 }
107
108 #[must_use]
110 pub fn with_color(mut self, color: Color) -> Self {
111 self.color = color;
112 self
113 }
114
115 #[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 #[must_use]
125 pub fn with_value_display(mut self, show: bool) -> Self {
126 self.show_value = show;
127 self
128 }
129
130 #[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 pub fn set_value(&mut self, value: f64) {
139 self.value = value.clamp(0.0, self.max);
140 }
141
142 #[must_use]
144 pub fn value(&self) -> f64 {
145 self.value
146 }
147
148 #[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 #[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) } else if pct >= self.warn_threshold {
165 Color::new(1.0, 0.7, 0.2, 1.0) } 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let mut gauge = Gauge::percentage(50.0).with_mode(GaugeMode::Arc);
898 gauge.bounds = Rect::new(0.0, 0.0, 3.0, 2.0); let mut canvas = MockCanvas::new();
900 gauge.paint(&mut canvas);
901 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 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 assert!(canvas.texts.iter().any(|(t, _)| t.contains("CPU")));
927 }
928
929 #[test]
930 fn test_gauge_arc_height_two() {
931 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 assert!(!canvas.texts.is_empty());
938 }
939
940 #[test]
941 fn test_gauge_arc_height_three_no_middle() {
942 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 assert!(canvas.texts.len() >= 2);
949 }
950
951 #[test]
952 fn test_gauge_arc_height_four() {
953 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 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); let mut canvas = MockCanvas::new();
967 gauge.paint(&mut canvas);
968 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 assert!(!canvas.texts.is_empty());
982 }
983
984 #[test]
985 fn test_gauge_verify_invalid() {
986 let mut gauge = Gauge::new(50.0, 100.0);
988 gauge.value = -10.0; 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; 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 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 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 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 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 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 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 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 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}