Skip to main content

egui_shadcn/
progress.rs

1//! Progress component - determinate and indeterminate progress bars.
2//!
3//! # Example
4//! ```ignore
5//! progress(ui, &theme, ProgressProps::new(Some(75.0)));
6//! ```
7
8use crate::theme::Theme;
9use egui::{Color32, Ui, Vec2};
10
11// =============================================================================
12// ProgressSize / ProgressVariant
13// =============================================================================
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub enum ProgressSize {
17    Size1,
18    #[default]
19    Size2,
20    Size3,
21}
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub enum ProgressVariant {
25    Classic,
26    #[default]
27    Surface,
28    Soft,
29}
30
31// =============================================================================
32// ProgressProps
33// =============================================================================
34
35#[derive(Clone, Debug)]
36pub struct ProgressProps {
37    pub value: Option<f32>, // None = indeterminate
38    pub max: f32,
39    pub size: ProgressSize,
40    pub variant: ProgressVariant,
41    pub color: Option<Color32>,
42    pub radius: Option<f32>,
43    pub duration_ms: u32,
44    pub high_contrast: bool,
45}
46
47impl ProgressProps {
48    pub fn new(value: Option<f32>) -> Self {
49        Self {
50            value,
51            max: 100.0,
52            size: ProgressSize::Size2,
53            variant: ProgressVariant::Surface,
54            color: None,
55            radius: None,
56            duration_ms: 1500,
57            high_contrast: false,
58        }
59    }
60
61    pub fn max(mut self, max: f32) -> Self {
62        self.max = max.max(0.01);
63        self
64    }
65
66    pub fn size(mut self, size: ProgressSize) -> Self {
67        self.size = size;
68        self
69    }
70
71    pub fn variant(mut self, variant: ProgressVariant) -> Self {
72        self.variant = variant;
73        self
74    }
75
76    pub fn color(mut self, color: Color32) -> Self {
77        self.color = Some(color);
78        self
79    }
80
81    pub fn radius(mut self, radius: f32) -> Self {
82        self.radius = Some(radius);
83        self
84    }
85
86    pub fn duration_ms(mut self, duration_ms: u32) -> Self {
87        self.duration_ms = duration_ms;
88        self
89    }
90
91    pub fn high_contrast(mut self, hc: bool) -> Self {
92        self.high_contrast = hc;
93        self
94    }
95}
96
97// =============================================================================
98// Main function
99// =============================================================================
100
101/// Render a progress bar.
102pub fn progress(ui: &mut Ui, theme: &Theme, props: ProgressProps) {
103    let accent = props.color.unwrap_or(theme.palette.primary);
104
105    let (bg_color, fg_color) = match props.variant {
106        ProgressVariant::Classic => (theme.palette.muted, accent),
107        ProgressVariant::Surface => (theme.palette.muted.gamma_multiply(0.5), accent),
108        ProgressVariant::Soft => (accent.gamma_multiply(0.2), accent),
109    };
110
111    let height = match props.size {
112        ProgressSize::Size1 => 4.0,
113        ProgressSize::Size2 => 8.0,
114        ProgressSize::Size3 => 12.0,
115    };
116
117    let available_width = ui.available_width();
118    let rounding = props.radius.unwrap_or(height / 2.0);
119
120    let (rect, _response) =
121        ui.allocate_exact_size(Vec2::new(available_width, height), egui::Sense::hover());
122
123    // Background
124    ui.painter().rect_filled(rect, rounding, bg_color);
125
126    // Foreground (progress)
127    if let Some(value) = props.value {
128        let progress = (value / props.max).clamp(0.0, 1.0);
129        let progress_width = rect.width() * progress;
130
131        if progress_width > 0.0 {
132            let progress_rect =
133                egui::Rect::from_min_size(rect.min, Vec2::new(progress_width, height));
134            ui.painter().rect_filled(progress_rect, rounding, fg_color);
135        }
136    } else {
137        // Indeterminate animation
138        let time = ui.ctx().input(|i| i.time) as f32;
139        let speed = 1000.0 / props.duration_ms.max(1) as f32 * 2.25;
140        let anim_progress = ((time * speed).sin() + 1.0) / 2.0;
141        let bar_width = rect.width() * 0.3;
142        let offset = (rect.width() - bar_width) * anim_progress;
143
144        let anim_rect = egui::Rect::from_min_size(
145            rect.min + Vec2::new(offset, 0.0),
146            Vec2::new(bar_width, height),
147        );
148        ui.painter().rect_filled(anim_rect, rounding, fg_color);
149        ui.ctx().request_repaint();
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn progress_size_default() {
159        assert_eq!(ProgressSize::default(), ProgressSize::Size2);
160    }
161
162    #[test]
163    fn progress_variant_default() {
164        assert_eq!(ProgressVariant::default(), ProgressVariant::Surface);
165    }
166
167    #[test]
168    fn progress_props_determinate() {
169        let props = ProgressProps::new(Some(50.0));
170        assert_eq!(props.value, Some(50.0));
171        assert_eq!(props.max, 100.0);
172    }
173
174    #[test]
175    fn progress_props_indeterminate() {
176        let props = ProgressProps::new(None);
177        assert!(props.value.is_none());
178    }
179}