Skip to main content

egui_typed_input/
impls.rs

1use core::str::FromStr;
2use egui::Color32;
3
4use crate::ValText;
5
6impl ValText<Color32, egui::ecolor::ParseHexColorError> {
7    /// A hex color starting with `#`, parsed using [`Color32::from_hex`].\
8    /// Supports the 3, 4, 6, and 8-digit formats.
9    pub fn color_hex() -> Self {
10        Self {
11            text: String::new(),
12            parsed_val: Some(Err(egui::ecolor::ParseHexColorError::MissingHash)),
13            value_parser: Box::new(Color32::from_hex),
14            input_validator: Box::new(|_, s, i| {
15                if i == 0 {
16                    return s.starts_with('#');
17                }
18                s.chars().all(|c| c.is_ascii_hexdigit())
19            }),
20        }
21    }
22}
23
24impl<T: FromStr> ValText<T, T::Err> {
25    /// Only allows (0,1,2,3,4,5,6,7,8,9,.) and (-,+) at the beginning
26    pub fn number() -> Self {
27        Self {
28            text: String::new(),
29            parsed_val: None,
30            value_parser: Box::new(|str| str.parse()),
31            input_validator: Box::new(|current_text, s, i| {
32                let current_has_no_dot = !current_text.contains('.');
33                (if i == 0 {
34                    s.starts_with('+') || s.starts_with('-')
35                } else { false })
36                || s.chars().all(|c| {
37                    (if current_has_no_dot { c == '.' } else { false })
38                    || c.is_ascii_digit()
39                })
40            }),
41        }
42    }
43
44    /// Only allows (0,1,2,3,4,5,6,7,8,9) and (-,+) at the beginning
45    pub fn number_int() -> Self {
46        Self {
47            text: String::new(),
48            parsed_val: None,
49            value_parser: Box::new(|str| str.parse()),
50            input_validator: Box::new(|_, s, i| {
51                (if i == 0 {
52                    s.starts_with('+') || s.starts_with('-')
53                } else { false })
54                || s.chars().all(|c| c.is_ascii_digit())
55            })
56        }
57    }
58
59    /// Only allows (0,1,2,3,4,5,6,7,8,9) and (+) at the beginning
60    pub fn number_uint() -> Self {
61        Self {
62            text: String::new(),
63            parsed_val: None,
64            value_parser: Box::new(|str| str.parse()),
65            input_validator: Box::new(|_, s, i| {
66                (if i == 0 {
67                    s.starts_with('+')
68                } else { false })
69                || s.chars().all(|c| c.is_ascii_digit())
70            })
71        }
72    }
73}
74
75#[derive(Debug, thiserror::Error)]
76pub enum PercentageParseError {
77    /// > 100
78    #[error("number is more then 100")]
79    OutOfRangeHigh,
80    /// < 0
81    #[error("number is less then 0")]
82    Neg,
83    #[error(transparent)]
84    ParseFloat(#[from] core::num::ParseFloatError),
85    #[error(transparent)]
86    ParseInt(#[from] core::num::ParseIntError),
87}
88
89// todo add macro to reduce code duplecation or use numtraits as default optional feature
90
91impl ValText<f64, PercentageParseError> {
92    // todo unit test
93    /// A numarical percentage in the range of 0-100.\
94    /// Only allows (0,1,2,3,4,5,6,7,8,9,.) and (+) at the beginning
95    pub fn percentage() -> Self {
96        Self {
97            text: String::new(),
98            parsed_val: None,
99            value_parser: Box::new(|str| {
100                let num = str.parse();
101                match num {
102                    Ok(num) => {
103                        if num > 100.0 {
104                            Err(PercentageParseError::OutOfRangeHigh)
105                        } else if num < 0.0 {
106                            Err(PercentageParseError::Neg)
107                        } else {
108                            Ok(num)
109                        }
110                    },
111                    Err(e) => Err(e.into()),
112                }
113            }),
114            input_validator: Box::new(|current_text, s, i| {
115                let current_text_no_des_len = current_text.split_once('.')
116                    .map_or(
117                        current_text.len(),
118                        |(pre_dot, _)| pre_dot.len()
119                    );
120                if current_text_no_des_len + s.len() > 3 && !current_text.contains('.') {
121                    return false;
122                }
123
124                let current_has_no_dot = !current_text.contains('.');
125                let all_num_or_dot = s.chars().all(|c| {
126                    (if current_has_no_dot { c == '.' } else { false })
127                    || c.is_ascii_digit()
128                });
129
130                if !current_text.is_empty()
131                    && current_text.as_bytes()[i.saturating_sub(1)] == b'.'
132                    && all_num_or_dot
133                {
134                    return true;
135                }
136
137                // only allow therd char if others are 00
138                if current_text_no_des_len == 2 {
139                    if s.starts_with('.') && all_num_or_dot {
140                        return true;
141                    } else if s == "0" {
142                        return current_text.starts_with("10") && !current_text.contains('.');
143                    }
144                    return false;
145                }
146
147                (if i == 0 {
148                    s.starts_with('+')
149                } else { false })
150                || all_num_or_dot
151            }),
152        }
153    }
154}
155
156impl ValText<f32, PercentageParseError> {
157    /// A numarical percentage in the range of 0-100.\
158    /// Only allows (0,1,2,3,4,5,6,7,8,9,.) and (+) at the beginning
159    pub fn percentage() -> Self {
160        Self {
161            text: String::new(),
162            parsed_val: None,
163            value_parser: Box::new(|str| {
164                let num = str.parse();
165                match num {
166                    Ok(num) => {
167                        if num > 100.0 {
168                            Err(PercentageParseError::OutOfRangeHigh)
169                        } else if num < 0.0 {
170                            Err(PercentageParseError::Neg)
171                        } else {
172                            Ok(num)
173                        }
174                    },
175                    Err(e) => Err(e.into()),
176                }
177            }),
178            input_validator: Box::new(|current_text, s, i| {
179                let current_text_no_des_len = current_text.split_once('.')
180                    .map_or(
181                        current_text.len(),
182                        |(pre_dot, _)| pre_dot.len()
183                    );
184                if current_text_no_des_len + s.len() > 3 && !current_text.contains('.') {
185                    return false;
186                }
187
188                let current_has_no_dot = !current_text.contains('.');
189                let all_num_or_dot = s.chars().all(|c| {
190                    (if current_has_no_dot { c == '.' } else { false })
191                    || c.is_ascii_digit()
192                });
193
194                if !current_text.is_empty()
195                    && current_text.as_bytes()[i.saturating_sub(1)] == b'.'
196                    && all_num_or_dot
197                {
198                    return true;
199                }
200
201                // only allow therd char if others are 00
202                if current_text_no_des_len == 2 {
203                    if s.starts_with('.') && all_num_or_dot {
204                        return true;
205                    } else if s == "0" {
206                        return current_text.starts_with("10") && !current_text.contains('.');
207                    }
208                    return false;
209                }
210
211                (if i == 0 {
212                    s.starts_with('+')
213                } else { false })
214                || all_num_or_dot
215            }),
216        }
217    }
218}
219
220// todo add allow % and require % at the end options
221impl ValText<u32, PercentageParseError> {
222    /// A numarical percentage in the range of 0-100.\
223    /// Only allows (0,1,2,3,4,5,6,7,8,9) and (+) at the beginning
224    pub fn percentage_uint() -> Self {
225        Self {
226            text: String::new(),
227            parsed_val: None,
228            value_parser: Box::new(|str| {
229                let num = str.parse();
230                match num {
231                    Ok(num) => {
232                        if num > 100 {
233                            Err(PercentageParseError::OutOfRangeHigh)
234                        } else {
235                            Ok(num)
236                        }
237                    },
238                    Err(e) => Err(e.into()),
239                }
240            }),
241            input_validator: Box::new(|current_text, s, i| {
242                if current_text.len() + s.len() > 3 {
243                    return false;
244                }
245
246                // only allow therd char if others are 00
247                if current_text.len() == 2 {
248                    if s == "0" {
249                        return current_text.starts_with("10");
250                    }
251                    return false;
252                }
253
254                (if i == 0 {
255                    s.starts_with('+')
256                } else { false })
257                || s.chars().all(|c| c.is_ascii_digit())
258            }),
259        }
260    }
261}