Skip to main content

tui_input/
input.rs

1//! Core logic for handling input.
2//!
3//! # Units
4//!
5//! A string has four different possible notions of length or position:
6//!
7//! - **bytes**:  indices into the UTF-8 encoding, used only internally.
8//! - **codepoints**:  Unicode scalar values (what [`str::chars`] yields).
9//!   This is what [`Input::cursor`] returns and what
10//!   [`InputRequest::SetCursor`] accepts.
11//! - **graphemes**:  user-perceived characters (per `unicode-segmentation`).
12//!   Movement and deletion ([`InputRequest::GoToPrevChar`],
13//!   [`InputRequest::GoToNextChar`], [`InputRequest::DeletePrevChar`],
14//!   [`InputRequest::DeleteNextChar`], [`InputRequest::GoToPrevWord`],
15//!   [`InputRequest::GoToNextWord`], [`InputRequest::DeletePrevWord`],
16//!   [`InputRequest::DeleteNextWord`]) step one *grapheme* or *word*
17//!   at a time, which may span multiple codepoints.
18//! - **display columns**:  terminal cell width (per `unicode-width`).
19//!   Returned by [`Input::visual_cursor`] and [`Input::visual_scroll`].
20//!
21//! All four can differ for one string.  For example, `🤦🏼‍♂️` is
22//! actually `"🤦🏼\u{200D}♂\u{FE0F}"`, which is 17 bytes, 5 codepoints,
23//! 1 grapheme, 2 display columns.
24//!
25//! # Example: Without any backend
26//!
27//! ```
28//! use tui_input::{Input, InputRequest, StateChanged};
29//!
30//! let mut input: Input = "Hello Worl".into();
31//!
32//! let req = InputRequest::InsertChar('d');
33//! let resp = input.handle(req);
34//!
35//! assert_eq!(resp, Some(StateChanged { value: true, cursor: true }));
36//! assert_eq!(input.cursor(), 11);
37//! assert_eq!(input.to_string(), "Hello World");
38//! ```
39
40mod value;
41
42use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
43use unicode_width::UnicodeWidthStr;
44
45use self::value::Value;
46
47fn prev_grapheme(s: &str, byte: usize) -> Option<usize> {
48    GraphemeCursor::new(byte, s.len(), true)
49        .prev_boundary(s, 0)
50        .ok()
51        .flatten()
52}
53
54fn next_grapheme(s: &str, byte: usize) -> Option<usize> {
55    GraphemeCursor::new(byte, s.len(), true)
56        .next_boundary(s, 0)
57        .ok()
58        .flatten()
59}
60
61fn is_word(s: &str) -> bool {
62    s.chars()
63        .any(|c| !c.is_whitespace() && !c.is_ascii_punctuation())
64}
65
66fn prev_word_byte(s: &str, byte: usize) -> usize {
67    let words = s
68        .split_word_bound_indices()
69        .filter(|(i, _)| *i < byte)
70        .rev();
71    for (i, word) in words {
72        if is_word(word) {
73            return i;
74        }
75    }
76    0
77}
78
79fn next_word_byte(s: &str, byte: usize) -> usize {
80    let words = s.split_word_bound_indices().filter(|(i, _)| *i > byte);
81    for (i, word) in words {
82        if is_word(word) {
83            return i;
84        }
85    }
86    s.len()
87}
88
89/// Display width of each grapheme.  Width of a sequence like ❤️‍🔥 is not
90/// the sum of its codepoints' widths, so whole graphemes are measured.
91pub(crate) fn widths(s: &str) -> impl Iterator<Item = usize> + '_ {
92    s.graphemes(true).map(UnicodeWidthStr::width)
93}
94
95pub(crate) fn codepoint_to_byte(s: &str, n: usize) -> usize {
96    s.char_indices().nth(n).map_or(s.len(), |(i, _)| i)
97}
98
99fn byte_to_codepoint(s: &str, byte: usize) -> usize {
100    s[..byte].chars().count()
101}
102
103enum Side {
104    Left,
105    Right,
106}
107
108/// Input requests are used to change the input state.
109///
110/// Different backends can be used to convert events into requests.
111#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
112#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
113pub enum InputRequest {
114    SetCursor(usize),
115    InsertChar(char),
116    GoToPrevChar,
117    GoToNextChar,
118    GoToPrevWord,
119    GoToNextWord,
120    GoToStart,
121    GoToEnd,
122    DeletePrevChar,
123    DeleteNextChar,
124    DeletePrevWord,
125    DeleteNextWord,
126    DeleteLine,
127    DeleteTillEnd,
128    DeleteFromStart,
129    Yank,
130}
131
132#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134pub struct StateChanged {
135    pub value: bool,
136    pub cursor: bool,
137}
138
139pub type InputResponse = Option<StateChanged>;
140
141/// The input buffer with cursor support.
142///
143/// Example:
144///
145/// ```
146/// use tui_input::Input;
147///
148/// let input: Input = "Hello World".into();
149///
150/// assert_eq!(input.cursor(), 11);
151/// assert_eq!(input.to_string(), "Hello World");
152/// ```
153#[derive(Default, Debug, Clone)]
154#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
155pub struct Input {
156    value: Value,
157    /// Codepoints preceding the cursor.  See the module-level `Units` section.
158    cursor: usize,
159    yank: Value,
160    last_was_cut: bool,
161}
162
163impl Input {
164    /// Initialize a new instance with a given value
165    /// Cursor will be set to the given value's length.
166    pub fn new(value: String) -> Self {
167        let value = Value::new(value);
168        let cursor = value.chars();
169        Self {
170            value,
171            cursor,
172            yank: Value::default(),
173            last_was_cut: false,
174        }
175    }
176
177    /// Set the value manually.
178    /// Cursor will be set to the given value's length.
179    pub fn with_value(mut self, value: String) -> Self {
180        self.value = Value::new(value);
181        self.cursor = self.value.chars();
182        self
183    }
184
185    /// Set the cursor manually.
186    /// If the input is larger than the value length, it'll be auto adjusted.
187    pub fn with_cursor(mut self, cursor: usize) -> Self {
188        self.cursor = cursor.min(self.value.chars());
189        self
190    }
191
192    // Reset the cursor and value to default
193    pub fn reset(&mut self) {
194        self.cursor = Default::default();
195        self.value = Default::default();
196    }
197
198    // Reset the cursor and value to default, returning the previous value
199    pub fn value_and_reset(&mut self) -> String {
200        let val = self.value.as_str().to_owned();
201        self.reset();
202        val
203    }
204
205    fn add_to_yank(&mut self, deleted: String, side: Side) {
206        if self.last_was_cut {
207            match side {
208                Side::Left => self.yank.edit().insert_str(0, &deleted),
209                Side::Right => self.yank.edit().push_str(&deleted),
210            }
211        } else {
212            self.yank = Value::new(deleted);
213        }
214    }
215
216    fn set_last_was_cut(&mut self, req: InputRequest) {
217        use InputRequest::*;
218        self.last_was_cut = matches!(
219            req,
220            DeleteLine | DeletePrevWord | DeleteNextWord | DeleteTillEnd
221        );
222    }
223
224    /// Handle request and emit response.
225    pub fn handle(&mut self, req: InputRequest) -> InputResponse {
226        use InputRequest::*;
227        let result = match req {
228            SetCursor(pos) => {
229                let pos = pos.min(self.value.chars());
230                if self.cursor == pos {
231                    None
232                } else {
233                    self.cursor = pos;
234                    Some(StateChanged {
235                        value: false,
236                        cursor: true,
237                    })
238                }
239            }
240            InsertChar(c) => {
241                let byte = codepoint_to_byte(self.value.as_str(), self.cursor);
242                self.value.edit().insert(byte, c);
243                self.cursor += 1;
244                Some(StateChanged {
245                    value: true,
246                    cursor: true,
247                })
248            }
249
250            DeletePrevChar => {
251                let s = self.value.as_str();
252                let byte = codepoint_to_byte(s, self.cursor);
253                let prev = prev_grapheme(s, byte)?;
254                let removed = s[prev..byte].chars().count();
255                self.value.edit().replace_range(prev..byte, "");
256                self.cursor -= removed;
257                Some(StateChanged {
258                    value: true,
259                    cursor: true,
260                })
261            }
262
263            DeleteNextChar => {
264                let s = self.value.as_str();
265                let byte = codepoint_to_byte(s, self.cursor);
266                let next = next_grapheme(s, byte)?;
267                self.value.edit().replace_range(byte..next, "");
268                Some(StateChanged {
269                    value: true,
270                    cursor: false,
271                })
272            }
273
274            GoToPrevChar => {
275                let s = self.value.as_str();
276                let byte = codepoint_to_byte(s, self.cursor);
277                let prev = prev_grapheme(s, byte)?;
278                self.cursor -= s[prev..byte].chars().count();
279                Some(StateChanged {
280                    value: false,
281                    cursor: true,
282                })
283            }
284
285            GoToPrevWord => {
286                let s = self.value.as_str();
287                let byte = codepoint_to_byte(s, self.cursor);
288                let prev = prev_word_byte(s, byte);
289                if self.cursor == 0 {
290                    None
291                } else {
292                    self.cursor = byte_to_codepoint(s, prev);
293                    Some(StateChanged {
294                        value: false,
295                        cursor: true,
296                    })
297                }
298            }
299
300            GoToNextChar => {
301                let s = self.value.as_str();
302                let byte = codepoint_to_byte(s, self.cursor);
303                let next = next_grapheme(s, byte)?;
304                self.cursor += s[byte..next].chars().count();
305                Some(StateChanged {
306                    value: false,
307                    cursor: true,
308                })
309            }
310
311            GoToNextWord => {
312                let s = self.value.as_str();
313                let byte = codepoint_to_byte(s, self.cursor);
314                let next = next_word_byte(s, byte);
315                if self.cursor == self.value.chars() {
316                    None
317                } else {
318                    self.cursor = byte_to_codepoint(s, next);
319                    Some(StateChanged {
320                        value: false,
321                        cursor: true,
322                    })
323                }
324            }
325
326            DeleteLine => {
327                if self.value.as_str().is_empty() {
328                    None
329                } else {
330                    let side = if self.cursor == self.value.chars() {
331                        Side::Left
332                    } else {
333                        Side::Right
334                    };
335                    self.add_to_yank(self.value.as_str().to_owned(), side);
336                    self.value.edit().clear();
337                    self.cursor = 0;
338                    Some(StateChanged {
339                        value: true,
340                        cursor: true,
341                    })
342                }
343            }
344
345            DeletePrevWord => {
346                if self.cursor == 0 {
347                    None
348                } else {
349                    let s = self.value.as_str();
350                    let byte = codepoint_to_byte(s, self.cursor);
351                    let prev = prev_word_byte(s, byte);
352                    let deleted = s[prev..byte].to_string();
353                    self.add_to_yank(deleted, Side::Left);
354                    self.value.edit().replace_range(prev..byte, "");
355                    self.cursor = byte_to_codepoint(self.value.as_str(), prev);
356                    Some(StateChanged {
357                        value: true,
358                        cursor: true,
359                    })
360                }
361            }
362
363            DeleteNextWord => {
364                let s = self.value.as_str();
365                let byte = codepoint_to_byte(s, self.cursor);
366                let next = next_word_byte(s, byte);
367                if self.cursor == self.value.chars() {
368                    None
369                } else {
370                    let deleted = s[byte..next].to_string();
371                    self.add_to_yank(deleted, Side::Right);
372                    self.value.edit().replace_range(byte..next, "");
373                    Some(StateChanged {
374                        value: true,
375                        cursor: false,
376                    })
377                }
378            }
379
380            GoToStart => {
381                if self.cursor == 0 {
382                    None
383                } else {
384                    self.cursor = 0;
385                    Some(StateChanged {
386                        value: false,
387                        cursor: true,
388                    })
389                }
390            }
391
392            GoToEnd => {
393                let count = self.value.chars();
394                if self.cursor == count {
395                    None
396                } else {
397                    self.cursor = count;
398                    Some(StateChanged {
399                        value: false,
400                        cursor: true,
401                    })
402                }
403            }
404
405            DeleteTillEnd => {
406                let byte = codepoint_to_byte(self.value.as_str(), self.cursor);
407                let deleted = self.value.as_str()[byte..].to_string();
408                self.add_to_yank(deleted, Side::Right);
409                self.value.edit().truncate(byte);
410                Some(StateChanged {
411                    value: true,
412                    cursor: false,
413                })
414            }
415
416            DeleteFromStart => {
417                if self.cursor == 0 {
418                    None
419                } else {
420                    let byte = codepoint_to_byte(self.value.as_str(), self.cursor);
421                    let deleted = self.value.edit().drain(..byte).collect();
422                    self.add_to_yank(deleted, Side::Left);
423                    self.cursor = 0;
424                    Some(StateChanged {
425                        value: true,
426                        cursor: true,
427                    })
428                }
429            }
430
431            Yank => {
432                if self.yank.as_str().is_empty() {
433                    None
434                } else {
435                    let byte = codepoint_to_byte(self.value.as_str(), self.cursor);
436                    self.value.edit().insert_str(byte, self.yank.as_str());
437                    self.cursor += self.yank.chars();
438                    Some(StateChanged {
439                        value: true,
440                        cursor: true,
441                    })
442                }
443            }
444        };
445        self.set_last_was_cut(req);
446        result
447    }
448
449    /// Get a reference to the current value.
450    pub fn value(&self) -> &str {
451        self.value.as_str()
452    }
453
454    /// Returns the number of **codepoints** preceding the cursor.  Movement
455    /// and deletion operations step one *grapheme* at a time, so a single
456    /// [`InputRequest::GoToNextChar`] or [`InputRequest::DeletePrevChar`]
457    /// may change this count by more than one.
458    pub fn cursor(&self) -> usize {
459        self.cursor
460    }
461
462    /// Returns the cursor's position in **display columns** (per
463    /// `unicode-width`).
464    pub fn visual_cursor(&self) -> usize {
465        let s = self.value.as_str();
466        widths(&s[..codepoint_to_byte(s, self.cursor)]).sum()
467    }
468
469    /// Get the scroll position, accounting for wide graphemes.
470    pub fn visual_scroll(&self, width: usize) -> usize {
471        let scroll = (self.visual_cursor()).max(width) - width;
472        let mut uscroll = 0;
473        let mut widths = widths(self.value());
474
475        while uscroll < scroll {
476            match widths.next() {
477                Some(w) => uscroll += w,
478                None => break,
479            }
480        }
481        uscroll
482    }
483}
484
485impl From<Input> for String {
486    fn from(input: Input) -> Self {
487        input.value.into()
488    }
489}
490
491impl From<String> for Input {
492    fn from(value: String) -> Self {
493        Self::new(value)
494    }
495}
496
497impl From<&str> for Input {
498    fn from(value: &str) -> Self {
499        Self::new(value.into())
500    }
501}
502
503impl std::fmt::Display for Input {
504    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
505        self.value.as_str().fmt(f)
506    }
507}
508
509#[cfg(test)]
510mod tests;