1use crate::theme::Theme;
9use egui::{Color32, Ui, Vec2};
10
11#[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#[derive(Clone, Debug)]
36pub struct ProgressProps {
37 pub value: Option<f32>, 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
97pub 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 ui.painter().rect_filled(rect, rounding, bg_color);
125
126 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 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}