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