gpui_kit_grid/state/editors/
number.rs1use gpui_kit::component::input::MaskPattern;
4use gpui_kit::{App, Window};
5use rust_i18n::t;
6
7use super::{InputField, initial_text, push_raw_text};
8use crate::{
9 model::{CellValue, GridRow, OwnedValue},
10 state::edit::{CellEditor, EditError, EditInput, EditorOpen, EditorStart},
11};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum NumberKind {
17 Int,
19 Float,
21 #[cfg(feature = "decimal")]
23 Decimal,
24}
25
26#[derive(Clone, Debug)]
37pub struct NumberEditor {
38 kind: Option<NumberKind>,
39 min: Option<OwnedValue>,
40 max: Option<OwnedValue>,
41 scale: Option<u32>,
42 decimal_separator: char,
43}
44
45impl Default for NumberEditor {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl NumberEditor {
52 pub fn new() -> Self {
55 Self {
56 kind: None,
57 min: None,
58 max: None,
59 scale: None,
60 decimal_separator: '.',
61 }
62 }
63
64 pub fn with_kind(mut self, kind: NumberKind) -> Self {
66 self.kind = Some(kind);
67 self
68 }
69
70 pub fn with_min(mut self, min: impl Into<OwnedValue>) -> Self {
72 self.min = Some(min.into());
73 self
74 }
75
76 pub fn with_max(mut self, max: impl Into<OwnedValue>) -> Self {
78 self.max = Some(max.into());
79 self
80 }
81
82 pub fn with_scale(mut self, scale: u32) -> Self {
85 self.scale = Some(scale);
86 self
87 }
88
89 pub fn with_decimal_separator(mut self, separator: char) -> Self {
94 self.decimal_separator = separator;
95 self
96 }
97
98 pub fn kind(&self) -> Option<NumberKind> {
100 self.kind
101 }
102
103 pub fn min(&self) -> Option<&OwnedValue> {
105 self.min.as_ref()
106 }
107
108 pub fn max(&self) -> Option<&OwnedValue> {
110 self.max.as_ref()
111 }
112
113 pub fn scale(&self) -> Option<u32> {
115 self.scale
116 }
117
118 pub fn decimal_separator(&self) -> char {
120 self.decimal_separator
121 }
122
123 fn kind_for(&self, value: CellValue<'_>) -> NumberKind {
125 self.kind.unwrap_or(match value {
126 CellValue::Int(_) => NumberKind::Int,
127 #[cfg(feature = "decimal")]
128 CellValue::Decimal(_) => NumberKind::Decimal,
129 _ => NumberKind::Float,
130 })
131 }
132
133 fn may_start(&self, text: &str) -> bool {
136 text.chars().all(|ch| {
137 ch.is_ascii_digit()
138 || matches!(ch, '-' | '+' | '.' | ',')
139 || ch == self.decimal_separator
140 })
141 }
142
143 fn parse(&self, draft: &str, kind: NumberKind) -> Result<OwnedValue, EditError> {
145 let separator = self.decimal_separator;
146 let thousands = if separator == '.' { ',' } else { '.' };
148 let text: String = draft
149 .chars()
150 .filter(|ch| !ch.is_whitespace() && *ch != thousands && *ch != '\'')
151 .map(|ch| if ch == separator { '.' } else { ch })
152 .collect();
153 if text.is_empty() {
154 return Ok(OwnedValue::Empty);
155 }
156 let not_a_number = || EditError::new(t!("live_grid.edit.not_a_number"));
157 let is_plain = text
160 .trim_start_matches(['-', '+'])
161 .chars()
162 .all(|ch| ch.is_ascii_digit() || ch == '.')
163 && text.matches('.').count() <= 1
164 && text.chars().any(|ch| ch.is_ascii_digit());
165 if !is_plain {
166 return Err(not_a_number());
167 }
168 let decimals = text
169 .split_once('.')
170 .map_or(0, |(_, fraction)| fraction.trim_end_matches('0').len());
171 if let Some(scale) = self.scale
172 && decimals > scale as usize
173 {
174 return Err(EditError::new(t!(
175 "live_grid.edit.too_many_decimals",
176 scale = scale
177 )));
178 }
179 let value = match kind {
180 NumberKind::Int if decimals > 0 => {
181 return Err(EditError::new(t!("live_grid.edit.not_a_whole_number")));
182 }
183 NumberKind::Int => {
184 let whole = text.split('.').next().unwrap_or_default();
185 OwnedValue::Int(whole.parse().map_err(|_| not_a_number())?)
186 }
187 NumberKind::Float => {
188 let value: f64 = text.parse().map_err(|_| not_a_number())?;
189 if !value.is_finite() {
190 return Err(not_a_number());
191 }
192 OwnedValue::Float(value)
193 }
194 #[cfg(feature = "decimal")]
195 NumberKind::Decimal => OwnedValue::Decimal(
196 rust_decimal::Decimal::from_str_exact(text.trim_start_matches('+'))
197 .map_err(|_| not_a_number())?,
198 ),
199 };
200 let text_of = |limit: &OwnedValue| {
201 let mut out = String::new();
202 push_raw_text(limit.as_cell(), separator, &mut out);
203 out
204 };
205 if let Some(min) = &self.min
206 && value.cmp_total(min).is_lt()
207 {
208 return Err(EditError::new(t!(
209 "live_grid.edit.below_min",
210 min = text_of(min)
211 )));
212 }
213 if let Some(max) = &self.max
214 && value.cmp_total(max).is_gt()
215 {
216 return Err(EditError::new(t!(
217 "live_grid.edit.above_max",
218 max = text_of(max)
219 )));
220 }
221 Ok(value)
222 }
223}
224
225impl<R: GridRow> CellEditor<R> for NumberEditor {
226 fn open(&self, start: &EditorStart<'_, R>, window: &mut Window, cx: &mut App) -> EditorOpen {
227 if let EditInput::Typing(text) = start.input()
228 && !self.may_start(text)
229 {
230 return EditorOpen::Refuse;
231 }
232 let kind = self.kind_for(start.value());
233 let text = initial_text(start, self.decimal_separator);
234 let mask = (self.decimal_separator == '.').then_some(MaskPattern::Number {
235 separator: None,
236 fraction: None,
237 });
238 let state = InputField::input_state(
239 &text,
240 |state| match mask {
241 Some(mask) => state.mask_pattern(mask),
242 None => state,
243 },
244 window,
245 cx,
246 );
247 let editor = self.clone();
248 EditorOpen::Field(Box::new(
249 InputField::new(
250 state,
251 text,
252 true,
253 Box::new(move |draft| editor.parse(draft, kind)),
254 )
255 .with_decimal_separator(self.decimal_separator),
256 ))
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 fn error(result: Result<OwnedValue, EditError>) -> String {
265 result.expect_err("refused").message().to_string()
266 }
267
268 #[test]
269 fn test_number_editor_parse() {
270 let editor = NumberEditor::new();
271 assert_eq!(
273 editor.parse("-12", NumberKind::Int),
274 Ok(OwnedValue::Int(-12))
275 );
276 assert_eq!(
277 editor.parse("-0.5", NumberKind::Float),
278 Ok(OwnedValue::Float(-0.5))
279 );
280 assert_eq!(editor.parse(" ", NumberKind::Float), Ok(OwnedValue::Empty));
281 assert_eq!(
283 editor.parse("1 234.50", NumberKind::Float),
284 Ok(OwnedValue::Float(1234.5))
285 );
286 assert_eq!(
287 editor.parse("1,234", NumberKind::Int),
288 Ok(OwnedValue::Int(1234))
289 );
290 assert_eq!(
292 error(editor.parse("1.5", NumberKind::Int)),
293 "Enter a whole number"
294 );
295 assert_eq!(
296 editor.parse("2.00", NumberKind::Int),
297 Ok(OwnedValue::Int(2))
298 );
299 for text in ["abc", "1e3", "inf", "NaN", "1.2.3", "-", "."] {
300 assert_eq!(
301 error(editor.parse(text, NumberKind::Float)),
302 "Enter a number",
303 "{text}"
304 );
305 }
306 let limited = NumberEditor::new()
308 .with_min(OwnedValue::Int(0))
309 .with_max(OwnedValue::Float(200.5))
310 .with_scale(2);
311 assert_eq!(
312 error(limited.parse("-1", NumberKind::Float)),
313 "Enter 0 or more"
314 );
315 assert_eq!(
316 error(limited.parse("201", NumberKind::Float)),
317 "Enter 200.5 or less"
318 );
319 assert_eq!(
320 error(limited.parse("1.234", NumberKind::Float)),
321 "Use at most 2 decimals"
322 );
323 assert_eq!(
324 limited.parse("1.230", NumberKind::Float),
325 Ok(OwnedValue::Float(1.23))
326 );
327 let comma = NumberEditor::new().with_decimal_separator(',');
329 assert_eq!(
330 comma.parse("1.234,5", NumberKind::Float),
331 Ok(OwnedValue::Float(1234.5))
332 );
333 assert_eq!(comma.decimal_separator(), ',');
334 assert!(editor.may_start("5") && editor.may_start("-") && editor.may_start("."));
336 assert!(!editor.may_start("a") && !editor.may_start("+a"));
337 assert_eq!(editor.kind_for(CellValue::Int(1)), NumberKind::Int);
339 assert_eq!(editor.kind_for(CellValue::Empty), NumberKind::Float);
340 assert_eq!(
341 editor
342 .clone()
343 .with_kind(NumberKind::Int)
344 .kind_for(CellValue::Float(1.)),
345 NumberKind::Int
346 );
347 assert_eq!(limited.scale(), Some(2));
348 assert_eq!(limited.min(), Some(&OwnedValue::Int(0)));
349 assert_eq!(limited.max(), Some(&OwnedValue::Float(200.5)));
350 assert_eq!(editor.kind(), None);
351 }
352
353 #[cfg(feature = "decimal")]
354 #[test]
355 fn test_number_editor_decimal() {
356 let editor = NumberEditor::new().with_scale(2);
357 let value = editor.parse("12.30", NumberKind::Decimal);
358 assert!(
359 matches!(&value, Ok(OwnedValue::Decimal(value)) if value.to_string() == "12.30"),
360 "{value:?}"
361 );
362 assert_eq!(
363 error(editor.parse("1.234", NumberKind::Decimal)),
364 "Use at most 2 decimals"
365 );
366 }
367}