1use dioxus::prelude::*;
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub enum ProgressStatus {
6 Normal,
7 Success,
8 Exception,
9 Active,
10}
11
12impl ProgressStatus {
13 fn as_class(&self) -> &'static str {
14 match self {
15 ProgressStatus::Normal => "adui-progress-status-normal",
16 ProgressStatus::Success => "adui-progress-status-success",
17 ProgressStatus::Exception => "adui-progress-status-exception",
18 ProgressStatus::Active => "adui-progress-status-active",
19 }
20 }
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum ProgressType {
26 Line,
27 Circle,
28}
29
30#[derive(Props, Clone, PartialEq)]
32pub struct ProgressProps {
33 #[props(default = 0.0)]
36 pub percent: f32,
37 #[props(optional)]
40 pub status: Option<ProgressStatus>,
41 #[props(default = true)]
43 pub show_info: bool,
44 #[props(default = ProgressType::Line)]
46 pub r#type: ProgressType,
47 #[props(optional)]
49 pub stroke_width: Option<f32>,
50 #[props(optional)]
52 pub class: Option<String>,
53 #[props(optional)]
55 pub style: Option<String>,
56}
57
58fn clamp_percent(value: f32) -> f32 {
59 if value.is_nan() {
60 0.0
61 } else {
62 value.clamp(0.0, 100.0)
63 }
64}
65
66fn resolve_status(percent: f32, status: Option<ProgressStatus>) -> ProgressStatus {
67 if let Some(s) = status {
68 s
69 } else if percent >= 100.0 {
70 ProgressStatus::Success
71 } else {
72 ProgressStatus::Normal
73 }
74}
75
76#[component]
78pub fn Progress(props: ProgressProps) -> Element {
79 let ProgressProps {
80 percent,
81 status,
82 show_info,
83 r#type,
84 stroke_width,
85 class,
86 style,
87 } = props;
88
89 let percent = clamp_percent(percent);
90 let status_value = resolve_status(percent, status);
91
92 let mut class_list = vec![
93 "adui-progress".to_string(),
94 status_value.as_class().to_string(),
95 ];
96 match r#type {
97 ProgressType::Line => class_list.push("adui-progress-line".into()),
98 ProgressType::Circle => class_list.push("adui-progress-circle".into()),
99 }
100 if let Some(extra) = class {
101 class_list.push(extra);
102 }
103 let class_attr = class_list.join(" ");
104 let style_attr = style.unwrap_or_default();
105
106 let display_text = format!("{}%", percent.round() as i32);
107
108 match r#type {
109 ProgressType::Line => {
110 let height = stroke_width.unwrap_or(6.0);
111 rsx! {
112 div { class: "{class_attr}", style: "{style_attr}",
113 div { class: "adui-progress-outer",
114 div { class: "adui-progress-inner",
115 div {
116 class: "adui-progress-bg",
117 style: "width:{percent}%;height:{height}px;",
118 }
119 }
120 }
121 if show_info {
122 span { class: "adui-progress-text", "{display_text}" }
123 }
124 }
125 }
126 }
127 ProgressType::Circle => {
128 let size = 80.0f32;
129 let border = stroke_width.unwrap_or(6.0);
130 let circle_style = format!(
131 "width:{size}px;height:{size}px;border-width:{border}px;background:conic-gradient(currentColor {percent}%, rgba(0,0,0,0.06) 0);",
132 );
133
134 rsx! {
135 div { class: "{class_attr}", style: "{style_attr}",
136 div { class: "adui-progress-circle-inner", style: "{circle_style}", }
137 if show_info {
138 div { class: "adui-progress-text", "{display_text}" }
139 }
140 }
141 }
142 }
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn clamp_percent_bounds_values() {
152 assert_eq!(clamp_percent(-10.0), 0.0);
153 assert_eq!(clamp_percent(0.0), 0.0);
154 assert_eq!(clamp_percent(50.0), 50.0);
155 assert_eq!(clamp_percent(120.0), 100.0);
156 }
157
158 #[test]
159 fn clamp_percent_handles_nan() {
160 assert_eq!(clamp_percent(f32::NAN), 0.0);
161 }
162
163 #[test]
164 fn clamp_percent_handles_infinity() {
165 assert_eq!(clamp_percent(f32::INFINITY), 100.0);
166 assert_eq!(clamp_percent(f32::NEG_INFINITY), 0.0);
167 }
168
169 #[test]
170 fn clamp_percent_boundary_values() {
171 assert_eq!(clamp_percent(0.0), 0.0);
172 assert_eq!(clamp_percent(100.0), 100.0);
173 assert_eq!(clamp_percent(0.1), 0.1);
174 assert_eq!(clamp_percent(99.9), 99.9);
175 assert_eq!(clamp_percent(-0.1), 0.0);
176 assert_eq!(clamp_percent(100.1), 100.0);
177 }
178
179 #[test]
180 fn resolve_status_defaults_to_success_when_full() {
181 assert_eq!(resolve_status(100.0, None), ProgressStatus::Success);
182 assert_eq!(resolve_status(50.0, None), ProgressStatus::Normal);
183 assert_eq!(
184 resolve_status(80.0, Some(ProgressStatus::Exception)),
185 ProgressStatus::Exception
186 );
187 }
188
189 #[test]
190 fn resolve_status_respects_explicit_status() {
191 assert_eq!(
192 resolve_status(0.0, Some(ProgressStatus::Success)),
193 ProgressStatus::Success
194 );
195 assert_eq!(
196 resolve_status(100.0, Some(ProgressStatus::Exception)),
197 ProgressStatus::Exception
198 );
199 assert_eq!(
200 resolve_status(50.0, Some(ProgressStatus::Active)),
201 ProgressStatus::Active
202 );
203 assert_eq!(
204 resolve_status(75.0, Some(ProgressStatus::Normal)),
205 ProgressStatus::Normal
206 );
207 }
208
209 #[test]
210 fn resolve_status_auto_success_at_100() {
211 assert_eq!(resolve_status(100.0, None), ProgressStatus::Success);
212 assert_eq!(resolve_status(100.1, None), ProgressStatus::Success);
213 }
214
215 #[test]
216 fn resolve_status_auto_normal_below_100() {
217 assert_eq!(resolve_status(0.0, None), ProgressStatus::Normal);
218 assert_eq!(resolve_status(50.0, None), ProgressStatus::Normal);
219 assert_eq!(resolve_status(99.9, None), ProgressStatus::Normal);
220 }
221
222 #[test]
223 fn progress_status_class_mapping() {
224 assert_eq!(
225 ProgressStatus::Normal.as_class(),
226 "adui-progress-status-normal"
227 );
228 assert_eq!(
229 ProgressStatus::Success.as_class(),
230 "adui-progress-status-success"
231 );
232 assert_eq!(
233 ProgressStatus::Exception.as_class(),
234 "adui-progress-status-exception"
235 );
236 assert_eq!(
237 ProgressStatus::Active.as_class(),
238 "adui-progress-status-active"
239 );
240 }
241
242 #[test]
243 fn progress_status_all_variants() {
244 let variants = [
245 ProgressStatus::Normal,
246 ProgressStatus::Success,
247 ProgressStatus::Exception,
248 ProgressStatus::Active,
249 ];
250 for variant in variants.iter() {
251 let class = variant.as_class();
252 assert!(!class.is_empty());
253 assert!(class.starts_with("adui-progress-status-"));
254 }
255 }
256
257 #[test]
258 fn progress_status_equality() {
259 assert_eq!(ProgressStatus::Normal, ProgressStatus::Normal);
260 assert_eq!(ProgressStatus::Success, ProgressStatus::Success);
261 assert_ne!(ProgressStatus::Normal, ProgressStatus::Success);
262 assert_ne!(ProgressStatus::Exception, ProgressStatus::Active);
263 }
264
265 #[test]
266 fn progress_status_clone() {
267 let original = ProgressStatus::Active;
268 let cloned = original;
269 assert_eq!(original, cloned);
270 assert_eq!(original.as_class(), cloned.as_class());
271 }
272
273 #[test]
274 fn progress_type_equality() {
275 assert_eq!(ProgressType::Line, ProgressType::Line);
276 assert_eq!(ProgressType::Circle, ProgressType::Circle);
277 assert_ne!(ProgressType::Line, ProgressType::Circle);
278 }
279
280 #[test]
281 fn progress_type_clone() {
282 let original = ProgressType::Circle;
283 let cloned = original;
284 assert_eq!(original, cloned);
285 }
286
287 #[test]
288 fn progress_type_debug() {
289 let line = ProgressType::Line;
290 let circle = ProgressType::Circle;
291 let line_str = format!("{:?}", line);
292 let circle_str = format!("{:?}", circle);
293 assert!(line_str.contains("Line"));
294 assert!(circle_str.contains("Circle"));
295 }
296
297 #[test]
298 fn progress_props_defaults() {
299 }
304
305 #[test]
306 fn clamp_percent_edge_cases() {
307 assert_eq!(clamp_percent(-0.1), 0.0);
309 assert_eq!(clamp_percent(-100.0), 0.0);
310
311 assert_eq!(clamp_percent(100.1), 100.0);
313 assert_eq!(clamp_percent(1000.0), 100.0);
314 }
315
316 #[test]
317 fn clamp_percent_precision() {
318 assert_eq!(clamp_percent(0.0001), 0.0001);
320 assert_eq!(clamp_percent(99.9999), 99.9999);
321 }
322
323 #[test]
324 fn resolve_status_explicit_overrides_auto() {
325 assert_eq!(
327 resolve_status(100.0, Some(ProgressStatus::Exception)),
328 ProgressStatus::Exception
329 );
330 assert_eq!(
331 resolve_status(0.0, Some(ProgressStatus::Success)),
332 ProgressStatus::Success
333 );
334 }
335
336 #[test]
337 fn progress_status_all_variants_equality() {
338 let statuses = [
339 ProgressStatus::Normal,
340 ProgressStatus::Success,
341 ProgressStatus::Exception,
342 ProgressStatus::Active,
343 ];
344 for (i, status1) in statuses.iter().enumerate() {
345 for (j, status2) in statuses.iter().enumerate() {
346 if i == j {
347 assert_eq!(status1, status2);
348 } else {
349 assert_ne!(status1, status2);
350 }
351 }
352 }
353 }
354
355 #[test]
356 fn progress_status_class_prefix() {
357 assert!(
359 ProgressStatus::Normal
360 .as_class()
361 .starts_with("adui-progress-status-")
362 );
363 assert!(
364 ProgressStatus::Success
365 .as_class()
366 .starts_with("adui-progress-status-")
367 );
368 assert!(
369 ProgressStatus::Exception
370 .as_class()
371 .starts_with("adui-progress-status-")
372 );
373 assert!(
374 ProgressStatus::Active
375 .as_class()
376 .starts_with("adui-progress-status-")
377 );
378 }
379
380 #[test]
381 fn progress_status_unique_classes() {
382 let classes: Vec<&str> = vec![
384 ProgressStatus::Normal.as_class(),
385 ProgressStatus::Success.as_class(),
386 ProgressStatus::Exception.as_class(),
387 ProgressStatus::Active.as_class(),
388 ];
389 for (i, class1) in classes.iter().enumerate() {
390 for (j, class2) in classes.iter().enumerate() {
391 if i != j {
392 assert_ne!(class1, class2);
393 }
394 }
395 }
396 }
397
398 #[test]
399 fn progress_type_all_variants() {
400 assert_eq!(ProgressType::Line, ProgressType::Line);
401 assert_eq!(ProgressType::Circle, ProgressType::Circle);
402 assert_ne!(ProgressType::Line, ProgressType::Circle);
403 }
404
405 #[test]
406 fn progress_type_copy_semantics() {
407 let progress_type = ProgressType::Circle;
409 let progress_type2 = progress_type;
410 assert_eq!(progress_type, progress_type2);
411 }
412
413 #[test]
414 fn clamp_percent_zero_and_hundred() {
415 assert_eq!(clamp_percent(0.0), 0.0);
417 assert_eq!(clamp_percent(100.0), 100.0);
418 }
419}