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, clear_text_area, 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_anchor: std::cell::Cell<(usize, usize)>,
58 scroll_left: std::cell::Cell<usize>,
60 #[allow(dead_code)]
62 last_viewport_height: std::cell::Cell<usize>,
63 last_viewport_width: std::cell::Cell<usize>,
65}
66
67impl Default for TextArea {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73#[derive(Debug, Clone, Default)]
75pub struct TextAreaState {
76 pub last_viewport_height: u16,
78 pub last_viewport_width: u16,
80}
81
82#[derive(Debug, Clone)]
83struct WrappedSlice<'a> {
84 text: &'a str,
85 start_byte: usize,
86 start_col: usize,
87 width: usize,
88}
89
90impl TextArea {
91 #[must_use]
93 pub fn new() -> Self {
94 Self {
95 editor: Editor::new(),
96 placeholder: String::new(),
97 focused: false,
98 show_line_numbers: false,
99 style: Style::default(),
100 cursor_line_style: None,
101 selection_style: Style::new().reverse(),
102 placeholder_style: Style::new().dim(),
103 line_number_style: Style::new().dim(),
104 soft_wrap: false,
105 max_height: 0,
106 scroll_anchor: std::cell::Cell::new((usize::MAX, 0)), scroll_left: std::cell::Cell::new(0),
108 last_viewport_height: std::cell::Cell::new(0),
109 last_viewport_width: std::cell::Cell::new(0),
110 }
111 }
112
113 pub fn handle_event(&mut self, event: &Event) -> bool {
119 match event {
120 Event::Key(key)
121 if key.kind == KeyEventKind::Press || key.kind == KeyEventKind::Repeat =>
122 {
123 self.handle_key(key)
124 }
125 Event::Paste(paste) => {
126 self.insert_text(&paste.text);
127 true
128 }
129 _ => false,
130 }
131 }
132
133 fn handle_key(&mut self, key: &KeyEvent) -> bool {
134 let ctrl = key.modifiers.contains(Modifiers::CTRL);
135 let shift = key.modifiers.contains(Modifiers::SHIFT);
136 let _alt = key.modifiers.contains(Modifiers::ALT);
137
138 match key.code {
139 KeyCode::Char(c) if !ctrl => {
140 self.insert_char(c);
141 true
142 }
143 KeyCode::Enter => {
144 self.insert_newline();
145 true
146 }
147 KeyCode::Backspace => {
148 if ctrl {
149 self.delete_word_backward();
150 } else {
151 self.delete_backward();
152 }
153 true
154 }
155 KeyCode::Delete => {
156 if ctrl {
157 self.delete_word_forward();
158 } else {
159 self.delete_forward();
160 }
161 true
162 }
163 KeyCode::Left => {
164 if ctrl && shift {
165 self.select_word_left();
166 } else if ctrl {
167 self.move_word_left();
168 } else if shift {
169 self.select_left();
170 } else {
171 self.move_left();
172 }
173 true
174 }
175 KeyCode::Right => {
176 if ctrl && shift {
177 self.select_word_right();
178 } else if ctrl {
179 self.move_word_right();
180 } else if shift {
181 self.select_right();
182 } else {
183 self.move_right();
184 }
185 true
186 }
187 KeyCode::Up => {
188 if shift {
189 self.select_up();
190 } else {
191 self.move_up();
192 }
193 true
194 }
195 KeyCode::Down => {
196 if shift {
197 self.select_down();
198 } else {
199 self.move_down();
200 }
201 true
202 }
203 KeyCode::Home => {
204 self.move_to_line_start();
205 true
206 }
207 KeyCode::End => {
208 self.move_to_line_end();
209 true
210 }
211 KeyCode::PageUp => {
212 let page = self.last_viewport_height.get().max(1);
213 if self.soft_wrap {
214 self.move_cursor_visual_up(page, shift);
215 } else {
216 for _ in 0..page {
217 if shift {
218 self.editor.select_up();
219 } else {
220 self.editor.move_up();
221 }
222 }
223 }
224 self.ensure_cursor_visible();
225 true
226 }
227 KeyCode::PageDown => {
228 let page = self.last_viewport_height.get().max(1);
229 if self.soft_wrap {
230 self.move_cursor_visual_down(page, shift);
231 } else {
232 for _ in 0..page {
233 if shift {
234 self.editor.select_down();
235 } else {
236 self.editor.move_down();
237 }
238 }
239 }
240 self.ensure_cursor_visible();
241 true
242 }
243 KeyCode::Char('a') if ctrl => {
244 self.select_all();
245 true
246 }
247 KeyCode::Char('k') if ctrl => {
249 self.delete_to_end_of_line();
250 true
251 }
252 KeyCode::Char('z') if ctrl => {
254 self.undo();
255 true
256 }
257 KeyCode::Char('y') if ctrl => {
259 self.redo();
260 true
261 }
262 _ => false,
263 }
264 }
265
266 #[must_use]
270 pub fn with_text(mut self, text: &str) -> Self {
271 self.editor = Editor::with_text(text);
272 self.editor.move_to_document_start();
273 self
274 }
275
276 #[must_use]
278 pub fn with_placeholder(mut self, text: impl Into<String>) -> Self {
279 self.placeholder = text.into();
280 self
281 }
282
283 #[must_use]
285 pub fn with_focus(mut self, focused: bool) -> Self {
286 self.focused = focused;
287 self
288 }
289
290 #[must_use]
292 pub fn with_line_numbers(mut self, show: bool) -> Self {
293 self.show_line_numbers = show;
294 self
295 }
296
297 #[must_use]
299 pub fn with_style(mut self, style: Style) -> Self {
300 self.style = style;
301 self
302 }
303
304 #[must_use]
306 pub fn with_cursor_line_style(mut self, style: Style) -> Self {
307 self.cursor_line_style = Some(style);
308 self
309 }
310
311 #[must_use]
313 pub fn with_selection_style(mut self, style: Style) -> Self {
314 self.selection_style = style;
315 self
316 }
317
318 #[must_use]
320 pub fn with_soft_wrap(mut self, wrap: bool) -> Self {
321 self.soft_wrap = wrap;
322 self
323 }
324
325 #[must_use]
327 pub fn with_max_height(mut self, max: usize) -> Self {
328 self.max_height = max;
329 self
330 }
331
332 #[must_use]
336 pub fn text(&self) -> String {
337 self.editor.text()
338 }
339
340 pub fn set_text(&mut self, text: &str) {
342 self.editor.set_text(text);
343 self.scroll_anchor.set((0, 0));
344 self.scroll_left.set(0);
345 }
346
347 #[must_use]
349 pub fn line_count(&self) -> usize {
350 self.editor.line_count()
351 }
352
353 #[inline]
355 #[must_use]
356 pub fn cursor(&self) -> CursorPosition {
357 self.editor.cursor()
358 }
359
360 pub fn set_cursor_position(&mut self, pos: CursorPosition) {
362 self.editor.set_cursor(pos);
363 self.ensure_cursor_visible();
364 }
365
366 #[inline]
368 #[must_use]
369 pub fn is_empty(&self) -> bool {
370 self.editor.is_empty()
371 }
372
373 #[must_use = "use the returned selection (if any)"]
375 pub fn selection(&self) -> Option<Selection> {
376 self.editor.selection()
377 }
378
379 #[must_use = "use the returned selected text (if any)"]
381 pub fn selected_text(&self) -> Option<String> {
382 self.editor.selected_text()
383 }
384
385 #[must_use]
387 pub fn is_focused(&self) -> bool {
388 self.focused
389 }
390
391 pub fn set_focused(&mut self, focused: bool) {
393 self.focused = focused;
394 }
395
396 #[must_use]
398 pub fn editor(&self) -> &Editor {
399 &self.editor
400 }
401
402 pub fn editor_mut(&mut self) -> &mut Editor {
404 &mut self.editor
405 }
406
407 pub fn insert_text(&mut self, text: &str) {
411 self.editor.insert_text(text);
412 self.ensure_cursor_visible();
413 }
414
415 pub fn insert_char(&mut self, ch: char) {
417 self.editor.insert_char(ch);
418 self.ensure_cursor_visible();
419 }
420
421 pub fn insert_newline(&mut self) {
423 self.editor.insert_newline();
424 self.ensure_cursor_visible();
425 }
426
427 pub fn delete_backward(&mut self) {
429 self.editor.delete_backward();
430 self.ensure_cursor_visible();
431 }
432
433 pub fn delete_forward(&mut self) {
435 self.editor.delete_forward();
436 self.ensure_cursor_visible();
437 }
438
439 pub fn delete_word_backward(&mut self) {
441 self.editor.delete_word_backward();
442 self.ensure_cursor_visible();
443 }
444
445 pub fn delete_word_forward(&mut self) {
447 self.editor.delete_word_forward();
448 self.ensure_cursor_visible();
449 }
450
451 pub fn delete_to_end_of_line(&mut self) {
453 self.editor.delete_to_end_of_line();
454 self.ensure_cursor_visible();
455 }
456
457 pub fn undo(&mut self) {
459 self.editor.undo();
460 self.ensure_cursor_visible();
461 }
462
463 pub fn redo(&mut self) {
465 self.editor.redo();
466 self.ensure_cursor_visible();
467 }
468
469 pub fn move_left(&mut self) {
473 self.editor.move_left();
474 self.ensure_cursor_visible();
475 }
476
477 pub fn move_right(&mut self) {
479 self.editor.move_right();
480 self.ensure_cursor_visible();
481 }
482
483 pub fn move_up(&mut self) {
485 if self.soft_wrap {
486 self.move_cursor_visual_up(1, false);
487 } else {
488 self.editor.move_up();
489 }
490 self.ensure_cursor_visible();
491 }
492
493 pub fn move_down(&mut self) {
495 if self.soft_wrap {
496 self.move_cursor_visual_down(1, false);
497 } else {
498 self.editor.move_down();
499 }
500 self.ensure_cursor_visible();
501 }
502
503 pub fn move_word_left(&mut self) {
505 self.editor.move_word_left();
506 self.ensure_cursor_visible();
507 }
508
509 pub fn move_word_right(&mut self) {
511 self.editor.move_word_right();
512 self.ensure_cursor_visible();
513 }
514
515 pub fn select_word_left(&mut self) {
517 self.editor.select_word_left();
518 self.ensure_cursor_visible();
519 }
520
521 pub fn select_word_right(&mut self) {
523 self.editor.select_word_right();
524 self.ensure_cursor_visible();
525 }
526
527 pub fn move_to_line_start(&mut self) {
529 self.editor.move_to_line_start();
530 self.ensure_cursor_visible();
531 }
532
533 pub fn move_to_line_end(&mut self) {
535 self.editor.move_to_line_end();
536 self.ensure_cursor_visible();
537 }
538
539 pub fn move_to_document_start(&mut self) {
541 self.editor.move_to_document_start();
542 self.ensure_cursor_visible();
543 }
544
545 pub fn move_to_document_end(&mut self) {
547 self.editor.move_to_document_end();
548 self.ensure_cursor_visible();
549 }
550
551 fn move_cursor_visual_down(&mut self, count: usize, extend_selection: bool) {
552 let width = self.last_viewport_width.get();
553 if width == 0 {
554 for _ in 0..count {
555 if extend_selection {
556 self.editor.select_down();
557 } else {
558 self.editor.move_down();
559 }
560 }
561 return;
562 }
563
564 let rope = self.editor.rope();
565 let mut cursor = self.editor.cursor();
566 let mut remaining = count;
567
568 let line_text = rope
569 .line(cursor.line)
570 .unwrap_or(std::borrow::Cow::Borrowed(""));
571 let line_text = line_text.trim_end_matches(['\n', '\r']);
572 let (mut current_v_row, _) =
573 Self::cursor_wrap_position(line_text, width, cursor.visual_col);
574
575 let initial_slices = Self::wrap_line_slices(line_text, width);
577 let initial_slice_start = initial_slices
578 .get(current_v_row)
579 .map(|s| s.start_col)
580 .unwrap_or(0);
581 let target_screen_x = cursor.visual_col.saturating_sub(initial_slice_start);
582
583 while remaining > 0 {
584 let line_text = rope
585 .line(cursor.line)
586 .unwrap_or(std::borrow::Cow::Borrowed(""));
587 let line_text = line_text.trim_end_matches(['\n', '\r']);
588 let wrap_count = Self::measure_wrap_count(line_text, width);
589
590 let available_in_line = wrap_count.saturating_sub(1).saturating_sub(current_v_row);
591
592 if remaining <= available_in_line {
593 current_v_row += remaining;
594 remaining = 0;
595 } else {
596 remaining -= available_in_line + 1;
597 if cursor.line + 1 < self.editor.line_count() {
598 cursor.line += 1;
599 current_v_row = 0;
600 } else {
601 current_v_row = wrap_count.saturating_sub(1);
602 remaining = 0;
603 }
604 }
605 }
606
607 let line_text = rope
608 .line(cursor.line)
609 .unwrap_or(std::borrow::Cow::Borrowed(""));
610 let line_text = line_text.trim_end_matches(['\n', '\r']);
611 let slices = Self::wrap_line_slices(line_text, width);
612
613 if let Some(slice) = slices.get(current_v_row) {
614 let target_in_slice = target_screen_x.min(slice.width);
615
616 let mut g_idx = 0;
617 let mut v_w = 0;
618 for g in slice.text.graphemes(true) {
619 let w = display_width(g);
620 if v_w + w > target_in_slice {
621 break;
622 }
623 v_w += w;
624 g_idx += 1;
625 }
626
627 let nav = CursorNavigator::new(rope);
628 let line_start_byte = nav.to_byte_index(nav.from_line_grapheme(cursor.line, 0));
629 let slice_byte_offset: usize = slice
630 .text
631 .graphemes(true)
632 .take(g_idx)
633 .map(|g| g.len())
634 .sum();
635 let final_byte = line_start_byte + slice.start_byte + slice_byte_offset;
636 cursor = nav.from_byte_index(final_byte);
637 }
638
639 if extend_selection {
640 self.editor.extend_selection_to(cursor);
641 } else {
642 self.editor.set_cursor(cursor);
643 }
644 }
645
646 fn move_cursor_visual_up(&mut self, count: usize, extend_selection: bool) {
647 let width = self.last_viewport_width.get();
648 if width == 0 {
649 for _ in 0..count {
650 if extend_selection {
651 self.editor.select_up();
652 } else {
653 self.editor.move_up();
654 }
655 }
656 return;
657 }
658
659 let rope = self.editor.rope();
660 let mut cursor = self.editor.cursor();
661 let mut remaining = count;
662
663 let line_text = rope
664 .line(cursor.line)
665 .unwrap_or(std::borrow::Cow::Borrowed(""));
666 let line_text = line_text.trim_end_matches(['\n', '\r']);
667 let (mut current_v_row, _) =
668 Self::cursor_wrap_position(line_text, width, cursor.visual_col);
669
670 let initial_slices = Self::wrap_line_slices(line_text, width);
672 let initial_slice_start = initial_slices
673 .get(current_v_row)
674 .map(|s| s.start_col)
675 .unwrap_or(0);
676 let target_screen_x = cursor.visual_col.saturating_sub(initial_slice_start);
677
678 while remaining > 0 {
679 if remaining <= current_v_row {
680 current_v_row -= remaining;
681 remaining = 0;
682 } else {
683 remaining -= current_v_row + 1;
684 if cursor.line > 0 {
685 cursor.line -= 1;
686 let line_text = rope
687 .line(cursor.line)
688 .unwrap_or(std::borrow::Cow::Borrowed(""));
689 let line_text = line_text.trim_end_matches(['\n', '\r']);
690 let wrap_count = Self::measure_wrap_count(line_text, width);
691 current_v_row = wrap_count.saturating_sub(1);
692 } else {
693 current_v_row = 0;
694 remaining = 0;
695 }
696 }
697 }
698
699 let line_text = rope
700 .line(cursor.line)
701 .unwrap_or(std::borrow::Cow::Borrowed(""));
702 let line_text = line_text.trim_end_matches(['\n', '\r']);
703 let slices = Self::wrap_line_slices(line_text, width);
704
705 if let Some(slice) = slices.get(current_v_row) {
706 let target_in_slice = target_screen_x.min(slice.width);
707
708 let mut g_idx = 0;
709 let mut v_w = 0;
710 for g in slice.text.graphemes(true) {
711 let w = display_width(g);
712 if v_w + w > target_in_slice {
713 break;
714 }
715 v_w += w;
716 g_idx += 1;
717 }
718
719 let nav = CursorNavigator::new(rope);
720 let line_start_byte = nav.to_byte_index(nav.from_line_grapheme(cursor.line, 0));
721 let slice_byte_offset: usize = slice
722 .text
723 .graphemes(true)
724 .take(g_idx)
725 .map(|g| g.len())
726 .sum();
727 let final_byte = line_start_byte + slice.start_byte + slice_byte_offset;
728 cursor = nav.from_byte_index(final_byte);
729 }
730
731 if extend_selection {
732 self.editor.extend_selection_to(cursor);
733 } else {
734 self.editor.set_cursor(cursor);
735 }
736 }
737
738 pub fn select_left(&mut self) {
742 self.editor.select_left();
743 self.ensure_cursor_visible();
744 }
745
746 pub fn select_right(&mut self) {
748 self.editor.select_right();
749 self.ensure_cursor_visible();
750 }
751
752 pub fn select_up(&mut self) {
754 if self.soft_wrap {
755 self.move_cursor_visual_up(1, true);
756 } else {
757 self.editor.select_up();
758 }
759 self.ensure_cursor_visible();
760 }
761
762 pub fn select_down(&mut self) {
764 if self.soft_wrap {
765 self.move_cursor_visual_down(1, true);
766 } else {
767 self.editor.select_down();
768 }
769 self.ensure_cursor_visible();
770 }
771
772 pub fn select_all(&mut self) {
774 self.editor.select_all();
775 }
776
777 pub fn clear_selection(&mut self) {
779 self.editor.clear_selection();
780 }
781
782 pub fn page_up(&mut self, state: &TextAreaState) {
786 let page = state.last_viewport_height.max(1) as usize;
787 let text_area_width = state
788 .last_viewport_width
789 .saturating_sub(self.gutter_width());
790 self.last_viewport_height.set(page);
791 self.last_viewport_width.set(text_area_width as usize);
792 if self.soft_wrap {
793 if text_area_width > 0 {
794 self.move_cursor_visual_up(page, false);
795 } else {
796 for _ in 0..page {
797 self.editor.move_up();
798 }
799 }
800 } else {
801 for _ in 0..page {
802 self.editor.move_up();
803 }
804 }
805 self.ensure_cursor_visible();
806 }
807
808 pub fn page_down(&mut self, state: &TextAreaState) {
810 let page = state.last_viewport_height.max(1) as usize;
811 let text_area_width = state
812 .last_viewport_width
813 .saturating_sub(self.gutter_width());
814 self.last_viewport_height.set(page);
815 self.last_viewport_width.set(text_area_width as usize);
816 if self.soft_wrap {
817 if text_area_width > 0 {
818 self.move_cursor_visual_down(page, false);
819 } else {
820 for _ in 0..page {
821 self.editor.move_down();
822 }
823 }
824 } else {
825 for _ in 0..page {
826 self.editor.move_down();
827 }
828 }
829 self.ensure_cursor_visible();
830 }
831
832 fn gutter_width(&self) -> u16 {
834 if !self.show_line_numbers {
835 return 0;
836 }
837 let digits = {
838 let mut count = self.line_count().max(1);
839 let mut d: u16 = 0;
840 while count > 0 {
841 d += 1;
842 count /= 10;
843 }
844 d
845 };
846 digits + 2 }
848
849 fn measure_wrap_count(line_text: &str, max_width: usize) -> usize {
853 if line_text.is_empty() {
854 return 1;
855 }
856
857 let mut count = 0;
858 let mut current_width = 0;
859 let mut has_content = false;
860
861 Self::run_wrapping_logic(line_text, max_width, |_, width, flush| {
862 if flush {
863 if width > 0 {
864 count += 1;
865 }
866 current_width = 0;
867 has_content = false;
868 } else {
869 current_width = width;
870 has_content = true;
871 }
872 });
873
874 if has_content || count == 0 {
879 count += 1;
880 }
881
882 count
883 }
884
885 fn run_wrapping_logic<F>(line_text: &str, max_width: usize, mut callback: F)
891 where
892 F: FnMut(usize, usize, bool),
893 {
894 let mut current_width = 0;
895 let mut byte_cursor = 0;
896
897 for segment in line_text.split_word_bounds() {
898 let seg_len = segment.len();
899 let seg_width: usize = segment.graphemes(true).map(display_width).sum();
900
901 if max_width > 0 && current_width + seg_width > max_width {
902 callback(byte_cursor, current_width, true);
904 current_width = 0;
905 }
906
907 if max_width > 0 && seg_width > max_width {
908 for grapheme in segment.graphemes(true) {
909 let g_width = display_width(grapheme);
910 let g_len = grapheme.len();
911
912 if max_width > 0 && current_width + g_width > max_width && current_width > 0 {
913 callback(byte_cursor, current_width, true);
914 current_width = 0;
915 }
916
917 current_width += g_width;
918 byte_cursor += g_len;
919 callback(byte_cursor, current_width, false);
920 }
921 continue;
922 }
923
924 current_width += seg_width;
925 byte_cursor += seg_len;
926 callback(byte_cursor, current_width, false);
927 }
928 }
929
930 fn wrap_line_slices<'a>(line_text: &'a str, max_width: usize) -> Vec<WrappedSlice<'a>> {
931 if line_text.is_empty() {
932 return vec![WrappedSlice {
933 text: "",
934 start_byte: 0,
935 start_col: 0,
936 width: 0,
937 }];
938 }
939
940 let mut slices = Vec::new();
941 let mut current_width = 0;
942 let mut slice_start_byte = 0;
943 let mut slice_start_col = 0;
944 let mut byte_cursor = 0;
945 let mut col_cursor = 0;
946
947 let push_current = |slices: &mut Vec<WrappedSlice<'a>>,
948 width: &mut usize,
949 start_byte: &mut usize,
950 start_col: &mut usize,
951 byte_cursor: usize,
952 col_cursor: usize| {
953 if byte_cursor == *start_byte && *width == 0 {
954 return;
955 }
956 slices.push(WrappedSlice {
957 text: &line_text[*start_byte..byte_cursor],
958 start_byte: *start_byte,
959 start_col: *start_col,
960 width: *width,
961 });
962 *start_byte = byte_cursor;
963 *start_col = col_cursor;
964 *width = 0;
965 };
966
967 for segment in line_text.split_word_bounds() {
968 let seg_len = segment.len();
969 let seg_width: usize = segment.graphemes(true).map(display_width).sum();
970
971 if max_width > 0 && current_width + seg_width > max_width {
972 push_current(
973 &mut slices,
974 &mut current_width,
975 &mut slice_start_byte,
976 &mut slice_start_col,
977 byte_cursor,
978 col_cursor,
979 );
980 }
981
982 if max_width > 0 && seg_width > max_width {
983 for grapheme in segment.graphemes(true) {
984 let g_width = display_width(grapheme);
985 let g_len = grapheme.len();
986
987 if max_width > 0 && current_width + g_width > max_width && current_width > 0 {
988 push_current(
989 &mut slices,
990 &mut current_width,
991 &mut slice_start_byte,
992 &mut slice_start_col,
993 byte_cursor,
994 col_cursor,
995 );
996 }
997
998 current_width += g_width;
999 byte_cursor += g_len;
1000 col_cursor += g_width;
1001 }
1002 continue;
1003 }
1004
1005 current_width += seg_width;
1006 byte_cursor += seg_len;
1007 col_cursor += seg_width;
1008 }
1009
1010 if byte_cursor > slice_start_byte || current_width > 0 || slices.is_empty() {
1011 slices.push(WrappedSlice {
1012 text: &line_text[slice_start_byte..byte_cursor],
1013 start_byte: slice_start_byte,
1014 start_col: slice_start_col,
1015 width: current_width,
1016 });
1017 }
1018
1019 slices
1020 }
1021
1022 fn cursor_wrap_position(
1023 line_text: &str,
1024 max_width: usize,
1025 cursor_col: usize,
1026 ) -> (usize, usize) {
1027 let slices = Self::wrap_line_slices(line_text, max_width);
1028 if slices.is_empty() {
1029 return (0, 0);
1030 }
1031
1032 for (idx, slice) in slices.iter().enumerate() {
1033 let end_col = slice.start_col.saturating_add(slice.width);
1034 let is_last = idx == slices.len().saturating_sub(1);
1037 if cursor_col < end_col || (cursor_col == end_col && is_last) {
1038 let col_in_slice = cursor_col.saturating_sub(slice.start_col);
1039 return (idx, col_in_slice.min(slice.width));
1040 }
1041 }
1042
1043 (0, 0)
1044 }
1045
1046 fn get_prev_char_width(&self) -> usize {
1048 let cursor = self.editor.cursor();
1049 if cursor.grapheme == 0 {
1050 return 0;
1051 }
1052 let rope = self.editor.rope();
1053 let line = rope
1054 .line(cursor.line)
1055 .unwrap_or(std::borrow::Cow::Borrowed(""));
1056
1057 line.graphemes(true)
1058 .nth(cursor.grapheme - 1)
1059 .map(display_width)
1060 .unwrap_or(0)
1061 }
1062
1063 fn ensure_cursor_visible(&self) {
1065 let cursor = self.editor.cursor();
1066
1067 let last_height = self.last_viewport_height.get();
1068 let vp_height = if last_height == 0 { 20 } else { last_height };
1069
1070 let last_width = self.last_viewport_width.get();
1071 let vp_width = if last_width == 0 { 80 } else { last_width };
1072
1073 if self.scroll_anchor.get().0 == usize::MAX {
1074 self.scroll_anchor.set((0, 0));
1075 }
1076
1077 self.ensure_cursor_visible_internal(vp_height, vp_width, cursor);
1078 }
1079
1080 fn ensure_cursor_visible_internal(
1081 &self,
1082 vp_height: usize,
1083 vp_width: usize,
1084 cursor: CursorPosition,
1085 ) {
1086 let (anchor_line, anchor_vrow) = self.scroll_anchor.get();
1087
1088 if !self.soft_wrap {
1089 if cursor.line < anchor_line {
1091 self.scroll_anchor.set((cursor.line, 0));
1092 } else if vp_height > 0 && cursor.line >= anchor_line + vp_height {
1093 self.scroll_anchor
1094 .set((cursor.line.saturating_sub(vp_height - 1), 0));
1095 }
1096
1097 let current_left = self.scroll_left.get();
1099 let visual_col = cursor.visual_col;
1100
1101 if visual_col < current_left {
1102 self.scroll_left.set(visual_col);
1103 } else if vp_width > 0 && visual_col >= current_left + vp_width {
1104 let candidate_scroll = visual_col.saturating_sub(vp_width - 1);
1105 let prev_width = self.get_prev_char_width();
1106 let max_scroll_for_prev = visual_col.saturating_sub(prev_width);
1107
1108 let new_scroll = if vp_width > prev_width {
1109 candidate_scroll.min(max_scroll_for_prev)
1110 } else {
1111 candidate_scroll
1112 };
1113
1114 self.scroll_left.set(new_scroll);
1115 }
1116 return;
1117 }
1118
1119 let rope = self.editor.rope();
1121
1122 if cursor.line < anchor_line {
1124 let line_text = rope
1125 .line(cursor.line)
1126 .unwrap_or(std::borrow::Cow::Borrowed(""));
1127 let line_text = line_text.trim_end_matches(['\n', '\r']);
1128 let (v_row, _) = Self::cursor_wrap_position(line_text, vp_width, cursor.visual_col);
1129 self.scroll_anchor.set((cursor.line, v_row));
1130 return;
1131 }
1132
1133 if cursor.line == anchor_line {
1134 let line_text = rope
1135 .line(cursor.line)
1136 .unwrap_or(std::borrow::Cow::Borrowed(""));
1137 let line_text = line_text.trim_end_matches(['\n', '\r']);
1138 let (v_row, _) = Self::cursor_wrap_position(line_text, vp_width, cursor.visual_col);
1139 if v_row < anchor_vrow {
1140 self.scroll_anchor.set((cursor.line, v_row));
1141 return;
1142 }
1143 }
1144
1145 let mut visual_rows_capacity = vp_height;
1147 let mut current_line = anchor_line;
1148 let mut current_v_start = anchor_vrow;
1149
1150 loop {
1151 if current_line > cursor.line {
1152 return;
1154 }
1155
1156 let line_text = rope
1157 .line(current_line)
1158 .unwrap_or(std::borrow::Cow::Borrowed(""));
1159 let line_text = line_text.trim_end_matches(['\n', '\r']);
1160 let wrap_count = Self::measure_wrap_count(line_text, vp_width);
1161
1162 if current_line == cursor.line {
1163 let (cursor_v_row, _) =
1164 Self::cursor_wrap_position(line_text, vp_width, cursor.visual_col);
1165 if cursor_v_row >= current_v_start {
1166 let displayed_row_index = cursor_v_row - current_v_start;
1167 if displayed_row_index < visual_rows_capacity {
1168 return;
1170 } else {
1171 break;
1173 }
1174 } else {
1175 return;
1177 }
1178 }
1179
1180 let rows_remaining_in_line = wrap_count.saturating_sub(current_v_start);
1181 if rows_remaining_in_line >= visual_rows_capacity {
1182 break;
1183 }
1184
1185 visual_rows_capacity -= rows_remaining_in_line;
1186 current_line += 1;
1187 current_v_start = 0;
1188
1189 if current_line >= self.editor.line_count() {
1190 break;
1191 }
1192 }
1193
1194 let mut needed = vp_height;
1196 let mut scan_line = cursor.line;
1197
1198 let line_text = rope
1200 .line(scan_line)
1201 .unwrap_or(std::borrow::Cow::Borrowed(""));
1202 let line_text = line_text.trim_end_matches(['\n', '\r']);
1203 let (cursor_v, _) = Self::cursor_wrap_position(line_text, vp_width, cursor.visual_col);
1204
1205 let rows_above = cursor_v + 1;
1206 if rows_above >= needed {
1207 let new_v_start = cursor_v + 1 - needed;
1208 self.scroll_anchor.set((scan_line, new_v_start));
1209 return;
1210 }
1211
1212 needed -= rows_above;
1213
1214 while scan_line > 0 {
1216 scan_line -= 1;
1217 let line_text = rope
1218 .line(scan_line)
1219 .unwrap_or(std::borrow::Cow::Borrowed(""));
1220 let line_text = line_text.trim_end_matches(['\n', '\r']);
1221 let wrap_count = Self::measure_wrap_count(line_text, vp_width);
1222
1223 if wrap_count >= needed {
1224 let new_v_start = wrap_count - needed;
1225 self.scroll_anchor.set((scan_line, new_v_start));
1226 return;
1227 }
1228 needed -= wrap_count;
1229 }
1230
1231 self.scroll_anchor.set((0, 0));
1233 }
1234}
1235
1236impl Widget for TextArea {
1237 fn render(&self, area: Rect, frame: &mut Frame) {
1238 if area.width < 1 || area.height < 1 {
1239 return;
1240 }
1241
1242 self.last_viewport_height.set(area.height as usize);
1243
1244 let deg = frame.buffer.degradation;
1245 let base_style = if deg.apply_styling() {
1246 self.style
1247 } else {
1248 Style::default()
1249 };
1250 clear_text_area(frame, area, base_style);
1251
1252 let gutter_w = self.gutter_width();
1253 let text_area_x = area.x.saturating_add(gutter_w);
1254 let text_area_w = area.width.saturating_sub(gutter_w) as usize;
1255 let vp_height = area.height as usize;
1256
1257 self.last_viewport_width.set(text_area_w);
1258
1259 let cursor = self.editor.cursor();
1260
1261 self.ensure_cursor_visible();
1262
1263 let (scroll_top_line, scroll_top_vrow) = self.scroll_anchor.get();
1265 let scroll_left = self.scroll_left.get();
1266
1267 let rope = self.editor.rope();
1268 let nav = CursorNavigator::new(rope);
1269
1270 let sel_range = self.editor.selection().and_then(|sel| {
1272 if sel.is_empty() {
1273 None
1274 } else {
1275 let (a, b) = sel.byte_range(&nav);
1276 Some((a, b))
1277 }
1278 });
1279
1280 if self.editor.is_empty() && !self.placeholder.is_empty() {
1282 let style = if deg.apply_styling() {
1283 self.placeholder_style
1284 } else {
1285 Style::default()
1286 };
1287 draw_text_span(
1288 frame,
1289 text_area_x,
1290 area.y,
1291 &self.placeholder,
1292 style,
1293 area.right(),
1294 );
1295 if self.focused {
1296 frame.set_cursor(Some((text_area_x, area.y)));
1297 }
1298 return;
1299 }
1300
1301 if self.soft_wrap {
1302 self.scroll_left.set(0);
1303
1304 let cursor_line_text = rope
1306 .line(cursor.line)
1307 .unwrap_or(std::borrow::Cow::Borrowed(""));
1308 let cursor_line_text = cursor_line_text.trim_end_matches(['\n', '\r']);
1309 let (cursor_wrap_idx, cursor_col_in_wrap) =
1310 Self::cursor_wrap_position(cursor_line_text, text_area_w, cursor.visual_col);
1311
1312 let mut current_y = area.y;
1314 let bottom_y = area.bottom();
1315 let line_count = self.editor.line_count();
1316
1317 for line_idx in scroll_top_line..line_count {
1318 if current_y >= bottom_y {
1319 break;
1320 }
1321
1322 let line_text = rope
1323 .line(line_idx)
1324 .unwrap_or(std::borrow::Cow::Borrowed(""));
1325 let line_text = line_text.trim_end_matches(['\n', '\r']);
1326
1327 let line_start_byte = nav.to_byte_index(nav.from_line_grapheme(line_idx, 0));
1328 let slices = Self::wrap_line_slices(line_text, text_area_w);
1329
1330 let start_slice = if line_idx == scroll_top_line {
1332 scroll_top_vrow
1333 } else {
1334 0
1335 };
1336
1337 for (slice_idx, slice) in slices.iter().enumerate().skip(start_slice) {
1338 if current_y >= bottom_y {
1339 break;
1340 }
1341
1342 if self.show_line_numbers && slice_idx == 0 {
1344 let style = if deg.apply_styling() {
1345 self.line_number_style
1346 } else {
1347 Style::default()
1348 };
1349 let num_str =
1350 format!("{:>width$} ", line_idx + 1, width = (gutter_w - 2) as usize);
1351 draw_text_span(frame, area.x, current_y, &num_str, style, text_area_x);
1352 }
1353
1354 if line_idx == cursor.line
1356 && slice_idx == cursor_wrap_idx
1357 && let Some(cl_style) = self.cursor_line_style
1358 && deg.apply_styling()
1359 {
1360 for cx in text_area_x..area.right() {
1361 if let Some(cell) = frame.buffer.get_mut(cx, current_y) {
1362 apply_style(cell, cl_style);
1363 }
1364 }
1365 }
1366
1367 let mut visual_x: usize = 0;
1369 let mut grapheme_byte_offset = line_start_byte + slice.start_byte;
1370
1371 for g in slice.text.graphemes(true) {
1372 let g_width = display_width(g);
1373 let g_byte_len = g.len();
1374
1375 if visual_x >= text_area_w {
1376 break;
1377 }
1378
1379 let px = text_area_x + visual_x as u16;
1380
1381 let mut g_style = base_style;
1383 if let Some((sel_start, sel_end)) = sel_range
1384 && grapheme_byte_offset >= sel_start
1385 && grapheme_byte_offset < sel_end
1386 && deg.apply_styling()
1387 {
1388 g_style = g_style.merge(&self.selection_style);
1389 }
1390
1391 if g_width > 0 {
1392 draw_text_span(frame, px, current_y, g, g_style, area.right());
1393 }
1394
1395 visual_x += g_width;
1396 grapheme_byte_offset += g_byte_len;
1397 }
1398
1399 if self.focused && line_idx == cursor.line && slice_idx == cursor_wrap_idx {
1401 let cursor_screen_x = text_area_x.saturating_add(cursor_col_in_wrap as u16);
1402 if cursor_screen_x < area.right() {
1403 frame.set_cursor(Some((cursor_screen_x, current_y)));
1404 }
1405 }
1406
1407 current_y += 1;
1408 }
1409 }
1410
1411 return;
1412 }
1413
1414 for row in 0..vp_height {
1416 let line_idx = scroll_top_line + row;
1417 let y = area.y.saturating_add(row as u16);
1418
1419 if line_idx >= self.editor.line_count() {
1420 break;
1421 }
1422
1423 if self.show_line_numbers {
1425 let style = if deg.apply_styling() {
1426 self.line_number_style
1427 } else {
1428 Style::default()
1429 };
1430 let num_str = format!("{:>width$} ", line_idx + 1, width = (gutter_w - 2) as usize);
1431 draw_text_span(frame, area.x, y, &num_str, style, text_area_x);
1432 }
1433
1434 if line_idx == cursor.line
1436 && let Some(cl_style) = self.cursor_line_style
1437 && deg.apply_styling()
1438 {
1439 for cx in text_area_x..area.right() {
1440 if let Some(cell) = frame.buffer.get_mut(cx, y) {
1441 apply_style(cell, cl_style);
1442 }
1443 }
1444 }
1445
1446 let line_text = rope
1448 .line(line_idx)
1449 .unwrap_or(std::borrow::Cow::Borrowed(""));
1450 let line_text = line_text.trim_end_matches(['\n', '\r']);
1451
1452 let line_start_byte = nav.to_byte_index(nav.from_line_grapheme(line_idx, 0));
1454
1455 let mut visual_x: usize = 0;
1457 let graphemes: Vec<&str> = line_text.graphemes(true).collect();
1458 let mut grapheme_byte_offset = line_start_byte;
1459
1460 for g in &graphemes {
1461 let g_width = display_width(g);
1462 let g_byte_len = g.len();
1463
1464 let mut g_style = base_style;
1466 if let Some((sel_start, sel_end)) = sel_range
1467 && grapheme_byte_offset >= sel_start
1468 && grapheme_byte_offset < sel_end
1469 && deg.apply_styling()
1470 {
1471 g_style = g_style.merge(&self.selection_style);
1472 }
1473
1474 if visual_x + g_width <= scroll_left {
1476 visual_x += g_width;
1477 grapheme_byte_offset += g_byte_len;
1478 continue;
1479 }
1480
1481 if visual_x < scroll_left {
1483 let end_x = visual_x + g_width;
1485 let visible_width = end_x.saturating_sub(scroll_left);
1486
1487 for i in 0..visible_width {
1489 let screen_x = i; let px = text_area_x + screen_x as u16;
1491 if px < area.right() {
1492 draw_text_span(frame, px, y, " ", g_style, area.right());
1493 }
1494 }
1495
1496 visual_x += g_width;
1497 grapheme_byte_offset += g_byte_len;
1498 continue;
1499 }
1500
1501 let screen_x = visual_x.saturating_sub(scroll_left);
1503 if screen_x >= text_area_w {
1504 break;
1505 }
1506
1507 let px = text_area_x + screen_x as u16;
1508
1509 if g_width > 0 {
1511 draw_text_span(frame, px, y, g, g_style, area.right());
1512 }
1513
1514 visual_x += g_width;
1515 grapheme_byte_offset += g_byte_len;
1516 }
1517 }
1518
1519 if self.focused {
1521 let cursor_row = cursor.line.saturating_sub(scroll_top_line);
1522 if cursor_row < vp_height {
1523 let cursor_screen_x = (cursor.visual_col.saturating_sub(scroll_left) as u16)
1524 .saturating_add(text_area_x);
1525 let cursor_screen_y = area.y.saturating_add(cursor_row as u16);
1526 if cursor_screen_x < area.right() && cursor_screen_y < area.bottom() {
1527 frame.set_cursor(Some((cursor_screen_x, cursor_screen_y)));
1528 }
1529 }
1530 }
1531 }
1532
1533 fn is_essential(&self) -> bool {
1534 true
1535 }
1536}
1537
1538impl StatefulWidget for TextArea {
1539 type State = TextAreaState;
1540
1541 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
1542 state.last_viewport_height = area.height;
1543 state.last_viewport_width = area.width;
1544 Widget::render(self, area, frame);
1545 }
1546}
1547
1548#[cfg(test)]
1549mod tests {
1550 use super::*;
1551
1552 fn raw_row_text(frame: &Frame, y: u16, width: u16) -> String {
1553 (0..width)
1554 .map(|x| {
1555 frame
1556 .buffer
1557 .get(x, y)
1558 .and_then(|cell| cell.content.as_char())
1559 .unwrap_or(' ')
1560 })
1561 .collect()
1562 }
1563
1564 #[test]
1565 fn new_textarea_is_empty() {
1566 let ta = TextArea::new();
1567 assert!(ta.is_empty());
1568 assert_eq!(ta.text(), "");
1569 assert_eq!(ta.line_count(), 1); }
1571
1572 #[test]
1573 fn with_text_builder() {
1574 let ta = TextArea::new().with_text("hello\nworld");
1575 assert_eq!(ta.text(), "hello\nworld");
1576 assert_eq!(ta.line_count(), 2);
1577 }
1578
1579 #[test]
1580 fn insert_text_and_newline() {
1581 let mut ta = TextArea::new();
1582 ta.insert_text("hello");
1583 ta.insert_newline();
1584 ta.insert_text("world");
1585 assert_eq!(ta.text(), "hello\nworld");
1586 assert_eq!(ta.line_count(), 2);
1587 }
1588
1589 #[test]
1590 fn delete_backward_works() {
1591 let mut ta = TextArea::new().with_text("hello");
1592 ta.move_to_document_end();
1593 ta.delete_backward();
1594 assert_eq!(ta.text(), "hell");
1595 }
1596
1597 #[test]
1598 fn cursor_movement() {
1599 let mut ta = TextArea::new().with_text("abc\ndef\nghi");
1600 ta.move_to_document_start();
1601 assert_eq!(ta.cursor().line, 0);
1602 assert_eq!(ta.cursor().grapheme, 0);
1603
1604 ta.move_down();
1605 assert_eq!(ta.cursor().line, 1);
1606
1607 ta.move_to_line_end();
1608 assert_eq!(ta.cursor().grapheme, 3);
1609
1610 ta.move_to_document_end();
1611 assert_eq!(ta.cursor().line, 2);
1612 }
1613
1614 #[test]
1615 fn undo_redo() {
1616 let mut ta = TextArea::new();
1617 ta.insert_text("abc");
1618 assert_eq!(ta.text(), "abc");
1619 ta.undo();
1620 assert_eq!(ta.text(), "");
1621 ta.redo();
1622 assert_eq!(ta.text(), "abc");
1623 }
1624
1625 #[test]
1626 fn selection_and_delete() {
1627 let mut ta = TextArea::new().with_text("hello world");
1628 ta.move_to_document_start();
1629 for _ in 0..5 {
1630 ta.select_right();
1631 }
1632 assert_eq!(ta.selected_text(), Some("hello".to_string()));
1633 ta.delete_backward();
1634 assert_eq!(ta.text(), " world");
1635 }
1636
1637 #[test]
1638 fn select_all() {
1639 let mut ta = TextArea::new().with_text("abc\ndef");
1640 ta.select_all();
1641 assert_eq!(ta.selected_text(), Some("abc\ndef".to_string()));
1642 }
1643
1644 #[test]
1645 fn set_text_resets() {
1646 let mut ta = TextArea::new().with_text("old");
1647 ta.insert_text(" stuff");
1648 ta.set_text("new");
1649 assert_eq!(ta.text(), "new");
1650 }
1651
1652 #[test]
1653 fn scroll_follows_cursor() {
1654 let mut ta = TextArea::new();
1655 for i in 0..50 {
1657 ta.insert_text(&format!("line {}\n", i));
1658 }
1659 assert!(ta.scroll_anchor.get().0 > 0);
1661 assert!(ta.cursor().line >= 49);
1662
1663 ta.move_to_document_start();
1665 assert_eq!(ta.scroll_anchor.get().0, 0);
1666 }
1667
1668 #[test]
1669 fn gutter_width_without_line_numbers() {
1670 let ta = TextArea::new();
1671 assert_eq!(ta.gutter_width(), 0);
1672 }
1673
1674 #[test]
1675 fn gutter_width_with_line_numbers() {
1676 let mut ta = TextArea::new().with_line_numbers(true);
1677 ta.insert_text("a\nb\nc");
1678 assert_eq!(ta.gutter_width(), 3); }
1680
1681 #[test]
1682 fn gutter_width_many_lines() {
1683 let mut ta = TextArea::new().with_line_numbers(true);
1684 for i in 0..100 {
1685 ta.insert_text(&format!("line {}\n", i));
1686 }
1687 assert_eq!(ta.gutter_width(), 5); }
1689
1690 #[test]
1691 fn focus_state() {
1692 let mut ta = TextArea::new();
1693 assert!(!ta.is_focused());
1694 ta.set_focused(true);
1695 assert!(ta.is_focused());
1696 }
1697
1698 #[test]
1699 fn word_movement() {
1700 let mut ta = TextArea::new().with_text("hello world foo");
1701 ta.move_to_document_start();
1702 ta.move_word_right();
1703 assert_eq!(ta.cursor().grapheme, 6);
1704 ta.move_word_left();
1705 assert_eq!(ta.cursor().grapheme, 0);
1706 }
1707
1708 #[test]
1709 fn page_up_down() {
1710 let mut ta = TextArea::new();
1711 for i in 0..50 {
1712 ta.insert_text(&format!("line {}\n", i));
1713 }
1714 ta.move_to_document_start();
1715 let state = TextAreaState {
1716 last_viewport_height: 10,
1717 last_viewport_width: 80,
1718 };
1719 ta.page_down(&state);
1720 assert!(ta.cursor().line >= 10);
1721 ta.page_up(&state);
1722 assert_eq!(ta.cursor().line, 0);
1723 }
1724
1725 #[test]
1726 fn insert_replaces_selection() {
1727 let mut ta = TextArea::new().with_text("hello world");
1728 ta.move_to_document_start();
1729 for _ in 0..5 {
1730 ta.select_right();
1731 }
1732 ta.insert_text("goodbye");
1733 assert_eq!(ta.text(), "goodbye world");
1734 }
1735
1736 #[test]
1737 fn insert_single_char() {
1738 let mut ta = TextArea::new();
1739 ta.insert_char('X');
1740 assert_eq!(ta.text(), "X");
1741 assert_eq!(ta.cursor().grapheme, 1);
1742 }
1743
1744 #[test]
1745 fn insert_multiline_text() {
1746 let mut ta = TextArea::new();
1747 ta.insert_text("line1\nline2\nline3");
1748 assert_eq!(ta.line_count(), 3);
1749 assert_eq!(ta.cursor().line, 2);
1750 }
1751
1752 #[test]
1753 fn delete_forward_works() {
1754 let mut ta = TextArea::new().with_text("hello");
1755 ta.move_to_document_start();
1756 ta.delete_forward();
1757 assert_eq!(ta.text(), "ello");
1758 }
1759
1760 #[test]
1761 fn delete_backward_at_line_start_joins_lines() {
1762 let mut ta = TextArea::new().with_text("abc\ndef");
1763 ta.move_to_document_start();
1765 ta.move_down();
1766 ta.move_to_line_start();
1767 ta.delete_backward();
1768 assert_eq!(ta.text(), "abcdef");
1769 assert_eq!(ta.line_count(), 1);
1770 }
1771
1772 #[test]
1773 fn cursor_horizontal_movement() {
1774 let mut ta = TextArea::new().with_text("abc");
1775 ta.move_to_document_start();
1776 ta.move_right();
1777 assert_eq!(ta.cursor().grapheme, 1);
1778 ta.move_right();
1779 assert_eq!(ta.cursor().grapheme, 2);
1780 ta.move_left();
1781 assert_eq!(ta.cursor().grapheme, 1);
1782 }
1783
1784 #[test]
1785 fn cursor_vertical_maintains_column() {
1786 let mut ta = TextArea::new().with_text("abcde\nfg\nhijkl");
1787 ta.move_to_document_start();
1788 ta.move_to_line_end(); ta.move_down(); assert_eq!(ta.cursor().line, 1);
1791 ta.move_down(); assert_eq!(ta.cursor().line, 2);
1793 }
1794
1795 #[test]
1796 fn selection_shift_arrow() {
1797 let mut ta = TextArea::new().with_text("abcdef");
1798 ta.move_to_document_start();
1799 ta.select_right();
1800 ta.select_right();
1801 ta.select_right();
1802 assert_eq!(ta.selected_text(), Some("abc".to_string()));
1803 }
1804
1805 #[test]
1806 fn selection_extends_up_down() {
1807 let mut ta = TextArea::new().with_text("line1\nline2\nline3");
1808 ta.move_to_document_start();
1809 ta.select_down();
1810 let sel = ta.selected_text().unwrap();
1811 assert!(sel.contains('\n'));
1812 }
1813
1814 #[test]
1815 fn undo_chain() {
1816 let mut ta = TextArea::new();
1817 ta.insert_text("a");
1818 ta.insert_text("b");
1819 ta.insert_text("c");
1820 assert_eq!(ta.text(), "abc");
1821 ta.undo();
1822 ta.undo();
1823 ta.undo();
1824 assert_eq!(ta.text(), "");
1825 }
1826
1827 #[test]
1828 fn redo_discarded_on_new_edit() {
1829 let mut ta = TextArea::new();
1830 ta.insert_text("abc");
1831 ta.undo();
1832 ta.insert_text("xyz");
1833 ta.redo(); assert_eq!(ta.text(), "xyz");
1835 }
1836
1837 #[test]
1838 fn clear_selection() {
1839 let mut ta = TextArea::new().with_text("hello");
1840 ta.select_all();
1841 assert!(ta.selection().is_some());
1842 ta.clear_selection();
1843 assert!(ta.selection().is_none());
1844 }
1845
1846 #[test]
1847 fn delete_word_backward() {
1848 let mut ta = TextArea::new().with_text("hello world");
1849 ta.move_to_document_end();
1850 ta.delete_word_backward();
1851 assert_eq!(ta.text(), "hello ");
1852 }
1853
1854 #[test]
1855 fn delete_to_end_of_line() {
1856 let mut ta = TextArea::new().with_text("hello world");
1857 ta.move_to_document_start();
1858 ta.move_right(); ta.delete_to_end_of_line();
1860 assert_eq!(ta.text(), "h");
1861 }
1862
1863 #[test]
1864 fn placeholder_builder() {
1865 let ta = TextArea::new().with_placeholder("Enter text...");
1866 assert!(ta.is_empty());
1867 assert_eq!(ta.placeholder, "Enter text...");
1868 }
1869
1870 #[test]
1871 fn soft_wrap_builder() {
1872 let ta = TextArea::new().with_soft_wrap(true);
1873 assert!(ta.soft_wrap);
1874 }
1875
1876 #[test]
1877 fn soft_wrap_renders_wrapped_lines() {
1878 use crate::Widget;
1879 use ftui_render::grapheme_pool::GraphemePool;
1880
1881 let ta = TextArea::new().with_soft_wrap(true).with_text("abcdef");
1882 let area = Rect::new(0, 0, 3, 2);
1883 let mut pool = GraphemePool::new();
1884 let mut frame = Frame::new(3, 2, &mut pool);
1885 Widget::render(&ta, area, &mut frame);
1886
1887 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('a'));
1888 assert_eq!(frame.buffer.get(2, 0).unwrap().content.as_char(), Some('c'));
1889 assert_eq!(frame.buffer.get(0, 1).unwrap().content.as_char(), Some('d'));
1890 assert_eq!(frame.buffer.get(2, 1).unwrap().content.as_char(), Some('f'));
1891 }
1892
1893 #[test]
1894 fn max_height_builder() {
1895 let ta = TextArea::new().with_max_height(10);
1896 assert_eq!(ta.max_height, 10);
1897 }
1898
1899 #[test]
1900 fn editor_access() {
1901 let mut ta = TextArea::new().with_text("test");
1902 assert_eq!(ta.editor().text(), "test");
1903 ta.editor_mut().insert_char('!');
1904 assert!(ta.text().contains('!'));
1905 }
1906
1907 #[test]
1908 fn move_to_line_start_and_end() {
1909 let mut ta = TextArea::new().with_text("hello world");
1910 ta.move_to_document_start();
1911 ta.move_to_line_end();
1912 assert_eq!(ta.cursor().grapheme, 11);
1913 ta.move_to_line_start();
1914 assert_eq!(ta.cursor().grapheme, 0);
1915 }
1916
1917 #[test]
1918 fn render_empty_with_placeholder() {
1919 use ftui_render::grapheme_pool::GraphemePool;
1920 let ta = TextArea::new()
1921 .with_placeholder("Type here")
1922 .with_focus(true);
1923 let mut pool = GraphemePool::new();
1924 let mut frame = Frame::new(20, 5, &mut pool);
1925 let area = Rect::new(0, 0, 20, 5);
1926 Widget::render(&ta, area, &mut frame);
1927 let cell = frame.buffer.get(0, 0).unwrap();
1929 assert_eq!(cell.content.as_char(), Some('T'));
1930 assert!(frame.cursor_position.is_some());
1932 }
1933
1934 #[test]
1935 fn render_with_content() {
1936 use ftui_render::grapheme_pool::GraphemePool;
1937 let ta = TextArea::new().with_text("abc\ndef").with_focus(true);
1938 let mut pool = GraphemePool::new();
1939 let mut frame = Frame::new(20, 5, &mut pool);
1940 let area = Rect::new(0, 0, 20, 5);
1941 Widget::render(&ta, area, &mut frame);
1942 let cell = frame.buffer.get(0, 0).unwrap();
1943 assert_eq!(cell.content.as_char(), Some('a'));
1944 }
1945
1946 #[test]
1947 fn render_shorter_text_clears_stale_suffix_and_extra_lines() {
1948 use ftui_render::grapheme_pool::GraphemePool;
1949
1950 let area = Rect::new(0, 0, 8, 3);
1951 let mut pool = GraphemePool::new();
1952 let mut frame = Frame::new(8, 3, &mut pool);
1953
1954 Widget::render(
1955 &TextArea::new().with_text("abcdef\nghijkl"),
1956 area,
1957 &mut frame,
1958 );
1959 Widget::render(&TextArea::new().with_text("hi"), area, &mut frame);
1960
1961 assert_eq!(raw_row_text(&frame, 0, 8), "hi ");
1962 assert_eq!(raw_row_text(&frame, 1, 8), " ");
1963 assert_eq!(raw_row_text(&frame, 2, 8), " ");
1964 }
1965
1966 #[test]
1967 fn render_line_numbers_without_styling() {
1968 use ftui_render::budget::DegradationLevel;
1969 use ftui_render::grapheme_pool::GraphemePool;
1970
1971 let ta = TextArea::new().with_text("a\nb").with_line_numbers(true);
1972 let mut pool = GraphemePool::new();
1973 let mut frame = Frame::new(8, 2, &mut pool);
1974 frame.set_degradation(DegradationLevel::NoStyling);
1975
1976 Widget::render(&ta, Rect::new(0, 0, 8, 2), &mut frame);
1977
1978 let cell = frame.buffer.get(0, 0).unwrap();
1979 assert_eq!(cell.content.as_char(), Some('1'));
1980 }
1981
1982 #[test]
1983 fn stateful_render_updates_viewport_state() {
1984 use ftui_render::grapheme_pool::GraphemePool;
1985
1986 let ta = TextArea::new();
1987 let mut state = TextAreaState::default();
1988 let mut pool = GraphemePool::new();
1989 let mut frame = Frame::new(10, 3, &mut pool);
1990 let area = Rect::new(0, 0, 10, 3);
1991
1992 StatefulWidget::render(&ta, area, &mut frame, &mut state);
1993
1994 assert_eq!(state.last_viewport_height, 3);
1995 assert_eq!(state.last_viewport_width, 10);
1996 }
1997
1998 #[test]
1999 fn render_zero_area_no_panic() {
2000 let ta = TextArea::new().with_text("test");
2001 use ftui_render::grapheme_pool::GraphemePool;
2002 let mut pool = GraphemePool::new();
2003 let mut frame = Frame::new(10, 10, &mut pool);
2004 Widget::render(&ta, Rect::new(0, 0, 0, 0), &mut frame);
2005 }
2006
2007 #[test]
2008 fn is_essential() {
2009 let ta = TextArea::new();
2010 assert!(Widget::is_essential(&ta));
2011 }
2012
2013 #[test]
2014 fn default_impl() {
2015 let ta = TextArea::default();
2016 assert!(ta.is_empty());
2017 }
2018
2019 #[test]
2020 fn insert_newline_splits_line() {
2021 let mut ta = TextArea::new().with_text("abcdef");
2022 ta.move_to_document_start();
2023 ta.move_right();
2024 ta.move_right();
2025 ta.move_right();
2026 ta.insert_newline();
2027 assert_eq!(ta.line_count(), 2);
2028 assert_eq!(ta.cursor().line, 1);
2029 }
2030
2031 #[test]
2032 fn unicode_grapheme_cluster() {
2033 let mut ta = TextArea::new();
2034 ta.insert_text("café");
2035 assert_eq!(ta.text(), "café");
2037 }
2038
2039 mod proptests {
2040 use super::*;
2041 use proptest::prelude::*;
2042
2043 proptest! {
2044 #[test]
2045 fn insert_delete_inverse(text in "[a-zA-Z0-9 ]{1,50}") {
2046 let mut ta = TextArea::new();
2047 ta.insert_text(&text);
2048 for _ in 0..text.len() {
2050 ta.delete_backward();
2051 }
2052 prop_assert!(ta.is_empty() || ta.text().is_empty());
2053 }
2054
2055 #[test]
2056 fn undo_redo_inverse(text in "[a-zA-Z0-9]{1,30}") {
2057 let mut ta = TextArea::new();
2058 ta.insert_text(&text);
2059 let after_insert = ta.text();
2060 ta.undo();
2061 ta.redo();
2062 prop_assert_eq!(ta.text(), after_insert);
2063 }
2064
2065 #[test]
2066 fn cursor_always_valid(ops in proptest::collection::vec(0u8..10, 1..20)) {
2067 let mut ta = TextArea::new().with_text("abc\ndef\nghi\njkl");
2068 for op in ops {
2069 match op {
2070 0 => ta.move_left(),
2071 1 => ta.move_right(),
2072 2 => ta.move_up(),
2073 3 => ta.move_down(),
2074 4 => ta.move_to_line_start(),
2075 5 => ta.move_to_line_end(),
2076 6 => ta.move_to_document_start(),
2077 7 => ta.move_to_document_end(),
2078 8 => ta.move_word_left(),
2079 _ => ta.move_word_right(),
2080 }
2081 let cursor = ta.cursor();
2082 prop_assert!(cursor.line < ta.line_count(),
2083 "cursor line {} >= line_count {}", cursor.line, ta.line_count());
2084 }
2085 }
2086
2087 #[test]
2088 fn selection_ordered(n in 1usize..20) {
2089 let mut ta = TextArea::new().with_text("hello world foo bar");
2090 ta.move_to_document_start();
2091 for _ in 0..n {
2092 ta.select_right();
2093 }
2094 if let Some(sel) = ta.selection() {
2095 prop_assert!(sel.anchor.line <= sel.head.line
2097 || (sel.anchor.line == sel.head.line
2098 && sel.anchor.grapheme <= sel.head.grapheme));
2099 }
2100 }
2101 }
2102 }
2103}