1use crate::component::{Component, EventCx};
2use crate::event::{Event, Key};
3use crate::geom::{Pos, Rect, Size};
4use crate::render::RenderCx;
5use crate::style::{Color, Style};
6use crate::text::Text;
7
8const MAX_UNDO: usize = 100;
10
11pub struct TextArea {
16 lines: Vec<String>,
17 cursor: (usize, usize), scroll_x: u16,
19 scroll_y: u16,
20 width: u16,
21 height: u16,
22 show_line_numbers: bool,
23 rect: Rect,
24 focused: bool,
25 style: Style,
26 focus_style: Style,
27 line_number_style: Style,
28 #[allow(dead_code)]
29 selection_style: Style,
30 undo_stack: Vec<(Vec<String>, (usize, usize))>,
31 redo_stack: Vec<(Vec<String>, (usize, usize))>,
32}
33
34impl TextArea {
35 pub fn new() -> Self {
36 Self {
37 lines: vec![String::new()],
38 cursor: (0, 0),
39 scroll_x: 0,
40 scroll_y: 0,
41 width: 40,
42 height: 10,
43 show_line_numbers: false,
44 rect: Rect::default(),
45 focused: false,
46 style: Style::default(),
47 focus_style: Style::default().bg(Color::White).fg(Color::Black),
48 line_number_style: Style::default().fg(Color::Gray),
49 selection_style: Style::default().bg(Color::White).fg(Color::Black),
50 undo_stack: Vec::new(),
51 redo_stack: Vec::new(),
52 }
53 }
54
55 pub fn text(mut self, text: impl Into<Text>) -> Self {
56 let text = text.into();
57 self.lines = text
58 .lines
59 .iter()
60 .map(|l| {
61 l.spans
62 .iter()
63 .map(|s| s.text.clone())
64 .collect::<Vec<_>>()
65 .join("")
66 })
67 .collect();
68 if self.lines.is_empty() {
69 self.lines = vec![String::new()];
70 }
71 self.cursor = (0, 0);
72 self
73 }
74
75 pub fn width(mut self, w: u16) -> Self {
76 self.width = w;
77 self
78 }
79
80 pub fn height(mut self, h: u16) -> Self {
81 self.height = h;
82 self
83 }
84
85 pub fn show_line_numbers(mut self, show: bool) -> Self {
86 self.show_line_numbers = show;
87 self
88 }
89
90 pub fn style(mut self, style: Style) -> Self {
91 self.style = style;
92 self
93 }
94
95 pub fn focus_style(mut self, style: Style) -> Self {
96 self.focus_style = style;
97 self
98 }
99
100 pub fn text_content(&self) -> String {
101 self.lines.join("\n")
102 }
103
104 pub fn cursor_pos(&self) -> (usize, usize) {
105 self.cursor
106 }
107
108 fn save_undo(&mut self) {
110 if self.undo_stack.len() >= MAX_UNDO {
111 self.undo_stack.remove(0);
112 }
113 self.undo_stack.push((self.lines.clone(), self.cursor));
114 self.redo_stack.clear();
115 }
116
117 fn clamp_cursor(&mut self) {
118 if self.cursor.0 >= self.lines.len() {
119 self.cursor.0 = self.lines.len().saturating_sub(1);
120 }
121 let line_len = self.lines[self.cursor.0].len();
122 if self.cursor.1 > line_len {
123 self.cursor.1 = line_len;
124 }
125 }
126
127 fn move_left(&mut self) {
130 if self.cursor.1 > 0 {
131 self.cursor.1 -= 1;
132 } else if self.cursor.0 > 0 {
133 self.cursor.0 -= 1;
134 self.cursor.1 = self.lines[self.cursor.0].len();
135 }
136 }
137
138 fn move_right(&mut self) {
139 if self.cursor.1 < self.lines[self.cursor.0].len() {
140 self.cursor.1 += 1;
141 } else if self.cursor.0 + 1 < self.lines.len() {
142 self.cursor.0 += 1;
143 self.cursor.1 = 0;
144 }
145 }
146
147 fn move_up(&mut self) {
148 if self.cursor.0 > 0 {
149 self.cursor.0 -= 1;
150 let line_len = self.lines[self.cursor.0].len();
151 if self.cursor.1 > line_len {
152 self.cursor.1 = line_len;
153 }
154 }
155 }
156
157 fn move_down(&mut self) {
158 if self.cursor.0 + 1 < self.lines.len() {
159 self.cursor.0 += 1;
160 let line_len = self.lines[self.cursor.0].len();
161 if self.cursor.1 > line_len {
162 self.cursor.1 = line_len;
163 }
164 }
165 }
166
167 fn move_home(&mut self) {
168 self.cursor.1 = 0;
169 }
170
171 fn move_end(&mut self) {
172 self.cursor.1 = self.lines[self.cursor.0].len();
173 }
174
175 fn move_page_up(&mut self, page_size: usize) {
176 for _ in 0..page_size {
177 self.move_up();
178 }
179 }
180
181 fn move_page_down(&mut self, page_size: usize) {
182 for _ in 0..page_size {
183 self.move_down();
184 }
185 }
186
187 fn insert_char(&mut self, c: char) {
190 self.save_undo();
191 self.lines[self.cursor.0].insert(self.cursor.1, c);
192 self.cursor.1 = (self.cursor.1 + c.len_utf8()).min(self.lines[self.cursor.0].len());
194 }
195
196 fn delete_backward(&mut self) {
197 if self.cursor.1 > 0 {
198 self.save_undo();
199 if let Some(idx) = self.prev_char_boundary() {
201 self.lines[self.cursor.0].remove(idx);
202 self.cursor.1 = idx;
203 }
204 } else if self.cursor.0 > 0 {
205 self.save_undo();
206 let rest = self.lines.remove(self.cursor.0);
208 self.cursor.0 -= 1;
209 self.cursor.1 = self.lines[self.cursor.0].len();
210 self.lines[self.cursor.0].push_str(&rest);
211 }
212 }
213
214 fn delete_forward(&mut self) {
215 if self.cursor.1 < self.lines[self.cursor.0].len() {
216 self.save_undo();
217 self.lines[self.cursor.0].remove(self.cursor.1);
218 } else if self.cursor.0 + 1 < self.lines.len() {
219 self.save_undo();
220 let next = self.lines.remove(self.cursor.0 + 1);
222 self.lines[self.cursor.0].push_str(&next);
223 }
224 }
225
226 fn insert_newline(&mut self) {
227 self.save_undo();
228 let rest = self.lines[self.cursor.0].split_off(self.cursor.1);
229 self.lines.insert(self.cursor.0 + 1, rest);
230 self.cursor.0 += 1;
231 self.cursor.1 = 0;
232 }
233
234 fn undo(&mut self) {
235 if let Some((lines, cursor)) = self.undo_stack.pop() {
236 self.redo_stack.push((self.lines.clone(), self.cursor));
237 self.lines = lines;
238 self.cursor = cursor;
239 self.clamp_cursor();
240 }
241 }
242
243 fn redo(&mut self) {
244 if let Some((lines, cursor)) = self.redo_stack.pop() {
245 self.undo_stack.push((self.lines.clone(), self.cursor));
246 self.lines = lines;
247 self.cursor = cursor;
248 self.clamp_cursor();
249 }
250 }
251
252 fn prev_char_boundary(&self) -> Option<usize> {
253 let line = &self.lines[self.cursor.0];
254 let mut indices: Vec<usize> = line.char_indices().map(|(i, _)| i).collect();
255 indices.push(line.len());
256 indices.into_iter().rev().find(|&i| i < self.cursor.1)
257 }
258}
259
260impl Component for TextArea {
261 fn render(&self, cx: &mut RenderCx) {
262 let gutter_width: u16 = if self.show_line_numbers {
263 6
265 } else {
266 0
267 };
268
269 let visible_height = self.rect.height.min(self.height);
270 let visible_width = self.rect.width.min(self.width);
271 let _text_width = visible_width.saturating_sub(gutter_width);
272
273 for i in 0..visible_height as usize {
274 let line_idx = self.scroll_y as usize + i;
275 if line_idx >= self.lines.len() {
276 break;
277 }
278
279 let row_y = self.rect.y + i as u16;
280 let is_cursor_line = self.focused && line_idx == self.cursor.0;
281
282 if self.show_line_numbers {
284 let num_str = format!(" {:>3} │ ", line_idx + 1);
285 if line_idx == self.cursor.0 && self.focused {
286 cx.buffer.write_text(
287 Pos { x: self.rect.x, y: row_y },
288 self.rect,
289 &num_str,
290 &self.focus_style,
291 );
292 } else {
293 cx.buffer.write_text(
294 Pos { x: self.rect.x, y: row_y },
295 self.rect,
296 &num_str,
297 &self.line_number_style,
298 );
299 }
300 }
301
302 let line = &self.lines[line_idx];
304 let display = if line.len() > self.scroll_x as usize {
305 let start = line
306 .char_indices()
307 .nth(self.scroll_x as usize)
308 .map(|(i, _)| i)
309 .unwrap_or(line.len());
310 &line[start..]
311 } else {
312 ""
313 };
314
315 let text_x = self.rect.x + gutter_width;
316
317 if is_cursor_line {
318 let cursor_char_idx = self.cursor.1.saturating_sub(self.scroll_x as usize);
320 if cursor_char_idx <= display.chars().count() {
321 let chars: Vec<char> = display.chars().collect();
322 let before: String = chars.iter().take(cursor_char_idx).collect();
323 let at = chars.get(cursor_char_idx).map(|c| c.to_string()).unwrap_or_default();
324 let after: String = chars.iter().skip(cursor_char_idx + 1).collect();
325
326 cx.buffer.write_text(
327 Pos { x: text_x, y: row_y },
328 self.rect,
329 &before,
330 &self.focus_style,
331 );
332 let at_x = text_x + str_width(&before);
333 cx.buffer.write_text(
334 Pos { x: at_x, y: row_y },
335 self.rect,
336 &at,
337 &self.focus_style,
338 );
339 cx.buffer.write_text(
340 Pos {
341 x: at_x + str_width(&at),
342 y: row_y,
343 },
344 self.rect,
345 &after,
346 &self.focus_style,
347 );
348 return; } else {
350 display.to_string()
351 };
352 }
353
354 cx.buffer.write_text(
355 Pos { x: text_x, y: row_y },
356 self.rect,
357 display,
358 &self.style,
359 );
360 }
361 }
362
363 fn measure(&self, _constraint: crate::layout::Constraint, _cx: &mut crate::component::MeasureCx) -> Size {
364 Size {
365 width: self.width,
366 height: self.height.min(self.lines.len() as u16),
367 }
368 }
369
370 fn event(&mut self, event: &Event, cx: &mut EventCx) {
371 match event {
372 Event::Focus => {
373 self.focused = true;
374 cx.invalidate_paint();
375 return;
376 }
377 Event::Blur => {
378 self.focused = false;
379 cx.invalidate_paint();
380 return;
381 }
382 _ => {}
383 }
384
385 if cx.phase() != crate::event::EventPhase::Target {
386 return;
387 }
388
389 if let Event::Key(key_event) = event {
390 let ctrl = key_event.modifiers.ctrl;
391 let shift = key_event.modifiers.shift;
392
393 match (&key_event.key, ctrl, shift) {
394 (Key::Left, false, _) => {
395 self.move_left();
396 cx.invalidate_paint();
397 }
398 (Key::Right, false, _) => {
399 self.move_right();
400 cx.invalidate_paint();
401 }
402 (Key::Up, false, _) => {
403 self.move_up();
404 cx.invalidate_paint();
405 }
406 (Key::Down, false, _) => {
407 self.move_down();
408 cx.invalidate_paint();
409 }
410 (Key::Home, false, _) => {
411 self.move_home();
412 cx.invalidate_paint();
413 }
414 (Key::End, false, _) => {
415 self.move_end();
416 cx.invalidate_paint();
417 }
418 (Key::PageUp, false, _) => {
419 let page = self.height.saturating_sub(1) as usize;
420 self.move_page_up(page);
421 cx.invalidate_paint();
422 }
423 (Key::PageDown, false, _) => {
424 let page = self.height.saturating_sub(1) as usize;
425 self.move_page_down(page);
426 cx.invalidate_paint();
427 }
428 (Key::Char(c), false, false) if *c != '\n' => {
429 self.insert_char(*c);
430 cx.invalidate_paint();
431 }
432 (Key::Backspace, false, _) => {
433 self.delete_backward();
434 cx.invalidate_paint();
435 }
436 (Key::Delete, false, _) => {
437 self.delete_forward();
438 cx.invalidate_paint();
439 }
440 (Key::Enter, false, _) => {
441 self.insert_newline();
442 cx.invalidate_paint();
443 }
444 (Key::Char('z'), true, _) => {
446 self.undo();
447 cx.invalidate_paint();
448 }
449 (Key::Char('y'), true, _) => {
451 self.redo();
452 cx.invalidate_paint();
453 }
454 _ => {}
455 }
456 }
457 }
458
459 fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) {
460 self.rect = rect;
461 }
462
463 fn focusable(&self) -> bool {
464 true
465 }
466
467 fn style(&self) -> Style {
468 self.style.clone()
469 }
470}
471
472pub(crate) fn str_width(s: &str) -> u16 {
474 s.chars()
475 .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16)
476 .sum()
477}