Skip to main content

gpui_kit_grid/state/editors/
number.rs

1//! `NumberEditor`: a number field.
2
3use 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/// The kind of number a [`NumberEditor`] commits.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum NumberKind {
17    /// `OwnedValue::Int`: a whole number.
18    Int,
19    /// `OwnedValue::Float`.
20    Float,
21    /// `OwnedValue::Decimal`: an exact decimal (the `decimal` feature).
22    #[cfg(feature = "decimal")]
23    Decimal,
24}
25
26/// Edits a cell as a number, on gpui-kit's `Input` with a number mask. The
27/// kind follows the cell's value (`Int`, `Float`, `Decimal`; `Float` for an
28/// empty cell) unless [`Self::with_kind`] sets it; an empty draft is
29/// `Empty`.
30///
31/// The field starts from the exact value, not the two decimals the cell
32/// shows. A draft is parsed with
33/// [`Self::with_decimal_separator`] between the whole and fractional
34/// digits; spaces and the other separator, a thousands one, are dropped.
35/// Typing starts an edit only with a digit, a sign or a separator.
36#[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    /// A number editor of the cell's kind, without limits, with `.` between
53    /// the whole and fractional digits.
54    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    /// Commits numbers of `kind`, whatever the cell holds.
65    pub fn with_kind(mut self, kind: NumberKind) -> Self {
66        self.kind = Some(kind);
67        self
68    }
69
70    /// Refuses a number below `min`.
71    pub fn with_min(mut self, min: impl Into<OwnedValue>) -> Self {
72        self.min = Some(min.into());
73        self
74    }
75
76    /// Refuses a number above `max`.
77    pub fn with_max(mut self, max: impl Into<OwnedValue>) -> Self {
78        self.max = Some(max.into());
79        self
80    }
81
82    /// Refuses a number with more than `scale` decimals (trailing zeros do
83    /// not count).
84    pub fn with_scale(mut self, scale: u32) -> Self {
85        self.scale = Some(scale);
86        self
87    }
88
89    /// Sets the character between the whole and fractional digits (default
90    /// `.`; `,` for a Russian-language product).
91    /// gpui-kit's number mask knows only `.`: with another separator the
92    /// field takes any text and the parser refuses what is not a number.
93    pub fn with_decimal_separator(mut self, separator: char) -> Self {
94        self.decimal_separator = separator;
95        self
96    }
97
98    /// The kind set with [`Self::with_kind`].
99    pub fn kind(&self) -> Option<NumberKind> {
100        self.kind
101    }
102
103    /// The smallest number taken.
104    pub fn min(&self) -> Option<&OwnedValue> {
105        self.min.as_ref()
106    }
107
108    /// The largest number taken.
109    pub fn max(&self) -> Option<&OwnedValue> {
110        self.max.as_ref()
111    }
112
113    /// The most decimals taken.
114    pub fn scale(&self) -> Option<u32> {
115        self.scale
116    }
117
118    /// The character between the whole and fractional digits.
119    pub fn decimal_separator(&self) -> char {
120        self.decimal_separator
121    }
122
123    /// The kind a cell of `value` is edited as.
124    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    /// Returns true if `text` may start a number: digits, a sign, a
134    /// separator.
135    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    /// Parses a draft as a number of `kind`.
144    fn parse(&self, draft: &str, kind: NumberKind) -> Result<OwnedValue, EditError> {
145        let separator = self.decimal_separator;
146        // Spaces and the thousands separator go; the decimal one becomes `.`.
147        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        // `parse::<f64>` takes "inf", "NaN" and exponents: a number of the
158        // field has digits, a sign and one point alone.
159        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        // The kind decides the value; an empty draft is `Empty`.
272        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        // Spaces and thousands separators go.
282        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        // A whole number has no decimals; trailing zeros are none.
291        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        // Limits and scale, with their texts.
307        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        // Another decimal separator: `.` is a thousands one then.
328        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        // Typing starts only a number.
335        assert!(editor.may_start("5") && editor.may_start("-") && editor.may_start("."));
336        assert!(!editor.may_start("a") && !editor.may_start("+a"));
337        // The kind follows the cell unless set.
338        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}