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    #[cfg(test)]
96    pub fn from_text(text: &str) -> Self {
97        let mut ta = Self::new();
98        ta.set_text(text);
99        ta
100    }
101
102    /// Replace the whole buffer, moving the cursor to the end and discarding
103    /// undo history, selection and scroll position.
104    pub fn set_text(&mut self, text: &str) {
105        self.lines = split_lines(text);
106        let row = self.lines.len() - 1;
107        let col = char_count(&self.lines[row]);
108        self.cursor = (row, col);
109        self.selection_anchor = None;
110        self.history.clear();
111        // The scroll starts over; the size is the field's, and stays.
112        let size = self.viewport.get();
113        self.viewport.set(Viewport {
114            width: size.width,
115            height: size.height,
116            ..Viewport::default()
117        });
118    }
119
120    /// Empty the buffer.
121    pub fn clear(&mut self) {
122        self.set_text("");
123    }
124
125    /// The buffer, one entry per line. Always holds at least one line.
126    pub fn lines(&self) -> &[String] {
127        &self.lines
128    }
129
130    /// The buffer as a single newline-joined string.
131    pub fn text(&self) -> String {
132        self.lines.join("\n")
133    }
134
135    /// Number of lines in the buffer.
136    pub fn line_count(&self) -> usize {
137        self.lines.len()
138    }
139
140    /// The line at `row`, if it exists.
141    pub fn line(&self, row: usize) -> Option<&str> {
142        self.lines.get(row).map(String::as_str)
143    }
144
145    /// True when the buffer holds no characters at all.
146    #[cfg(test)]
147    pub fn is_empty(&self) -> bool {
148        self.lines.len() == 1 && self.lines[0].is_empty()
149    }
150
151    /// Cursor position as `(row, character column)`.
152    pub fn cursor(&self) -> (usize, usize) {
153        self.cursor
154    }
155
156    /// Move the cursor to an absolute position, clamped into the buffer. This
157    /// cancels any selection.
158    #[cfg(test)]
159    pub fn set_cursor(&mut self, row: usize, col: usize) {
160        self.selection_anchor = None;
161        self.cursor = self.clamp_position((row, col));
162    }
163
164    /// Selected range as `(start, end)` with `start <= end`, if a selection is
165    /// active and non-empty.
166    pub fn selection(&self) -> Option<((usize, usize), (usize, usize))> {
167        let anchor = self.selection_anchor?;
168        if anchor == self.cursor {
169            return None;
170        }
171        Some(if anchor <= self.cursor {
172            (anchor, self.cursor)
173        } else {
174            (self.cursor, anchor)
175        })
176    }
177
178    /// Begin a selection anchored at the cursor.
179    #[cfg(test)]
180    pub fn start_selection(&mut self) {
181        self.selection_anchor = Some(self.cursor);
182    }
183
184    /// Drop any selection, leaving the cursor where it is.
185    pub fn cancel_selection(&mut self) {
186        self.selection_anchor = None;
187    }
188
189    /// Select the entire buffer.
190    pub fn select_all(&mut self) {
191        self.selection_anchor = Some((0, 0));
192        let row = self.lines.len() - 1;
193        self.cursor = (row, char_count(&self.lines[row]));
194    }
195
196    /// Text held by the yank buffer, as left by the last copy, cut or kill.
197    #[cfg(test)]
198    pub fn yanked_text(&self) -> &str {
199        &self.yank
200    }
201
202    pub fn set_style(&mut self, style: Style) {
203        self.style = style;
204    }
205
206    pub fn set_cursor_style(&mut self, style: Style) {
207        self.cursor_style = style;
208    }
209
210    pub fn set_selection_style(&mut self, style: Style) {
211        self.selection_style = style;
212    }
213
214    /// Whether the cursor cell is highlighted. Unfocused inputs hide it.
215    pub fn set_cursor_visible(&mut self, visible: bool) {
216        self.cursor_visible = visible;
217    }
218
219    /// Width of a tab stop, in columns. Tab keys insert this many spaces.
220    #[cfg(test)]
221    pub fn set_tab_len(&mut self, len: usize) {
222        self.tab_len = len.max(1);
223    }
224
225    /// Scroll position as `(first visible row, first visible terminal column)`
226    /// from the last render.
227    pub fn scroll_offsets(&self) -> (usize, usize) {
228        let vp = self.viewport.get();
229        (vp.row, vp.col)
230    }
231
232    /// Number of characters in the line the cursor is on.
233    fn line_len(&self, row: usize) -> usize {
234        self.lines.get(row).map(|l| char_count(l)).unwrap_or(0)
235    }
236
237    /// Clamp a position onto a real character boundary inside the buffer.
238    fn clamp_position(&self, (row, col): (usize, usize)) -> (usize, usize) {
239        let row = row.min(self.lines.len() - 1);
240        (row, col.min(self.line_len(row)))
241    }
242}
243
244/// Split `text` into buffer lines, tolerating CRLF and always yielding at least
245/// one line.
246fn split_lines(text: &str) -> Vec<String> {
247    if text.is_empty() {
248        return vec![String::new()];
249    }
250    text.replace("\r\n", "\n")
251        .split('\n')
252        .map(|s| s.replace('\r', ""))
253        .collect()
254}
255
256/// Character count of a line.
257fn char_count(line: &str) -> usize {
258    line.chars().count()
259}
260
261/// Byte offset of character `col`, or the line length when `col` is past the end.
262fn byte_of_char(line: &str, col: usize) -> usize {
263    line.char_indices()
264        .nth(col)
265        .map(|(i, _)| i)
266        .unwrap_or(line.len())
267}