Skip to main content

datui_lib/widgets/textarea/
mod.rs

1//! A text editing widget built directly on ratatui.
2//!
3//! [`TextArea`] owns a buffer of lines, a cursor, a selection, an undo history
4//! and a yank buffer, and knows how to draw itself into a [`Rect`]. It has no
5//! opinion about history of previously submitted values, theming or focus:
6//! that belongs to [`crate::widgets::text_input::TextInput`], which wraps this
7//! type and is what the rest of datui uses.
8//!
9//! Coordinates are `(row, column)` pairs where the column is a *character*
10//! index, not a terminal column. Character indices are what the editing and
11//! cursor logic works in; display width is resolved only while rendering, in
12//! [`render`].
13
14mod cursor;
15mod edit;
16mod history;
17mod input;
18mod render;
19mod wrap;
20
21#[cfg(test)]
22mod tests;
23
24use std::cell::Cell;
25
26use ratatui::style::{Modifier, Style};
27
28pub use cursor::CursorMove;
29pub use input::{Input, Key};
30
31use history::History;
32
33/// How much of the buffer was on screen the last time it was drawn.
34///
35/// Kept in a [`Cell`] so that rendering, which only has `&self`, can record the
36/// scroll position it settled on and the page size that `PageUp`/`PageDown`
37/// should use.
38#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
39pub(super) struct Viewport {
40    /// First visible row.
41    pub row: usize,
42    /// First visible terminal column.
43    pub col: usize,
44    pub height: u16,
45    pub width: u16,
46}
47
48/// A multi-line text editor.
49#[derive(Debug, Clone)]
50pub struct TextArea {
51    lines: Vec<String>,
52    cursor: (usize, usize),
53    /// Fixed end of the selection, if a selection is active. The moving end is
54    /// the cursor.
55    selection_anchor: Option<(usize, usize)>,
56    /// Text held by the most recent copy, cut or kill, pasted by `paste`.
57    yank: String,
58    history: History,
59    style: Style,
60    cursor_style: Style,
61    selection_style: Style,
62    cursor_visible: bool,
63    tab_len: usize,
64    /// Long lines break onto the rows below instead of scrolling sideways.
65    wrap: bool,
66    viewport: Cell<Viewport>,
67}
68
69impl Default for TextArea {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl TextArea {
76    /// An empty editor holding a single empty line.
77    pub fn new() -> Self {
78        Self {
79            lines: vec![String::new()],
80            cursor: (0, 0),
81            selection_anchor: None,
82            yank: String::new(),
83            history: History::default(),
84            style: Style::default(),
85            cursor_style: Style::default().add_modifier(Modifier::REVERSED),
86            selection_style: Style::default().add_modifier(Modifier::REVERSED),
87            cursor_visible: true,
88            tab_len: 4,
89            wrap: false,
90            viewport: Cell::new(Viewport::default()),
91        }
92    }
93
94    /// An editor holding `text`, with the cursor at the end of it.
95    pub fn from_text(text: &str) -> Self {
96        let mut ta = Self::new();
97        ta.set_text(text);
98        ta
99    }
100
101    /// Replace the whole buffer, moving the cursor to the end and discarding
102    /// undo history, selection and scroll position.
103    pub fn set_text(&mut self, text: &str) {
104        self.lines = split_lines(text);
105        let row = self.lines.len() - 1;
106        let col = char_count(&self.lines[row]);
107        self.cursor = (row, col);
108        self.selection_anchor = None;
109        self.history.clear();
110        // The scroll starts over; the size is the field's, and stays.
111        let size = self.viewport.get();
112        self.viewport.set(Viewport {
113            width: size.width,
114            height: size.height,
115            ..Viewport::default()
116        });
117    }
118
119    /// Empty the buffer.
120    pub fn clear(&mut self) {
121        self.set_text("");
122    }
123
124    /// The buffer, one entry per line. Always holds at least one line.
125    pub fn lines(&self) -> &[String] {
126        &self.lines
127    }
128
129    /// The buffer as a single newline-joined string.
130    pub fn text(&self) -> String {
131        self.lines.join("\n")
132    }
133
134    /// Number of lines in the buffer.
135    pub fn line_count(&self) -> usize {
136        self.lines.len()
137    }
138
139    /// The line at `row`, if it exists.
140    pub fn line(&self, row: usize) -> Option<&str> {
141        self.lines.get(row).map(String::as_str)
142    }
143
144    /// True when the buffer holds no characters at all.
145    pub fn is_empty(&self) -> bool {
146        self.lines.len() == 1 && self.lines[0].is_empty()
147    }
148
149    /// Cursor position as `(row, character column)`.
150    pub fn cursor(&self) -> (usize, usize) {
151        self.cursor
152    }
153
154    /// Move the cursor to an absolute position, clamped into the buffer. This
155    /// cancels any selection.
156    pub fn set_cursor(&mut self, row: usize, col: usize) {
157        self.selection_anchor = None;
158        self.cursor = self.clamp_position((row, col));
159    }
160
161    /// Selected range as `(start, end)` with `start <= end`, if a selection is
162    /// active and non-empty.
163    pub fn selection(&self) -> Option<((usize, usize), (usize, usize))> {
164        let anchor = self.selection_anchor?;
165        if anchor == self.cursor {
166            return None;
167        }
168        Some(if anchor <= self.cursor {
169            (anchor, self.cursor)
170        } else {
171            (self.cursor, anchor)
172        })
173    }
174
175    /// Begin a selection anchored at the cursor.
176    pub fn start_selection(&mut self) {
177        self.selection_anchor = Some(self.cursor);
178    }
179
180    /// Drop any selection, leaving the cursor where it is.
181    pub fn cancel_selection(&mut self) {
182        self.selection_anchor = None;
183    }
184
185    /// Select the entire buffer.
186    pub fn select_all(&mut self) {
187        self.selection_anchor = Some((0, 0));
188        let row = self.lines.len() - 1;
189        self.cursor = (row, char_count(&self.lines[row]));
190    }
191
192    /// Text held by the yank buffer, as left by the last copy, cut or kill.
193    pub fn yanked_text(&self) -> &str {
194        &self.yank
195    }
196
197    /// Overwrite the yank buffer, for example from an external paste.
198    pub fn set_yank(&mut self, text: impl Into<String>) {
199        self.yank = text.into();
200    }
201
202    /// Style applied to the text.
203    pub fn style(&self) -> Style {
204        self.style
205    }
206
207    pub fn set_style(&mut self, style: Style) {
208        self.style = style;
209    }
210
211    pub fn set_cursor_style(&mut self, style: Style) {
212        self.cursor_style = style;
213    }
214
215    pub fn set_selection_style(&mut self, style: Style) {
216        self.selection_style = style;
217    }
218
219    /// Whether the cursor cell is highlighted. Unfocused inputs hide it.
220    pub fn set_cursor_visible(&mut self, visible: bool) {
221        self.cursor_visible = visible;
222    }
223
224    pub fn cursor_visible(&self) -> bool {
225        self.cursor_visible
226    }
227
228    /// Width of a tab stop, in columns. Tab keys insert this many spaces.
229    pub fn set_tab_len(&mut self, len: usize) {
230        self.tab_len = len.max(1);
231    }
232
233    pub fn tab_len(&self) -> usize {
234        self.tab_len
235    }
236
237    /// Scroll position as `(first visible row, first visible terminal column)`
238    /// from the last render.
239    pub fn scroll_offsets(&self) -> (usize, usize) {
240        let vp = self.viewport.get();
241        (vp.row, vp.col)
242    }
243
244    /// Number of characters in the line the cursor is on.
245    fn line_len(&self, row: usize) -> usize {
246        self.lines.get(row).map(|l| char_count(l)).unwrap_or(0)
247    }
248
249    /// Clamp a position onto a real character boundary inside the buffer.
250    fn clamp_position(&self, (row, col): (usize, usize)) -> (usize, usize) {
251        let row = row.min(self.lines.len() - 1);
252        (row, col.min(self.line_len(row)))
253    }
254}
255
256/// Split `text` into buffer lines, tolerating CRLF and always yielding at least
257/// one line.
258fn split_lines(text: &str) -> Vec<String> {
259    if text.is_empty() {
260        return vec![String::new()];
261    }
262    text.replace("\r\n", "\n")
263        .split('\n')
264        .map(|s| s.replace('\r', ""))
265        .collect()
266}
267
268/// Character count of a line.
269fn char_count(line: &str) -> usize {
270    line.chars().count()
271}
272
273/// Byte offset of character `col`, or the line length when `col` is past the end.
274fn byte_of_char(line: &str, col: usize) -> usize {
275    line.char_indices()
276        .nth(col)
277        .map(|(i, _)| i)
278        .unwrap_or(line.len())
279}