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    #[must_use]
10    pub fn color_hex() -> Self {
11        Self {
12            text: String::new(),
13            parsed_val: Some(Err(egui::ecolor::ParseHexColorError::MissingHash)),
14            value_parser: Box::new(|str| Color32::from_hex(str)),
15            input_validator: Box::new(|_, s, i| {
16                if i == 0 {
17                    return s.starts_with('#');
18                }
19                s.chars().all(|c| c.is_ascii_hexdigit())
20            }),
21        }
22    }
23}
24
25impl<T: FromStr> ValText<T, T::Err> {
26    /// Only allows (0,1,2,3,4,5,6,7,8,9,.) and (-,+) at the beginning
27    #[must_use]
28    pub fn number() -> Self {
29        Self {
30            text: String::new(),
31            parsed_val: None,
32            value_parser: Box::new(|str| str.parse()),
33            input_validator: Box::new(|current_text, s, i| {
34                let current_has_no_dot = !current_text.contains('.');
35                (if i == 0 {
36                    s.starts_with('+') || s.starts_with('-')
37                } else { false })
38                || s.chars().all(|c| {
39                    (if current_has_no_dot { c == '.' } else { false })
40                    || c.is_ascii_digit()
41                })
42            }),
43        }
44    }
45
46    /// Only allows (0,1,2,3,4,5,6,7,8,9) and (-,+) at the beginning
47    #[must_use]
48    pub fn number_int() -> Self {
49        Self {
50            text: String::new(),
51            parsed_val: None,
52            value_parser: Box::new(|str| str.parse()),
53            input_validator: Box::new(|_, s, i| {
54                (if i == 0 {
55                    s.starts_with('+') || s.starts_with('-')
56                } else { false })
57                || s.chars().all(|c| c.is_ascii_digit())
58            })
59        }
60    }
61
62    /// Only allows (0,1,2,3,4,5,6,7,8,9) and (+) at the beginning
63    #[must_use]
64    pub fn number_uint() -> Self {
65        Self {
66            text: String::new(),
67            parsed_val: None,
68            value_parser: Box::new(|str| str.parse()),
69            input_validator: Box::new(|_, s, i| {
70                (if i == 0 {
71                    s.starts_with('+')
72                } else { false })
73                || s.chars().all(|c| c.is_ascii_digit())
74            })
75        }
76    }
77}
78
79#[derive(Debug, thiserror::Error)]
80pub enum PercentageParseError {
81    /// > 100
82    #[error("number is more then 100")]
83    OutOfRangeHigh,
84    /// < 0
85    #[error("number is less then 0")]
86    Neg,
87    #[error(transparent)]
88    ParseFloat(#[from] core::num::ParseFloatError),
89    #[error(transparent)]
90    ParseInt(#[from] core::num::ParseIntError),
91}
92
93impl ValText<f64, PercentageParseError> {
94    // todo unit test
95    /// A numarical percentage in the range of 0-100.\
96    /// Only allows (0,1,2,3,4,5,6,7,8,9,.) and (+) at the beginning
97    #[must_use]
98    pub fn percentage() -> Self {
99        Self {
100            text: String::new(),
101            parsed_val: None,
102            value_parser: Box::new(|str| {
103                let num = str.parse();
104                match num {
105                    Ok(num) => {
106                        if num > 100.0 {
107                            Err(PercentageParseError::OutOfRangeHigh)
108                        } else if num < 0.0 {
109                            Err(PercentageParseError::Neg)
110                        } else {
111                            Ok(num)
112                        }
113                    },
114                    Err(e) => Err(e.into()),
115                }
116            }),
117            input_validator: Box::new(|current_text, s, i| {
118                let current_text_no_des_len = current_text.split_once('.')
119                    .map(|(pre_dot, _)| pre_dot.len())
120                    .unwrap_or(current_text.len());
121                if current_text_no_des_len + s.len() > 3 && !current_text.contains('.') {
122                    return false;
123                }
124
125                let current_has_no_dot = !current_text.contains('.');
126                let all_num_or_dot = s.chars().all(|c| {
127                    (if current_has_no_dot { c == '.' } else { false })
128                    || c.is_ascii_digit()
129                });
130
131                if !current_text.is_empty()
132                    && current_text.as_bytes()[i.saturating_sub(1)] == b'.'
133                    && all_num_or_dot
134                {
135                    return true;
136                }
137
138                // only allow therd char if others are 00
139                if current_text_no_des_len == 2 {
140                    if s.starts_with('.') && all_num_or_dot {
141                        return true;
142                    } else if s == "0" {
143                        return current_text.starts_with("10") && !current_text.contains('.');
144                    }
145                    return false;
146                }
147
148                (if i == 0 {
149                    s.starts_with('+')
150                } else { false })
151                || all_num_or_dot
152            }),
153        }
154    }
155}
156
157impl ValText<f32, PercentageParseError> {
158    /// A numarical percentage in the range of 0-100.\
159    /// Only allows (0,1,2,3,4,5,6,7,8,9,.) and (+) at the beginning
160    #[must_use]
161    pub fn percentage() -> Self {
162        Self {
163            text: String::new(),
164            parsed_val: None,
165            value_parser: Box::new(|str| {
166                let num = str.parse();
167                match num {
168                    Ok(num) => {
169                        if num > 100.0 {
170                            Err(PercentageParseError::OutOfRangeHigh)
171                        } else if num < 0.0 {
172                            Err(PercentageParseError::Neg)
173                        } else {
174                            Ok(num)
175                        }
176                    },
177                    Err(e) => Err(e.into()),
178                }
179            }),
180            input_validator: Box::new(|current_text, s, i| {
181                let current_text_no_des_len = current_text.split_once('.')
182                    .map(|(pre_dot, _)| pre_dot.len())
183                    .unwrap_or(current_text.len());
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
220impl ValText<u32, PercentageParseError> {
221    /// A numarical percentage in the range of 0-100.\
222    /// Only allows (0,1,2,3,4,5,6,7,8,9) and (+) at the beginning
223    #[must_use]
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}