presentar_widgets/
progress_bar.rs

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