1use ftui_core::event::{Event, KeyCode, KeyEvent, KeyEventKind, Modifiers};
16use ftui_core::geometry::Rect;
17use ftui_render::frame::Frame;
18use ftui_style::Style;
19use ftui_text::editor::{Editor, Selection};
20use ftui_text::wrap::display_width;
21use ftui_text::{CursorNavigator, CursorPosition};
22use unicode_segmentation::UnicodeSegmentation;
23
24use crate::{StatefulWidget, Widget, apply_style, draw_text_span};
25
26#[derive(Debug, Clone)]
28pub struct TextArea {
29 editor: Editor,
30 placeholder: String,
32 focused: bool,
34 show_line_numbers: bool,
36 style: Style,
38 cursor_line_style: Option<Style>,
40 selection_style: Style,
42 placeholder_style: Style,
44 line_number_style: Style,
46 soft_wrap: bool,
51 max_height: usize,
53 scroll_top: std::cell::Cell<usize>,
55 scroll_left: std::cell::Cell<usize>,
57 #[allow(dead_code)]
59 last_viewport_height: std::cell::Cell<usize>,
60 last_viewport_width: std::cell::Cell<usize>,
62}
63
64impl Default for TextArea {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70#[derive(Debug, Clone, Default)]
72pub struct TextAreaState {
73 pub last_viewport_height: u16,
75 pub last_viewport_width: u16,
77}
78
79#[derive(Debug, Clone)]
80struct WrappedSlice {
81 text: String,
82 start_byte: usize,
83 start_col: usize,
84 width: usize,
85}
86
87impl TextArea {
88 #[must_use]
90 pub fn new() -> Self {
91 Self {
92 editor: Editor::new(),
93 placeholder: String::new(),
94 focused: false,
95 show_line_numbers: false,
96 style: Style::default(),
97 cursor_line_style: None,
98 selection_style: Style::new().reverse(),
99 placeholder_style: Style::new().dim(),
100 line_number_style: Style::new().dim(),
101 soft_wrap: false,
102 max_height: 0,
103 scroll_top: std::cell::Cell::new(usize::MAX), scroll_left: std::cell::Cell::new(0),
105 last_viewport_height: std::cell::Cell::new(0),
106 last_viewport_width: std::cell::Cell::new(0),
107 }
108 }
109
110 pub fn handle_event(&mut self, event: &Event) -> bool {
116 match event {
117 Event::Key(key)
118 if key.kind == KeyEventKind::Press || key.kind == KeyEventKind::Repeat =>
119 {
120 self.handle_key(key)
121 }
122 Event::Paste(paste) => {
123 self.insert_text(&paste.text);
124 true
125 }
126 _ => false,
127 }
128 }
129
130 fn handle_key(&mut self, key: &KeyEvent) -> bool {
131 let ctrl = key.modifiers.contains(Modifiers::CTRL);
132 let shift = key.modifiers.contains(Modifiers::SHIFT);
133 let _alt = key.modifiers.contains(Modifiers::ALT);
134
135 match key.code {
136 KeyCode::Char(c) if !ctrl => {
137 self.insert_char(c);
138 true
139 }
140 KeyCode::Enter => {
141 self.insert_newline();
142 true
143 }
144 KeyCode::Backspace => {
145 if ctrl {
146 self.delete_word_backward();
147 } else {
148 self.delete_backward();
149 }
150 true
151 }
152 KeyCode::Delete => {
153 self.delete_forward();
154 true
155 }
156 KeyCode::Left => {
157 if ctrl {
158 self.move_word_left();
159 } else if shift {
160 self.select_left();
161 } else {
162 self.move_left();
163 }
164 true
165 }
166 KeyCode::Right => {
167 if ctrl {
168 self.move_word_right();
169 } else if shift {
170 self.select_right();
171 } else {
172 self.move_right();
173 }
174 true
175 }
176 KeyCode::Up => {
177 if shift {
178 self.select_up();
179 } else {
180 self.move_up();
181 }
182 true
183 }
184 KeyCode::Down => {
185 if shift {
186 self.select_down();
187 } else {
188 self.move_down();
189 }
190 true
191 }
192 KeyCode::Home => {
193 self.move_to_line_start();
194 true
195 }
196 KeyCode::End => {
197 self.move_to_line_end();
198 true
199 }
200 KeyCode::PageUp => {
201 let page = self.last_viewport_height.get().max(1);
202 for _ in 0..page {
203 self.editor.move_up();
204 }
205 self.ensure_cursor_visible();
206 true
207 }
208 KeyCode::PageDown => {
209 let page = self.last_viewport_height.get().max(1);
210 for _ in 0..page {
211 self.editor.move_down();
212 }
213 self.ensure_cursor_visible();
214 true
215 }
216 KeyCode::Char('a') if ctrl => {
217 self.select_all();
218 true
219 }
220 KeyCode::Char('k') if ctrl => {
222 self.delete_to_end_of_line();
223 true
224 }
225 KeyCode::Char('z') if ctrl => {
227 self.undo();
228 true
229 }
230 KeyCode::Char('y') if ctrl => {
232 self.redo();
233 true
234 }
235 _ => false,
236 }
237 }
238
239 #[must_use]
243 pub fn with_text(mut self, text: &str) -> Self {
244 self.editor = Editor::with_text(text);
245 self.editor.move_to_document_start();
246 self
247 }
248
249 #[must_use]
251 pub fn with_placeholder(mut self, text: impl Into<String>) -> Self {
252 self.placeholder = text.into();
253 self
254 }
255
256 #[must_use]
258 pub fn with_focus(mut self, focused: bool) -> Self {
259 self.focused = focused;
260 self
261 }
262
263 #[must_use]
265 pub fn with_line_numbers(mut self, show: bool) -> Self {
266 self.show_line_numbers = show;
267 self
268 }
269
270 #[must_use]
272 pub fn with_style(mut self, style: Style) -> Self {
273 self.style = style;
274 self
275 }
276
277 #[must_use]
279 pub fn with_cursor_line_style(mut self, style: Style) -> Self {
280 self.cursor_line_style = Some(style);
281 self
282 }
283
284 #[must_use]
286 pub fn with_selection_style(mut self, style: Style) -> Self {
287 self.selection_style = style;
288 self
289 }
290
291 #[must_use]
293 pub fn with_soft_wrap(mut self, wrap: bool) -> Self {
294 self.soft_wrap = wrap;
295 self
296 }
297
298 #[must_use]
300 pub fn with_max_height(mut self, max: usize) -> Self {
301 self.max_height = max;
302 self
303 }
304
305 #[must_use]
309 pub fn text(&self) -> String {
310 self.editor.text()
311 }
312
313 pub fn set_text(&mut self, text: &str) {
315 self.editor.set_text(text);
316 self.scroll_top.set(0);
317 self.scroll_left.set(0);
318 }
319
320 #[must_use]
322 pub fn line_count(&self) -> usize {
323 self.editor.line_count()
324 }
325
326 #[must_use]
328 pub fn cursor(&self) -> CursorPosition {
329 self.editor.cursor()
330 }
331
332 pub fn set_cursor_position(&mut self, pos: CursorPosition) {
334 self.editor.set_cursor(pos);
335 self.ensure_cursor_visible();
336 }
337
338 #[must_use]
340 pub fn is_empty(&self) -> bool {
341 self.editor.is_empty()
342 }
343
344 #[must_use]
346 pub fn selection(&self) -> Option<Selection> {
347 self.editor.selection()
348 }
349
350 #[must_use]
352 pub fn selected_text(&self) -> Option<String> {
353 self.editor.selected_text()
354 }
355
356 #[must_use]
358 pub fn is_focused(&self) -> bool {
359 self.focused
360 }
361
362 pub fn set_focused(&mut self, focused: bool) {
364 self.focused = focused;
365 }
366
367 #[must_use]
369 pub fn editor(&self) -> &Editor {
370 &self.editor
371 }
372
373 pub fn editor_mut(&mut self) -> &mut Editor {
375 &mut self.editor
376 }
377
378 pub fn insert_text(&mut self, text: &str) {
382 self.editor.insert_text(text);
383 self.ensure_cursor_visible();
384 }
385
386 pub fn insert_char(&mut self, ch: char) {
388 self.editor.insert_char(ch);
389 self.ensure_cursor_visible();
390 }
391
392 pub fn insert_newline(&mut self) {
394 self.editor.insert_newline();
395 self.ensure_cursor_visible();
396 }
397
398 pub fn delete_backward(&mut self) {
400 self.editor.delete_backward();
401 self.ensure_cursor_visible();
402 }
403
404 pub fn delete_forward(&mut self) {
406 self.editor.delete_forward();
407 self.ensure_cursor_visible();
408 }
409
410 pub fn delete_word_backward(&mut self) {
412 self.editor.delete_word_backward();
413 self.ensure_cursor_visible();
414 }
415
416 pub fn delete_to_end_of_line(&mut self) {
418 self.editor.delete_to_end_of_line();
419 self.ensure_cursor_visible();
420 }
421
422 pub fn undo(&mut self) {
424 self.editor.undo();
425 self.ensure_cursor_visible();
426 }
427
428 pub fn redo(&mut self) {
430 self.editor.redo();
431 self.ensure_cursor_visible();
432 }
433
434 pub fn move_left(&mut self) {
438 self.editor.move_left();
439 self.ensure_cursor_visible();
440 }
441
442 pub fn move_right(&mut self) {
444 self.editor.move_right();
445 self.ensure_cursor_visible();
446 }
447
448 pub fn move_up(&mut self) {
450 self.editor.move_up();
451 self.ensure_cursor_visible();
452 }
453
454 pub fn move_down(&mut self) {
456 self.editor.move_down();
457 self.ensure_cursor_visible();
458 }
459
460 pub fn move_word_left(&mut self) {
462 self.editor.move_word_left();
463 self.ensure_cursor_visible();
464 }
465
466 pub fn move_word_right(&mut self) {
468 self.editor.move_word_right();
469 self.ensure_cursor_visible();
470 }
471
472 pub fn move_to_line_start(&mut self) {
474 self.editor.move_to_line_start();
475 self.ensure_cursor_visible();
476 }
477
478 pub fn move_to_line_end(&mut self) {
480 self.editor.move_to_line_end();
481 self.ensure_cursor_visible();
482 }
483
484 pub fn move_to_document_start(&mut self) {
486 self.editor.move_to_document_start();
487 self.ensure_cursor_visible();
488 }
489
490 pub fn move_to_document_end(&mut self) {
492 self.editor.move_to_document_end();
493 self.ensure_cursor_visible();
494 }
495
496 pub fn select_left(&mut self) {
500 self.editor.select_left();
501 self.ensure_cursor_visible();
502 }
503
504 pub fn select_right(&mut self) {
506 self.editor.select_right();
507 self.ensure_cursor_visible();
508 }
509
510 pub fn select_up(&mut self) {
512 self.editor.select_up();
513 self.ensure_cursor_visible();
514 }
515
516 pub fn select_down(&mut self) {
518 self.editor.select_down();
519 self.ensure_cursor_visible();
520 }
521
522 pub fn select_all(&mut self) {
524 self.editor.select_all();
525 }
526
527 pub fn clear_selection(&mut self) {
529 self.editor.clear_selection();
530 }
531
532 pub fn page_up(&mut self, state: &TextAreaState) {
536 let page = state.last_viewport_height.max(1) as usize;
537 for _ in 0..page {
538 self.editor.move_up();
539 }
540 self.ensure_cursor_visible();
541 }
542
543 pub fn page_down(&mut self, state: &TextAreaState) {
545 let page = state.last_viewport_height.max(1) as usize;
546 for _ in 0..page {
547 self.editor.move_down();
548 }
549 self.ensure_cursor_visible();
550 }
551
552 fn gutter_width(&self) -> u16 {
554 if !self.show_line_numbers {
555 return 0;
556 }
557 let digits = {
558 let mut count = self.line_count().max(1);
559 let mut d: u16 = 0;
560 while count > 0 {
561 d += 1;
562 count /= 10;
563 }
564 d
565 };
566 digits + 2 }
568
569 fn measure_wrap_count(line_text: &str, max_width: usize) -> usize {
573 if line_text.is_empty() {
574 return 1;
575 }
576
577 let mut count = 0;
578 let mut current_width = 0;
579 let mut has_content = false;
580
581 Self::run_wrapping_logic(line_text, max_width, |_, width, flush| {
582 if flush {
583 count += 1;
584 current_width = 0;
585 has_content = false;
586 } else {
587 current_width = width;
588 has_content = true;
589 }
590 });
591
592 if has_content || count == 0 {
597 count += 1;
598 }
599
600 count
601 }
602
603 fn run_wrapping_logic<F>(line_text: &str, max_width: usize, mut callback: F)
609 where
610 F: FnMut(usize, usize, bool),
611 {
612 let mut current_width = 0;
613 let mut byte_cursor = 0;
614
615 for segment in line_text.split_word_bounds() {
616 let seg_len = segment.len();
617 let seg_width: usize = segment.graphemes(true).map(display_width).sum();
618
619 if max_width > 0 && current_width + seg_width > max_width {
620 callback(byte_cursor, current_width, true);
622 current_width = 0;
623 }
624
625 if max_width > 0 && seg_width > max_width {
626 for grapheme in segment.graphemes(true) {
627 let g_width = display_width(grapheme);
628 let g_len = grapheme.len();
629
630 if max_width > 0 && current_width + g_width > max_width && current_width > 0 {
631 callback(byte_cursor, current_width, true);
632 current_width = 0;
633 }
634
635 current_width += g_width;
636 byte_cursor += g_len;
637 callback(byte_cursor, current_width, false);
638 }
639 continue;
640 }
641
642 current_width += seg_width;
643 byte_cursor += seg_len;
644 callback(byte_cursor, current_width, false);
645 }
646 }
647
648 fn wrap_line_slices(line_text: &str, max_width: usize) -> Vec<WrappedSlice> {
649 if line_text.is_empty() {
650 return vec![WrappedSlice {
651 text: String::new(),
652 start_byte: 0,
653 start_col: 0,
654 width: 0,
655 }];
656 }
657
658 let mut slices = Vec::new();
659 let mut current_text = String::new();
660 let mut current_width = 0;
661 let mut slice_start_byte = 0;
662 let mut slice_start_col = 0;
663 let mut byte_cursor = 0;
664 let mut col_cursor = 0;
665
666 let push_current = |slices: &mut Vec<WrappedSlice>,
667 text: &mut String,
668 width: &mut usize,
669 start_byte: &mut usize,
670 start_col: &mut usize,
671 byte_cursor: usize,
672 col_cursor: usize| {
673 if text.is_empty() && *width == 0 {
676 return;
677 }
678 slices.push(WrappedSlice {
679 text: std::mem::take(text),
680 start_byte: *start_byte,
681 start_col: *start_col,
682 width: *width,
683 });
684 *start_byte = byte_cursor;
685 *start_col = col_cursor;
686 *width = 0;
687 };
688
689 for segment in line_text.split_word_bounds() {
690 let seg_len = segment.len();
691 let seg_width: usize = segment.graphemes(true).map(display_width).sum();
692
693 if max_width > 0 && current_width + seg_width > max_width {
694 push_current(
695 &mut slices,
696 &mut current_text,
697 &mut current_width,
698 &mut slice_start_byte,
699 &mut slice_start_col,
700 byte_cursor,
701 col_cursor,
702 );
703 }
704
705 if max_width > 0 && seg_width > max_width {
706 for grapheme in segment.graphemes(true) {
707 let g_width = display_width(grapheme);
708 let g_len = grapheme.len();
709
710 if max_width > 0 && current_width + g_width > max_width && current_width > 0 {
711 push_current(
712 &mut slices,
713 &mut current_text,
714 &mut current_width,
715 &mut slice_start_byte,
716 &mut slice_start_col,
717 byte_cursor,
718 col_cursor,
719 );
720 }
721
722 current_text.push_str(grapheme);
723 current_width += g_width;
724 byte_cursor += g_len;
725 col_cursor += g_width;
726 }
727 continue;
728 }
729
730 current_text.push_str(segment);
731 current_width += seg_width;
732 byte_cursor += seg_len;
733 col_cursor += seg_width;
734 }
735
736 if !current_text.is_empty() || current_width > 0 || slices.is_empty() {
737 slices.push(WrappedSlice {
738 text: current_text,
739 start_byte: slice_start_byte,
740 start_col: slice_start_col,
741 width: current_width,
742 });
743 }
744
745 slices
746 }
747
748 fn cursor_wrap_position(
749 line_text: &str,
750 max_width: usize,
751 cursor_col: usize,
752 ) -> (usize, usize) {
753 let slices = Self::wrap_line_slices(line_text, max_width);
754 if slices.is_empty() {
755 return (0, 0);
756 }
757
758 for (idx, slice) in slices.iter().enumerate() {
759 let end_col = slice.start_col.saturating_add(slice.width);
760 if cursor_col <= end_col || idx == slices.len().saturating_sub(1) {
761 let col_in_slice = cursor_col.saturating_sub(slice.start_col);
762 return (idx, col_in_slice.min(slice.width));
763 }
764 }
765
766 (0, 0)
767 }
768
769 fn get_prev_char_width(&self) -> usize {
771 let cursor = self.editor.cursor();
772 if cursor.grapheme == 0 {
773 return 0;
774 }
775 let rope = self.editor.rope();
776 let line = rope
777 .line(cursor.line)
778 .unwrap_or(std::borrow::Cow::Borrowed(""));
779
780 line.graphemes(true)
781 .nth(cursor.grapheme - 1)
782 .map(display_width)
783 .unwrap_or(0)
784 }
785
786 fn ensure_cursor_visible(&mut self) {
788 let cursor = self.editor.cursor();
789
790 let last_height = self.last_viewport_height.get();
791
792 let vp_height = if last_height == 0 { 20 } else { last_height };
795
796 let last_width = self.last_viewport_width.get();
797
798 let vp_width = if last_width == 0 { 80 } else { last_width };
799
800 if self.scroll_top.get() == usize::MAX {
801 self.scroll_top.set(0);
802 }
803
804 self.ensure_cursor_visible_internal(vp_height, vp_width, cursor);
805 }
806
807 fn ensure_cursor_visible_internal(
808 &mut self,
809
810 vp_height: usize,
811
812 vp_width: usize,
813
814 cursor: CursorPosition,
815 ) {
816 let current_top = self.scroll_top.get();
817
818 if cursor.line < current_top {
821 self.scroll_top.set(cursor.line);
822 } else if vp_height > 0 && cursor.line >= current_top + vp_height {
823 self.scroll_top
824 .set(cursor.line.saturating_sub(vp_height - 1));
825 }
826
827 if !self.soft_wrap {
830 let current_left = self.scroll_left.get();
831
832 let visual_col = cursor.visual_col;
833
834 if visual_col < current_left {
837 self.scroll_left.set(visual_col);
838 }
839 else if vp_width > 0 && visual_col >= current_left + vp_width {
845 let candidate_scroll = visual_col.saturating_sub(vp_width - 1);
846 let prev_width = self.get_prev_char_width();
847 let max_scroll_for_prev = visual_col.saturating_sub(prev_width);
848
849 self.scroll_left
850 .set(candidate_scroll.min(max_scroll_for_prev));
851 }
852 }
853 }
854}
855
856impl Widget for TextArea {
857 fn render(&self, area: Rect, frame: &mut Frame) {
858 if area.width < 1 || area.height < 1 {
859 return;
860 }
861
862 self.last_viewport_height.set(area.height as usize);
863
864 let deg = frame.buffer.degradation;
865 if deg.apply_styling() {
866 crate::set_style_area(&mut frame.buffer, area, self.style);
867 }
868
869 let gutter_w = self.gutter_width();
870 let text_area_x = area.x.saturating_add(gutter_w);
871 let text_area_w = area.width.saturating_sub(gutter_w) as usize;
872 let vp_height = area.height as usize;
873
874 self.last_viewport_width.set(text_area_w);
875
876 let cursor = self.editor.cursor();
877 let mut scroll_top = if self.scroll_top.get() == usize::MAX {
879 0
880 } else {
881 self.scroll_top.get()
882 };
883 if vp_height > 0 {
884 if cursor.line < scroll_top {
885 scroll_top = cursor.line;
886 } else if cursor.line >= scroll_top + vp_height {
887 scroll_top = cursor.line.saturating_sub(vp_height - 1);
888 }
889 }
890 self.scroll_top.set(scroll_top);
891
892 let mut scroll_left = self.scroll_left.get();
893 if !self.soft_wrap && text_area_w > 0 {
894 let visual_col = cursor.visual_col;
895 if visual_col < scroll_left {
896 scroll_left = visual_col;
897 } else if visual_col >= scroll_left + text_area_w {
898 let candidate_scroll = visual_col.saturating_sub(text_area_w - 1);
899 let prev_width = self.get_prev_char_width();
900 let max_scroll_for_prev = visual_col.saturating_sub(prev_width);
901
902 scroll_left = candidate_scroll.min(max_scroll_for_prev);
903 }
904 }
905 self.scroll_left.set(scroll_left);
906
907 let rope = self.editor.rope();
908 let nav = CursorNavigator::new(rope);
909
910 let sel_range = self.editor.selection().and_then(|sel| {
912 if sel.is_empty() {
913 None
914 } else {
915 let (a, b) = sel.byte_range(&nav);
916 Some((a, b))
917 }
918 });
919
920 if self.editor.is_empty() && !self.placeholder.is_empty() {
922 let style = if deg.apply_styling() {
923 self.placeholder_style
924 } else {
925 Style::default()
926 };
927 draw_text_span(
928 frame,
929 text_area_x,
930 area.y,
931 &self.placeholder,
932 style,
933 area.right(),
934 );
935 if self.focused {
936 frame.set_cursor(Some((text_area_x, area.y)));
937 }
938 return;
939 }
940
941 if self.soft_wrap {
942 self.scroll_left.set(0);
943
944 let mut cursor_virtual = 0;
946 for line_idx in 0..cursor.line {
947 let line_text = rope
948 .line(line_idx)
949 .unwrap_or(std::borrow::Cow::Borrowed(""));
950 let line_text = line_text.strip_suffix('\n').unwrap_or(&line_text);
951 cursor_virtual += Self::measure_wrap_count(line_text, text_area_w);
952 }
953
954 let cursor_line_text = rope
955 .line(cursor.line)
956 .unwrap_or(std::borrow::Cow::Borrowed(""));
957 let cursor_line_text = cursor_line_text
958 .strip_suffix('\n')
959 .unwrap_or(&cursor_line_text);
960 let (cursor_wrap_idx, cursor_col_in_wrap) =
961 Self::cursor_wrap_position(cursor_line_text, text_area_w, cursor.visual_col);
962 cursor_virtual = cursor_virtual.saturating_add(cursor_wrap_idx);
963
964 let mut scroll_virtual = self.scroll_top.get();
966 if cursor_virtual < scroll_virtual {
967 scroll_virtual = cursor_virtual;
968 } else if cursor_virtual >= scroll_virtual + vp_height {
969 scroll_virtual = cursor_virtual.saturating_sub(vp_height - 1);
970 }
971 self.scroll_top.set(scroll_virtual);
972
973 let mut virtual_index = 0usize;
975 for line_idx in 0..self.editor.line_count() {
976 if virtual_index >= scroll_virtual + vp_height {
977 break;
978 }
979
980 let line_text = rope
981 .line(line_idx)
982 .unwrap_or(std::borrow::Cow::Borrowed(""));
983 let line_text = line_text.strip_suffix('\n').unwrap_or(&line_text);
984
985 let wrap_count = Self::measure_wrap_count(line_text, text_area_w);
987 if virtual_index + wrap_count <= scroll_virtual {
988 virtual_index += wrap_count;
989 continue;
990 }
991
992 let line_start_byte = nav.to_byte_index(nav.from_line_grapheme(line_idx, 0));
993 let slices = Self::wrap_line_slices(line_text, text_area_w);
994
995 for (slice_idx, slice) in slices.iter().enumerate() {
996 if virtual_index < scroll_virtual {
997 virtual_index += 1;
998 continue;
999 }
1000
1001 let row = virtual_index.saturating_sub(scroll_virtual);
1002 if row >= vp_height {
1003 break;
1004 }
1005
1006 let y = area.y.saturating_add(row as u16);
1007
1008 if self.show_line_numbers && slice_idx == 0 {
1010 let style = if deg.apply_styling() {
1011 self.line_number_style
1012 } else {
1013 Style::default()
1014 };
1015 let num_str =
1016 format!("{:>width$} ", line_idx + 1, width = (gutter_w - 2) as usize);
1017 draw_text_span(frame, area.x, y, &num_str, style, text_area_x);
1018 }
1019
1020 if line_idx == cursor.line
1022 && slice_idx == cursor_wrap_idx
1023 && let Some(cl_style) = self.cursor_line_style
1024 && deg.apply_styling()
1025 {
1026 for cx in text_area_x..area.right() {
1027 if let Some(cell) = frame.buffer.get_mut(cx, y) {
1028 apply_style(cell, cl_style);
1029 }
1030 }
1031 }
1032
1033 let mut visual_x: usize = 0;
1035 let mut grapheme_byte_offset = line_start_byte + slice.start_byte;
1036
1037 for g in slice.text.graphemes(true) {
1038 let g_width = display_width(g);
1039 let g_byte_len = g.len();
1040
1041 if visual_x >= text_area_w {
1042 break;
1043 }
1044
1045 let px = text_area_x + visual_x as u16;
1046
1047 let mut g_style = self.style;
1049 if let Some((sel_start, sel_end)) = sel_range
1050 && grapheme_byte_offset >= sel_start
1051 && grapheme_byte_offset < sel_end
1052 && deg.apply_styling()
1053 {
1054 g_style = g_style.merge(&self.selection_style);
1055 }
1056
1057 if g_width > 0 {
1058 draw_text_span(frame, px, y, g, g_style, area.right());
1059 }
1060
1061 visual_x += g_width;
1062 grapheme_byte_offset += g_byte_len;
1063 }
1064
1065 virtual_index += 1;
1066 }
1067 }
1068
1069 if self.focused && cursor_virtual >= scroll_virtual {
1071 let row = cursor_virtual.saturating_sub(scroll_virtual);
1072 if row < vp_height {
1073 let cursor_screen_x = text_area_x.saturating_add(cursor_col_in_wrap as u16);
1074 let cursor_screen_y = area.y.saturating_add(row as u16);
1075 if cursor_screen_x < area.right() && cursor_screen_y < area.bottom() {
1076 frame.set_cursor(Some((cursor_screen_x, cursor_screen_y)));
1077 }
1078 }
1079 }
1080
1081 return;
1082 }
1083
1084 for row in 0..vp_height {
1086 let line_idx = scroll_top + row;
1087 let y = area.y.saturating_add(row as u16);
1088
1089 if line_idx >= self.editor.line_count() {
1090 break;
1091 }
1092
1093 if self.show_line_numbers {
1095 let style = if deg.apply_styling() {
1096 self.line_number_style
1097 } else {
1098 Style::default()
1099 };
1100 let num_str = format!("{:>width$} ", line_idx + 1, width = (gutter_w - 2) as usize);
1101 draw_text_span(frame, area.x, y, &num_str, style, text_area_x);
1102 }
1103
1104 if line_idx == cursor.line
1106 && let Some(cl_style) = self.cursor_line_style
1107 && deg.apply_styling()
1108 {
1109 for cx in text_area_x..area.right() {
1110 if let Some(cell) = frame.buffer.get_mut(cx, y) {
1111 apply_style(cell, cl_style);
1112 }
1113 }
1114 }
1115
1116 let line_text = rope
1118 .line(line_idx)
1119 .unwrap_or(std::borrow::Cow::Borrowed(""));
1120 let line_text = line_text.strip_suffix('\n').unwrap_or(&line_text);
1121
1122 let line_start_byte = nav.to_byte_index(nav.from_line_grapheme(line_idx, 0));
1124
1125 let mut visual_x: usize = 0;
1127 let graphemes: Vec<&str> = line_text.graphemes(true).collect();
1128 let mut grapheme_byte_offset = line_start_byte;
1129
1130 for g in &graphemes {
1131 let g_width = display_width(g);
1132 let g_byte_len = g.len();
1133
1134 if visual_x + g_width <= scroll_left {
1136 visual_x += g_width;
1137 grapheme_byte_offset += g_byte_len;
1138 continue;
1139 }
1140
1141 if visual_x < scroll_left {
1143 visual_x += g_width;
1144 grapheme_byte_offset += g_byte_len;
1145 continue;
1146 }
1147
1148 let screen_x = visual_x.saturating_sub(scroll_left);
1150 if screen_x >= text_area_w {
1151 break;
1152 }
1153
1154 let px = text_area_x + screen_x as u16;
1155
1156 let mut g_style = self.style;
1158 if let Some((sel_start, sel_end)) = sel_range
1159 && grapheme_byte_offset >= sel_start
1160 && grapheme_byte_offset < sel_end
1161 && deg.apply_styling()
1162 {
1163 g_style = g_style.merge(&self.selection_style);
1164 }
1165
1166 if g_width > 0 {
1168 draw_text_span(frame, px, y, g, g_style, area.right());
1169 }
1170
1171 visual_x += g_width;
1172 grapheme_byte_offset += g_byte_len;
1173 }
1174 }
1175
1176 if self.focused {
1178 let cursor_row = cursor.line.saturating_sub(scroll_top);
1179 if cursor_row < vp_height {
1180 let cursor_screen_x = (cursor.visual_col.saturating_sub(scroll_left) as u16)
1181 .saturating_add(text_area_x);
1182 let cursor_screen_y = area.y.saturating_add(cursor_row as u16);
1183 if cursor_screen_x < area.right() && cursor_screen_y < area.bottom() {
1184 frame.set_cursor(Some((cursor_screen_x, cursor_screen_y)));
1185 }
1186 }
1187 }
1188 }
1189
1190 fn is_essential(&self) -> bool {
1191 true
1192 }
1193}
1194
1195impl StatefulWidget for TextArea {
1196 type State = TextAreaState;
1197
1198 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
1199 state.last_viewport_height = area.height;
1200 state.last_viewport_width = area.width;
1201 Widget::render(self, area, frame);
1202 }
1203}
1204
1205#[cfg(test)]
1206mod tests {
1207 use super::*;
1208
1209 #[test]
1210 fn new_textarea_is_empty() {
1211 let ta = TextArea::new();
1212 assert!(ta.is_empty());
1213 assert_eq!(ta.text(), "");
1214 assert_eq!(ta.line_count(), 1); }
1216
1217 #[test]
1218 fn with_text_builder() {
1219 let ta = TextArea::new().with_text("hello\nworld");
1220 assert_eq!(ta.text(), "hello\nworld");
1221 assert_eq!(ta.line_count(), 2);
1222 }
1223
1224 #[test]
1225 fn insert_text_and_newline() {
1226 let mut ta = TextArea::new();
1227 ta.insert_text("hello");
1228 ta.insert_newline();
1229 ta.insert_text("world");
1230 assert_eq!(ta.text(), "hello\nworld");
1231 assert_eq!(ta.line_count(), 2);
1232 }
1233
1234 #[test]
1235 fn delete_backward_works() {
1236 let mut ta = TextArea::new().with_text("hello");
1237 ta.move_to_document_end();
1238 ta.delete_backward();
1239 assert_eq!(ta.text(), "hell");
1240 }
1241
1242 #[test]
1243 fn cursor_movement() {
1244 let mut ta = TextArea::new().with_text("abc\ndef\nghi");
1245 ta.move_to_document_start();
1246 assert_eq!(ta.cursor().line, 0);
1247 assert_eq!(ta.cursor().grapheme, 0);
1248
1249 ta.move_down();
1250 assert_eq!(ta.cursor().line, 1);
1251
1252 ta.move_to_line_end();
1253 assert_eq!(ta.cursor().grapheme, 3);
1254
1255 ta.move_to_document_end();
1256 assert_eq!(ta.cursor().line, 2);
1257 }
1258
1259 #[test]
1260 fn undo_redo() {
1261 let mut ta = TextArea::new();
1262 ta.insert_text("abc");
1263 assert_eq!(ta.text(), "abc");
1264 ta.undo();
1265 assert_eq!(ta.text(), "");
1266 ta.redo();
1267 assert_eq!(ta.text(), "abc");
1268 }
1269
1270 #[test]
1271 fn selection_and_delete() {
1272 let mut ta = TextArea::new().with_text("hello world");
1273 ta.move_to_document_start();
1274 for _ in 0..5 {
1275 ta.select_right();
1276 }
1277 assert_eq!(ta.selected_text(), Some("hello".to_string()));
1278 ta.delete_backward();
1279 assert_eq!(ta.text(), " world");
1280 }
1281
1282 #[test]
1283 fn select_all() {
1284 let mut ta = TextArea::new().with_text("abc\ndef");
1285 ta.select_all();
1286 assert_eq!(ta.selected_text(), Some("abc\ndef".to_string()));
1287 }
1288
1289 #[test]
1290 fn set_text_resets() {
1291 let mut ta = TextArea::new().with_text("old");
1292 ta.insert_text(" stuff");
1293 ta.set_text("new");
1294 assert_eq!(ta.text(), "new");
1295 }
1296
1297 #[test]
1298 fn scroll_follows_cursor() {
1299 let mut ta = TextArea::new();
1300 for i in 0..50 {
1302 ta.insert_text(&format!("line {}\n", i));
1303 }
1304 assert!(ta.scroll_top.get() > 0);
1306 assert!(ta.cursor().line >= 49);
1307
1308 ta.move_to_document_start();
1310 assert_eq!(ta.scroll_top.get(), 0);
1311 }
1312
1313 #[test]
1314 fn gutter_width_without_line_numbers() {
1315 let ta = TextArea::new();
1316 assert_eq!(ta.gutter_width(), 0);
1317 }
1318
1319 #[test]
1320 fn gutter_width_with_line_numbers() {
1321 let mut ta = TextArea::new().with_line_numbers(true);
1322 ta.insert_text("a\nb\nc");
1323 assert_eq!(ta.gutter_width(), 3); }
1325
1326 #[test]
1327 fn gutter_width_many_lines() {
1328 let mut ta = TextArea::new().with_line_numbers(true);
1329 for i in 0..100 {
1330 ta.insert_text(&format!("line {}\n", i));
1331 }
1332 assert_eq!(ta.gutter_width(), 5); }
1334
1335 #[test]
1336 fn focus_state() {
1337 let mut ta = TextArea::new();
1338 assert!(!ta.is_focused());
1339 ta.set_focused(true);
1340 assert!(ta.is_focused());
1341 }
1342
1343 #[test]
1344 fn word_movement() {
1345 let mut ta = TextArea::new().with_text("hello world foo");
1346 ta.move_to_document_start();
1347 ta.move_word_right();
1348 assert_eq!(ta.cursor().grapheme, 6);
1349 ta.move_word_left();
1350 assert_eq!(ta.cursor().grapheme, 0);
1351 }
1352
1353 #[test]
1354 fn page_up_down() {
1355 let mut ta = TextArea::new();
1356 for i in 0..50 {
1357 ta.insert_text(&format!("line {}\n", i));
1358 }
1359 ta.move_to_document_start();
1360 let state = TextAreaState {
1361 last_viewport_height: 10,
1362 last_viewport_width: 80,
1363 };
1364 ta.page_down(&state);
1365 assert!(ta.cursor().line >= 10);
1366 ta.page_up(&state);
1367 assert_eq!(ta.cursor().line, 0);
1368 }
1369
1370 #[test]
1371 fn insert_replaces_selection() {
1372 let mut ta = TextArea::new().with_text("hello world");
1373 ta.move_to_document_start();
1374 for _ in 0..5 {
1375 ta.select_right();
1376 }
1377 ta.insert_text("goodbye");
1378 assert_eq!(ta.text(), "goodbye world");
1379 }
1380
1381 #[test]
1382 fn insert_single_char() {
1383 let mut ta = TextArea::new();
1384 ta.insert_char('X');
1385 assert_eq!(ta.text(), "X");
1386 assert_eq!(ta.cursor().grapheme, 1);
1387 }
1388
1389 #[test]
1390 fn insert_multiline_text() {
1391 let mut ta = TextArea::new();
1392 ta.insert_text("line1\nline2\nline3");
1393 assert_eq!(ta.line_count(), 3);
1394 assert_eq!(ta.cursor().line, 2);
1395 }
1396
1397 #[test]
1398 fn delete_forward_works() {
1399 let mut ta = TextArea::new().with_text("hello");
1400 ta.move_to_document_start();
1401 ta.delete_forward();
1402 assert_eq!(ta.text(), "ello");
1403 }
1404
1405 #[test]
1406 fn delete_backward_at_line_start_joins_lines() {
1407 let mut ta = TextArea::new().with_text("abc\ndef");
1408 ta.move_to_document_start();
1410 ta.move_down();
1411 ta.move_to_line_start();
1412 ta.delete_backward();
1413 assert_eq!(ta.text(), "abcdef");
1414 assert_eq!(ta.line_count(), 1);
1415 }
1416
1417 #[test]
1418 fn cursor_horizontal_movement() {
1419 let mut ta = TextArea::new().with_text("abc");
1420 ta.move_to_document_start();
1421 ta.move_right();
1422 assert_eq!(ta.cursor().grapheme, 1);
1423 ta.move_right();
1424 assert_eq!(ta.cursor().grapheme, 2);
1425 ta.move_left();
1426 assert_eq!(ta.cursor().grapheme, 1);
1427 }
1428
1429 #[test]
1430 fn cursor_vertical_maintains_column() {
1431 let mut ta = TextArea::new().with_text("abcde\nfg\nhijkl");
1432 ta.move_to_document_start();
1433 ta.move_to_line_end(); ta.move_down(); assert_eq!(ta.cursor().line, 1);
1436 ta.move_down(); assert_eq!(ta.cursor().line, 2);
1438 }
1439
1440 #[test]
1441 fn selection_shift_arrow() {
1442 let mut ta = TextArea::new().with_text("abcdef");
1443 ta.move_to_document_start();
1444 ta.select_right();
1445 ta.select_right();
1446 ta.select_right();
1447 assert_eq!(ta.selected_text(), Some("abc".to_string()));
1448 }
1449
1450 #[test]
1451 fn selection_extends_up_down() {
1452 let mut ta = TextArea::new().with_text("line1\nline2\nline3");
1453 ta.move_to_document_start();
1454 ta.select_down();
1455 let sel = ta.selected_text().unwrap();
1456 assert!(sel.contains('\n'));
1457 }
1458
1459 #[test]
1460 fn undo_chain() {
1461 let mut ta = TextArea::new();
1462 ta.insert_text("a");
1463 ta.insert_text("b");
1464 ta.insert_text("c");
1465 assert_eq!(ta.text(), "abc");
1466 ta.undo();
1467 ta.undo();
1468 ta.undo();
1469 assert_eq!(ta.text(), "");
1470 }
1471
1472 #[test]
1473 fn redo_discarded_on_new_edit() {
1474 let mut ta = TextArea::new();
1475 ta.insert_text("abc");
1476 ta.undo();
1477 ta.insert_text("xyz");
1478 ta.redo(); assert_eq!(ta.text(), "xyz");
1480 }
1481
1482 #[test]
1483 fn clear_selection() {
1484 let mut ta = TextArea::new().with_text("hello");
1485 ta.select_all();
1486 assert!(ta.selection().is_some());
1487 ta.clear_selection();
1488 assert!(ta.selection().is_none());
1489 }
1490
1491 #[test]
1492 fn delete_word_backward() {
1493 let mut ta = TextArea::new().with_text("hello world");
1494 ta.move_to_document_end();
1495 ta.delete_word_backward();
1496 assert_eq!(ta.text(), "hello ");
1497 }
1498
1499 #[test]
1500 fn delete_to_end_of_line() {
1501 let mut ta = TextArea::new().with_text("hello world");
1502 ta.move_to_document_start();
1503 ta.move_right(); ta.delete_to_end_of_line();
1505 assert_eq!(ta.text(), "h");
1506 }
1507
1508 #[test]
1509 fn placeholder_builder() {
1510 let ta = TextArea::new().with_placeholder("Enter text...");
1511 assert!(ta.is_empty());
1512 assert_eq!(ta.placeholder, "Enter text...");
1513 }
1514
1515 #[test]
1516 fn soft_wrap_builder() {
1517 let ta = TextArea::new().with_soft_wrap(true);
1518 assert!(ta.soft_wrap);
1519 }
1520
1521 #[test]
1522 fn soft_wrap_renders_wrapped_lines() {
1523 use crate::Widget;
1524 use ftui_render::grapheme_pool::GraphemePool;
1525
1526 let ta = TextArea::new().with_soft_wrap(true).with_text("abcdef");
1527 let area = Rect::new(0, 0, 3, 2);
1528 let mut pool = GraphemePool::new();
1529 let mut frame = Frame::new(3, 2, &mut pool);
1530 Widget::render(&ta, area, &mut frame);
1531
1532 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('a'));
1533 assert_eq!(frame.buffer.get(2, 0).unwrap().content.as_char(), Some('c'));
1534 assert_eq!(frame.buffer.get(0, 1).unwrap().content.as_char(), Some('d'));
1535 assert_eq!(frame.buffer.get(2, 1).unwrap().content.as_char(), Some('f'));
1536 }
1537
1538 #[test]
1539 fn max_height_builder() {
1540 let ta = TextArea::new().with_max_height(10);
1541 assert_eq!(ta.max_height, 10);
1542 }
1543
1544 #[test]
1545 fn editor_access() {
1546 let mut ta = TextArea::new().with_text("test");
1547 assert_eq!(ta.editor().text(), "test");
1548 ta.editor_mut().insert_char('!');
1549 assert!(ta.text().contains('!'));
1550 }
1551
1552 #[test]
1553 fn move_to_line_start_and_end() {
1554 let mut ta = TextArea::new().with_text("hello world");
1555 ta.move_to_document_start();
1556 ta.move_to_line_end();
1557 assert_eq!(ta.cursor().grapheme, 11);
1558 ta.move_to_line_start();
1559 assert_eq!(ta.cursor().grapheme, 0);
1560 }
1561
1562 #[test]
1563 fn render_empty_with_placeholder() {
1564 use ftui_render::grapheme_pool::GraphemePool;
1565 let ta = TextArea::new()
1566 .with_placeholder("Type here")
1567 .with_focus(true);
1568 let mut pool = GraphemePool::new();
1569 let mut frame = Frame::new(20, 5, &mut pool);
1570 let area = Rect::new(0, 0, 20, 5);
1571 Widget::render(&ta, area, &mut frame);
1572 let cell = frame.buffer.get(0, 0).unwrap();
1574 assert_eq!(cell.content.as_char(), Some('T'));
1575 assert!(frame.cursor_position.is_some());
1577 }
1578
1579 #[test]
1580 fn render_with_content() {
1581 use ftui_render::grapheme_pool::GraphemePool;
1582 let ta = TextArea::new().with_text("abc\ndef").with_focus(true);
1583 let mut pool = GraphemePool::new();
1584 let mut frame = Frame::new(20, 5, &mut pool);
1585 let area = Rect::new(0, 0, 20, 5);
1586 Widget::render(&ta, area, &mut frame);
1587 let cell = frame.buffer.get(0, 0).unwrap();
1588 assert_eq!(cell.content.as_char(), Some('a'));
1589 }
1590
1591 #[test]
1592 fn render_line_numbers_without_styling() {
1593 use ftui_render::budget::DegradationLevel;
1594 use ftui_render::grapheme_pool::GraphemePool;
1595
1596 let ta = TextArea::new().with_text("a\nb").with_line_numbers(true);
1597 let mut pool = GraphemePool::new();
1598 let mut frame = Frame::new(8, 2, &mut pool);
1599 frame.set_degradation(DegradationLevel::NoStyling);
1600
1601 Widget::render(&ta, Rect::new(0, 0, 8, 2), &mut frame);
1602
1603 let cell = frame.buffer.get(0, 0).unwrap();
1604 assert_eq!(cell.content.as_char(), Some('1'));
1605 }
1606
1607 #[test]
1608 fn stateful_render_updates_viewport_state() {
1609 use ftui_render::grapheme_pool::GraphemePool;
1610
1611 let ta = TextArea::new();
1612 let mut state = TextAreaState::default();
1613 let mut pool = GraphemePool::new();
1614 let mut frame = Frame::new(10, 3, &mut pool);
1615 let area = Rect::new(0, 0, 10, 3);
1616
1617 StatefulWidget::render(&ta, area, &mut frame, &mut state);
1618
1619 assert_eq!(state.last_viewport_height, 3);
1620 assert_eq!(state.last_viewport_width, 10);
1621 }
1622
1623 #[test]
1624 fn render_zero_area_no_panic() {
1625 let ta = TextArea::new().with_text("test");
1626 use ftui_render::grapheme_pool::GraphemePool;
1627 let mut pool = GraphemePool::new();
1628 let mut frame = Frame::new(10, 10, &mut pool);
1629 Widget::render(&ta, Rect::new(0, 0, 0, 0), &mut frame);
1630 }
1631
1632 #[test]
1633 fn is_essential() {
1634 let ta = TextArea::new();
1635 assert!(Widget::is_essential(&ta));
1636 }
1637
1638 #[test]
1639 fn default_impl() {
1640 let ta = TextArea::default();
1641 assert!(ta.is_empty());
1642 }
1643
1644 #[test]
1645 fn insert_newline_splits_line() {
1646 let mut ta = TextArea::new().with_text("abcdef");
1647 ta.move_to_document_start();
1648 ta.move_right();
1649 ta.move_right();
1650 ta.move_right();
1651 ta.insert_newline();
1652 assert_eq!(ta.line_count(), 2);
1653 assert_eq!(ta.cursor().line, 1);
1654 }
1655
1656 #[test]
1657 fn unicode_grapheme_cluster() {
1658 let mut ta = TextArea::new();
1659 ta.insert_text("café");
1660 assert_eq!(ta.text(), "café");
1662 }
1663
1664 mod proptests {
1665 use super::*;
1666 use proptest::prelude::*;
1667
1668 proptest! {
1669 #[test]
1670 fn insert_delete_inverse(text in "[a-zA-Z0-9 ]{1,50}") {
1671 let mut ta = TextArea::new();
1672 ta.insert_text(&text);
1673 for _ in 0..text.len() {
1675 ta.delete_backward();
1676 }
1677 prop_assert!(ta.is_empty() || ta.text().is_empty());
1678 }
1679
1680 #[test]
1681 fn undo_redo_inverse(text in "[a-zA-Z0-9]{1,30}") {
1682 let mut ta = TextArea::new();
1683 ta.insert_text(&text);
1684 let after_insert = ta.text();
1685 ta.undo();
1686 ta.redo();
1687 prop_assert_eq!(ta.text(), after_insert);
1688 }
1689
1690 #[test]
1691 fn cursor_always_valid(ops in proptest::collection::vec(0u8..10, 1..20)) {
1692 let mut ta = TextArea::new().with_text("abc\ndef\nghi\njkl");
1693 for op in ops {
1694 match op {
1695 0 => ta.move_left(),
1696 1 => ta.move_right(),
1697 2 => ta.move_up(),
1698 3 => ta.move_down(),
1699 4 => ta.move_to_line_start(),
1700 5 => ta.move_to_line_end(),
1701 6 => ta.move_to_document_start(),
1702 7 => ta.move_to_document_end(),
1703 8 => ta.move_word_left(),
1704 _ => ta.move_word_right(),
1705 }
1706 let cursor = ta.cursor();
1707 prop_assert!(cursor.line < ta.line_count(),
1708 "cursor line {} >= line_count {}", cursor.line, ta.line_count());
1709 }
1710 }
1711
1712 #[test]
1713 fn selection_ordered(n in 1usize..20) {
1714 let mut ta = TextArea::new().with_text("hello world foo bar");
1715 ta.move_to_document_start();
1716 for _ in 0..n {
1717 ta.select_right();
1718 }
1719 if let Some(sel) = ta.selection() {
1720 prop_assert!(sel.anchor.line <= sel.head.line
1722 || (sel.anchor.line == sel.head.line
1723 && sel.anchor.grapheme <= sel.head.grapheme));
1724 }
1725 }
1726 }
1727 }
1728}