datui_lib/widgets/textarea/
mod.rs1mod 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
39pub(super) struct Viewport {
40 pub row: usize,
42 pub col: usize,
44 pub height: u16,
45 pub width: u16,
46}
47
48#[derive(Debug, Clone)]
50pub struct TextArea {
51 lines: Vec<String>,
52 cursor: (usize, usize),
53 selection_anchor: Option<(usize, usize)>,
56 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 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 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 #[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 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 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 pub fn clear(&mut self) {
122 self.set_text("");
123 }
124
125 pub fn lines(&self) -> &[String] {
127 &self.lines
128 }
129
130 pub fn text(&self) -> String {
132 self.lines.join("\n")
133 }
134
135 pub fn line_count(&self) -> usize {
137 self.lines.len()
138 }
139
140 pub fn line(&self, row: usize) -> Option<&str> {
142 self.lines.get(row).map(String::as_str)
143 }
144
145 #[cfg(test)]
147 pub fn is_empty(&self) -> bool {
148 self.lines.len() == 1 && self.lines[0].is_empty()
149 }
150
151 pub fn cursor(&self) -> (usize, usize) {
153 self.cursor
154 }
155
156 #[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 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 #[cfg(test)]
180 pub fn start_selection(&mut self) {
181 self.selection_anchor = Some(self.cursor);
182 }
183
184 pub fn cancel_selection(&mut self) {
186 self.selection_anchor = None;
187 }
188
189 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 #[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 pub fn set_cursor_visible(&mut self, visible: bool) {
216 self.cursor_visible = visible;
217 }
218
219 #[cfg(test)]
221 pub fn set_tab_len(&mut self, len: usize) {
222 self.tab_len = len.max(1);
223 }
224
225 pub fn scroll_offsets(&self) -> (usize, usize) {
228 let vp = self.viewport.get();
229 (vp.row, vp.col)
230 }
231
232 fn line_len(&self, row: usize) -> usize {
234 self.lines.get(row).map(|l| char_count(l)).unwrap_or(0)
235 }
236
237 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
244fn 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
256fn char_count(line: &str) -> usize {
258 line.chars().count()
259}
260
261fn 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}