Skip to main content

dioxus_element_plug/components/
rate.rs

1use dioxus::prelude::*;
2
3/// Rate size variants
4#[derive(Clone, PartialEq)]
5pub enum RateSize {
6    Large,
7    Default,
8    Small,
9}
10
11impl RateSize {
12    pub fn as_class(&self) -> &'static str {
13        match self {
14            RateSize::Large => "el-rate--large",
15            RateSize::Default => "",
16            RateSize::Small => "el-rate--small",
17        }
18    }
19}
20
21/// Rate props
22#[derive(Props, Clone, PartialEq)]
23pub struct RateProps {
24    /// Current rating value
25    #[props(default = 0.0)]
26    pub model_value: f64,
27
28    /// Maximum rating value (number of stars)
29    #[props(default = 5)]
30    pub max: u32,
31
32    /// Whether the rate is disabled
33    #[props(default = false)]
34    pub disabled: bool,
35
36    /// Whether to allow half stars
37    #[props(default = false)]
38    pub allow_half: bool,
39
40    /// Whether to allow clearing (click again to clear)
41    #[props(default = false)]
42    pub clearable: bool,
43
44    /// Whether to show text
45    #[props(default = false)]
46    pub show_text: bool,
47
48    /// Whether to show score
49    #[props(default = false)]
50    pub show_score: bool,
51
52    /// Text array for each rating level
53    #[props(default)]
54    pub texts: Vec<String>,
55
56    /// Color for active stars
57    #[props(default = "#F7BA2A".to_string())]
58    pub active_color: String,
59
60    /// Color for inactive stars
61    #[props(default = "#C6D1DE".to_string())]
62    pub inactive_color: String,
63
64    /// Rate size
65    #[props(default = RateSize::Default)]
66    pub size: RateSize,
67
68    /// Change event handler
69    #[props(default)]
70    pub on_change: Option<EventHandler<f64>>,
71
72    /// Additional CSS classes
73    #[props(default)]
74    pub class: Option<String>,
75
76    /// Inline styles
77    #[props(default)]
78    pub style: Option<String>,
79}
80
81/// Rate component for star ratings
82///
83/// ## Example
84///
85/// ```rust,ignore
86/// rsx! {
87///     Rate {
88///         model_value: 3.5,
89///         allow_half: true,
90///         max: 5,
91///         on_change: move |v| println!("Rating: {}", v),
92///     }
93/// }
94/// ```
95#[component]
96pub fn Rate(props: RateProps) -> Element {
97    let mut class_names = vec!["el-rate".to_string()];
98
99    let size_class = props.size.as_class();
100    if !size_class.is_empty() {
101        class_names.push(size_class.to_string());
102    }
103
104    if props.disabled {
105        class_names.push("is-disabled".to_string());
106    }
107
108    if let Some(ref custom_class) = props.class {
109        class_names.push(custom_class.clone());
110    }
111
112    let class_string = class_names.join(" ");
113    let style_string = props.style.clone().unwrap_or_default();
114
115    let current_value = props.model_value;
116    let text_index = if current_value > 0.0 {
117        (current_value.ceil() as usize).saturating_sub(1).min(props.texts.len().saturating_sub(1))
118    } else {
119        0
120    };
121
122    let display_text = if props.show_text && !props.texts.is_empty() {
123        props.texts.get(text_index).cloned().unwrap_or_default()
124    } else if props.show_score {
125        format!("{}", current_value)
126    } else {
127        String::new()
128    };
129
130    let stars: Vec<(f64, String, &'static str)> = (1..=props.max)
131        .map(|i| {
132            let star_value = i as f64;
133            let is_active = star_value <= current_value;
134            let is_half = props.allow_half && star_value - 0.5 == current_value;
135            let color = if is_active { props.active_color.clone() } else { props.inactive_color.clone() };
136            let star_char = if is_active { "★" } else if is_half { "⯨" } else { "☆" };
137            (star_value, color, star_char)
138        })
139        .collect();
140
141    rsx! {
142        div {
143            class: "{class_string}",
144            style: "{style_string}",
145            role: "slider",
146            aria_valuenow: "{current_value}",
147            aria_valuemax: "{props.max}",
148            for (star_value, color, star_char) in stars.into_iter() {
149                span {
150                    class: "el-rate__item",
151                    style: "cursor: pointer;",
152                    onclick: move |_| {
153                        if !props.disabled {
154                            if props.clearable && star_value == current_value {
155                                if let Some(handler) = props.on_change {
156                                    handler.call(0.0);
157                                }
158                            } else {
159                                if let Some(handler) = props.on_change {
160                                    handler.call(star_value);
161                                }
162                            }
163                        }
164                    },
165                    i {
166                        class: "el-rate__icon",
167                        style: "color: {color};",
168                        "{star_char}"
169                    }
170                }
171            }
172            if props.show_text || props.show_score {
173                span {
174                    class: "el-rate__text",
175                    "{display_text}"
176                }
177            }
178        }
179    }
180}