1use presentar_core::{
4 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
5 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
6};
7use std::any::Any;
8use std::time::Duration;
9
10#[derive(Debug, Clone)]
12pub struct Meter {
13 value: f64,
14 max: f64,
15 label: String,
16 fill_color: Color,
17 gradient_end: Option<Color>,
18 show_percentage: bool,
19 bounds: Rect,
20}
21
22impl Meter {
23 #[must_use]
25 pub fn new(value: f64, max: f64) -> Self {
26 Self {
27 value,
28 max,
29 label: String::new(),
30 fill_color: Color::GREEN,
31 gradient_end: None,
32 show_percentage: true,
33 bounds: Rect::new(0.0, 0.0, 0.0, 0.0),
34 }
35 }
36
37 #[must_use]
39 pub fn percentage(value: f64) -> Self {
40 Self::new(value, 100.0)
41 }
42
43 #[must_use]
45 pub fn with_label(mut self, label: impl Into<String>) -> Self {
46 self.label = label.into();
47 self
48 }
49
50 #[must_use]
52 pub fn with_color(mut self, color: Color) -> Self {
53 self.fill_color = color;
54 self
55 }
56
57 #[must_use]
59 pub fn with_gradient(mut self, start: Color, end: Color) -> Self {
60 self.fill_color = start;
61 self.gradient_end = Some(end);
62 self
63 }
64
65 #[must_use]
67 pub fn with_percentage_text(mut self, show: bool) -> Self {
68 self.show_percentage = show;
69 self
70 }
71
72 pub fn set_value(&mut self, value: f64) {
74 self.value = value.clamp(0.0, self.max);
75 }
76
77 #[must_use]
79 pub fn value(&self) -> f64 {
80 self.value
81 }
82
83 #[must_use]
85 pub fn ratio(&self) -> f64 {
86 if self.max == 0.0 {
87 0.0
88 } else {
89 (self.value / self.max).clamp(0.0, 1.0)
90 }
91 }
92
93 fn color_at(&self, t: f64) -> Color {
94 match self.gradient_end {
95 Some(end) => self.fill_color.lerp(&end, t as f32),
96 None => self.fill_color,
97 }
98 }
99}
100
101impl Brick for Meter {
102 fn brick_name(&self) -> &'static str {
103 "meter"
104 }
105
106 fn assertions(&self) -> &[BrickAssertion] {
107 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
108 ASSERTIONS
109 }
110
111 fn budget(&self) -> BrickBudget {
112 BrickBudget::uniform(16)
113 }
114
115 fn verify(&self) -> BrickVerification {
116 let mut passed = Vec::new();
117 let mut failed = Vec::new();
118
119 if self.value >= 0.0 && self.value <= self.max {
121 passed.push(BrickAssertion::max_latency_ms(16));
122 } else {
123 failed.push((
124 BrickAssertion::max_latency_ms(16),
125 format!("Value {} outside range [0, {}]", self.value, self.max),
126 ));
127 }
128
129 BrickVerification {
130 passed,
131 failed,
132 verification_time: Duration::from_micros(10),
133 }
134 }
135
136 fn to_html(&self) -> String {
137 String::new()
138 }
139
140 fn to_css(&self) -> String {
141 String::new()
142 }
143}
144
145impl Widget for Meter {
146 fn type_id(&self) -> TypeId {
147 TypeId::of::<Self>()
148 }
149
150 fn measure(&self, constraints: Constraints) -> Size {
151 let width = constraints.max_width.max(10.0);
152 let height = 1.0;
153 constraints.constrain(Size::new(width, height))
154 }
155
156 fn layout(&mut self, bounds: Rect) -> LayoutResult {
157 self.bounds = bounds;
158 LayoutResult {
159 size: Size::new(bounds.width, bounds.height.max(1.0)),
160 }
161 }
162
163 fn paint(&self, canvas: &mut dyn Canvas) {
164 let width = self.bounds.width as usize;
165 if width == 0 {
166 return;
167 }
168
169 let label_width = if self.label.is_empty() {
170 0
171 } else {
172 self.label.len() + 1
173 };
174
175 let pct_text = if self.show_percentage {
176 format!("{:5.1}%", self.ratio() * 100.0)
177 } else {
178 String::new()
179 };
180 let pct_width = pct_text.len();
181
182 let bar_width = width.saturating_sub(label_width + pct_width + 2);
183 if bar_width == 0 {
184 return;
185 }
186
187 if !self.label.is_empty() {
189 canvas.draw_text(
190 &self.label,
191 Point::new(self.bounds.x, self.bounds.y),
192 &TextStyle::default(),
193 );
194 }
195
196 let filled = ((self.ratio() * bar_width as f64).round() as usize).min(bar_width);
197
198 let mut bar = String::with_capacity(bar_width + 2);
199 bar.push('[');
200 for i in 0..bar_width {
201 if i < filled {
202 bar.push('█');
203 } else {
204 bar.push(' ');
205 }
206 }
207 bar.push(']');
208
209 let bar_x = self.bounds.x + label_width as f32;
210 let style = TextStyle {
211 color: self.color_at(0.5),
212 ..Default::default()
213 };
214 canvas.draw_text(&bar, Point::new(bar_x, self.bounds.y), &style);
215
216 if self.show_percentage {
217 let pct_x = bar_x + bar_width as f32 + 2.0;
218 canvas.draw_text(
219 &pct_text,
220 Point::new(pct_x, self.bounds.y),
221 &TextStyle::default(),
222 );
223 }
224 }
225
226 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
227 None
228 }
229
230 fn children(&self) -> &[Box<dyn Widget>] {
231 &[]
232 }
233
234 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
235 &mut []
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use presentar_core::{Canvas, TextStyle};
243
244 struct MockCanvas {
245 texts: Vec<(String, Point)>,
246 rects: Vec<Rect>,
247 }
248
249 impl MockCanvas {
250 fn new() -> Self {
251 Self {
252 texts: vec![],
253 rects: vec![],
254 }
255 }
256 }
257
258 impl Canvas for MockCanvas {
259 fn fill_rect(&mut self, rect: Rect, _color: Color) {
260 self.rects.push(rect);
261 }
262 fn stroke_rect(&mut self, _rect: Rect, _color: Color, _width: f32) {}
263 fn draw_text(&mut self, text: &str, position: Point, _style: &TextStyle) {
264 self.texts.push((text.to_string(), position));
265 }
266 fn draw_line(&mut self, _from: Point, _to: Point, _color: Color, _width: f32) {}
267 fn fill_circle(&mut self, _center: Point, _radius: f32, _color: Color) {}
268 fn stroke_circle(&mut self, _center: Point, _radius: f32, _color: Color, _width: f32) {}
269 fn fill_arc(
270 &mut self,
271 _center: Point,
272 _radius: f32,
273 _start: f32,
274 _end: f32,
275 _color: Color,
276 ) {
277 }
278 fn draw_path(&mut self, _points: &[Point], _color: Color, _width: f32) {}
279 fn fill_polygon(&mut self, _points: &[Point], _color: Color) {}
280 fn push_clip(&mut self, _rect: Rect) {}
281 fn pop_clip(&mut self) {}
282 fn push_transform(&mut self, _transform: presentar_core::Transform2D) {}
283 fn pop_transform(&mut self) {}
284 }
285
286 #[test]
287 fn test_meter_creation() {
288 let meter = Meter::new(50.0, 100.0);
289 assert_eq!(meter.value, 50.0);
290 assert_eq!(meter.max, 100.0);
291 }
292
293 #[test]
294 fn test_meter_ratio() {
295 let meter = Meter::percentage(75.0);
296 assert!((meter.ratio() - 0.75).abs() < f64::EPSILON);
297 }
298
299 #[test]
300 fn test_meter_assertions_not_empty() {
301 let meter = Meter::percentage(50.0);
302 assert!(!meter.assertions().is_empty());
303 }
304
305 #[test]
306 fn test_meter_verify_pass() {
307 let meter = Meter::percentage(50.0);
308 assert!(meter.verify().is_valid());
309 }
310
311 #[test]
312 fn test_meter_percentage() {
313 let meter = Meter::percentage(80.0);
314 assert_eq!(meter.max, 100.0);
315 assert_eq!(meter.value(), 80.0);
316 }
317
318 #[test]
319 fn test_meter_with_label() {
320 let meter = Meter::percentage(50.0).with_label("CPU");
321 assert_eq!(meter.label, "CPU");
322 }
323
324 #[test]
325 fn test_meter_with_color() {
326 let meter = Meter::percentage(50.0).with_color(Color::RED);
327 assert_eq!(meter.fill_color, Color::RED);
328 }
329
330 #[test]
331 fn test_meter_with_gradient() {
332 let meter = Meter::percentage(50.0).with_gradient(Color::GREEN, Color::RED);
333 assert_eq!(meter.fill_color, Color::GREEN);
334 assert_eq!(meter.gradient_end, Some(Color::RED));
335 }
336
337 #[test]
338 fn test_meter_with_percentage_text() {
339 let meter = Meter::percentage(50.0).with_percentage_text(false);
340 assert!(!meter.show_percentage);
341 }
342
343 #[test]
344 fn test_meter_set_value() {
345 let mut meter = Meter::percentage(50.0);
346 meter.set_value(75.0);
347 assert_eq!(meter.value(), 75.0);
348 }
349
350 #[test]
351 fn test_meter_set_value_clamped() {
352 let mut meter = Meter::percentage(50.0);
353 meter.set_value(150.0);
354 assert_eq!(meter.value(), 100.0);
355
356 meter.set_value(-10.0);
357 assert_eq!(meter.value(), 0.0);
358 }
359
360 #[test]
361 fn test_meter_ratio_zero_max() {
362 let meter = Meter::new(50.0, 0.0);
363 assert_eq!(meter.ratio(), 0.0);
364 }
365
366 #[test]
367 fn test_meter_ratio_clamped() {
368 let meter = Meter::new(150.0, 100.0);
369 assert_eq!(meter.ratio(), 1.0);
370 }
371
372 #[test]
373 fn test_meter_color_at_no_gradient() {
374 let meter = Meter::percentage(50.0).with_color(Color::BLUE);
375 let color = meter.color_at(0.5);
376 assert_eq!(color, Color::BLUE);
377 }
378
379 #[test]
380 fn test_meter_color_at_with_gradient() {
381 let meter = Meter::percentage(50.0).with_gradient(Color::GREEN, Color::RED);
382 let color = meter.color_at(0.0);
383 assert_eq!(color, Color::GREEN);
384 let color = meter.color_at(1.0);
385 assert_eq!(color, Color::RED);
386 }
387
388 #[test]
389 fn test_meter_verify_out_of_range() {
390 let mut meter = Meter::new(50.0, 100.0);
391 meter.value = -10.0;
392 assert!(!meter.verify().is_valid());
393 }
394
395 #[test]
396 fn test_meter_measure() {
397 let meter = Meter::percentage(50.0);
398 let constraints = Constraints::new(0.0, 100.0, 0.0, 10.0);
399 let size = meter.measure(constraints);
400 assert!(size.width >= 10.0);
401 assert_eq!(size.height, 1.0);
402 }
403
404 #[test]
405 fn test_meter_layout() {
406 let mut meter = Meter::percentage(50.0);
407 let bounds = Rect::new(0.0, 0.0, 80.0, 1.0);
408 let result = meter.layout(bounds);
409 assert_eq!(result.size.width, 80.0);
410 assert_eq!(result.size.height, 1.0);
411 }
412
413 #[test]
414 fn test_meter_paint() {
415 let mut meter = Meter::percentage(50.0).with_label("Test");
416 meter.bounds = Rect::new(0.0, 0.0, 40.0, 1.0);
417 let mut canvas = MockCanvas::new();
418 meter.paint(&mut canvas);
419 assert!(!canvas.texts.is_empty());
420 }
421
422 #[test]
423 fn test_meter_paint_without_label() {
424 let mut meter = Meter::percentage(50.0);
425 meter.bounds = Rect::new(0.0, 0.0, 40.0, 1.0);
426 let mut canvas = MockCanvas::new();
427 meter.paint(&mut canvas);
428 assert!(!canvas.texts.is_empty());
429 }
430
431 #[test]
432 fn test_meter_paint_without_percentage() {
433 let mut meter = Meter::percentage(50.0).with_percentage_text(false);
434 meter.bounds = Rect::new(0.0, 0.0, 40.0, 1.0);
435 let mut canvas = MockCanvas::new();
436 meter.paint(&mut canvas);
437 assert!(!canvas.texts.is_empty());
438 }
439
440 #[test]
441 fn test_meter_paint_zero_width() {
442 let mut meter = Meter::percentage(50.0);
443 meter.bounds = Rect::new(0.0, 0.0, 0.0, 1.0);
444 let mut canvas = MockCanvas::new();
445 meter.paint(&mut canvas);
446 assert!(canvas.texts.is_empty());
447 }
448
449 #[test]
450 fn test_meter_paint_tiny_bar() {
451 let mut meter = Meter::percentage(50.0).with_label("Very Long Label");
452 meter.bounds = Rect::new(0.0, 0.0, 10.0, 1.0);
453 let mut canvas = MockCanvas::new();
454 meter.paint(&mut canvas);
455 }
456
457 #[test]
458 fn test_meter_event() {
459 let mut meter = Meter::percentage(50.0);
460 let event = Event::KeyDown {
461 key: presentar_core::Key::Enter,
462 };
463 assert!(meter.event(&event).is_none());
464 }
465
466 #[test]
467 fn test_meter_children() {
468 let meter = Meter::percentage(50.0);
469 assert!(meter.children().is_empty());
470 }
471
472 #[test]
473 fn test_meter_children_mut() {
474 let mut meter = Meter::percentage(50.0);
475 assert!(meter.children_mut().is_empty());
476 }
477
478 #[test]
479 fn test_meter_type_id() {
480 let meter = Meter::percentage(50.0);
481 assert_eq!(Widget::type_id(&meter), TypeId::of::<Meter>());
482 }
483
484 #[test]
485 fn test_meter_brick_name() {
486 let meter = Meter::percentage(50.0);
487 assert_eq!(meter.brick_name(), "meter");
488 }
489
490 #[test]
491 fn test_meter_budget() {
492 let meter = Meter::percentage(50.0);
493 let budget = meter.budget();
494 assert!(budget.measure_ms > 0);
495 }
496
497 #[test]
498 fn test_meter_to_html() {
499 let meter = Meter::percentage(50.0);
500 assert!(meter.to_html().is_empty());
501 }
502
503 #[test]
504 fn test_meter_to_css() {
505 let meter = Meter::percentage(50.0);
506 assert!(meter.to_css().is_empty());
507 }
508
509 #[test]
510 fn test_meter_full() {
511 let mut meter = Meter::percentage(100.0);
512 meter.bounds = Rect::new(0.0, 0.0, 50.0, 1.0);
513 let mut canvas = MockCanvas::new();
514 meter.paint(&mut canvas);
515 assert!(!canvas.texts.is_empty());
516 }
517
518 #[test]
519 fn test_meter_empty() {
520 let mut meter = Meter::percentage(0.0);
521 meter.bounds = Rect::new(0.0, 0.0, 50.0, 1.0);
522 let mut canvas = MockCanvas::new();
523 meter.paint(&mut canvas);
524 assert!(!canvas.texts.is_empty());
525 }
526
527 #[test]
528 fn test_meter_verify_value_over_max() {
529 let mut meter = Meter::new(50.0, 100.0);
530 meter.value = 150.0;
531 assert!(!meter.verify().is_valid());
532 }
533
534 #[test]
535 fn test_meter_layout_with_small_height() {
536 let mut meter = Meter::percentage(50.0);
537 let bounds = Rect::new(0.0, 0.0, 80.0, 0.5);
538 let result = meter.layout(bounds);
539 assert_eq!(result.size.height, 1.0);
540 }
541
542 #[test]
543 fn test_meter_paint_with_gradient() {
544 let mut meter = Meter::percentage(50.0).with_gradient(Color::GREEN, Color::RED);
545 meter.bounds = Rect::new(0.0, 0.0, 50.0, 1.0);
546 let mut canvas = MockCanvas::new();
547 meter.paint(&mut canvas);
548 assert!(!canvas.texts.is_empty());
549 }
550
551 #[test]
552 fn test_meter_clone() {
553 let meter = Meter::percentage(75.0)
554 .with_label("Clone Test")
555 .with_color(Color::BLUE);
556 let cloned = meter.clone();
557 assert_eq!(cloned.value, 75.0);
558 assert_eq!(cloned.label, "Clone Test");
559 assert_eq!(cloned.fill_color, Color::BLUE);
560 }
561
562 #[test]
563 fn test_meter_debug() {
564 let meter = Meter::percentage(50.0);
565 let debug = format!("{:?}", meter);
566 assert!(debug.contains("Meter"));
567 assert!(debug.contains("value"));
568 }
569
570 #[test]
571 fn test_meter_measure_small_constraints() {
572 let meter = Meter::percentage(50.0);
573 let constraints = Constraints::new(0.0, 5.0, 0.0, 10.0);
574 let size = meter.measure(constraints);
575 assert_eq!(size.width, 5.0);
577 }
578}