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 #[inline]
328 #[must_use]
329 pub fn cursor(&self) -> CursorPosition {
330 self.editor.cursor()
331 }
332
333 pub fn set_cursor_position(&mut self, pos: CursorPosition) {
335 self.editor.set_cursor(pos);
336 self.ensure_cursor_visible();
337 }
338
339 #[inline]
341 #[must_use]
342 pub fn is_empty(&self) -> bool {
343 self.editor.is_empty()
344 }
345
346 #[must_use = "use the returned selection (if any)"]
348 pub fn selection(&self) -> Option<Selection> {
349 self.editor.selection()
350 }
351
352 #[must_use = "use the returned selected text (if any)"]
354 pub fn selected_text(&self) -> Option<String> {
355 self.editor.selected_text()
356 }
357
358 #[must_use]
360 pub fn is_focused(&self) -> bool {
361 self.focused
362 }
363
364 pub fn set_focused(&mut self, focused: bool) {
366 self.focused = focused;
367 }
368
369 #[must_use]
371 pub fn editor(&self) -> &Editor {
372 &self.editor
373 }
374
375 pub fn editor_mut(&mut self) -> &mut Editor {
377 &mut self.editor
378 }
379
380 pub fn insert_text(&mut self, text: &str) {
384 self.editor.insert_text(text);
385 self.ensure_cursor_visible();
386 }
387
388 pub fn insert_char(&mut self, ch: char) {
390 self.editor.insert_char(ch);
391 self.ensure_cursor_visible();
392 }
393
394 pub fn insert_newline(&mut self) {
396 self.editor.insert_newline();
397 self.ensure_cursor_visible();
398 }
399
400 pub fn delete_backward(&mut self) {
402 self.editor.delete_backward();
403 self.ensure_cursor_visible();
404 }
405
406 pub fn delete_forward(&mut self) {
408 self.editor.delete_forward();
409 self.ensure_cursor_visible();
410 }
411
412 pub fn delete_word_backward(&mut self) {
414 self.editor.delete_word_backward();
415 self.ensure_cursor_visible();
416 }
417
418 pub fn delete_to_end_of_line(&mut self) {
420 self.editor.delete_to_end_of_line();
421 self.ensure_cursor_visible();
422 }
423
424 pub fn undo(&mut self) {
426 self.editor.undo();
427 self.ensure_cursor_visible();
428 }
429
430 pub fn redo(&mut self) {
432 self.editor.redo();
433 self.ensure_cursor_visible();
434 }
435
436 pub fn move_left(&mut self) {
440 self.editor.move_left();
441 self.ensure_cursor_visible();
442 }
443
444 pub fn move_right(&mut self) {
446 self.editor.move_right();
447 self.ensure_cursor_visible();
448 }
449
450 pub fn move_up(&mut self) {
452 self.editor.move_up();
453 self.ensure_cursor_visible();
454 }
455
456 pub fn move_down(&mut self) {
458 self.editor.move_down();
459 self.ensure_cursor_visible();
460 }
461
462 pub fn move_word_left(&mut self) {
464 self.editor.move_word_left();
465 self.ensure_cursor_visible();
466 }
467
468 pub fn move_word_right(&mut self) {
470 self.editor.move_word_right();
471 self.ensure_cursor_visible();
472 }
473
474 pub fn move_to_line_start(&mut self) {
476 self.editor.move_to_line_start();
477 self.ensure_cursor_visible();
478 }
479
480 pub fn move_to_line_end(&mut self) {
482 self.editor.move_to_line_end();
483 self.ensure_cursor_visible();
484 }
485
486 pub fn move_to_document_start(&mut self) {
488 self.editor.move_to_document_start();
489 self.ensure_cursor_visible();
490 }
491
492 pub fn move_to_document_end(&mut self) {
494 self.editor.move_to_document_end();
495 self.ensure_cursor_visible();
496 }
497
498 pub fn select_left(&mut self) {
502 self.editor.select_left();
503 self.ensure_cursor_visible();
504 }
505
506 pub fn select_right(&mut self) {
508 self.editor.select_right();
509 self.ensure_cursor_visible();
510 }
511
512 pub fn select_up(&mut self) {
514 self.editor.select_up();
515 self.ensure_cursor_visible();
516 }
517
518 pub fn select_down(&mut self) {
520 self.editor.select_down();
521 self.ensure_cursor_visible();
522 }
523
524 pub fn select_all(&mut self) {
526 self.editor.select_all();
527 }
528
529 pub fn clear_selection(&mut self) {
531 self.editor.clear_selection();
532 }
533
534 pub fn page_up(&mut self, state: &TextAreaState) {
538 let page = state.last_viewport_height.max(1) as usize;
539 for _ in 0..page {
540 self.editor.move_up();
541 }
542 self.ensure_cursor_visible();
543 }
544
545 pub fn page_down(&mut self, state: &TextAreaState) {
547 let page = state.last_viewport_height.max(1) as usize;
548 for _ in 0..page {
549 self.editor.move_down();
550 }
551 self.ensure_cursor_visible();
552 }
553
554 fn gutter_width(&self) -> u16 {
556 if !self.show_line_numbers {
557 return 0;
558 }
559 let digits = {
560 let mut count = self.line_count().max(1);
561 let mut d: u16 = 0;
562 while count > 0 {
563 d += 1;
564 count /= 10;
565 }
566 d
567 };
568 digits + 2 }
570
571 fn measure_wrap_count(line_text: &str, max_width: usize) -> usize {
575 if line_text.is_empty() {
576 return 1;
577 }
578
579 let mut count = 0;
580 let mut current_width = 0;
581 let mut has_content = false;
582
583 Self::run_wrapping_logic(line_text, max_width, |_, width, flush| {
584 if flush {
585 count += 1;
586 current_width = 0;
587 has_content = false;
588 } else {
589 current_width = width;
590 has_content = true;
591 }
592 });
593
594 if has_content || count == 0 {
599 count += 1;
600 }
601
602 count
603 }
604
605 fn run_wrapping_logic<F>(line_text: &str, max_width: usize, mut callback: F)
611 where
612 F: FnMut(usize, usize, bool),
613 {
614 let mut current_width = 0;
615 let mut byte_cursor = 0;
616
617 for segment in line_text.split_word_bounds() {
618 let seg_len = segment.len();
619 let seg_width: usize = segment.graphemes(true).map(display_width).sum();
620
621 if max_width > 0 && current_width + seg_width > max_width {
622 callback(byte_cursor, current_width, true);
624 current_width = 0;
625 }
626
627 if max_width > 0 && seg_width > max_width {
628 for grapheme in segment.graphemes(true) {
629 let g_width = display_width(grapheme);
630 let g_len = grapheme.len();
631
632 if max_width > 0 && current_width + g_width > max_width && current_width > 0 {
633 callback(byte_cursor, current_width, true);
634 current_width = 0;
635 }
636
637 current_width += g_width;
638 byte_cursor += g_len;
639 callback(byte_cursor, current_width, false);
640 }
641 continue;
642 }
643
644 current_width += seg_width;
645 byte_cursor += seg_len;
646 callback(byte_cursor, current_width, false);
647 }
648 }
649
650 fn wrap_line_slices(line_text: &str, max_width: usize) -> Vec<WrappedSlice> {
651 if line_text.is_empty() {
652 return vec![WrappedSlice {
653 text: String::new(),
654 start_byte: 0,
655 start_col: 0,
656 width: 0,
657 }];
658 }
659
660 let mut slices = Vec::new();
661 let mut current_text = String::new();
662 let mut current_width = 0;
663 let mut slice_start_byte = 0;
664 let mut slice_start_col = 0;
665 let mut byte_cursor = 0;
666 let mut col_cursor = 0;
667
668 let push_current = |slices: &mut Vec<WrappedSlice>,
669 text: &mut String,
670 width: &mut usize,
671 start_byte: &mut usize,
672 start_col: &mut usize,
673 byte_cursor: usize,
674 col_cursor: usize| {
675 if text.is_empty() && *width == 0 {
678 return;
679 }
680 slices.push(WrappedSlice {
681 text: std::mem::take(text),
682 start_byte: *start_byte,
683 start_col: *start_col,
684 width: *width,
685 });
686 *start_byte = byte_cursor;
687 *start_col = col_cursor;
688 *width = 0;
689 };
690
691 for segment in line_text.split_word_bounds() {
692 let seg_len = segment.len();
693 let seg_width: usize = segment.graphemes(true).map(display_width).sum();
694
695 if max_width > 0 && current_width + seg_width > max_width {
696 push_current(
697 &mut slices,
698 &mut current_text,
699 &mut current_width,
700 &mut slice_start_byte,
701 &mut slice_start_col,
702 byte_cursor,
703 col_cursor,
704 );
705 }
706
707 if max_width > 0 && seg_width > max_width {
708 for grapheme in segment.graphemes(true) {
709 let g_width = display_width(grapheme);
710 let g_len = grapheme.len();
711
712 if max_width > 0 && current_width + g_width > max_width && current_width > 0 {
713 push_current(
714 &mut slices,
715 &mut current_text,
716 &mut current_width,
717 &mut slice_start_byte,
718 &mut slice_start_col,
719 byte_cursor,
720 col_cursor,
721 );
722 }
723
724 current_text.push_str(grapheme);
725 current_width += g_width;
726 byte_cursor += g_len;
727 col_cursor += g_width;
728 }
729 continue;
730 }
731
732 current_text.push_str(segment);
733 current_width += seg_width;
734 byte_cursor += seg_len;
735 col_cursor += seg_width;
736 }
737
738 if !current_text.is_empty() || current_width > 0 || slices.is_empty() {
739 slices.push(WrappedSlice {
740 text: current_text,
741 start_byte: slice_start_byte,
742 start_col: slice_start_col,
743 width: current_width,
744 });
745 }
746
747 slices
748 }
749
750 fn cursor_wrap_position(
751 line_text: &str,
752 max_width: usize,
753 cursor_col: usize,
754 ) -> (usize, usize) {
755 let slices = Self::wrap_line_slices(line_text, max_width);
756 if slices.is_empty() {
757 return (0, 0);
758 }
759
760 for (idx, slice) in slices.iter().enumerate() {
761 let end_col = slice.start_col.saturating_add(slice.width);
762 if cursor_col <= end_col || idx == slices.len().saturating_sub(1) {
763 let col_in_slice = cursor_col.saturating_sub(slice.start_col);
764 return (idx, col_in_slice.min(slice.width));
765 }
766 }
767
768 (0, 0)
769 }
770
771 fn get_prev_char_width(&self) -> usize {
773 let cursor = self.editor.cursor();
774 if cursor.grapheme == 0 {
775 return 0;
776 }
777 let rope = self.editor.rope();
778 let line = rope
779 .line(cursor.line)
780 .unwrap_or(std::borrow::Cow::Borrowed(""));
781
782 line.graphemes(true)
783 .nth(cursor.grapheme - 1)
784 .map(display_width)
785 .unwrap_or(0)
786 }
787
788 fn ensure_cursor_visible(&mut self) {
790 let cursor = self.editor.cursor();
791
792 let last_height = self.last_viewport_height.get();
793
794 let vp_height = if last_height == 0 { 20 } else { last_height };
797
798 let last_width = self.last_viewport_width.get();
799
800 let vp_width = if last_width == 0 { 80 } else { last_width };
801
802 if self.scroll_top.get() == usize::MAX {
803 self.scroll_top.set(0);
804 }
805
806 self.ensure_cursor_visible_internal(vp_height, vp_width, cursor);
807 }
808
809 fn ensure_cursor_visible_internal(
810 &mut self,
811
812 vp_height: usize,
813
814 vp_width: usize,
815
816 cursor: CursorPosition,
817 ) {
818 let current_top = self.scroll_top.get();
819
820 if cursor.line < current_top {
823 self.scroll_top.set(cursor.line);
824 } else if vp_height > 0 && cursor.line >= current_top + vp_height {
825 self.scroll_top
826 .set(cursor.line.saturating_sub(vp_height - 1));
827 }
828
829 if !self.soft_wrap {
832 let current_left = self.scroll_left.get();
833
834 let visual_col = cursor.visual_col;
835
836 if visual_col < current_left {
839 self.scroll_left.set(visual_col);
840 }
841 else if vp_width > 0 && visual_col >= current_left + vp_width {
847 let candidate_scroll = visual_col.saturating_sub(vp_width - 1);
848 let prev_width = self.get_prev_char_width();
849 let max_scroll_for_prev = visual_col.saturating_sub(prev_width);
850
851 self.scroll_left
852 .set(candidate_scroll.min(max_scroll_for_prev));
853 }
854 }
855 }
856}
857
858impl Widget for TextArea {
859 fn render(&self, area: Rect, frame: &mut Frame) {
860 if area.width < 1 || area.height < 1 {
861 return;
862 }
863
864 self.last_viewport_height.set(area.height as usize);
865
866 let deg = frame.buffer.degradation;
867 if deg.apply_styling() {
868 crate::set_style_area(&mut frame.buffer, area, self.style);
869 }
870
871 let gutter_w = self.gutter_width();
872 let text_area_x = area.x.saturating_add(gutter_w);
873 let text_area_w = area.width.saturating_sub(gutter_w) as usize;
874 let vp_height = area.height as usize;
875
876 self.last_viewport_width.set(text_area_w);
877
878 let cursor = self.editor.cursor();
879
880 let mut scroll_top = if self.scroll_top.get() == usize::MAX {
882 0
883 } else {
884 self.scroll_top.get()
885 };
886
887 if !self.soft_wrap && vp_height > 0 {
891 if cursor.line < scroll_top {
892 scroll_top = cursor.line;
893 } else if cursor.line >= scroll_top + vp_height {
894 scroll_top = cursor.line.saturating_sub(vp_height - 1);
895 }
896 self.scroll_top.set(scroll_top);
897 }
898
899 let mut scroll_left = self.scroll_left.get();
900 if !self.soft_wrap && text_area_w > 0 {
901 let visual_col = cursor.visual_col;
902 if visual_col < scroll_left {
903 scroll_left = visual_col;
904 } else if visual_col >= scroll_left + text_area_w {
905 let candidate_scroll = visual_col.saturating_sub(text_area_w - 1);
906 let prev_width = self.get_prev_char_width();
907 let max_scroll_for_prev = visual_col.saturating_sub(prev_width);
908
909 scroll_left = candidate_scroll.min(max_scroll_for_prev);
910 }
911 }
912 self.scroll_left.set(scroll_left);
913
914 let rope = self.editor.rope();
915 let nav = CursorNavigator::new(rope);
916
917 let sel_range = self.editor.selection().and_then(|sel| {
919 if sel.is_empty() {
920 None
921 } else {
922 let (a, b) = sel.byte_range(&nav);
923 Some((a, b))
924 }
925 });
926
927 if self.editor.is_empty() && !self.placeholder.is_empty() {
929 let style = if deg.apply_styling() {
930 self.placeholder_style
931 } else {
932 Style::default()
933 };
934 draw_text_span(
935 frame,
936 text_area_x,
937 area.y,
938 &self.placeholder,
939 style,
940 area.right(),
941 );
942 if self.focused {
943 frame.set_cursor(Some((text_area_x, area.y)));
944 }
945 return;
946 }
947
948 if self.soft_wrap {
949 self.scroll_left.set(0);
950
951 let mut cursor_virtual = 0;
953 for line_idx in 0..cursor.line {
954 let line_text = rope
955 .line(line_idx)
956 .unwrap_or(std::borrow::Cow::Borrowed(""));
957 let line_text = line_text.strip_suffix('\n').unwrap_or(&line_text);
958 cursor_virtual += Self::measure_wrap_count(line_text, text_area_w);
959 }
960
961 let cursor_line_text = rope
962 .line(cursor.line)
963 .unwrap_or(std::borrow::Cow::Borrowed(""));
964 let cursor_line_text = cursor_line_text
965 .strip_suffix('\n')
966 .unwrap_or(&cursor_line_text);
967 let (cursor_wrap_idx, cursor_col_in_wrap) =
968 Self::cursor_wrap_position(cursor_line_text, text_area_w, cursor.visual_col);
969 cursor_virtual = cursor_virtual.saturating_add(cursor_wrap_idx);
970
971 let mut scroll_virtual = self.scroll_top.get();
973 if cursor_virtual < scroll_virtual {
974 scroll_virtual = cursor_virtual;
975 } else if cursor_virtual >= scroll_virtual + vp_height {
976 scroll_virtual = cursor_virtual.saturating_sub(vp_height - 1);
977 }
978 self.scroll_top.set(scroll_virtual);
979
980 let mut virtual_index = 0usize;
982 for line_idx in 0..self.editor.line_count() {
983 if virtual_index >= scroll_virtual + vp_height {
984 break;
985 }
986
987 let line_text = rope
988 .line(line_idx)
989 .unwrap_or(std::borrow::Cow::Borrowed(""));
990 let line_text = line_text.strip_suffix('\n').unwrap_or(&line_text);
991
992 let wrap_count = Self::measure_wrap_count(line_text, text_area_w);
994 if virtual_index + wrap_count <= scroll_virtual {
995 virtual_index += wrap_count;
996 continue;
997 }
998
999 let line_start_byte = nav.to_byte_index(nav.from_line_grapheme(line_idx, 0));
1000 let slices = Self::wrap_line_slices(line_text, text_area_w);
1001
1002 for (slice_idx, slice) in slices.iter().enumerate() {
1003 if virtual_index < scroll_virtual {
1004 virtual_index += 1;
1005 continue;
1006 }
1007
1008 let row = virtual_index.saturating_sub(scroll_virtual);
1009 if row >= vp_height {
1010 break;
1011 }
1012
1013 let y = area.y.saturating_add(row as u16);
1014
1015 if self.show_line_numbers && slice_idx == 0 {
1017 let style = if deg.apply_styling() {
1018 self.line_number_style
1019 } else {
1020 Style::default()
1021 };
1022 let num_str =
1023 format!("{:>width$} ", line_idx + 1, width = (gutter_w - 2) as usize);
1024 draw_text_span(frame, area.x, y, &num_str, style, text_area_x);
1025 }
1026
1027 if line_idx == cursor.line
1029 && slice_idx == cursor_wrap_idx
1030 && let Some(cl_style) = self.cursor_line_style
1031 && deg.apply_styling()
1032 {
1033 for cx in text_area_x..area.right() {
1034 if let Some(cell) = frame.buffer.get_mut(cx, y) {
1035 apply_style(cell, cl_style);
1036 }
1037 }
1038 }
1039
1040 let mut visual_x: usize = 0;
1042 let mut grapheme_byte_offset = line_start_byte + slice.start_byte;
1043
1044 for g in slice.text.graphemes(true) {
1045 let g_width = display_width(g);
1046 let g_byte_len = g.len();
1047
1048 if visual_x >= text_area_w {
1049 break;
1050 }
1051
1052 let px = text_area_x + visual_x as u16;
1053
1054 let mut g_style = self.style;
1056 if let Some((sel_start, sel_end)) = sel_range
1057 && grapheme_byte_offset >= sel_start
1058 && grapheme_byte_offset < sel_end
1059 && deg.apply_styling()
1060 {
1061 g_style = g_style.merge(&self.selection_style);
1062 }
1063
1064 if g_width > 0 {
1065 draw_text_span(frame, px, y, g, g_style, area.right());
1066 }
1067
1068 visual_x += g_width;
1069 grapheme_byte_offset += g_byte_len;
1070 }
1071
1072 virtual_index += 1;
1073 }
1074 }
1075
1076 if self.focused && cursor_virtual >= scroll_virtual {
1078 let row = cursor_virtual.saturating_sub(scroll_virtual);
1079 if row < vp_height {
1080 let cursor_screen_x = text_area_x.saturating_add(cursor_col_in_wrap as u16);
1081 let cursor_screen_y = area.y.saturating_add(row as u16);
1082 if cursor_screen_x < area.right() && cursor_screen_y < area.bottom() {
1083 frame.set_cursor(Some((cursor_screen_x, cursor_screen_y)));
1084 }
1085 }
1086 }
1087
1088 return;
1089 }
1090
1091 for row in 0..vp_height {
1093 let line_idx = scroll_top + row;
1094 let y = area.y.saturating_add(row as u16);
1095
1096 if line_idx >= self.editor.line_count() {
1097 break;
1098 }
1099
1100 if self.show_line_numbers {
1102 let style = if deg.apply_styling() {
1103 self.line_number_style
1104 } else {
1105 Style::default()
1106 };
1107 let num_str = format!("{:>width$} ", line_idx + 1, width = (gutter_w - 2) as usize);
1108 draw_text_span(frame, area.x, y, &num_str, style, text_area_x);
1109 }
1110
1111 if line_idx == cursor.line
1113 && let Some(cl_style) = self.cursor_line_style
1114 && deg.apply_styling()
1115 {
1116 for cx in text_area_x..area.right() {
1117 if let Some(cell) = frame.buffer.get_mut(cx, y) {
1118 apply_style(cell, cl_style);
1119 }
1120 }
1121 }
1122
1123 let line_text = rope
1125 .line(line_idx)
1126 .unwrap_or(std::borrow::Cow::Borrowed(""));
1127 let line_text = line_text.strip_suffix('\n').unwrap_or(&line_text);
1128
1129 let line_start_byte = nav.to_byte_index(nav.from_line_grapheme(line_idx, 0));
1131
1132 let mut visual_x: usize = 0;
1134 let graphemes: Vec<&str> = line_text.graphemes(true).collect();
1135 let mut grapheme_byte_offset = line_start_byte;
1136
1137 for g in &graphemes {
1138 let g_width = display_width(g);
1139 let g_byte_len = g.len();
1140
1141 if visual_x + g_width <= scroll_left {
1143 visual_x += g_width;
1144 grapheme_byte_offset += g_byte_len;
1145 continue;
1146 }
1147
1148 if visual_x < scroll_left {
1150 visual_x += g_width;
1151 grapheme_byte_offset += g_byte_len;
1152 continue;
1153 }
1154
1155 let screen_x = visual_x.saturating_sub(scroll_left);
1157 if screen_x >= text_area_w {
1158 break;
1159 }
1160
1161 let px = text_area_x + screen_x as u16;
1162
1163 let mut g_style = self.style;
1165 if let Some((sel_start, sel_end)) = sel_range
1166 && grapheme_byte_offset >= sel_start
1167 && grapheme_byte_offset < sel_end
1168 && deg.apply_styling()
1169 {
1170 g_style = g_style.merge(&self.selection_style);
1171 }
1172
1173 if g_width > 0 {
1175 draw_text_span(frame, px, y, g, g_style, area.right());
1176 }
1177
1178 visual_x += g_width;
1179 grapheme_byte_offset += g_byte_len;
1180 }
1181 }
1182
1183 if self.focused {
1185 let cursor_row = cursor.line.saturating_sub(scroll_top);
1186 if cursor_row < vp_height {
1187 let cursor_screen_x = (cursor.visual_col.saturating_sub(scroll_left) as u16)
1188 .saturating_add(text_area_x);
1189 let cursor_screen_y = area.y.saturating_add(cursor_row as u16);
1190 if cursor_screen_x < area.right() && cursor_screen_y < area.bottom() {
1191 frame.set_cursor(Some((cursor_screen_x, cursor_screen_y)));
1192 }
1193 }
1194 }
1195 }
1196
1197 fn is_essential(&self) -> bool {
1198 true
1199 }
1200}
1201
1202impl StatefulWidget for TextArea {
1203 type State = TextAreaState;
1204
1205 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
1206 state.last_viewport_height = area.height;
1207 state.last_viewport_width = area.width;
1208 Widget::render(self, area, frame);
1209 }
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214 use super::*;
1215
1216 #[test]
1217 fn new_textarea_is_empty() {
1218 let ta = TextArea::new();
1219 assert!(ta.is_empty());
1220 assert_eq!(ta.text(), "");
1221 assert_eq!(ta.line_count(), 1); }
1223
1224 #[test]
1225 fn with_text_builder() {
1226 let ta = TextArea::new().with_text("hello\nworld");
1227 assert_eq!(ta.text(), "hello\nworld");
1228 assert_eq!(ta.line_count(), 2);
1229 }
1230
1231 #[test]
1232 fn insert_text_and_newline() {
1233 let mut ta = TextArea::new();
1234 ta.insert_text("hello");
1235 ta.insert_newline();
1236 ta.insert_text("world");
1237 assert_eq!(ta.text(), "hello\nworld");
1238 assert_eq!(ta.line_count(), 2);
1239 }
1240
1241 #[test]
1242 fn delete_backward_works() {
1243 let mut ta = TextArea::new().with_text("hello");
1244 ta.move_to_document_end();
1245 ta.delete_backward();
1246 assert_eq!(ta.text(), "hell");
1247 }
1248
1249 #[test]
1250 fn cursor_movement() {
1251 let mut ta = TextArea::new().with_text("abc\ndef\nghi");
1252 ta.move_to_document_start();
1253 assert_eq!(ta.cursor().line, 0);
1254 assert_eq!(ta.cursor().grapheme, 0);
1255
1256 ta.move_down();
1257 assert_eq!(ta.cursor().line, 1);
1258
1259 ta.move_to_line_end();
1260 assert_eq!(ta.cursor().grapheme, 3);
1261
1262 ta.move_to_document_end();
1263 assert_eq!(ta.cursor().line, 2);
1264 }
1265
1266 #[test]
1267 fn undo_redo() {
1268 let mut ta = TextArea::new();
1269 ta.insert_text("abc");
1270 assert_eq!(ta.text(), "abc");
1271 ta.undo();
1272 assert_eq!(ta.text(), "");
1273 ta.redo();
1274 assert_eq!(ta.text(), "abc");
1275 }
1276
1277 #[test]
1278 fn selection_and_delete() {
1279 let mut ta = TextArea::new().with_text("hello world");
1280 ta.move_to_document_start();
1281 for _ in 0..5 {
1282 ta.select_right();
1283 }
1284 assert_eq!(ta.selected_text(), Some("hello".to_string()));
1285 ta.delete_backward();
1286 assert_eq!(ta.text(), " world");
1287 }
1288
1289 #[test]
1290 fn select_all() {
1291 let mut ta = TextArea::new().with_text("abc\ndef");
1292 ta.select_all();
1293 assert_eq!(ta.selected_text(), Some("abc\ndef".to_string()));
1294 }
1295
1296 #[test]
1297 fn set_text_resets() {
1298 let mut ta = TextArea::new().with_text("old");
1299 ta.insert_text(" stuff");
1300 ta.set_text("new");
1301 assert_eq!(ta.text(), "new");
1302 }
1303
1304 #[test]
1305 fn scroll_follows_cursor() {
1306 let mut ta = TextArea::new();
1307 for i in 0..50 {
1309 ta.insert_text(&format!("line {}\n", i));
1310 }
1311 assert!(ta.scroll_top.get() > 0);
1313 assert!(ta.cursor().line >= 49);
1314
1315 ta.move_to_document_start();
1317 assert_eq!(ta.scroll_top.get(), 0);
1318 }
1319
1320 #[test]
1321 fn gutter_width_without_line_numbers() {
1322 let ta = TextArea::new();
1323 assert_eq!(ta.gutter_width(), 0);
1324 }
1325
1326 #[test]
1327 fn gutter_width_with_line_numbers() {
1328 let mut ta = TextArea::new().with_line_numbers(true);
1329 ta.insert_text("a\nb\nc");
1330 assert_eq!(ta.gutter_width(), 3); }
1332
1333 #[test]
1334 fn gutter_width_many_lines() {
1335 let mut ta = TextArea::new().with_line_numbers(true);
1336 for i in 0..100 {
1337 ta.insert_text(&format!("line {}\n", i));
1338 }
1339 assert_eq!(ta.gutter_width(), 5); }
1341
1342 #[test]
1343 fn focus_state() {
1344 let mut ta = TextArea::new();
1345 assert!(!ta.is_focused());
1346 ta.set_focused(true);
1347 assert!(ta.is_focused());
1348 }
1349
1350 #[test]
1351 fn word_movement() {
1352 let mut ta = TextArea::new().with_text("hello world foo");
1353 ta.move_to_document_start();
1354 ta.move_word_right();
1355 assert_eq!(ta.cursor().grapheme, 6);
1356 ta.move_word_left();
1357 assert_eq!(ta.cursor().grapheme, 0);
1358 }
1359
1360 #[test]
1361 fn page_up_down() {
1362 let mut ta = TextArea::new();
1363 for i in 0..50 {
1364 ta.insert_text(&format!("line {}\n", i));
1365 }
1366 ta.move_to_document_start();
1367 let state = TextAreaState {
1368 last_viewport_height: 10,
1369 last_viewport_width: 80,
1370 };
1371 ta.page_down(&state);
1372 assert!(ta.cursor().line >= 10);
1373 ta.page_up(&state);
1374 assert_eq!(ta.cursor().line, 0);
1375 }
1376
1377 #[test]
1378 fn insert_replaces_selection() {
1379 let mut ta = TextArea::new().with_text("hello world");
1380 ta.move_to_document_start();
1381 for _ in 0..5 {
1382 ta.select_right();
1383 }
1384 ta.insert_text("goodbye");
1385 assert_eq!(ta.text(), "goodbye world");
1386 }
1387
1388 #[test]
1389 fn insert_single_char() {
1390 let mut ta = TextArea::new();
1391 ta.insert_char('X');
1392 assert_eq!(ta.text(), "X");
1393 assert_eq!(ta.cursor().grapheme, 1);
1394 }
1395
1396 #[test]
1397 fn insert_multiline_text() {
1398 let mut ta = TextArea::new();
1399 ta.insert_text("line1\nline2\nline3");
1400 assert_eq!(ta.line_count(), 3);
1401 assert_eq!(ta.cursor().line, 2);
1402 }
1403
1404 #[test]
1405 fn delete_forward_works() {
1406 let mut ta = TextArea::new().with_text("hello");
1407 ta.move_to_document_start();
1408 ta.delete_forward();
1409 assert_eq!(ta.text(), "ello");
1410 }
1411
1412 #[test]
1413 fn delete_backward_at_line_start_joins_lines() {
1414 let mut ta = TextArea::new().with_text("abc\ndef");
1415 ta.move_to_document_start();
1417 ta.move_down();
1418 ta.move_to_line_start();
1419 ta.delete_backward();
1420 assert_eq!(ta.text(), "abcdef");
1421 assert_eq!(ta.line_count(), 1);
1422 }
1423
1424 #[test]
1425 fn cursor_horizontal_movement() {
1426 let mut ta = TextArea::new().with_text("abc");
1427 ta.move_to_document_start();
1428 ta.move_right();
1429 assert_eq!(ta.cursor().grapheme, 1);
1430 ta.move_right();
1431 assert_eq!(ta.cursor().grapheme, 2);
1432 ta.move_left();
1433 assert_eq!(ta.cursor().grapheme, 1);
1434 }
1435
1436 #[test]
1437 fn cursor_vertical_maintains_column() {
1438 let mut ta = TextArea::new().with_text("abcde\nfg\nhijkl");
1439 ta.move_to_document_start();
1440 ta.move_to_line_end(); ta.move_down(); assert_eq!(ta.cursor().line, 1);
1443 ta.move_down(); assert_eq!(ta.cursor().line, 2);
1445 }
1446
1447 #[test]
1448 fn selection_shift_arrow() {
1449 let mut ta = TextArea::new().with_text("abcdef");
1450 ta.move_to_document_start();
1451 ta.select_right();
1452 ta.select_right();
1453 ta.select_right();
1454 assert_eq!(ta.selected_text(), Some("abc".to_string()));
1455 }
1456
1457 #[test]
1458 fn selection_extends_up_down() {
1459 let mut ta = TextArea::new().with_text("line1\nline2\nline3");
1460 ta.move_to_document_start();
1461 ta.select_down();
1462 let sel = ta.selected_text().unwrap();
1463 assert!(sel.contains('\n'));
1464 }
1465
1466 #[test]
1467 fn undo_chain() {
1468 let mut ta = TextArea::new();
1469 ta.insert_text("a");
1470 ta.insert_text("b");
1471 ta.insert_text("c");
1472 assert_eq!(ta.text(), "abc");
1473 ta.undo();
1474 ta.undo();
1475 ta.undo();
1476 assert_eq!(ta.text(), "");
1477 }
1478
1479 #[test]
1480 fn redo_discarded_on_new_edit() {
1481 let mut ta = TextArea::new();
1482 ta.insert_text("abc");
1483 ta.undo();
1484 ta.insert_text("xyz");
1485 ta.redo(); assert_eq!(ta.text(), "xyz");
1487 }
1488
1489 #[test]
1490 fn clear_selection() {
1491 let mut ta = TextArea::new().with_text("hello");
1492 ta.select_all();
1493 assert!(ta.selection().is_some());
1494 ta.clear_selection();
1495 assert!(ta.selection().is_none());
1496 }
1497
1498 #[test]
1499 fn delete_word_backward() {
1500 let mut ta = TextArea::new().with_text("hello world");
1501 ta.move_to_document_end();
1502 ta.delete_word_backward();
1503 assert_eq!(ta.text(), "hello ");
1504 }
1505
1506 #[test]
1507 fn delete_to_end_of_line() {
1508 let mut ta = TextArea::new().with_text("hello world");
1509 ta.move_to_document_start();
1510 ta.move_right(); ta.delete_to_end_of_line();
1512 assert_eq!(ta.text(), "h");
1513 }
1514
1515 #[test]
1516 fn placeholder_builder() {
1517 let ta = TextArea::new().with_placeholder("Enter text...");
1518 assert!(ta.is_empty());
1519 assert_eq!(ta.placeholder, "Enter text...");
1520 }
1521
1522 #[test]
1523 fn soft_wrap_builder() {
1524 let ta = TextArea::new().with_soft_wrap(true);
1525 assert!(ta.soft_wrap);
1526 }
1527
1528 #[test]
1529 fn soft_wrap_renders_wrapped_lines() {
1530 use crate::Widget;
1531 use ftui_render::grapheme_pool::GraphemePool;
1532
1533 let ta = TextArea::new().with_soft_wrap(true).with_text("abcdef");
1534 let area = Rect::new(0, 0, 3, 2);
1535 let mut pool = GraphemePool::new();
1536 let mut frame = Frame::new(3, 2, &mut pool);
1537 Widget::render(&ta, area, &mut frame);
1538
1539 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('a'));
1540 assert_eq!(frame.buffer.get(2, 0).unwrap().content.as_char(), Some('c'));
1541 assert_eq!(frame.buffer.get(0, 1).unwrap().content.as_char(), Some('d'));
1542 assert_eq!(frame.buffer.get(2, 1).unwrap().content.as_char(), Some('f'));
1543 }
1544
1545 #[test]
1546 fn max_height_builder() {
1547 let ta = TextArea::new().with_max_height(10);
1548 assert_eq!(ta.max_height, 10);
1549 }
1550
1551 #[test]
1552 fn editor_access() {
1553 let mut ta = TextArea::new().with_text("test");
1554 assert_eq!(ta.editor().text(), "test");
1555 ta.editor_mut().insert_char('!');
1556 assert!(ta.text().contains('!'));
1557 }
1558
1559 #[test]
1560 fn move_to_line_start_and_end() {
1561 let mut ta = TextArea::new().with_text("hello world");
1562 ta.move_to_document_start();
1563 ta.move_to_line_end();
1564 assert_eq!(ta.cursor().grapheme, 11);
1565 ta.move_to_line_start();
1566 assert_eq!(ta.cursor().grapheme, 0);
1567 }
1568
1569 #[test]
1570 fn render_empty_with_placeholder() {
1571 use ftui_render::grapheme_pool::GraphemePool;
1572 let ta = TextArea::new()
1573 .with_placeholder("Type here")
1574 .with_focus(true);
1575 let mut pool = GraphemePool::new();
1576 let mut frame = Frame::new(20, 5, &mut pool);
1577 let area = Rect::new(0, 0, 20, 5);
1578 Widget::render(&ta, area, &mut frame);
1579 let cell = frame.buffer.get(0, 0).unwrap();
1581 assert_eq!(cell.content.as_char(), Some('T'));
1582 assert!(frame.cursor_position.is_some());
1584 }
1585
1586 #[test]
1587 fn render_with_content() {
1588 use ftui_render::grapheme_pool::GraphemePool;
1589 let ta = TextArea::new().with_text("abc\ndef").with_focus(true);
1590 let mut pool = GraphemePool::new();
1591 let mut frame = Frame::new(20, 5, &mut pool);
1592 let area = Rect::new(0, 0, 20, 5);
1593 Widget::render(&ta, area, &mut frame);
1594 let cell = frame.buffer.get(0, 0).unwrap();
1595 assert_eq!(cell.content.as_char(), Some('a'));
1596 }
1597
1598 #[test]
1599 fn render_line_numbers_without_styling() {
1600 use ftui_render::budget::DegradationLevel;
1601 use ftui_render::grapheme_pool::GraphemePool;
1602
1603 let ta = TextArea::new().with_text("a\nb").with_line_numbers(true);
1604 let mut pool = GraphemePool::new();
1605 let mut frame = Frame::new(8, 2, &mut pool);
1606 frame.set_degradation(DegradationLevel::NoStyling);
1607
1608 Widget::render(&ta, Rect::new(0, 0, 8, 2), &mut frame);
1609
1610 let cell = frame.buffer.get(0, 0).unwrap();
1611 assert_eq!(cell.content.as_char(), Some('1'));
1612 }
1613
1614 #[test]
1615 fn stateful_render_updates_viewport_state() {
1616 use ftui_render::grapheme_pool::GraphemePool;
1617
1618 let ta = TextArea::new();
1619 let mut state = TextAreaState::default();
1620 let mut pool = GraphemePool::new();
1621 let mut frame = Frame::new(10, 3, &mut pool);
1622 let area = Rect::new(0, 0, 10, 3);
1623
1624 StatefulWidget::render(&ta, area, &mut frame, &mut state);
1625
1626 assert_eq!(state.last_viewport_height, 3);
1627 assert_eq!(state.last_viewport_width, 10);
1628 }
1629
1630 #[test]
1631 fn render_zero_area_no_panic() {
1632 let ta = TextArea::new().with_text("test");
1633 use ftui_render::grapheme_pool::GraphemePool;
1634 let mut pool = GraphemePool::new();
1635 let mut frame = Frame::new(10, 10, &mut pool);
1636 Widget::render(&ta, Rect::new(0, 0, 0, 0), &mut frame);
1637 }
1638
1639 #[test]
1640 fn is_essential() {
1641 let ta = TextArea::new();
1642 assert!(Widget::is_essential(&ta));
1643 }
1644
1645 #[test]
1646 fn default_impl() {
1647 let ta = TextArea::default();
1648 assert!(ta.is_empty());
1649 }
1650
1651 #[test]
1652 fn insert_newline_splits_line() {
1653 let mut ta = TextArea::new().with_text("abcdef");
1654 ta.move_to_document_start();
1655 ta.move_right();
1656 ta.move_right();
1657 ta.move_right();
1658 ta.insert_newline();
1659 assert_eq!(ta.line_count(), 2);
1660 assert_eq!(ta.cursor().line, 1);
1661 }
1662
1663 #[test]
1664 fn unicode_grapheme_cluster() {
1665 let mut ta = TextArea::new();
1666 ta.insert_text("café");
1667 assert_eq!(ta.text(), "café");
1669 }
1670
1671 mod proptests {
1672 use super::*;
1673 use proptest::prelude::*;
1674
1675 proptest! {
1676 #[test]
1677 fn insert_delete_inverse(text in "[a-zA-Z0-9 ]{1,50}") {
1678 let mut ta = TextArea::new();
1679 ta.insert_text(&text);
1680 for _ in 0..text.len() {
1682 ta.delete_backward();
1683 }
1684 prop_assert!(ta.is_empty() || ta.text().is_empty());
1685 }
1686
1687 #[test]
1688 fn undo_redo_inverse(text in "[a-zA-Z0-9]{1,30}") {
1689 let mut ta = TextArea::new();
1690 ta.insert_text(&text);
1691 let after_insert = ta.text();
1692 ta.undo();
1693 ta.redo();
1694 prop_assert_eq!(ta.text(), after_insert);
1695 }
1696
1697 #[test]
1698 fn cursor_always_valid(ops in proptest::collection::vec(0u8..10, 1..20)) {
1699 let mut ta = TextArea::new().with_text("abc\ndef\nghi\njkl");
1700 for op in ops {
1701 match op {
1702 0 => ta.move_left(),
1703 1 => ta.move_right(),
1704 2 => ta.move_up(),
1705 3 => ta.move_down(),
1706 4 => ta.move_to_line_start(),
1707 5 => ta.move_to_line_end(),
1708 6 => ta.move_to_document_start(),
1709 7 => ta.move_to_document_end(),
1710 8 => ta.move_word_left(),
1711 _ => ta.move_word_right(),
1712 }
1713 let cursor = ta.cursor();
1714 prop_assert!(cursor.line < ta.line_count(),
1715 "cursor line {} >= line_count {}", cursor.line, ta.line_count());
1716 }
1717 }
1718
1719 #[test]
1720 fn selection_ordered(n in 1usize..20) {
1721 let mut ta = TextArea::new().with_text("hello world foo bar");
1722 ta.move_to_document_start();
1723 for _ in 0..n {
1724 ta.select_right();
1725 }
1726 if let Some(sel) = ta.selection() {
1727 prop_assert!(sel.anchor.line <= sel.head.line
1729 || (sel.anchor.line == sel.head.line
1730 && sel.anchor.grapheme <= sel.head.grapheme));
1731 }
1732 }
1733 }
1734 }
1735}