tui_textarea/
cursor.rs

1use crate::widget::Viewport;
2use crate::word::{
3    find_word_inclusive_end_forward, find_word_start_backward, find_word_start_forward,
4};
5#[cfg(feature = "arbitrary")]
6use arbitrary::Arbitrary;
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9use std::cmp;
10
11/// Specify how to move the cursor.
12///
13/// This type is marked as `#[non_exhaustive]` since more variations may be supported in the future.
14#[non_exhaustive]
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18pub enum CursorMove {
19    /// Move cursor forward by one character. When the cursor is at the end of line, it moves to the head of next line.
20    /// ```
21    /// use tui_textarea::{TextArea, CursorMove};
22    ///
23    /// let mut textarea = TextArea::from(["abc"]);
24    ///
25    /// textarea.move_cursor(CursorMove::Forward);
26    /// assert_eq!(textarea.cursor(), (0, 1));
27    /// textarea.move_cursor(CursorMove::Forward);
28    /// assert_eq!(textarea.cursor(), (0, 2));
29    /// ```
30    Forward,
31    /// Move cursor backward by one character. When the cursor is at the head of line, it moves to the end of previous
32    /// line.
33    /// ```
34    /// use tui_textarea::{TextArea, CursorMove};
35    ///
36    /// let mut textarea = TextArea::from(["abc"]);
37    ///
38    /// textarea.move_cursor(CursorMove::Forward);
39    /// textarea.move_cursor(CursorMove::Forward);
40    /// textarea.move_cursor(CursorMove::Back);
41    /// assert_eq!(textarea.cursor(), (0, 1));
42    /// ```
43    Back,
44    /// Move cursor up by one line.
45    /// ```
46    /// use tui_textarea::{TextArea, CursorMove};
47    ///
48    /// let mut textarea = TextArea::from(["a", "b", "c"]);
49    ///
50    /// textarea.move_cursor(CursorMove::Down);
51    /// textarea.move_cursor(CursorMove::Down);
52    /// textarea.move_cursor(CursorMove::Up);
53    /// assert_eq!(textarea.cursor(), (1, 0));
54    /// ```
55    Up,
56    /// Move cursor down by one line.
57    /// ```
58    /// use tui_textarea::{TextArea, CursorMove};
59    ///
60    /// let mut textarea = TextArea::from(["a", "b", "c"]);
61    ///
62    /// textarea.move_cursor(CursorMove::Down);
63    /// assert_eq!(textarea.cursor(), (1, 0));
64    /// textarea.move_cursor(CursorMove::Down);
65    /// assert_eq!(textarea.cursor(), (2, 0));
66    /// ```
67    Down,
68    /// Move cursor to the head of line. When the cursor is at the head of line, it moves to the end of previous line.
69    /// ```
70    /// use tui_textarea::{TextArea, CursorMove};
71    ///
72    /// let mut textarea = TextArea::from(["abc"]);
73    ///
74    /// textarea.move_cursor(CursorMove::Forward);
75    /// textarea.move_cursor(CursorMove::Forward);
76    /// textarea.move_cursor(CursorMove::Head);
77    /// assert_eq!(textarea.cursor(), (0, 0));
78    /// ```
79    Head,
80    /// Move cursor to the end of line. When the cursor is at the end of line, it moves to the head of next line.
81    /// ```
82    /// use tui_textarea::{TextArea, CursorMove};
83    ///
84    /// let mut textarea = TextArea::from(["abc"]);
85    ///
86    /// textarea.move_cursor(CursorMove::End);
87    /// assert_eq!(textarea.cursor(), (0, 3));
88    /// ```
89    End,
90    /// Move cursor to the top of lines.
91    /// ```
92    /// use tui_textarea::{TextArea, CursorMove};
93    ///
94    /// let mut textarea = TextArea::from(["a", "b", "c"]);
95    ///
96    /// textarea.move_cursor(CursorMove::Down);
97    /// textarea.move_cursor(CursorMove::Down);
98    /// textarea.move_cursor(CursorMove::Top);
99    /// assert_eq!(textarea.cursor(), (0, 0));
100    /// ```
101    Top,
102    /// Move cursor to the bottom of lines.
103    /// ```
104    /// use tui_textarea::{TextArea, CursorMove};
105    ///
106    /// let mut textarea = TextArea::from(["a", "b", "c"]);
107    ///
108    /// textarea.move_cursor(CursorMove::Bottom);
109    /// assert_eq!(textarea.cursor(), (2, 0));
110    /// ```
111    Bottom,
112    /// Move cursor forward by one word. Word boundary appears at spaces, punctuations, and others. For example
113    /// `fn foo(a)` consists of words `fn`, `foo`, `(`, `a`, `)`. When the cursor is at the end of line, it moves to the
114    /// head of next line.
115    /// ```
116    /// use tui_textarea::{TextArea, CursorMove};
117    ///
118    /// let mut textarea = TextArea::from(["aaa bbb ccc"]);
119    ///
120    /// textarea.move_cursor(CursorMove::WordForward);
121    /// assert_eq!(textarea.cursor(), (0, 4));
122    /// textarea.move_cursor(CursorMove::WordForward);
123    /// assert_eq!(textarea.cursor(), (0, 8));
124    /// ```
125    WordForward,
126    /// Move cursor forward to the next end of word. Word boundary appears at spaces, punctuations, and others. For example
127    /// `fn foo(a)` consists of words `fn`, `foo`, `(`, `a`, `)`. When the cursor is at the end of line, it moves to the
128    /// end of the first word of the next line. This is similar to the 'e' mapping of Vim in normal mode.
129    /// ```
130    /// use tui_textarea::{TextArea, CursorMove};
131    ///
132    /// let mut textarea = TextArea::from([
133    ///     "aaa bbb [[[ccc]]]",
134    ///     "",
135    ///     " ddd",
136    /// ]);
137    ///
138    ///
139    /// textarea.move_cursor(CursorMove::WordEnd);
140    /// assert_eq!(textarea.cursor(), (0, 2));      // At the end of 'aaa'
141    /// textarea.move_cursor(CursorMove::WordEnd);
142    /// assert_eq!(textarea.cursor(), (0, 6));      // At the end of 'bbb'
143    /// textarea.move_cursor(CursorMove::WordEnd);
144    /// assert_eq!(textarea.cursor(), (0, 10));     // At the end of '[[['
145    /// textarea.move_cursor(CursorMove::WordEnd);
146    /// assert_eq!(textarea.cursor(), (0, 13));     // At the end of 'ccc'
147    /// textarea.move_cursor(CursorMove::WordEnd);
148    /// assert_eq!(textarea.cursor(), (0, 16));     // At the end of ']]]'
149    /// textarea.move_cursor(CursorMove::WordEnd);
150    /// assert_eq!(textarea.cursor(), (2, 3));      // At the end of 'ddd'
151    /// ```
152    WordEnd,
153    /// Move cursor backward by one word.  Word boundary appears at spaces, punctuations, and others. For example
154    /// `fn foo(a)` consists of words `fn`, `foo`, `(`, `a`, `)`.When the cursor is at the head of line, it moves to
155    /// the end of previous line.
156    /// ```
157    /// use tui_textarea::{TextArea, CursorMove};
158    ///
159    /// let mut textarea = TextArea::from(["aaa bbb ccc"]);
160    ///
161    /// textarea.move_cursor(CursorMove::End);
162    /// textarea.move_cursor(CursorMove::WordBack);
163    /// assert_eq!(textarea.cursor(), (0, 8));
164    /// textarea.move_cursor(CursorMove::WordBack);
165    /// assert_eq!(textarea.cursor(), (0, 4));
166    /// textarea.move_cursor(CursorMove::WordBack);
167    /// assert_eq!(textarea.cursor(), (0, 0));
168    /// ```
169    WordBack,
170    /// Move cursor down by one paragraph. Paragraph is a chunk of non-empty lines. Cursor moves to the first line of paragraph.
171    /// ```
172    /// use tui_textarea::{TextArea, CursorMove};
173    ///
174    /// // aaa
175    /// //
176    /// // bbb
177    /// //
178    /// // ccc
179    /// // ddd
180    /// let mut textarea = TextArea::from(["aaa", "", "bbb", "", "ccc", "ddd"]);
181    ///
182    /// textarea.move_cursor(CursorMove::ParagraphForward);
183    /// assert_eq!(textarea.cursor(), (2, 0));
184    /// textarea.move_cursor(CursorMove::ParagraphForward);
185    /// assert_eq!(textarea.cursor(), (4, 0));
186    /// ```
187    ParagraphForward,
188    /// Move cursor up by one paragraph. Paragraph is a chunk of non-empty lines. Cursor moves to the first line of paragraph.
189    /// ```
190    /// use tui_textarea::{TextArea, CursorMove};
191    ///
192    /// // aaa
193    /// //
194    /// // bbb
195    /// //
196    /// // ccc
197    /// // ddd
198    /// let mut textarea = TextArea::from(["aaa", "", "bbb", "", "ccc", "ddd"]);
199    ///
200    /// textarea.move_cursor(CursorMove::Bottom);
201    /// textarea.move_cursor(CursorMove::ParagraphBack);
202    /// assert_eq!(textarea.cursor(), (4, 0));
203    /// textarea.move_cursor(CursorMove::ParagraphBack);
204    /// assert_eq!(textarea.cursor(), (2, 0));
205    /// textarea.move_cursor(CursorMove::ParagraphBack);
206    /// assert_eq!(textarea.cursor(), (0, 0));
207    /// ```
208    ParagraphBack,
209    /// Move cursor to (row, col) position. When the position points outside the text, the cursor position is made fit
210    /// within the text. Note that row and col are 0-based. (0, 0) means the first character of the first line.
211    ///
212    /// When there are 10 lines, jumping to row 15 moves the cursor to the last line (row is 9 in the case). When there
213    /// are 10 characters in the line, jumping to col 15 moves the cursor to end of the line (col is 10 in the case).
214    /// ```
215    /// use tui_textarea::{TextArea, CursorMove};
216    ///
217    /// let mut textarea = TextArea::from(["aaaa", "bbbb", "cccc"]);
218    ///
219    /// textarea.move_cursor(CursorMove::Jump(1, 2));
220    /// assert_eq!(textarea.cursor(), (1, 2));
221    ///
222    /// textarea.move_cursor(CursorMove::Jump(10,  10));
223    /// assert_eq!(textarea.cursor(), (2, 4));
224    /// ```
225    Jump(u16, u16),
226    /// Move cursor to keep it within the viewport. For example, when a viewport displays line 8 to line 16:
227    ///
228    /// - cursor at line 4 is moved to line 8
229    /// - cursor at line 20 is moved to line 16
230    /// - cursor at line 12 is not moved
231    ///
232    /// This is useful when you moved a cursor but you don't want to move the viewport.
233    /// ```
234    /// # use ratatui::buffer::Buffer;
235    /// # use ratatui::layout::Rect;
236    /// # use ratatui::widgets::Widget as _;
237    /// use tui_textarea::{TextArea, CursorMove};
238    ///
239    /// // Let's say terminal height is 8.
240    ///
241    /// // Create textarea with 20 lines "0", "1", "2", "3", ...
242    /// // The viewport is displaying from line 1 to line 8.
243    /// let mut textarea: TextArea = (0..20).into_iter().map(|i| i.to_string()).collect();
244    /// # // Call `render` at least once to populate terminal size
245    /// # let r = Rect { x: 0, y: 0, width: 24, height: 8 };
246    /// # let mut b = Buffer::empty(r.clone());
247    /// # textarea.render(r, &mut b);
248    ///
249    /// // Move cursor to the end of lines (line 20). It is outside the viewport (line 1 to line 8)
250    /// textarea.move_cursor(CursorMove::Bottom);
251    /// assert_eq!(textarea.cursor(), (19, 0));
252    ///
253    /// // Cursor is moved to line 8 to enter the viewport
254    /// textarea.move_cursor(CursorMove::InViewport);
255    /// assert_eq!(textarea.cursor(), (7, 0));
256    /// ```
257    InViewport,
258}
259
260impl CursorMove {
261    pub(crate) fn next_cursor(
262        &self,
263        (row, col): (usize, usize),
264        lines: &[String],
265        viewport: &Viewport,
266    ) -> Option<(usize, usize)> {
267        use CursorMove::*;
268
269        fn fit_col(col: usize, line: &str) -> usize {
270            cmp::min(col, line.chars().count())
271        }
272
273        match self {
274            Forward if col >= lines[row].chars().count() => {
275                (row + 1 < lines.len()).then(|| (row + 1, 0))
276            }
277            Forward => Some((row, col + 1)),
278            Back if col == 0 => {
279                let row = row.checked_sub(1)?;
280                Some((row, lines[row].chars().count()))
281            }
282            Back => Some((row, col - 1)),
283            Up => {
284                let row = row.checked_sub(1)?;
285                Some((row, fit_col(col, &lines[row])))
286            }
287            Down => Some((row + 1, fit_col(col, lines.get(row + 1)?))),
288            Head => Some((row, 0)),
289            End => Some((row, lines[row].chars().count())),
290            Top => Some((0, fit_col(col, &lines[0]))),
291            Bottom => {
292                let row = lines.len() - 1;
293                Some((row, fit_col(col, &lines[row])))
294            }
295            WordEnd => {
296                // `+ 1` for not accepting the current cursor position
297                if let Some(col) = find_word_inclusive_end_forward(&lines[row], col + 1) {
298                    Some((row, col))
299                } else {
300                    let mut row = row;
301                    loop {
302                        if row == lines.len() - 1 {
303                            break Some((row, lines[row].chars().count()));
304                        }
305                        row += 1;
306                        if let Some(col) = find_word_inclusive_end_forward(&lines[row], 0) {
307                            break Some((row, col));
308                        }
309                    }
310                }
311            }
312            WordForward => {
313                if let Some(col) = find_word_start_forward(&lines[row], col) {
314                    Some((row, col))
315                } else if row + 1 < lines.len() {
316                    Some((row + 1, 0))
317                } else {
318                    Some((row, lines[row].chars().count()))
319                }
320            }
321            WordBack => {
322                if let Some(col) = find_word_start_backward(&lines[row], col) {
323                    Some((row, col))
324                } else if row > 0 {
325                    Some((row - 1, lines[row - 1].chars().count()))
326                } else {
327                    Some((row, 0))
328                }
329            }
330            ParagraphForward => {
331                let mut prev_is_empty = lines[row].is_empty();
332                for row in row + 1..lines.len() {
333                    let line = &lines[row];
334                    let is_empty = line.is_empty();
335                    if !is_empty && prev_is_empty {
336                        return Some((row, fit_col(col, line)));
337                    }
338                    prev_is_empty = is_empty;
339                }
340                let row = lines.len() - 1;
341                Some((row, fit_col(col, &lines[row])))
342            }
343            ParagraphBack => {
344                let row = row.checked_sub(1)?;
345                let mut prev_is_empty = lines[row].is_empty();
346                for row in (0..row).rev() {
347                    let is_empty = lines[row].is_empty();
348                    if is_empty && !prev_is_empty {
349                        return Some((row + 1, fit_col(col, &lines[row + 1])));
350                    }
351                    prev_is_empty = is_empty;
352                }
353                Some((0, fit_col(col, &lines[0])))
354            }
355            Jump(row, col) => {
356                let row = cmp::min(*row as usize, lines.len() - 1);
357                let col = fit_col(*col as usize, &lines[row]);
358                Some((row, col))
359            }
360            InViewport => {
361                let (row_top, col_top, row_bottom, col_bottom) = viewport.position();
362
363                let row = row.clamp(row_top as usize, row_bottom as usize);
364                let row = cmp::min(row, lines.len() - 1);
365                let col = col.clamp(col_top as usize, col_bottom as usize);
366                let col = fit_col(col, &lines[row]);
367
368                Some((row, col))
369            }
370        }
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    // Separate tests for tui-rs support
377    #[test]
378    fn in_viewport() {
379        use crate::ratatui::buffer::Buffer;
380        use crate::ratatui::layout::Rect;
381        use crate::ratatui::widgets::Widget as _;
382        use crate::{CursorMove, TextArea};
383
384        let mut textarea: TextArea = (0..20).map(|i| i.to_string()).collect();
385        let r = Rect {
386            x: 0,
387            y: 0,
388            width: 24,
389            height: 8,
390        };
391        let mut b = Buffer::empty(r);
392        textarea.render(r, &mut b);
393
394        textarea.move_cursor(CursorMove::Bottom);
395        assert_eq!(textarea.cursor(), (19, 0));
396
397        textarea.move_cursor(CursorMove::InViewport);
398        assert_eq!(textarea.cursor(), (7, 0));
399    }
400}