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