1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult},
5 Canvas, Color, Constraints, Event, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
12pub enum ProgressMode {
13 #[default]
15 Determinate,
16 Indeterminate,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ProgressBar {
23 value: f32,
25 mode: ProgressMode,
27 min_width: f32,
29 height: f32,
31 corner_radius: f32,
33 track_color: Color,
35 fill_color: Color,
37 show_label: bool,
39 label_color: Color,
41 accessible_name_value: Option<String>,
43 test_id_value: Option<String>,
45 #[serde(skip)]
47 bounds: Rect,
48}
49
50impl Default for ProgressBar {
51 fn default() -> Self {
52 Self {
53 value: 0.0,
54 mode: ProgressMode::Determinate,
55 min_width: 100.0,
56 height: 8.0,
57 corner_radius: 4.0,
58 track_color: Color::new(0.88, 0.88, 0.88, 1.0), fill_color: Color::new(0.13, 0.59, 0.95, 1.0), show_label: false,
61 label_color: Color::BLACK,
62 accessible_name_value: None,
63 test_id_value: None,
64 bounds: Rect::default(),
65 }
66 }
67}
68
69impl ProgressBar {
70 #[must_use]
72 pub fn new() -> Self {
73 Self::default()
74 }
75
76 #[must_use]
78 pub fn with_value(value: f32) -> Self {
79 Self::default().value(value)
80 }
81
82 #[must_use]
84 pub fn value(mut self, value: f32) -> Self {
85 self.value = value.clamp(0.0, 1.0);
86 self
87 }
88
89 #[must_use]
91 pub const fn mode(mut self, mode: ProgressMode) -> Self {
92 self.mode = mode;
93 self
94 }
95
96 #[must_use]
98 pub const fn indeterminate(self) -> Self {
99 self.mode(ProgressMode::Indeterminate)
100 }
101
102 #[must_use]
104 pub fn min_width(mut self, width: f32) -> Self {
105 self.min_width = width.max(20.0);
106 self
107 }
108
109 #[must_use]
111 pub fn height(mut self, height: f32) -> Self {
112 self.height = height.max(4.0);
113 self
114 }
115
116 #[must_use]
118 pub fn corner_radius(mut self, radius: f32) -> Self {
119 self.corner_radius = radius.max(0.0);
120 self
121 }
122
123 #[must_use]
125 pub const fn track_color(mut self, color: Color) -> Self {
126 self.track_color = color;
127 self
128 }
129
130 #[must_use]
132 pub const fn fill_color(mut self, color: Color) -> Self {
133 self.fill_color = color;
134 self
135 }
136
137 #[must_use]
139 pub const fn with_label(mut self) -> Self {
140 self.show_label = true;
141 self
142 }
143
144 #[must_use]
146 pub const fn show_label(mut self, show: bool) -> Self {
147 self.show_label = show;
148 self
149 }
150
151 #[must_use]
153 pub const fn label_color(mut self, color: Color) -> Self {
154 self.label_color = color;
155 self
156 }
157
158 #[must_use]
160 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
161 self.accessible_name_value = Some(name.into());
162 self
163 }
164
165 #[must_use]
167 pub fn test_id(mut self, id: impl Into<String>) -> Self {
168 self.test_id_value = Some(id.into());
169 self
170 }
171
172 #[must_use]
174 pub const fn get_value(&self) -> f32 {
175 self.value
176 }
177
178 #[must_use]
180 pub const fn get_mode(&self) -> ProgressMode {
181 self.mode
182 }
183
184 #[must_use]
186 pub fn percentage(&self) -> u8 {
187 (self.value * 100.0).round() as u8
188 }
189
190 #[must_use]
192 pub fn is_complete(&self) -> bool {
193 self.mode == ProgressMode::Determinate && self.value >= 1.0
194 }
195
196 #[must_use]
198 pub fn is_indeterminate(&self) -> bool {
199 self.mode == ProgressMode::Indeterminate
200 }
201
202 pub fn set_value(&mut self, value: f32) {
204 self.value = value.clamp(0.0, 1.0);
205 }
206
207 pub fn increment(&mut self, delta: f32) {
209 self.value = (self.value + delta).clamp(0.0, 1.0);
210 }
211
212 fn fill_width(&self, total_width: f32) -> f32 {
214 total_width * self.value
215 }
216
217 #[must_use]
219 pub const fn get_track_color(&self) -> Color {
220 self.track_color
221 }
222
223 #[must_use]
225 pub const fn get_fill_color(&self) -> Color {
226 self.fill_color
227 }
228
229 #[must_use]
231 pub const fn get_label_color(&self) -> Color {
232 self.label_color
233 }
234
235 #[must_use]
237 pub const fn is_label_shown(&self) -> bool {
238 self.show_label
239 }
240
241 #[must_use]
243 pub const fn get_min_width(&self) -> f32 {
244 self.min_width
245 }
246
247 #[must_use]
249 pub const fn get_height(&self) -> f32 {
250 self.height
251 }
252
253 #[must_use]
255 pub const fn get_corner_radius(&self) -> f32 {
256 self.corner_radius
257 }
258}
259
260impl Widget for ProgressBar {
261 fn type_id(&self) -> TypeId {
262 TypeId::of::<Self>()
263 }
264
265 fn measure(&self, constraints: Constraints) -> Size {
266 let preferred_height = if self.show_label {
267 self.height + 20.0
268 } else {
269 self.height
270 };
271 let preferred = Size::new(self.min_width, preferred_height);
272 constraints.constrain(preferred)
273 }
274
275 fn layout(&mut self, bounds: Rect) -> LayoutResult {
276 self.bounds = bounds;
277 LayoutResult {
278 size: bounds.size(),
279 }
280 }
281
282 fn paint(&self, canvas: &mut dyn Canvas) {
283 let track_rect = Rect::new(self.bounds.x, self.bounds.y, self.bounds.width, self.height);
285 canvas.fill_rect(track_rect, self.track_color);
286
287 if self.mode == ProgressMode::Determinate && self.value > 0.0 {
289 let fill_width = self.fill_width(track_rect.width);
290 let fill_rect = Rect::new(track_rect.x, track_rect.y, fill_width, self.height);
291 canvas.fill_rect(fill_rect, self.fill_color);
292 }
293 }
294
295 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
296 None
298 }
299
300 fn children(&self) -> &[Box<dyn Widget>] {
301 &[]
302 }
303
304 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
305 &mut []
306 }
307
308 fn is_interactive(&self) -> bool {
309 false
310 }
311
312 fn is_focusable(&self) -> bool {
313 false
314 }
315
316 fn accessible_name(&self) -> Option<&str> {
317 self.accessible_name_value.as_deref()
318 }
319
320 fn accessible_role(&self) -> AccessibleRole {
321 AccessibleRole::ProgressBar
322 }
323
324 fn test_id(&self) -> Option<&str> {
325 self.test_id_value.as_deref()
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
336 fn test_progress_mode_default() {
337 assert_eq!(ProgressMode::default(), ProgressMode::Determinate);
338 }
339
340 #[test]
341 fn test_progress_mode_equality() {
342 assert_eq!(ProgressMode::Determinate, ProgressMode::Determinate);
343 assert_eq!(ProgressMode::Indeterminate, ProgressMode::Indeterminate);
344 assert_ne!(ProgressMode::Determinate, ProgressMode::Indeterminate);
345 }
346
347 #[test]
350 fn test_progress_bar_new() {
351 let pb = ProgressBar::new();
352 assert_eq!(pb.get_value(), 0.0);
353 assert_eq!(pb.get_mode(), ProgressMode::Determinate);
354 }
355
356 #[test]
357 fn test_progress_bar_with_value() {
358 let pb = ProgressBar::with_value(0.5);
359 assert_eq!(pb.get_value(), 0.5);
360 }
361
362 #[test]
363 fn test_progress_bar_default() {
364 let pb = ProgressBar::default();
365 assert_eq!(pb.get_value(), 0.0);
366 assert_eq!(pb.get_mode(), ProgressMode::Determinate);
367 assert!(!pb.is_label_shown());
368 }
369
370 #[test]
371 fn test_progress_bar_builder() {
372 let pb = ProgressBar::new()
373 .value(0.75)
374 .min_width(200.0)
375 .height(12.0)
376 .corner_radius(6.0)
377 .track_color(Color::WHITE)
378 .fill_color(Color::new(0.0, 1.0, 0.0, 1.0))
379 .with_label()
380 .label_color(Color::BLACK)
381 .accessible_name("Loading progress")
382 .test_id("main-progress");
383
384 assert_eq!(pb.get_value(), 0.75);
385 assert_eq!(pb.get_min_width(), 200.0);
386 assert_eq!(pb.get_height(), 12.0);
387 assert_eq!(pb.get_corner_radius(), 6.0);
388 assert_eq!(pb.get_track_color(), Color::WHITE);
389 assert_eq!(pb.get_fill_color(), Color::new(0.0, 1.0, 0.0, 1.0));
390 assert!(pb.is_label_shown());
391 assert_eq!(pb.get_label_color(), Color::BLACK);
392 assert_eq!(Widget::accessible_name(&pb), Some("Loading progress"));
393 assert_eq!(Widget::test_id(&pb), Some("main-progress"));
394 }
395
396 #[test]
399 fn test_progress_bar_value_clamped_min() {
400 let pb = ProgressBar::new().value(-0.5);
401 assert_eq!(pb.get_value(), 0.0);
402 }
403
404 #[test]
405 fn test_progress_bar_value_clamped_max() {
406 let pb = ProgressBar::new().value(1.5);
407 assert_eq!(pb.get_value(), 1.0);
408 }
409
410 #[test]
411 fn test_progress_bar_set_value() {
412 let mut pb = ProgressBar::new();
413 pb.set_value(0.6);
414 assert_eq!(pb.get_value(), 0.6);
415 }
416
417 #[test]
418 fn test_progress_bar_set_value_clamped() {
419 let mut pb = ProgressBar::new();
420 pb.set_value(2.0);
421 assert_eq!(pb.get_value(), 1.0);
422 pb.set_value(-1.0);
423 assert_eq!(pb.get_value(), 0.0);
424 }
425
426 #[test]
427 fn test_progress_bar_increment() {
428 let mut pb = ProgressBar::with_value(0.3);
429 pb.increment(0.2);
430 assert!((pb.get_value() - 0.5).abs() < 0.001);
431 }
432
433 #[test]
434 fn test_progress_bar_increment_clamped() {
435 let mut pb = ProgressBar::with_value(0.9);
436 pb.increment(0.5);
437 assert_eq!(pb.get_value(), 1.0);
438 }
439
440 #[test]
441 fn test_progress_bar_percentage() {
442 let pb = ProgressBar::with_value(0.0);
443 assert_eq!(pb.percentage(), 0);
444
445 let pb = ProgressBar::with_value(0.5);
446 assert_eq!(pb.percentage(), 50);
447
448 let pb = ProgressBar::with_value(1.0);
449 assert_eq!(pb.percentage(), 100);
450
451 let pb = ProgressBar::with_value(0.333);
452 assert_eq!(pb.percentage(), 33);
453 }
454
455 #[test]
458 fn test_progress_bar_mode() {
459 let pb = ProgressBar::new().mode(ProgressMode::Indeterminate);
460 assert_eq!(pb.get_mode(), ProgressMode::Indeterminate);
461 }
462
463 #[test]
464 fn test_progress_bar_indeterminate() {
465 let pb = ProgressBar::new().indeterminate();
466 assert!(pb.is_indeterminate());
467 }
468
469 #[test]
470 fn test_progress_bar_is_complete() {
471 let pb = ProgressBar::with_value(1.0);
472 assert!(pb.is_complete());
473
474 let pb = ProgressBar::with_value(0.99);
475 assert!(!pb.is_complete());
476
477 let pb = ProgressBar::with_value(1.0).indeterminate();
478 assert!(!pb.is_complete());
479 }
480
481 #[test]
484 fn test_progress_bar_min_width_min() {
485 let pb = ProgressBar::new().min_width(5.0);
486 assert_eq!(pb.get_min_width(), 20.0);
487 }
488
489 #[test]
490 fn test_progress_bar_height_min() {
491 let pb = ProgressBar::new().height(1.0);
492 assert_eq!(pb.get_height(), 4.0);
493 }
494
495 #[test]
496 fn test_progress_bar_corner_radius() {
497 let pb = ProgressBar::new().corner_radius(10.0);
498 assert_eq!(pb.get_corner_radius(), 10.0);
499 }
500
501 #[test]
502 fn test_progress_bar_corner_radius_min() {
503 let pb = ProgressBar::new().corner_radius(-5.0);
504 assert_eq!(pb.get_corner_radius(), 0.0);
505 }
506
507 #[test]
510 fn test_progress_bar_colors() {
511 let track = Color::new(0.78, 0.78, 0.78, 1.0);
512 let fill = Color::new(0.0, 0.5, 1.0, 1.0);
513 let label = Color::new(0.2, 0.2, 0.2, 1.0);
514
515 let pb = ProgressBar::new()
516 .track_color(track)
517 .fill_color(fill)
518 .label_color(label);
519
520 assert_eq!(pb.get_track_color(), track);
521 assert_eq!(pb.get_fill_color(), fill);
522 assert_eq!(pb.get_label_color(), label);
523 }
524
525 #[test]
528 fn test_progress_bar_show_label() {
529 let pb = ProgressBar::new().show_label(true);
530 assert!(pb.is_label_shown());
531
532 let pb = ProgressBar::new().show_label(false);
533 assert!(!pb.is_label_shown());
534 }
535
536 #[test]
537 fn test_progress_bar_with_label() {
538 let pb = ProgressBar::new().with_label();
539 assert!(pb.is_label_shown());
540 }
541
542 #[test]
545 fn test_progress_bar_fill_width() {
546 let pb = ProgressBar::with_value(0.5);
547 assert_eq!(pb.fill_width(100.0), 50.0);
548
549 let pb = ProgressBar::with_value(0.0);
550 assert_eq!(pb.fill_width(100.0), 0.0);
551
552 let pb = ProgressBar::with_value(1.0);
553 assert_eq!(pb.fill_width(100.0), 100.0);
554 }
555
556 #[test]
559 fn test_progress_bar_type_id() {
560 let pb = ProgressBar::new();
561 assert_eq!(Widget::type_id(&pb), TypeId::of::<ProgressBar>());
562 }
563
564 #[test]
565 fn test_progress_bar_measure() {
566 let pb = ProgressBar::new().min_width(150.0).height(10.0);
567 let size = pb.measure(Constraints::loose(Size::new(300.0, 100.0)));
568 assert_eq!(size.width, 150.0);
569 assert_eq!(size.height, 10.0);
570 }
571
572 #[test]
573 fn test_progress_bar_measure_with_label() {
574 let pb = ProgressBar::new()
575 .min_width(150.0)
576 .height(10.0)
577 .with_label();
578 let size = pb.measure(Constraints::loose(Size::new(300.0, 100.0)));
579 assert_eq!(size.width, 150.0);
580 assert_eq!(size.height, 30.0); }
582
583 #[test]
584 fn test_progress_bar_layout() {
585 let mut pb = ProgressBar::new();
586 let bounds = Rect::new(10.0, 20.0, 200.0, 8.0);
587 let result = pb.layout(bounds);
588 assert_eq!(result.size, Size::new(200.0, 8.0));
589 assert_eq!(pb.bounds, bounds);
590 }
591
592 #[test]
593 fn test_progress_bar_children() {
594 let pb = ProgressBar::new();
595 assert!(pb.children().is_empty());
596 }
597
598 #[test]
599 fn test_progress_bar_is_interactive() {
600 let pb = ProgressBar::new();
601 assert!(!pb.is_interactive());
602 }
603
604 #[test]
605 fn test_progress_bar_is_focusable() {
606 let pb = ProgressBar::new();
607 assert!(!pb.is_focusable());
608 }
609
610 #[test]
611 fn test_progress_bar_accessible_role() {
612 let pb = ProgressBar::new();
613 assert_eq!(pb.accessible_role(), AccessibleRole::ProgressBar);
614 }
615
616 #[test]
617 fn test_progress_bar_accessible_name() {
618 let pb = ProgressBar::new().accessible_name("Download progress");
619 assert_eq!(Widget::accessible_name(&pb), Some("Download progress"));
620 }
621
622 #[test]
623 fn test_progress_bar_accessible_name_none() {
624 let pb = ProgressBar::new();
625 assert_eq!(Widget::accessible_name(&pb), None);
626 }
627
628 #[test]
629 fn test_progress_bar_test_id() {
630 let pb = ProgressBar::new().test_id("upload-progress");
631 assert_eq!(Widget::test_id(&pb), Some("upload-progress"));
632 }
633
634 use presentar_core::draw::DrawCommand;
637 use presentar_core::RecordingCanvas;
638
639 #[test]
640 fn test_progress_bar_paint_draws_track() {
641 let mut pb = ProgressBar::new();
642 pb.layout(Rect::new(0.0, 0.0, 200.0, 8.0));
643
644 let mut canvas = RecordingCanvas::new();
645 pb.paint(&mut canvas);
646
647 assert!(canvas.command_count() >= 1);
649
650 match &canvas.commands()[0] {
651 DrawCommand::Rect { bounds, style, .. } => {
652 assert_eq!(bounds.width, 200.0);
653 assert_eq!(bounds.height, 8.0);
654 assert!(style.fill.is_some());
655 }
656 _ => panic!("Expected Rect command for track"),
657 }
658 }
659
660 #[test]
661 fn test_progress_bar_paint_zero_percent() {
662 let mut pb = ProgressBar::with_value(0.0);
663 pb.layout(Rect::new(0.0, 0.0, 200.0, 8.0));
664
665 let mut canvas = RecordingCanvas::new();
666 pb.paint(&mut canvas);
667
668 assert_eq!(canvas.command_count(), 1);
670 }
671
672 #[test]
673 fn test_progress_bar_paint_50_percent() {
674 let mut pb = ProgressBar::with_value(0.5);
675 pb.layout(Rect::new(0.0, 0.0, 200.0, 8.0));
676
677 let mut canvas = RecordingCanvas::new();
678 pb.paint(&mut canvas);
679
680 assert_eq!(canvas.command_count(), 2);
682
683 match &canvas.commands()[1] {
685 DrawCommand::Rect { bounds, .. } => {
686 assert_eq!(bounds.width, 100.0);
687 }
688 _ => panic!("Expected Rect command for fill"),
689 }
690 }
691
692 #[test]
693 fn test_progress_bar_paint_100_percent() {
694 let mut pb = ProgressBar::with_value(1.0);
695 pb.layout(Rect::new(0.0, 0.0, 200.0, 8.0));
696
697 let mut canvas = RecordingCanvas::new();
698 pb.paint(&mut canvas);
699
700 assert_eq!(canvas.command_count(), 2);
702
703 match &canvas.commands()[1] {
705 DrawCommand::Rect { bounds, .. } => {
706 assert_eq!(bounds.width, 200.0);
707 }
708 _ => panic!("Expected Rect command for fill"),
709 }
710 }
711
712 #[test]
713 fn test_progress_bar_paint_25_percent() {
714 let mut pb = ProgressBar::with_value(0.25);
715 pb.layout(Rect::new(0.0, 0.0, 200.0, 8.0));
716
717 let mut canvas = RecordingCanvas::new();
718 pb.paint(&mut canvas);
719
720 match &canvas.commands()[1] {
721 DrawCommand::Rect { bounds, .. } => {
722 assert_eq!(bounds.width, 50.0);
723 }
724 _ => panic!("Expected Rect command for fill"),
725 }
726 }
727
728 #[test]
729 fn test_progress_bar_paint_indeterminate_no_fill() {
730 let mut pb = ProgressBar::with_value(0.5).indeterminate();
731 pb.layout(Rect::new(0.0, 0.0, 200.0, 8.0));
732
733 let mut canvas = RecordingCanvas::new();
734 pb.paint(&mut canvas);
735
736 assert_eq!(canvas.command_count(), 1);
738 }
739
740 #[test]
741 fn test_progress_bar_paint_uses_track_color() {
742 let track_color = Color::new(0.9, 0.9, 0.9, 1.0);
743 let mut pb = ProgressBar::new().track_color(track_color);
744 pb.layout(Rect::new(0.0, 0.0, 200.0, 8.0));
745
746 let mut canvas = RecordingCanvas::new();
747 pb.paint(&mut canvas);
748
749 match &canvas.commands()[0] {
750 DrawCommand::Rect { style, .. } => {
751 assert_eq!(style.fill, Some(track_color));
752 }
753 _ => panic!("Expected Rect command"),
754 }
755 }
756
757 #[test]
758 fn test_progress_bar_paint_uses_fill_color() {
759 let fill_color = Color::new(0.0, 0.8, 0.0, 1.0);
760 let mut pb = ProgressBar::with_value(0.5).fill_color(fill_color);
761 pb.layout(Rect::new(0.0, 0.0, 200.0, 8.0));
762
763 let mut canvas = RecordingCanvas::new();
764 pb.paint(&mut canvas);
765
766 match &canvas.commands()[1] {
767 DrawCommand::Rect { style, .. } => {
768 assert_eq!(style.fill, Some(fill_color));
769 }
770 _ => panic!("Expected Rect command"),
771 }
772 }
773
774 #[test]
775 fn test_progress_bar_paint_position_from_layout() {
776 let mut pb = ProgressBar::with_value(0.5);
777 pb.layout(Rect::new(50.0, 100.0, 200.0, 8.0));
778
779 let mut canvas = RecordingCanvas::new();
780 pb.paint(&mut canvas);
781
782 match &canvas.commands()[0] {
784 DrawCommand::Rect { bounds, .. } => {
785 assert_eq!(bounds.x, 50.0);
786 assert_eq!(bounds.y, 100.0);
787 }
788 _ => panic!("Expected Rect command"),
789 }
790
791 match &canvas.commands()[1] {
793 DrawCommand::Rect { bounds, .. } => {
794 assert_eq!(bounds.x, 50.0);
795 assert_eq!(bounds.y, 100.0);
796 }
797 _ => panic!("Expected Rect command"),
798 }
799 }
800
801 #[test]
802 fn test_progress_bar_paint_uses_height() {
803 let mut pb = ProgressBar::new().height(16.0);
804 pb.layout(Rect::new(0.0, 0.0, 200.0, 16.0));
805
806 let mut canvas = RecordingCanvas::new();
807 pb.paint(&mut canvas);
808
809 match &canvas.commands()[0] {
810 DrawCommand::Rect { bounds, .. } => {
811 assert_eq!(bounds.height, 16.0);
812 }
813 _ => panic!("Expected Rect command"),
814 }
815 }
816}