adui_dioxus/components/
progress.rs

1use dioxus::prelude::*;
2
3/// Visual status of a Progress bar.
4#[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/// Progress visual type.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum ProgressType {
26    Line,
27    Circle,
28}
29
30/// Props for the Progress component (MVP subset).
31#[derive(Props, Clone, PartialEq)]
32pub struct ProgressProps {
33    /// Percentage in the range [0.0, 100.0]. Values outside this range
34    /// will be clamped.
35    #[props(default = 0.0)]
36    pub percent: f32,
37    /// Optional status. When omitted and `percent >= 100`, the component
38    /// will treat the status as `Success` for styling.
39    #[props(optional)]
40    pub status: Option<ProgressStatus>,
41    /// Whether to render the textual percentage.
42    #[props(default = true)]
43    pub show_info: bool,
44    /// Visual type of the progress indicator.
45    #[props(default = ProgressType::Line)]
46    pub r#type: ProgressType,
47    /// Optional stroke width (height for line, border width for circle).
48    #[props(optional)]
49    pub stroke_width: Option<f32>,
50    /// Extra CSS class name on the root element.
51    #[props(optional)]
52    pub class: Option<String>,
53    /// Inline style on the root element.
54    #[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/// Ant Design flavored Progress (MVP: line + simple circle).
77#[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 resolve_status_defaults_to_success_when_full() {
160        assert_eq!(resolve_status(100.0, None), ProgressStatus::Success);
161        assert_eq!(resolve_status(50.0, None), ProgressStatus::Normal);
162        assert_eq!(
163            resolve_status(80.0, Some(ProgressStatus::Exception)),
164            ProgressStatus::Exception
165        );
166    }
167}