1
//! Numeric amount entry widget.
2
//!
3
//! Wraps an [`Editor`] and restricts inserted characters to the subset
4
//! valid for rational amounts: digits, `-`, `.`, `/`.
5

            
6
use super::editor::{EditMode, Editor};
7

            
8
/// A text field accepting only amount characters (digits, `-`, `.`, `/`).
9
#[derive(Debug, Clone)]
10
pub struct AmountWidget {
11
    editor: Editor,
12
}
13

            
14
impl AmountWidget {
15
    #[must_use]
16
129
    pub fn new(mode: EditMode) -> Self {
17
129
        Self {
18
129
            editor: Editor::new(mode),
19
129
        }
20
129
    }
21

            
22
    #[must_use]
23
18
    pub fn with_value(mode: EditMode, value: impl Into<String>) -> Self {
24
18
        Self {
25
18
            editor: Editor::with_buffer(mode, value),
26
18
        }
27
18
    }
28

            
29
    /// Buffer text (the current amount string).
30
    #[must_use]
31
33
    pub fn value(&self) -> &str {
32
33
        self.editor.buffer()
33
33
    }
34

            
35
    /// Insert `c` only if it is a valid amount character.
36
29
    pub fn insert_char(&mut self, c: char) {
37
29
        if is_amount_char(c) {
38
23
            self.editor.insert_char(c);
39
23
        }
40
29
    }
41

            
42
    /// Direct access to the inner editor for motion and delete operations.
43
    #[must_use]
44
2
    pub fn as_editor_mut(&mut self) -> &mut Editor {
45
2
        &mut self.editor
46
2
    }
47
}
48

            
49
29
fn is_amount_char(c: char) -> bool {
50
29
    c.is_ascii_digit() || matches!(c, '-' | '.' | '/')
51
29
}
52

            
53
#[cfg(test)]
54
mod tests {
55
    use super::*;
56

            
57
    #[test]
58
1
    fn accepts_digits_and_separators() {
59
1
        let mut w = AmountWidget::new(EditMode::Emacs);
60
7
        for c in "100.5/3".chars() {
61
7
            w.insert_char(c);
62
7
        }
63
1
        assert_eq!(w.value(), "100.5/3");
64
1
    }
65

            
66
    #[test]
67
1
    fn accepts_negative_sign() {
68
1
        let mut w = AmountWidget::new(EditMode::Emacs);
69
1
        w.insert_char('-');
70
1
        w.insert_char('5');
71
1
        assert_eq!(w.value(), "-5");
72
1
    }
73

            
74
    #[test]
75
1
    fn rejects_letters() {
76
1
        let mut w = AmountWidget::new(EditMode::Emacs);
77
1
        w.insert_char('a');
78
1
        w.insert_char('b');
79
1
        assert_eq!(w.value(), "");
80
1
    }
81

            
82
    #[test]
83
1
    fn rejects_space_and_punctuation() {
84
1
        let mut w = AmountWidget::new(EditMode::Emacs);
85
1
        w.insert_char(' ');
86
1
        w.insert_char('@');
87
1
        w.insert_char('#');
88
1
        assert_eq!(w.value(), "");
89
1
    }
90

            
91
    #[test]
92
1
    fn delete_backward_via_editor_mut() {
93
1
        let mut w = AmountWidget::new(EditMode::Emacs);
94
1
        w.insert_char('5');
95
1
        w.insert_char('0');
96
1
        w.as_editor_mut().delete_backward();
97
1
        assert_eq!(w.value(), "5");
98
1
    }
99

            
100
    #[test]
101
1
    fn with_value_sets_initial_buffer() {
102
1
        let w = AmountWidget::with_value(EditMode::Emacs, "42/7");
103
1
        assert_eq!(w.value(), "42/7");
104
1
    }
105
}