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 let mut scroll_top = if self.scroll_top.get() == usize::MAX {
881 0
882 } else {
883 self.scroll_top.get()
884 };
885 if vp_height > 0 {
886 if cursor.line < scroll_top {
887 scroll_top = cursor.line;
888 } else if cursor.line >= scroll_top + vp_height {
889 scroll_top = cursor.line.saturating_sub(vp_height - 1);
890 }
891 }
892 self.scroll_top.set(scroll_top);
893
894 let mut scroll_left = self.scroll_left.get();
895 if !self.soft_wrap && text_area_w > 0 {
896 let visual_col = cursor.visual_col;
897 if visual_col < scroll_left {
898 scroll_left = visual_col;
899 } else if visual_col >= scroll_left + text_area_w {
900 let candidate_scroll = visual_col.saturating_sub(text_area_w - 1);
901 let prev_width = self.get_prev_char_width();
902 let max_scroll_for_prev = visual_col.saturating_sub(prev_width);
903
904 scroll_left = candidate_scroll.min(max_scroll_for_prev);
905 }
906 }
907 self.scroll_left.set(scroll_left);
908
909 let rope = self.editor.rope();
910 let nav = CursorNavigator::new(rope);
911
912 let sel_range = self.editor.selection().and_then(|sel| {
914 if sel.is_empty() {
915 None
916 } else {
917 let (a, b) = sel.byte_range(&nav);
918 Some((a, b))
919 }
920 });
921
922 if self.editor.is_empty() && !self.placeholder.is_empty() {
924 let style = if deg.apply_styling() {
925 self.placeholder_style
926 } else {
927 Style::default()
928 };
929 draw_text_span(
930 frame,
931 text_area_x,
932 area.y,
933 &self.placeholder,
934 style,
935 area.right(),
936 );
937 if self.focused {
938 frame.set_cursor(Some((text_area_x, area.y)));
939 }
940 return;
941 }
942
943 if self.soft_wrap {
944 self.scroll_left.set(0);
945
946 let mut cursor_virtual = 0;
948 for line_idx in 0..cursor.line {
949 let line_text = rope
950 .line(line_idx)
951 .unwrap_or(std::borrow::Cow::Borrowed(""));
952 let line_text = line_text.strip_suffix('\n').unwrap_or(&line_text);
953 cursor_virtual += Self::measure_wrap_count(line_text, text_area_w);
954 }
955
956 let cursor_line_text = rope
957 .line(cursor.line)
958 .unwrap_or(std::borrow::Cow::Borrowed(""));
959 let cursor_line_text = cursor_line_text
960 .strip_suffix('\n')
961 .unwrap_or(&cursor_line_text);
962 let (cursor_wrap_idx, cursor_col_in_wrap) =
963 Self::cursor_wrap_position(cursor_line_text, text_area_w, cursor.visual_col);
964 cursor_virtual = cursor_virtual.saturating_add(cursor_wrap_idx);
965
966 let mut scroll_virtual = self.scroll_top.get();
968 if cursor_virtual < scroll_virtual {
969 scroll_virtual = cursor_virtual;
970 } else if cursor_virtual >= scroll_virtual + vp_height {
971 scroll_virtual = cursor_virtual.saturating_sub(vp_height - 1);
972 }
973 self.scroll_top.set(scroll_virtual);
974
975 let mut virtual_index = 0usize;
977 for line_idx in 0..self.editor.line_count() {
978 if virtual_index >= scroll_virtual + vp_height {
979 break;
980 }
981
982 let line_text = rope
983 .line(line_idx)
984 .unwrap_or(std::borrow::Cow::Borrowed(""));
985 let line_text = line_text.strip_suffix('\n').unwrap_or(&line_text);
986
987 let wrap_count = Self::measure_wrap_count(line_text, text_area_w);
989 if virtual_index + wrap_count <= scroll_virtual {
990 virtual_index += wrap_count;
991 continue;
992 }
993
994 let line_start_byte = nav.to_byte_index(nav.from_line_grapheme(line_idx, 0));
995 let slices = Self::wrap_line_slices(line_text, text_area_w);
996
997 for (slice_idx, slice) in slices.iter().enumerate() {
998 if virtual_index < scroll_virtual {
999 virtual_index += 1;
1000 continue;
1001 }
1002
1003 let row = virtual_index.saturating_sub(scroll_virtual);
1004 if row >= vp_height {
1005 break;
1006 }
1007
1008 let y = area.y.saturating_add(row as u16);
1009
1010 if self.show_line_numbers && slice_idx == 0 {
1012 let style = if deg.apply_styling() {
1013 self.line_number_style
1014 } else {
1015 Style::default()
1016 };
1017 let num_str =
1018 format!("{:>width$} ", line_idx + 1, width = (gutter_w - 2) as usize);
1019 draw_text_span(frame, area.x, y, &num_str, style, text_area_x);
1020 }
1021
1022 if line_idx == cursor.line
1024 && slice_idx == cursor_wrap_idx
1025 && let Some(cl_style) = self.cursor_line_style
1026 && deg.apply_styling()
1027 {
1028 for cx in text_area_x..area.right() {
1029 if let Some(cell) = frame.buffer.get_mut(cx, y) {
1030 apply_style(cell, cl_style);
1031 }
1032 }
1033 }
1034
1035 let mut visual_x: usize = 0;
1037 let mut grapheme_byte_offset = line_start_byte + slice.start_byte;
1038
1039 for g in slice.text.graphemes(true) {
1040 let g_width = display_width(g);
1041 let g_byte_len = g.len();
1042
1043 if visual_x >= text_area_w {
1044 break;
1045 }
1046
1047 let px = text_area_x + visual_x as u16;
1048
1049 let mut g_style = self.style;
1051 if let Some((sel_start, sel_end)) = sel_range
1052 && grapheme_byte_offset >= sel_start
1053 && grapheme_byte_offset < sel_end
1054 && deg.apply_styling()
1055 {
1056 g_style = g_style.merge(&self.selection_style);
1057 }
1058
1059 if g_width > 0 {
1060 draw_text_span(frame, px, y, g, g_style, area.right());
1061 }
1062
1063 visual_x += g_width;
1064 grapheme_byte_offset += g_byte_len;
1065 }
1066
1067 virtual_index += 1;
1068 }
1069 }
1070
1071 if self.focused && cursor_virtual >= scroll_virtual {
1073 let row = cursor_virtual.saturating_sub(scroll_virtual);
1074 if row < vp_height {
1075 let cursor_screen_x = text_area_x.saturating_add(cursor_col_in_wrap as u16);
1076 let cursor_screen_y = area.y.saturating_add(row as u16);
1077 if cursor_screen_x < area.right() && cursor_screen_y < area.bottom() {
1078 frame.set_cursor(Some((cursor_screen_x, cursor_screen_y)));
1079 }
1080 }
1081 }
1082
1083 return;
1084 }
1085
1086 for row in 0..vp_height {
1088 let line_idx = scroll_top + row;
1089 let y = area.y.saturating_add(row as u16);
1090
1091 if line_idx >= self.editor.line_count() {
1092 break;
1093 }
1094
1095 if self.show_line_numbers {
1097 let style = if deg.apply_styling() {
1098 self.line_number_style
1099 } else {
1100 Style::default()
1101 };
1102 let num_str = format!("{:>width$} ", line_idx + 1, width = (gutter_w - 2) as usize);
1103 draw_text_span(frame, area.x, y, &num_str, style, text_area_x);
1104 }
1105
1106 if line_idx == cursor.line
1108 && let Some(cl_style) = self.cursor_line_style
1109 && deg.apply_styling()
1110 {
1111 for cx in text_area_x..area.right() {
1112 if let Some(cell) = frame.buffer.get_mut(cx, y) {
1113 apply_style(cell, cl_style);
1114 }
1115 }
1116 }
1117
1118 let line_text = rope
1120 .line(line_idx)
1121 .unwrap_or(std::borrow::Cow::Borrowed(""));
1122 let line_text = line_text.strip_suffix('\n').unwrap_or(&line_text);
1123
1124 let line_start_byte = nav.to_byte_index(nav.from_line_grapheme(line_idx, 0));
1126
1127 let mut visual_x: usize = 0;
1129 let graphemes: Vec<&str> = line_text.graphemes(true).collect();
1130 let mut grapheme_byte_offset = line_start_byte;
1131
1132 for g in &graphemes {
1133 let g_width = display_width(g);
1134 let g_byte_len = g.len();
1135
1136 if visual_x + g_width <= scroll_left {
1138 visual_x += g_width;
1139 grapheme_byte_offset += g_byte_len;
1140 continue;
1141 }
1142
1143 if visual_x < scroll_left {
1145 visual_x += g_width;
1146 grapheme_byte_offset += g_byte_len;
1147 continue;
1148 }
1149
1150 let screen_x = visual_x.saturating_sub(scroll_left);
1152 if screen_x >= text_area_w {
1153 break;
1154 }
1155
1156 let px = text_area_x + screen_x as u16;
1157
1158 let mut g_style = self.style;
1160 if let Some((sel_start, sel_end)) = sel_range
1161 && grapheme_byte_offset >= sel_start
1162 && grapheme_byte_offset < sel_end
1163 && deg.apply_styling()
1164 {
1165 g_style = g_style.merge(&self.selection_style);
1166 }
1167
1168 if g_width > 0 {
1170 draw_text_span(frame, px, y, g, g_style, area.right());
1171 }
1172
1173 visual_x += g_width;
1174 grapheme_byte_offset += g_byte_len;
1175 }
1176 }
1177
1178 if self.focused {
1180 let cursor_row = cursor.line.saturating_sub(scroll_top);
1181 if cursor_row < vp_height {
1182 let cursor_screen_x = (cursor.visual_col.saturating_sub(scroll_left) as u16)
1183 .saturating_add(text_area_x);
1184 let cursor_screen_y = area.y.saturating_add(cursor_row as u16);
1185 if cursor_screen_x < area.right() && cursor_screen_y < area.bottom() {
1186 frame.set_cursor(Some((cursor_screen_x, cursor_screen_y)));
1187 }
1188 }
1189 }
1190 }
1191
1192 fn is_essential(&self) -> bool {
1193 true
1194 }
1195}
1196
1197impl StatefulWidget for TextArea {
1198 type State = TextAreaState;
1199
1200 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
1201 state.last_viewport_height = area.height;
1202 state.last_viewport_width = area.width;
1203 Widget::render(self, area, frame);
1204 }
1205}
1206
1207#[cfg(test)]
1208mod tests {
1209 use super::*;
1210
1211 #[test]
1212 fn new_textarea_is_empty() {
1213 let ta = TextArea::new();
1214 assert!(ta.is_empty());
1215 assert_eq!(ta.text(), "");
1216 assert_eq!(ta.line_count(), 1); }
1218
1219 #[test]
1220 fn with_text_builder() {
1221 let ta = TextArea::new().with_text("hello\nworld");
1222 assert_eq!(ta.text(), "hello\nworld");
1223 assert_eq!(ta.line_count(), 2);
1224 }
1225
1226 #[test]
1227 fn insert_text_and_newline() {
1228 let mut ta = TextArea::new();
1229 ta.insert_text("hello");
1230 ta.insert_newline();
1231 ta.insert_text("world");
1232 assert_eq!(ta.text(), "hello\nworld");
1233 assert_eq!(ta.line_count(), 2);
1234 }
1235
1236 #[test]
1237 fn delete_backward_works() {
1238 let mut ta = TextArea::new().with_text("hello");
1239 ta.move_to_document_end();
1240 ta.delete_backward();
1241 assert_eq!(ta.text(), "hell");
1242 }
1243
1244 #[test]
1245 fn cursor_movement() {
1246 let mut ta = TextArea::new().with_text("abc\ndef\nghi");
1247 ta.move_to_document_start();
1248 assert_eq!(ta.cursor().line, 0);
1249 assert_eq!(ta.cursor().grapheme, 0);
1250
1251 ta.move_down();
1252 assert_eq!(ta.cursor().line, 1);
1253
1254 ta.move_to_line_end();
1255 assert_eq!(ta.cursor().grapheme, 3);
1256
1257 ta.move_to_document_end();
1258 assert_eq!(ta.cursor().line, 2);
1259 }
1260
1261 #[test]
1262 fn undo_redo() {
1263 let mut ta = TextArea::new();
1264 ta.insert_text("abc");
1265 assert_eq!(ta.text(), "abc");
1266 ta.undo();
1267 assert_eq!(ta.text(), "");
1268 ta.redo();
1269 assert_eq!(ta.text(), "abc");
1270 }
1271
1272 #[test]
1273 fn selection_and_delete() {
1274 let mut ta = TextArea::new().with_text("hello world");
1275 ta.move_to_document_start();
1276 for _ in 0..5 {
1277 ta.select_right();
1278 }
1279 assert_eq!(ta.selected_text(), Some("hello".to_string()));
1280 ta.delete_backward();
1281 assert_eq!(ta.text(), " world");
1282 }
1283
1284 #[test]
1285 fn select_all() {
1286 let mut ta = TextArea::new().with_text("abc\ndef");
1287 ta.select_all();
1288 assert_eq!(ta.selected_text(), Some("abc\ndef".to_string()));
1289 }
1290
1291 #[test]
1292 fn set_text_resets() {
1293 let mut ta = TextArea::new().with_text("old");
1294 ta.insert_text(" stuff");
1295 ta.set_text("new");
1296 assert_eq!(ta.text(), "new");
1297 }
1298
1299 #[test]
1300 fn scroll_follows_cursor() {
1301 let mut ta = TextArea::new();
1302 for i in 0..50 {
1304 ta.insert_text(&format!("line {}\n", i));
1305 }
1306 assert!(ta.scroll_top.get() > 0);
1308 assert!(ta.cursor().line >= 49);
1309
1310 ta.move_to_document_start();
1312 assert_eq!(ta.scroll_top.get(), 0);
1313 }
1314
1315 #[test]
1316 fn gutter_width_without_line_numbers() {
1317 let ta = TextArea::new();
1318 assert_eq!(ta.gutter_width(), 0);
1319 }
1320
1321 #[test]
1322 fn gutter_width_with_line_numbers() {
1323 let mut ta = TextArea::new().with_line_numbers(true);
1324 ta.insert_text("a\nb\nc");
1325 assert_eq!(ta.gutter_width(), 3); }
1327
1328 #[test]
1329 fn gutter_width_many_lines() {
1330 let mut ta = TextArea::new().with_line_numbers(true);
1331 for i in 0..100 {
1332 ta.insert_text(&format!("line {}\n", i));
1333 }
1334 assert_eq!(ta.gutter_width(), 5); }
1336
1337 #[test]
1338 fn focus_state() {
1339 let mut ta = TextArea::new();
1340 assert!(!ta.is_focused());
1341 ta.set_focused(true);
1342 assert!(ta.is_focused());
1343 }
1344
1345 #[test]
1346 fn word_movement() {
1347 let mut ta = TextArea::new().with_text("hello world foo");
1348 ta.move_to_document_start();
1349 ta.move_word_right();
1350 assert_eq!(ta.cursor().grapheme, 6);
1351 ta.move_word_left();
1352 assert_eq!(ta.cursor().grapheme, 0);
1353 }
1354
1355 #[test]
1356 fn page_up_down() {
1357 let mut ta = TextArea::new();
1358 for i in 0..50 {
1359 ta.insert_text(&format!("line {}\n", i));
1360 }
1361 ta.move_to_document_start();
1362 let state = TextAreaState {
1363 last_viewport_height: 10,
1364 last_viewport_width: 80,
1365 };
1366 ta.page_down(&state);
1367 assert!(ta.cursor().line >= 10);
1368 ta.page_up(&state);
1369 assert_eq!(ta.cursor().line, 0);
1370 }
1371
1372 #[test]
1373 fn insert_replaces_selection() {
1374 let mut ta = TextArea::new().with_text("hello world");
1375 ta.move_to_document_start();
1376 for _ in 0..5 {
1377 ta.select_right();
1378 }
1379 ta.insert_text("goodbye");
1380 assert_eq!(ta.text(), "goodbye world");
1381 }
1382
1383 #[test]
1384 fn insert_single_char() {
1385 let mut ta = TextArea::new();
1386 ta.insert_char('X');
1387 assert_eq!(ta.text(), "X");
1388 assert_eq!(ta.cursor().grapheme, 1);
1389 }
1390
1391 #[test]
1392 fn insert_multiline_text() {
1393 let mut ta = TextArea::new();
1394 ta.insert_text("line1\nline2\nline3");
1395 assert_eq!(ta.line_count(), 3);
1396 assert_eq!(ta.cursor().line, 2);
1397 }
1398
1399 #[test]
1400 fn delete_forward_works() {
1401 let mut ta = TextArea::new().with_text("hello");
1402 ta.move_to_document_start();
1403 ta.delete_forward();
1404 assert_eq!(ta.text(), "ello");
1405 }
1406
1407 #[test]
1408 fn delete_backward_at_line_start_joins_lines() {
1409 let mut ta = TextArea::new().with_text("abc\ndef");
1410 ta.move_to_document_start();
1412 ta.move_down();
1413 ta.move_to_line_start();
1414 ta.delete_backward();
1415 assert_eq!(ta.text(), "abcdef");
1416 assert_eq!(ta.line_count(), 1);
1417 }
1418
1419 #[test]
1420 fn cursor_horizontal_movement() {
1421 let mut ta = TextArea::new().with_text("abc");
1422 ta.move_to_document_start();
1423 ta.move_right();
1424 assert_eq!(ta.cursor().grapheme, 1);
1425 ta.move_right();
1426 assert_eq!(ta.cursor().grapheme, 2);
1427 ta.move_left();
1428 assert_eq!(ta.cursor().grapheme, 1);
1429 }
1430
1431 #[test]
1432 fn cursor_vertical_maintains_column() {
1433 let mut ta = TextArea::new().with_text("abcde\nfg\nhijkl");
1434 ta.move_to_document_start();
1435 ta.move_to_line_end(); ta.move_down(); assert_eq!(ta.cursor().line, 1);
1438 ta.move_down(); assert_eq!(ta.cursor().line, 2);
1440 }
1441
1442 #[test]
1443 fn selection_shift_arrow() {
1444 let mut ta = TextArea::new().with_text("abcdef");
1445 ta.move_to_document_start();
1446 ta.select_right();
1447 ta.select_right();
1448 ta.select_right();
1449 assert_eq!(ta.selected_text(), Some("abc".to_string()));
1450 }
1451
1452 #[test]
1453 fn selection_extends_up_down() {
1454 let mut ta = TextArea::new().with_text("line1\nline2\nline3");
1455 ta.move_to_document_start();
1456 ta.select_down();
1457 let sel = ta.selected_text().unwrap();
1458 assert!(sel.contains('\n'));
1459 }
1460
1461 #[test]
1462 fn undo_chain() {
1463 let mut ta = TextArea::new();
1464 ta.insert_text("a");
1465 ta.insert_text("b");
1466 ta.insert_text("c");
1467 assert_eq!(ta.text(), "abc");
1468 ta.undo();
1469 ta.undo();
1470 ta.undo();
1471 assert_eq!(ta.text(), "");
1472 }
1473
1474 #[test]
1475 fn redo_discarded_on_new_edit() {
1476 let mut ta = TextArea::new();
1477 ta.insert_text("abc");
1478 ta.undo();
1479 ta.insert_text("xyz");
1480 ta.redo(); assert_eq!(ta.text(), "xyz");
1482 }
1483
1484 #[test]
1485 fn clear_selection() {
1486 let mut ta = TextArea::new().with_text("hello");
1487 ta.select_all();
1488 assert!(ta.selection().is_some());
1489 ta.clear_selection();
1490 assert!(ta.selection().is_none());
1491 }
1492
1493 #[test]
1494 fn delete_word_backward() {
1495 let mut ta = TextArea::new().with_text("hello world");
1496 ta.move_to_document_end();
1497 ta.delete_word_backward();
1498 assert_eq!(ta.text(), "hello ");
1499 }
1500
1501 #[test]
1502 fn delete_to_end_of_line() {
1503 let mut ta = TextArea::new().with_text("hello world");
1504 ta.move_to_document_start();
1505 ta.move_right(); ta.delete_to_end_of_line();
1507 assert_eq!(ta.text(), "h");
1508 }
1509
1510 #[test]
1511 fn placeholder_builder() {
1512 let ta = TextArea::new().with_placeholder("Enter text...");
1513 assert!(ta.is_empty());
1514 assert_eq!(ta.placeholder, "Enter text...");
1515 }
1516
1517 #[test]
1518 fn soft_wrap_builder() {
1519 let ta = TextArea::new().with_soft_wrap(true);
1520 assert!(ta.soft_wrap);
1521 }
1522
1523 #[test]
1524 fn soft_wrap_renders_wrapped_lines() {
1525 use crate::Widget;
1526 use ftui_render::grapheme_pool::GraphemePool;
1527
1528 let ta = TextArea::new().with_soft_wrap(true).with_text("abcdef");
1529 let area = Rect::new(0, 0, 3, 2);
1530 let mut pool = GraphemePool::new();
1531 let mut frame = Frame::new(3, 2, &mut pool);
1532 Widget::render(&ta, area, &mut frame);
1533
1534 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('a'));
1535 assert_eq!(frame.buffer.get(2, 0).unwrap().content.as_char(), Some('c'));
1536 assert_eq!(frame.buffer.get(0, 1).unwrap().content.as_char(), Some('d'));
1537 assert_eq!(frame.buffer.get(2, 1).unwrap().content.as_char(), Some('f'));
1538 }
1539
1540 #[test]
1541 fn max_height_builder() {
1542 let ta = TextArea::new().with_max_height(10);
1543 assert_eq!(ta.max_height, 10);
1544 }
1545
1546 #[test]
1547 fn editor_access() {
1548 let mut ta = TextArea::new().with_text("test");
1549 assert_eq!(ta.editor().text(), "test");
1550 ta.editor_mut().insert_char('!');
1551 assert!(ta.text().contains('!'));
1552 }
1553
1554 #[test]
1555 fn move_to_line_start_and_end() {
1556 let mut ta = TextArea::new().with_text("hello world");
1557 ta.move_to_document_start();
1558 ta.move_to_line_end();
1559 assert_eq!(ta.cursor().grapheme, 11);
1560 ta.move_to_line_start();
1561 assert_eq!(ta.cursor().grapheme, 0);
1562 }
1563
1564 #[test]
1565 fn render_empty_with_placeholder() {
1566 use ftui_render::grapheme_pool::GraphemePool;
1567 let ta = TextArea::new()
1568 .with_placeholder("Type here")
1569 .with_focus(true);
1570 let mut pool = GraphemePool::new();
1571 let mut frame = Frame::new(20, 5, &mut pool);
1572 let area = Rect::new(0, 0, 20, 5);
1573 Widget::render(&ta, area, &mut frame);
1574 let cell = frame.buffer.get(0, 0).unwrap();
1576 assert_eq!(cell.content.as_char(), Some('T'));
1577 assert!(frame.cursor_position.is_some());
1579 }
1580
1581 #[test]
1582 fn render_with_content() {
1583 use ftui_render::grapheme_pool::GraphemePool;
1584 let ta = TextArea::new().with_text("abc\ndef").with_focus(true);
1585 let mut pool = GraphemePool::new();
1586 let mut frame = Frame::new(20, 5, &mut pool);
1587 let area = Rect::new(0, 0, 20, 5);
1588 Widget::render(&ta, area, &mut frame);
1589 let cell = frame.buffer.get(0, 0).unwrap();
1590 assert_eq!(cell.content.as_char(), Some('a'));
1591 }
1592
1593 #[test]
1594 fn render_line_numbers_without_styling() {
1595 use ftui_render::budget::DegradationLevel;
1596 use ftui_render::grapheme_pool::GraphemePool;
1597
1598 let ta = TextArea::new().with_text("a\nb").with_line_numbers(true);
1599 let mut pool = GraphemePool::new();
1600 let mut frame = Frame::new(8, 2, &mut pool);
1601 frame.set_degradation(DegradationLevel::NoStyling);
1602
1603 Widget::render(&ta, Rect::new(0, 0, 8, 2), &mut frame);
1604
1605 let cell = frame.buffer.get(0, 0).unwrap();
1606 assert_eq!(cell.content.as_char(), Some('1'));
1607 }
1608
1609 #[test]
1610 fn stateful_render_updates_viewport_state() {
1611 use ftui_render::grapheme_pool::GraphemePool;
1612
1613 let ta = TextArea::new();
1614 let mut state = TextAreaState::default();
1615 let mut pool = GraphemePool::new();
1616 let mut frame = Frame::new(10, 3, &mut pool);
1617 let area = Rect::new(0, 0, 10, 3);
1618
1619 StatefulWidget::render(&ta, area, &mut frame, &mut state);
1620
1621 assert_eq!(state.last_viewport_height, 3);
1622 assert_eq!(state.last_viewport_width, 10);
1623 }
1624
1625 #[test]
1626 fn render_zero_area_no_panic() {
1627 let ta = TextArea::new().with_text("test");
1628 use ftui_render::grapheme_pool::GraphemePool;
1629 let mut pool = GraphemePool::new();
1630 let mut frame = Frame::new(10, 10, &mut pool);
1631 Widget::render(&ta, Rect::new(0, 0, 0, 0), &mut frame);
1632 }
1633
1634 #[test]
1635 fn is_essential() {
1636 let ta = TextArea::new();
1637 assert!(Widget::is_essential(&ta));
1638 }
1639
1640 #[test]
1641 fn default_impl() {
1642 let ta = TextArea::default();
1643 assert!(ta.is_empty());
1644 }
1645
1646 #[test]
1647 fn insert_newline_splits_line() {
1648 let mut ta = TextArea::new().with_text("abcdef");
1649 ta.move_to_document_start();
1650 ta.move_right();
1651 ta.move_right();
1652 ta.move_right();
1653 ta.insert_newline();
1654 assert_eq!(ta.line_count(), 2);
1655 assert_eq!(ta.cursor().line, 1);
1656 }
1657
1658 #[test]
1659 fn unicode_grapheme_cluster() {
1660 let mut ta = TextArea::new();
1661 ta.insert_text("café");
1662 assert_eq!(ta.text(), "café");
1664 }
1665
1666 mod proptests {
1667 use super::*;
1668 use proptest::prelude::*;
1669
1670 proptest! {
1671 #[test]
1672 fn insert_delete_inverse(text in "[a-zA-Z0-9 ]{1,50}") {
1673 let mut ta = TextArea::new();
1674 ta.insert_text(&text);
1675 for _ in 0..text.len() {
1677 ta.delete_backward();
1678 }
1679 prop_assert!(ta.is_empty() || ta.text().is_empty());
1680 }
1681
1682 #[test]
1683 fn undo_redo_inverse(text in "[a-zA-Z0-9]{1,30}") {
1684 let mut ta = TextArea::new();
1685 ta.insert_text(&text);
1686 let after_insert = ta.text();
1687 ta.undo();
1688 ta.redo();
1689 prop_assert_eq!(ta.text(), after_insert);
1690 }
1691
1692 #[test]
1693 fn cursor_always_valid(ops in proptest::collection::vec(0u8..10, 1..20)) {
1694 let mut ta = TextArea::new().with_text("abc\ndef\nghi\njkl");
1695 for op in ops {
1696 match op {
1697 0 => ta.move_left(),
1698 1 => ta.move_right(),
1699 2 => ta.move_up(),
1700 3 => ta.move_down(),
1701 4 => ta.move_to_line_start(),
1702 5 => ta.move_to_line_end(),
1703 6 => ta.move_to_document_start(),
1704 7 => ta.move_to_document_end(),
1705 8 => ta.move_word_left(),
1706 _ => ta.move_word_right(),
1707 }
1708 let cursor = ta.cursor();
1709 prop_assert!(cursor.line < ta.line_count(),
1710 "cursor line {} >= line_count {}", cursor.line, ta.line_count());
1711 }
1712 }
1713
1714 #[test]
1715 fn selection_ordered(n in 1usize..20) {
1716 let mut ta = TextArea::new().with_text("hello world foo bar");
1717 ta.move_to_document_start();
1718 for _ in 0..n {
1719 ta.select_right();
1720 }
1721 if let Some(sel) = ta.selection() {
1722 prop_assert!(sel.anchor.line <= sel.head.line
1724 || (sel.anchor.line == sel.head.line
1725 && sel.anchor.grapheme <= sel.head.grapheme));
1726 }
1727 }
1728 }
1729 }
1730}