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}