Skip to main content

dioxus_element_plug/components/
progress.rs

1use dioxus::prelude::*;
2
3/// Progress type
4#[derive(Clone, PartialEq)]
5pub enum ProgressType {
6    Line,
7    Circle,
8    Dashboard,
9}
10
11impl ProgressType {
12    pub fn as_str(&self) -> &'static str {
13        match self {
14            ProgressType::Line => "line",
15            ProgressType::Circle => "circle",
16            ProgressType::Dashboard => "dashboard",
17        }
18    }
19}
20
21/// Progress status
22#[derive(Clone, PartialEq)]
23pub enum ProgressStatus {
24    Default,
25    Success,
26    Exception,
27    Warning,
28}
29
30impl ProgressStatus {
31    pub fn as_class(&self) -> &'static str {
32        match self {
33            ProgressStatus::Default => "",
34            ProgressStatus::Success => "is-success",
35            ProgressStatus::Exception => "is-exception",
36            ProgressStatus::Warning => "is-warning",
37        }
38    }
39}
40
41/// Progress props
42#[derive(Props, Clone, PartialEq)]
43pub struct ProgressProps {
44    /// Progress type
45    #[props(default = ProgressType::Line)]
46    pub progress_type: ProgressType,
47
48    /// Percentage (0-100)
49    #[props(default = 0)]
50    pub percentage: u32,
51
52    /// Current status
53    #[props(default = ProgressStatus::Default)]
54    pub status: ProgressStatus,
55
56    /// Whether to set indeterminate progress
57    #[props(default = false)]
58    pub indeterminate: bool,
59
60    /// Stroke width in pixels
61    #[props(default = 6)]
62    pub stroke_width: u32,
63
64    /// Whether to place percentage inside progress bar
65    #[props(default = false)]
66    pub text_inside: bool,
67
68    /// Canvas width for circle/dashboard type
69    #[props(default = 126)]
70    pub width: u32,
71
72    /// Whether to show percentage text
73    #[props(default = true)]
74    pub show_text: bool,
75
76    /// Background color of progress bar
77    #[props(default)]
78    pub color: Option<String>,
79
80    /// Whether to show stripes
81    #[props(default = false)]
82    pub striped: bool,
83
84    /// Whether stripes flow
85    #[props(default = false)]
86    pub striped_flow: bool,
87
88    /// Additional CSS classes
89    #[props(default)]
90    pub class: Option<String>,
91
92    /// Inline styles
93    #[props(default)]
94    pub style: Option<String>,
95}
96
97/// Progress component for showing task completion
98///
99/// ## Example
100///
101/// ```rust,ignore
102/// rsx! {
103///     Progress { percentage: 70, status: ProgressStatus::Success }
104/// }
105/// ```
106#[component]
107pub fn Progress(props: ProgressProps) -> Element {
108    let percentage = props.percentage.min(100);
109
110    let mut class_names = vec!["el-progress".to_string()];
111    class_names.push(format!("el-progress--{}", props.progress_type.as_str()));
112
113    let status_class = props.status.as_class();
114    if !status_class.is_empty() {
115        class_names.push(status_class.to_string());
116    }
117
118    if props.indeterminate {
119        class_names.push("el-progress--indeterminate".to_string());
120    }
121
122    if props.striped {
123        class_names.push("el-progress--striped".to_string());
124    }
125
126    if props.striped_flow {
127        class_names.push("el-progress--striped-flow".to_string());
128    }
129
130    if props.text_inside && props.progress_type == ProgressType::Line {
131        class_names.push("el-progress--text-inside".to_string());
132    }
133
134    if let Some(ref custom_class) = props.class {
135        class_names.push(custom_class.clone());
136    }
137
138    let class_string = class_names.join(" ");
139    let style_string = props.style.clone().unwrap_or_default();
140
141    let bar_color = props.color.clone().unwrap_or_else(|| {
142        match props.status {
143            ProgressStatus::Success => "#67C23A".to_string(),
144            ProgressStatus::Exception => "#F56C6C".to_string(),
145            ProgressStatus::Warning => "#E6A23C".to_string(),
146            ProgressStatus::Default => "#409EFF".to_string(),
147        }
148    });
149
150    let display_text = format!("{}%", percentage);
151
152    if props.progress_type == ProgressType::Line {
153        let bar_style = format!("width: {}%; background-color: {};", percentage, bar_color);
154        let height_style = format!("height: {}px;", props.stroke_width);
155
156        rsx! {
157            div {
158                class: "{class_string}",
159                style: "{style_string}",
160                div {
161                    class: "el-progress__outer",
162                    style: "{height_style}",
163                    div {
164                        class: "el-progress__inner",
165                        div {
166                            class: "el-progress__bar",
167                            style: "{bar_style}",
168                            if props.text_inside && props.show_text {
169                                div {
170                                    class: "el-progress__text el-progress__text--inside",
171                                    "{display_text}"
172                                }
173                            }
174                        }
175                    }
176                }
177                if props.show_text && !props.text_inside {
178                    div {
179                        class: "el-progress__text",
180                        if props.status == ProgressStatus::Success {
181                            i { class: "el-icon-circle-check" }
182                        } else if props.status == ProgressStatus::Exception {
183                            i { class: "el-icon-circle-close" }
184                        } else if props.status == ProgressStatus::Warning {
185                            i { class: "el-icon-warning" }
186                        } else {
187                            "{display_text}"
188                        }
189                    }
190                }
191            }
192        }
193    } else {
194        // Circle / Dashboard - using CSS conic-gradient
195        let size = props.width;
196        let deg = (percentage as f64 / 100.0) * 360.0;
197        let bg_gradient = if props.progress_type == ProgressType::Dashboard {
198            format!("conic-gradient(from 135deg, {} 0deg, {} {}deg, transparent {}deg)",
199                bar_color, bar_color, deg * 0.75, deg * 0.75)
200        } else {
201            format!("conic-gradient({} {}deg, var(--el-fill-color-light) {}deg)",
202                bar_color, deg, deg)
203        };
204
205        let inner_size = size - props.stroke_width * 2;
206
207        rsx! {
208            div {
209                class: "{class_string}",
210                style: "{style_string}",
211                div {
212                    class: "el-progress-circle",
213                    style: "width: {size}px; height: {size}px; border-radius: 50%; background: {bg_gradient}; display: flex; align-items: center; justify-content: center;",
214                    div {
215                        style: "width: {inner_size}px; height: {inner_size}px; border-radius: 50%; background: var(--el-bg-color); display: flex; align-items: center; justify-content: center;",
216                        if props.show_text {
217                            div {
218                                class: "el-progress__text",
219                                if props.status == ProgressStatus::Success {
220                                    i { class: "el-icon-circle-check" }
221                                } else if props.status == ProgressStatus::Exception {
222                                    i { class: "el-icon-circle-close" }
223                                } else {
224                                    "{display_text}"
225                                }
226                            }
227                        }
228                    }
229                }
230            }
231        }
232    }
233}