Skip to main content

ratatui_textarea/
cursor.rs

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