fm/modes/menu/
input.rs

1use unicode_segmentation::UnicodeSegmentation;
2
3/// Holds the chars typed by the user and the cursor position.
4/// Methods allow mutation of this content and movement of the cursor.
5#[derive(Clone, Default)]
6pub struct Input {
7    /// The input typed by the user
8    symbols: Vec<String>,
9    /// The index of the cursor in that string
10    cursor_index: usize,
11}
12
13impl Input {
14    const RIGHT_GAP: usize = 4;
15
16    /// Empty the content and move the cursor to start.
17    pub fn reset(&mut self) {
18        self.symbols.clear();
19        self.cursor_index = 0;
20    }
21
22    /// Current index of the cursor
23    #[must_use]
24    pub fn index(&self) -> usize {
25        self.cursor_index
26    }
27
28    #[must_use]
29    pub fn len(&self) -> usize {
30        self.symbols.len()
31    }
32
33    #[must_use]
34    pub fn is_empty(&self) -> bool {
35        self.symbols.is_empty()
36    }
37
38    /// Returns the content typed by the user as a String.
39    #[must_use]
40    pub fn string(&self) -> String {
41        self.symbols.join("")
42    }
43
44    /// Returns the index of the first displayed symbol on screen.
45    /// It's the position of the left window of displayed symbols.
46    ///
47    /// If the input is short enough to be displayed completely, it's 0.
48    /// If a text is too long to be displayed completely in the available rect,
49    /// we scroll the text and display a window around it.
50    ///
51    /// It's the index of the cursor + a gap - the available space, clamped to 0.
52    /// For example :
53    /// input has 10 chars (self.len() = 10) like "abcdefghij"
54    /// index is 6 : "abcdef|ghij" where | represents the cursor which doesn't take space.
55    /// available space is 5, we can display at most 5 symbols.
56    /// RIGHT_GAP is 4 - 4 chars should always be displayed at the right of the screen after the cursor, if possible.
57    /// Then left index si 6 + 4 - 5 = 5
58    /// We'll see abcd[ef|ghi]j, where [ ] represents the displayed text.
59    #[inline]
60    fn left_index(&self, available_space: usize) -> usize {
61        if self.input_is_short_enough(available_space) {
62            0
63        } else {
64            (self.index() + Self::RIGHT_GAP).saturating_sub(available_space)
65        }
66    }
67
68    #[inline]
69    fn input_is_short_enough(&self, available_space: usize) -> bool {
70        self.len() <= available_space
71    }
72
73    /// Index on screen of the cursor.
74    /// It's the index minus the left index considering the available space.
75    #[inline]
76    pub fn display_index(&self, available_space: usize) -> usize {
77        self.index()
78            .saturating_sub(self.left_index(available_space))
79    }
80
81    /// Returns the displayable input as a string accounting for available space.
82    /// If the text is short enough to be displayed completely it's the string it self.
83    pub fn scrolled_string(&self, available_space: usize) -> String {
84        if self.input_is_short_enough(available_space) {
85            self.string()
86        } else {
87            self.symbols
88                .iter()
89                .skip(self.left_index(available_space))
90                .take(available_space)
91                .map(|s| s.to_string())
92                .collect::<Vec<_>>()
93                .join("")
94        }
95    }
96
97    /// Returns a string of * for every char typed.
98    #[must_use]
99    pub fn password(&self) -> String {
100        "*".repeat(self.len())
101    }
102
103    /// Insert an utf-8 char into the input at cursor index.
104    pub fn insert(&mut self, c: char) {
105        self.symbols.insert(self.cursor_index, String::from(c));
106        self.cursor_index += 1;
107    }
108
109    /// Insert a pasted string at current position.
110    pub fn insert_string(&mut self, pasted: &str) {
111        UnicodeSegmentation::graphemes(pasted, true)
112            .collect::<Vec<&str>>()
113            .iter()
114            .map(|s| (*s).to_string())
115            .for_each(|s| {
116                self.symbols.insert(self.cursor_index, s);
117                self.cursor_index += 1;
118            })
119    }
120    /// Move the cursor to the start
121    pub fn cursor_start(&mut self) {
122        self.cursor_index = 0;
123    }
124
125    /// Move the cursor to the end
126    pub fn cursor_end(&mut self) {
127        self.cursor_index = self.len();
128    }
129
130    /// Move the cursor left if possible
131    pub fn cursor_left(&mut self) {
132        if self.cursor_index > 0 {
133            self.cursor_index -= 1;
134        }
135    }
136
137    /// Move the cursor right if possible
138    pub fn cursor_right(&mut self) {
139        if self.cursor_index < self.len() {
140            self.cursor_index += 1;
141        }
142    }
143
144    /// Move the cursor to the given index, limited to the length of the input.
145    ///
146    /// Used when the user click on the input string.
147    pub fn cursor_move(&mut self, index: usize) {
148        if index <= self.len() {
149            self.cursor_index = index
150        } else {
151            self.cursor_end()
152        }
153    }
154
155    /// Backspace, delete the char under the cursor and move left
156    pub fn delete_char_left(&mut self) {
157        if self.cursor_index > 0 && !self.symbols.is_empty() {
158            self.symbols.remove(self.cursor_index - 1);
159            self.cursor_index -= 1;
160        }
161    }
162
163    /// Delete all chars right to the cursor
164    pub fn delete_chars_right(&mut self) {
165        self.symbols = self
166            .symbols
167            .iter()
168            .take(self.cursor_index)
169            .map(std::string::ToString::to_string)
170            .collect();
171    }
172
173    pub fn delete_line(&mut self) {
174        self.symbols = vec![];
175        self.cursor_index = 0;
176    }
177
178    /// Deletes left symbols until a word is reached or the start of the line
179    /// A word is delimited by a "separator"
180    /// \t/\\.,;!?%_-+*(){}[]
181    pub fn delete_left(&mut self) {
182        while self.cursor_index > 0 {
183            self.symbols.remove(self.cursor_index.saturating_sub(1));
184            self.cursor_index -= 1;
185            if self.is_empty() || is_separator(&self.symbols[self.cursor_index.saturating_sub(1)]) {
186                break;
187            }
188        }
189    }
190
191    /// Replace the content with the new content.
192    /// Put the cursor at the end.
193    ///
194    /// To avoid splitting graphemes at wrong place, the new content is read
195    /// as Unicode Graphemes with
196    /// ```rust
197    /// unicode_segmentation::UnicodeSegmentation::graphemes(content, true)
198    /// ```
199    /// See [`UnicodeSegmentation`] for more information.
200    pub fn replace(&mut self, content: &str) {
201        self.symbols = UnicodeSegmentation::graphemes(content, true)
202            .collect::<Vec<&str>>()
203            .iter()
204            .map(|s| (*s).to_string())
205            .collect();
206        self.cursor_end();
207    }
208
209    /// Move the cursor to the previous "word".
210    /// A word is delimited by a "separator"
211    /// \t/\\.,;!?%_-+*(){}[]
212    pub fn next_word(&mut self) {
213        while self.cursor_index < self.symbols.len() {
214            self.cursor_index += 1;
215            if self.cursor_index == self.symbols.len()
216                || is_separator(&self.symbols[self.cursor_index])
217            {
218                break;
219            }
220        }
221    }
222
223    /// Move the cursor to the previous "word".
224    /// A word is delimited by a "separator"
225    /// \t/\\.,;!?%_-+*(){}[]
226    pub fn previous_word(&mut self) {
227        while self.cursor_index > 0 {
228            self.cursor_index -= 1;
229            if self.cursor_index == 0
230                || is_separator(&self.symbols[self.cursor_index.saturating_sub(1)])
231            {
232                break;
233            }
234        }
235    }
236}
237
238#[rustfmt::skip]
239#[inline]
240fn is_separator(word: &str) -> bool {
241    matches!(word, " " | "\t" | "/" | "\\" | "." | "," | ";" | "!" | "?" | "%" | "_" | "-" | "+" | "*" | "(" | ")" | "{" | "}" | "[" | "]") 
242}