1use ratatui::{
29 Frame,
30 layout::Rect,
31 style::{Color, Style},
32 text::{Line, Span},
33 widgets::{Block, Borders, Paragraph},
34};
35
36use crate::traits::{ClickRegion, FocusId};
37
38fn char_to_byte_index(s: &str, char_idx: usize) -> usize {
40 s.char_indices()
41 .nth(char_idx)
42 .map(|(i, _)| i)
43 .unwrap_or(s.len())
44}
45
46fn char_at(s: &str, index: usize) -> Option<char> {
48 s.chars().nth(index)
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum TextAreaAction {
54 Focus,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum TabConfig {
61 Spaces(usize),
63 Literal,
65}
66
67impl Default for TabConfig {
68 fn default() -> Self {
69 TabConfig::Spaces(4)
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
75pub enum WrapMode {
76 #[default]
78 None,
79 Soft,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85pub enum CursorMode {
86 #[default]
88 Block,
89 Terminal,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
95pub enum ScrollMode {
96 #[default]
98 Minimal,
99 CenterTracking,
101}
102
103pub struct TextAreaRender {
105 pub click_region: ClickRegion<TextAreaAction>,
107 pub cursor_position: Option<(u16, u16)>,
109}
110
111#[derive(Debug, Clone)]
113pub struct TextAreaState {
114 pub lines: Vec<String>,
116 pub cursor_line: usize,
118 pub cursor_col: usize,
120 pub scroll_y: usize,
122 pub scroll_x: usize,
124 pub visible_height: usize,
126 pub focused: bool,
128 pub enabled: bool,
130 pub tab_config: TabConfig,
132}
133
134impl Default for TextAreaState {
135 fn default() -> Self {
136 Self {
137 lines: vec![String::new()],
138 cursor_line: 0,
139 cursor_col: 0,
140 scroll_y: 0,
141 scroll_x: 0,
142 visible_height: 0,
143 focused: false,
144 enabled: true,
145 tab_config: TabConfig::default(),
146 }
147 }
148}
149
150impl TextAreaState {
151 pub fn new(text: impl Into<String>) -> Self {
155 let text = text.into();
156 let lines: Vec<String> = if text.is_empty() {
157 vec![String::new()]
158 } else {
159 text.lines().map(|s| s.to_string()).collect()
160 };
161 let lines = if lines.is_empty() {
163 vec![String::new()]
164 } else {
165 lines
166 };
167
168 Self {
169 lines,
170 cursor_line: 0,
171 cursor_col: 0,
172 scroll_y: 0,
173 scroll_x: 0,
174 visible_height: 0,
175 focused: false,
176 enabled: true,
177 tab_config: TabConfig::default(),
178 }
179 }
180
181 pub fn empty() -> Self {
183 Self::default()
184 }
185
186 pub fn with_tab_config(mut self, config: TabConfig) -> Self {
188 self.tab_config = config;
189 self
190 }
191
192 pub fn insert_char(&mut self, c: char) {
198 if !self.enabled {
199 return;
200 }
201 let byte_pos = char_to_byte_index(&self.lines[self.cursor_line], self.cursor_col);
202 self.lines[self.cursor_line].insert(byte_pos, c);
203 self.cursor_col += 1;
204 }
205
206 pub fn insert_str(&mut self, s: &str) {
208 if !self.enabled {
209 return;
210 }
211 for c in s.chars() {
212 if c == '\n' {
213 self.insert_newline();
214 } else if c != '\r' {
215 self.insert_char(c);
216 }
217 }
218 }
219
220 pub fn insert_newline(&mut self) {
222 if !self.enabled {
223 return;
224 }
225
226 let byte_pos = char_to_byte_index(&self.lines[self.cursor_line], self.cursor_col);
227
228 let rest = self.lines[self.cursor_line][byte_pos..].to_string();
230 self.lines[self.cursor_line].truncate(byte_pos);
231
232 self.cursor_line += 1;
234 self.lines.insert(self.cursor_line, rest);
235 self.cursor_col = 0;
236
237 self.ensure_cursor_visible();
238 }
239
240 pub fn insert_tab(&mut self) {
242 if !self.enabled {
243 return;
244 }
245 match self.tab_config {
246 TabConfig::Spaces(n) => {
247 for _ in 0..n {
248 self.insert_char(' ');
249 }
250 }
251 TabConfig::Literal => {
252 self.insert_char('\t');
253 }
254 }
255 }
256
257 pub fn delete_char_backward(&mut self) -> bool {
266 if !self.enabled {
267 return false;
268 }
269
270 if self.cursor_col > 0 {
271 self.cursor_col -= 1;
273 let byte_pos = char_to_byte_index(&self.lines[self.cursor_line], self.cursor_col);
274 if let Some(c) = self.lines[self.cursor_line][byte_pos..].chars().next() {
275 self.lines[self.cursor_line].replace_range(byte_pos..byte_pos + c.len_utf8(), "");
276 return true;
277 }
278 } else if self.cursor_line > 0 {
279 let current_line = self.lines.remove(self.cursor_line);
281 self.cursor_line -= 1;
282 self.cursor_col = self.lines[self.cursor_line].chars().count();
283 self.lines[self.cursor_line].push_str(¤t_line);
284 self.ensure_cursor_visible();
285 return true;
286 }
287 false
288 }
289
290 pub fn delete_char_forward(&mut self) -> bool {
295 if !self.enabled {
296 return false;
297 }
298
299 let line_len = self.lines[self.cursor_line].chars().count();
300
301 if self.cursor_col < line_len {
302 let byte_pos = char_to_byte_index(&self.lines[self.cursor_line], self.cursor_col);
304 if let Some(c) = self.lines[self.cursor_line][byte_pos..].chars().next() {
305 self.lines[self.cursor_line].replace_range(byte_pos..byte_pos + c.len_utf8(), "");
306 return true;
307 }
308 } else if self.cursor_line + 1 < self.lines.len() {
309 let next_line = self.lines.remove(self.cursor_line + 1);
311 self.lines[self.cursor_line].push_str(&next_line);
312 return true;
313 }
314 false
315 }
316
317 pub fn delete_word_backward(&mut self) -> bool {
321 if !self.enabled || (self.cursor_col == 0 && self.cursor_line == 0) {
322 return false;
323 }
324
325 if self.cursor_col == 0 {
327 return self.delete_char_backward();
328 }
329
330 let start_col = self.cursor_col;
331 let line = &self.lines[self.cursor_line];
332
333 while self.cursor_col > 0 {
335 if let Some(c) = char_at(line, self.cursor_col - 1) {
336 if c.is_whitespace() {
337 self.cursor_col -= 1;
338 } else {
339 break;
340 }
341 } else {
342 break;
343 }
344 }
345
346 while self.cursor_col > 0 {
348 if let Some(c) = char_at(&self.lines[self.cursor_line], self.cursor_col - 1) {
349 if !c.is_whitespace() {
350 self.delete_char_backward();
351 } else {
352 break;
353 }
354 } else {
355 break;
356 }
357 }
358
359 start_col != self.cursor_col
360 }
361
362 pub fn delete_word_forward(&mut self) -> bool {
366 if !self.enabled {
367 return false;
368 }
369
370 let line_len = self.lines[self.cursor_line].chars().count();
371
372 if self.cursor_col >= line_len {
374 if self.cursor_line + 1 < self.lines.len() {
375 return self.delete_char_forward();
376 }
377 return false;
378 }
379
380 let start_col = self.cursor_col;
381
382 while self.cursor_col < self.lines[self.cursor_line].chars().count() {
384 if let Some(c) = char_at(&self.lines[self.cursor_line], self.cursor_col) {
385 if !c.is_whitespace() {
386 self.delete_char_forward();
387 } else {
388 break;
389 }
390 } else {
391 break;
392 }
393 }
394
395 while self.cursor_col < self.lines[self.cursor_line].chars().count() {
397 if let Some(c) = char_at(&self.lines[self.cursor_line], self.cursor_col) {
398 if c.is_whitespace() {
399 self.delete_char_forward();
400 } else {
401 break;
402 }
403 } else {
404 break;
405 }
406 }
407
408 start_col != self.cursor_col || self.lines[self.cursor_line].chars().count() < line_len
409 }
410
411 pub fn delete_line(&mut self) {
415 if !self.enabled {
416 return;
417 }
418
419 if self.lines.len() == 1 {
420 self.lines[0].clear();
421 self.cursor_col = 0;
422 } else {
423 self.lines.remove(self.cursor_line);
424 if self.cursor_line >= self.lines.len() {
425 self.cursor_line = self.lines.len().saturating_sub(1);
426 }
427 let new_line_len = self.lines[self.cursor_line].chars().count();
429 self.cursor_col = self.cursor_col.min(new_line_len);
430 }
431 self.ensure_cursor_visible();
432 }
433
434 pub fn delete_to_line_start(&mut self) {
436 if !self.enabled || self.cursor_col == 0 {
437 return;
438 }
439
440 let line = &self.lines[self.cursor_line];
441 let byte_pos = char_to_byte_index(line, self.cursor_col);
442 self.lines[self.cursor_line] = line[byte_pos..].to_string();
443 self.cursor_col = 0;
444 }
445
446 pub fn delete_to_line_end(&mut self) {
448 if !self.enabled {
449 return;
450 }
451
452 let line = &self.lines[self.cursor_line];
453 let byte_pos = char_to_byte_index(line, self.cursor_col);
454 self.lines[self.cursor_line] = line[..byte_pos].to_string();
455 }
456
457 pub fn move_left(&mut self) {
465 if self.cursor_col > 0 {
466 self.cursor_col -= 1;
467 } else if self.cursor_line > 0 {
468 self.cursor_line -= 1;
469 self.cursor_col = self.lines[self.cursor_line].chars().count();
470 self.ensure_cursor_visible();
471 }
472 }
473
474 pub fn move_right(&mut self) {
478 let line_len = self.lines[self.cursor_line].chars().count();
479 if self.cursor_col < line_len {
480 self.cursor_col += 1;
481 } else if self.cursor_line + 1 < self.lines.len() {
482 self.cursor_line += 1;
483 self.cursor_col = 0;
484 self.ensure_cursor_visible();
485 }
486 }
487
488 pub fn move_line_start(&mut self) {
490 self.cursor_col = 0;
491 }
492
493 pub fn move_line_end(&mut self) {
495 self.cursor_col = self.lines[self.cursor_line].chars().count();
496 }
497
498 pub fn move_word_left(&mut self) {
500 if self.cursor_col == 0 {
501 if self.cursor_line > 0 {
502 self.cursor_line -= 1;
503 self.cursor_col = self.lines[self.cursor_line].chars().count();
504 self.ensure_cursor_visible();
505 }
506 return;
507 }
508
509 let line = &self.lines[self.cursor_line];
510
511 while self.cursor_col > 0 {
513 if let Some(c) = char_at(line, self.cursor_col - 1) {
514 if c.is_whitespace() {
515 self.cursor_col -= 1;
516 } else {
517 break;
518 }
519 } else {
520 break;
521 }
522 }
523
524 while self.cursor_col > 0 {
526 if let Some(c) = char_at(line, self.cursor_col - 1) {
527 if !c.is_whitespace() {
528 self.cursor_col -= 1;
529 } else {
530 break;
531 }
532 } else {
533 break;
534 }
535 }
536 }
537
538 pub fn move_word_right(&mut self) {
540 let line = &self.lines[self.cursor_line];
541 let line_len = line.chars().count();
542
543 if self.cursor_col >= line_len {
544 if self.cursor_line + 1 < self.lines.len() {
545 self.cursor_line += 1;
546 self.cursor_col = 0;
547 self.ensure_cursor_visible();
548 }
549 return;
550 }
551
552 while self.cursor_col < line_len {
554 if let Some(c) = char_at(&self.lines[self.cursor_line], self.cursor_col) {
555 if !c.is_whitespace() {
556 self.cursor_col += 1;
557 } else {
558 break;
559 }
560 } else {
561 break;
562 }
563 }
564
565 let line_len = self.lines[self.cursor_line].chars().count();
567 while self.cursor_col < line_len {
568 if let Some(c) = char_at(&self.lines[self.cursor_line], self.cursor_col) {
569 if c.is_whitespace() {
570 self.cursor_col += 1;
571 } else {
572 break;
573 }
574 } else {
575 break;
576 }
577 }
578 }
579
580 pub fn move_up(&mut self) {
586 if self.cursor_line > 0 {
587 self.cursor_line -= 1;
588 let new_line_len = self.lines[self.cursor_line].chars().count();
590 self.cursor_col = self.cursor_col.min(new_line_len);
591 self.ensure_cursor_visible();
592 }
593 }
594
595 pub fn move_down(&mut self) {
597 if self.cursor_line + 1 < self.lines.len() {
598 self.cursor_line += 1;
599 let new_line_len = self.lines[self.cursor_line].chars().count();
601 self.cursor_col = self.cursor_col.min(new_line_len);
602 self.ensure_cursor_visible();
603 }
604 }
605
606 pub fn move_page_up(&mut self) {
608 let page_size = self.visible_height.max(1);
609 if self.cursor_line >= page_size {
610 self.cursor_line -= page_size;
611 } else {
612 self.cursor_line = 0;
613 }
614 let new_line_len = self.lines[self.cursor_line].chars().count();
616 self.cursor_col = self.cursor_col.min(new_line_len);
617 self.ensure_cursor_visible();
618 }
619
620 pub fn move_page_down(&mut self) {
622 let page_size = self.visible_height.max(1);
623 let max_line = self.lines.len().saturating_sub(1);
624 self.cursor_line = (self.cursor_line + page_size).min(max_line);
625 let new_line_len = self.lines[self.cursor_line].chars().count();
627 self.cursor_col = self.cursor_col.min(new_line_len);
628 self.ensure_cursor_visible();
629 }
630
631 pub fn move_to_start(&mut self) {
633 self.cursor_line = 0;
634 self.cursor_col = 0;
635 self.ensure_cursor_visible();
636 }
637
638 pub fn move_to_end(&mut self) {
640 self.cursor_line = self.lines.len().saturating_sub(1);
641 self.cursor_col = self.lines[self.cursor_line].chars().count();
642 self.ensure_cursor_visible();
643 }
644
645 pub fn scroll_to_cursor(&mut self) {
651 if self.cursor_line < self.scroll_y {
653 self.scroll_y = self.cursor_line;
654 } else if self.visible_height > 0 && self.cursor_line >= self.scroll_y + self.visible_height
655 {
656 self.scroll_y = self.cursor_line - self.visible_height + 1;
657 }
658 }
659
660 pub fn ensure_cursor_visible(&mut self) {
662 self.scroll_to_cursor();
663 }
664
665 pub fn scroll_up(&mut self) {
667 self.scroll_y = self.scroll_y.saturating_sub(1);
668 }
669
670 pub fn scroll_down(&mut self) {
672 let max_scroll = self.lines.len().saturating_sub(self.visible_height.max(1));
673 if self.scroll_y < max_scroll {
674 self.scroll_y += 1;
675 }
676 }
677
678 pub fn scroll_left(&mut self) {
680 self.scroll_x = self.scroll_x.saturating_sub(4);
681 }
682
683 pub fn scroll_right(&mut self) {
685 self.scroll_x += 4;
686 }
687
688 pub fn text(&self) -> String {
694 self.lines.join("\n")
695 }
696
697 pub fn set_text(&mut self, text: impl Into<String>) {
701 let text = text.into();
702 self.lines = if text.is_empty() {
703 vec![String::new()]
704 } else {
705 text.lines().map(|s| s.to_string()).collect()
706 };
707 if self.lines.is_empty() {
708 self.lines.push(String::new());
709 }
710 self.cursor_line = self.lines.len().saturating_sub(1);
711 self.cursor_col = self.lines[self.cursor_line].chars().count();
712 self.scroll_y = 0;
713 self.scroll_x = 0;
714 }
715
716 pub fn clear(&mut self) {
718 self.lines = vec![String::new()];
719 self.cursor_line = 0;
720 self.cursor_col = 0;
721 self.scroll_y = 0;
722 self.scroll_x = 0;
723 }
724
725 pub fn line_count(&self) -> usize {
727 self.lines.len()
728 }
729
730 pub fn visual_line_count(&self, content_width: usize) -> usize {
736 if content_width == 0 {
737 return self.lines.len();
738 }
739 self.lines
740 .iter()
741 .map(|line| {
742 let char_count = line.chars().count();
743 if char_count == 0 {
744 1
745 } else {
746 (char_count + content_width - 1) / content_width
747 }
748 })
749 .sum::<usize>()
750 .max(1)
751 }
752
753 pub fn current_line(&self) -> &str {
755 &self.lines[self.cursor_line]
756 }
757
758 pub fn is_empty(&self) -> bool {
760 self.lines.len() == 1 && self.lines[0].is_empty()
761 }
762
763 pub fn len(&self) -> usize {
765 let line_chars: usize = self.lines.iter().map(|l| l.chars().count()).sum();
766 let newlines = self.lines.len().saturating_sub(1);
767 line_chars + newlines
768 }
769
770 pub fn text_before_cursor(&self) -> &str {
772 let line = &self.lines[self.cursor_line];
773 let byte_pos = char_to_byte_index(line, self.cursor_col);
774 &line[..byte_pos]
775 }
776
777 pub fn text_after_cursor(&self) -> &str {
779 let line = &self.lines[self.cursor_line];
780 let byte_pos = char_to_byte_index(line, self.cursor_col);
781 &line[byte_pos..]
782 }
783}
784
785#[derive(Debug, Clone)]
787pub struct TextAreaStyle {
788 pub focused_border: Color,
790 pub unfocused_border: Color,
792 pub disabled_border: Color,
794 pub text_fg: Color,
796 pub cursor_fg: Color,
798 pub placeholder_fg: Color,
800 pub line_number_fg: Color,
802 pub current_line_bg: Option<Color>,
804 pub show_line_numbers: bool,
806 pub cursor_mode: CursorMode,
808 pub scroll_mode: ScrollMode,
810}
811
812impl Default for TextAreaStyle {
813 fn default() -> Self {
814 Self {
815 focused_border: Color::Yellow,
816 unfocused_border: Color::Gray,
817 disabled_border: Color::DarkGray,
818 text_fg: Color::White,
819 cursor_fg: Color::Yellow,
820 placeholder_fg: Color::DarkGray,
821 line_number_fg: Color::DarkGray,
822 current_line_bg: None,
823 show_line_numbers: false,
824 cursor_mode: CursorMode::default(),
825 scroll_mode: ScrollMode::default(),
826 }
827 }
828}
829
830impl From<&crate::theme::Theme> for TextAreaStyle {
831 fn from(theme: &crate::theme::Theme) -> Self {
832 let p = &theme.palette;
833 Self {
834 focused_border: p.border_focused,
835 unfocused_border: p.border,
836 disabled_border: p.border_disabled,
837 text_fg: p.text,
838 cursor_fg: p.primary,
839 placeholder_fg: p.text_placeholder,
840 line_number_fg: p.text_disabled,
841 current_line_bg: None,
842 show_line_numbers: false,
843 cursor_mode: CursorMode::default(),
844 scroll_mode: ScrollMode::default(),
845 }
846 }
847}
848
849impl TextAreaStyle {
850 pub fn focused_border(mut self, color: Color) -> Self {
852 self.focused_border = color;
853 self
854 }
855
856 pub fn unfocused_border(mut self, color: Color) -> Self {
858 self.unfocused_border = color;
859 self
860 }
861
862 pub fn disabled_border(mut self, color: Color) -> Self {
864 self.disabled_border = color;
865 self
866 }
867
868 pub fn text_fg(mut self, color: Color) -> Self {
870 self.text_fg = color;
871 self
872 }
873
874 pub fn cursor_fg(mut self, color: Color) -> Self {
876 self.cursor_fg = color;
877 self
878 }
879
880 pub fn placeholder_fg(mut self, color: Color) -> Self {
882 self.placeholder_fg = color;
883 self
884 }
885
886 pub fn line_number_fg(mut self, color: Color) -> Self {
888 self.line_number_fg = color;
889 self
890 }
891
892 pub fn current_line_bg(mut self, color: Option<Color>) -> Self {
894 self.current_line_bg = color;
895 self
896 }
897
898 pub fn show_line_numbers(mut self, show: bool) -> Self {
900 self.show_line_numbers = show;
901 self
902 }
903
904 pub fn cursor_mode(mut self, mode: CursorMode) -> Self {
906 self.cursor_mode = mode;
907 self
908 }
909
910 pub fn scroll_mode(mut self, mode: ScrollMode) -> Self {
912 self.scroll_mode = mode;
913 self
914 }
915}
916
917pub struct TextArea<'a> {
921 label: Option<&'a str>,
922 placeholder: Option<&'a str>,
923 style: TextAreaStyle,
924 focus_id: FocusId,
925 with_border: bool,
926 wrap_mode: WrapMode,
927 title: Option<Line<'a>>,
929 content_lines: Option<Vec<Line<'a>>>,
931 border_color_override: Option<Color>,
933}
934
935impl TextArea<'_> {
936 pub fn new() -> Self {
938 Self {
939 label: None,
940 placeholder: None,
941 style: TextAreaStyle::default(),
942 focus_id: FocusId::default(),
943 with_border: true,
944 wrap_mode: WrapMode::default(),
945 title: None,
946 content_lines: None,
947 border_color_override: None,
948 }
949 }
950}
951
952impl Default for TextArea<'_> {
953 fn default() -> Self {
954 Self::new()
955 }
956}
957
958impl<'a> TextArea<'a> {
959 pub fn label(mut self, label: &'a str) -> Self {
961 self.label = Some(label);
962 self
963 }
964
965 pub fn placeholder(mut self, placeholder: &'a str) -> Self {
967 self.placeholder = Some(placeholder);
968 self
969 }
970
971 pub fn style(mut self, style: TextAreaStyle) -> Self {
973 self.style = style;
974 self
975 }
976
977 pub fn theme(self, theme: &crate::theme::Theme) -> Self {
979 self.style(TextAreaStyle::from(theme))
980 }
981
982 pub fn focus_id(mut self, id: FocusId) -> Self {
984 self.focus_id = id;
985 self
986 }
987
988 pub fn with_border(mut self, with_border: bool) -> Self {
990 self.with_border = with_border;
991 self
992 }
993
994 pub fn wrap_mode(mut self, mode: WrapMode) -> Self {
996 self.wrap_mode = mode;
997 self
998 }
999
1000 pub fn title(mut self, title: Line<'a>) -> Self {
1002 self.title = Some(title);
1003 self
1004 }
1005
1006 pub fn content_lines(mut self, lines: Vec<Line<'a>>) -> Self {
1011 self.content_lines = Some(lines);
1012 self
1013 }
1014
1015 pub fn border_color(mut self, color: Color) -> Self {
1017 self.border_color_override = Some(color);
1018 self
1019 }
1020
1021 pub fn render_stateful(
1023 self,
1024 frame: &mut Frame,
1025 area: Rect,
1026 state: &mut TextAreaState,
1027 ) -> TextAreaRender {
1028 let border_color = if let Some(override_color) = self.border_color_override {
1029 override_color
1030 } else if !state.enabled {
1031 self.style.disabled_border
1032 } else if state.focused {
1033 self.style.focused_border
1034 } else {
1035 self.style.unfocused_border
1036 };
1037
1038 let block = if self.with_border {
1039 let mut block = Block::default()
1040 .borders(Borders::ALL)
1041 .border_style(Style::default().fg(border_color));
1042 if let Some(title) = self.title {
1043 block = block.title(title);
1044 } else if let Some(label) = self.label {
1045 block = block.title(format!(" {} ", label));
1046 }
1047 Some(block)
1048 } else {
1049 None
1050 };
1051
1052 let inner_area = if let Some(ref b) = block {
1053 b.inner(area)
1054 } else {
1055 area
1056 };
1057
1058 state.visible_height = inner_area.height as usize;
1060
1061 let line_num_width = if self.style.show_line_numbers {
1063 let max_line = state.lines.len();
1064 let digits = max_line.to_string().len();
1065 digits + 2 } else {
1067 0
1068 };
1069
1070 let content_width = (inner_area.width as usize).saturating_sub(line_num_width);
1072
1073 let use_terminal_cursor = self.style.cursor_mode == CursorMode::Terminal;
1074
1075 if state.is_empty() && !state.focused {
1077 if let Some(placeholder) = self.placeholder {
1078 let display_line = Line::from(Span::styled(
1079 placeholder,
1080 Style::default().fg(self.style.placeholder_fg),
1081 ));
1082 let paragraph = Paragraph::new(display_line);
1083
1084 if let Some(block) = block {
1085 frame.render_widget(block, area);
1086 }
1087 frame.render_widget(paragraph, inner_area);
1088 return TextAreaRender {
1089 click_region: ClickRegion::new(area, TextAreaAction::Focus),
1090 cursor_position: None,
1091 };
1092 }
1093 }
1094
1095 let mut display_lines: Vec<Line> = Vec::new();
1096 let mut cursor_screen_pos: Option<(u16, u16)> = None;
1097
1098 if self.wrap_mode == WrapMode::Soft && content_width > 0 {
1099 let mut visual_rows: Vec<(usize, usize)> = Vec::new();
1101 for (li, line) in state.lines.iter().enumerate() {
1102 let char_count = line.chars().count();
1103 if char_count == 0 {
1104 visual_rows.push((li, 0));
1105 } else {
1106 let mut col = 0;
1107 loop {
1108 visual_rows.push((li, col));
1109 col += content_width;
1110 if col >= char_count {
1111 break;
1112 }
1113 }
1114 }
1115 }
1116
1117 let total_visual_rows = visual_rows.len();
1118
1119 let cursor_visual_row = visual_rows
1121 .iter()
1122 .enumerate()
1123 .rev()
1124 .find(|(_, (li, vc))| {
1125 *li == state.cursor_line && state.cursor_col >= *vc
1126 })
1127 .map(|(i, _)| i)
1128 .unwrap_or(0);
1129
1130 let effective_scroll_vr =
1132 if self.style.scroll_mode == ScrollMode::CenterTracking && state.visible_height > 0
1133 {
1134 let half_height = state.visible_height / 2;
1135 if total_visual_rows <= state.visible_height
1136 || cursor_visual_row <= half_height
1137 {
1138 0
1139 } else if cursor_visual_row + half_height >= total_visual_rows {
1140 total_visual_rows.saturating_sub(state.visible_height)
1141 } else {
1142 cursor_visual_row.saturating_sub(half_height)
1143 }
1144 } else {
1145 visual_rows
1147 .iter()
1148 .position(|(li, _)| *li >= state.scroll_y)
1149 .unwrap_or(0)
1150 };
1151
1152 let start_vr = effective_scroll_vr;
1153 let end_vr = (start_vr + state.visible_height).min(total_visual_rows);
1154
1155 for (vr_offset, vr_idx) in (start_vr..end_vr).enumerate() {
1156 let (line_idx, start_col) = visual_rows[vr_idx];
1157 let is_cursor_line = line_idx == state.cursor_line;
1158 let display_row = vr_offset as u16;
1159
1160 let line = &state.lines[line_idx];
1161 let chars: Vec<char> = line.chars().collect();
1162 let visible_chars: String =
1163 chars.iter().skip(start_col).take(content_width).collect();
1164
1165 let mut spans = Vec::new();
1166
1167 if self.style.show_line_numbers {
1169 if start_col == 0 {
1170 let line_num = format!(
1171 "{:>width$} ",
1172 line_idx + 1,
1173 width = line_num_width.saturating_sub(2)
1174 );
1175 spans.push(Span::styled(
1176 line_num,
1177 Style::default().fg(self.style.line_number_fg),
1178 ));
1179 } else {
1180 spans.push(Span::raw(" ".repeat(line_num_width)));
1181 }
1182 }
1183
1184 let line_style = if is_cursor_line {
1185 if let Some(bg) = self.style.current_line_bg {
1186 Style::default().fg(self.style.text_fg).bg(bg)
1187 } else {
1188 Style::default().fg(self.style.text_fg)
1189 }
1190 } else {
1191 Style::default().fg(self.style.text_fg)
1192 };
1193
1194 let is_last_vr_for_line =
1197 vr_idx + 1 >= visual_rows.len() || visual_rows[vr_idx + 1].0 != line_idx;
1198 let cursor_on_this_vr = is_cursor_line
1199 && state.cursor_col >= start_col
1200 && (is_last_vr_for_line || state.cursor_col < start_col + content_width);
1201
1202 if cursor_on_this_vr && state.focused {
1203 let cursor_visible_col = state.cursor_col - start_col;
1204 let visible_char_count = visible_chars.chars().count();
1205
1206 if use_terminal_cursor {
1207 spans.push(Span::styled(visible_chars, line_style));
1208 let cx =
1209 inner_area.x + line_num_width as u16 + cursor_visible_col as u16;
1210 let cy = inner_area.y + display_row;
1211 if cx < inner_area.x + inner_area.width
1212 && cy < inner_area.y + inner_area.height
1213 {
1214 cursor_screen_pos = Some((cx, cy));
1215 }
1216 } else if cursor_visible_col <= visible_char_count {
1217 let before: String =
1218 visible_chars.chars().take(cursor_visible_col).collect();
1219 let cursor_char: String = visible_chars
1220 .chars()
1221 .skip(cursor_visible_col)
1222 .take(1)
1223 .collect();
1224 let after: String =
1225 visible_chars.chars().skip(cursor_visible_col + 1).collect();
1226
1227 if !before.is_empty() {
1228 spans.push(Span::styled(before, line_style));
1229 }
1230 let cursor_style = Style::default()
1231 .fg(self.style.cursor_fg)
1232 .bg(self.style.text_fg);
1233 let cursor_display =
1234 if cursor_char.is_empty() { " " } else { &cursor_char };
1235 spans.push(Span::styled(cursor_display.to_string(), cursor_style));
1236 if !after.is_empty() {
1237 spans.push(Span::styled(after, line_style));
1238 }
1239 } else {
1240 spans.push(Span::styled(visible_chars, line_style));
1241 }
1242 } else {
1243 spans.push(Span::styled(visible_chars, line_style));
1244 }
1245
1246 display_lines.push(Line::from(spans));
1247 }
1248 } else {
1249 let effective_scroll_y =
1251 if self.style.scroll_mode == ScrollMode::CenterTracking && state.visible_height > 0 {
1252 let total_lines = state.lines.len();
1254 let half_height = state.visible_height / 2;
1255 if total_lines <= state.visible_height || state.cursor_line <= half_height {
1256 0
1257 } else if state.cursor_line + half_height >= total_lines {
1258 total_lines.saturating_sub(state.visible_height)
1259 } else {
1260 state.cursor_line.saturating_sub(half_height)
1261 }
1262 } else {
1263 state.scroll_y
1264 };
1265
1266 let start_line = effective_scroll_y;
1268 let end_line = (start_line + state.visible_height).min(state.lines.len());
1269
1270 for line_idx in start_line..end_line {
1271 let is_cursor_line = line_idx == state.cursor_line;
1272 let display_row = (line_idx - start_line) as u16;
1273
1274 if let Some(ref content) = self.content_lines {
1276 if line_idx < content.len() {
1277 let mut spans = Vec::new();
1278
1279 if self.style.show_line_numbers {
1281 let line_num = format!(
1282 "{:>width$} ",
1283 line_idx + 1,
1284 width = line_num_width.saturating_sub(2)
1285 );
1286 spans.push(Span::styled(
1287 line_num,
1288 Style::default().fg(self.style.line_number_fg),
1289 ));
1290 }
1291
1292 spans.extend(content[line_idx].spans.iter().cloned());
1294 display_lines.push(Line::from(spans));
1295
1296 if is_cursor_line && state.focused && use_terminal_cursor {
1298 let cursor_visible_col = state.cursor_col.saturating_sub(state.scroll_x);
1299 let cx = inner_area.x + line_num_width as u16 + cursor_visible_col as u16;
1300 let cy = inner_area.y + display_row;
1301 if cx < inner_area.x + inner_area.width
1302 && cy < inner_area.y + inner_area.height
1303 {
1304 cursor_screen_pos = Some((cx, cy));
1305 }
1306 }
1307 continue;
1308 }
1309 }
1310
1311 let line = &state.lines[line_idx];
1312
1313 let chars: Vec<char> = line.chars().collect();
1315 let visible_chars: String = chars
1316 .iter()
1317 .skip(state.scroll_x)
1318 .take(content_width)
1319 .collect();
1320
1321 let mut spans = Vec::new();
1322
1323 if self.style.show_line_numbers {
1325 let line_num = format!(
1326 "{:>width$} ",
1327 line_idx + 1,
1328 width = line_num_width.saturating_sub(2)
1329 );
1330 spans.push(Span::styled(
1331 line_num,
1332 Style::default().fg(self.style.line_number_fg),
1333 ));
1334 }
1335
1336 let line_style = if is_cursor_line {
1338 if let Some(bg) = self.style.current_line_bg {
1339 Style::default().fg(self.style.text_fg).bg(bg)
1340 } else {
1341 Style::default().fg(self.style.text_fg)
1342 }
1343 } else {
1344 Style::default().fg(self.style.text_fg)
1345 };
1346
1347 if is_cursor_line && state.focused {
1349 let cursor_visible_col = state.cursor_col.saturating_sub(state.scroll_x);
1350 let visible_char_count = visible_chars.chars().count();
1351
1352 if use_terminal_cursor {
1353 spans.push(Span::styled(visible_chars, line_style));
1355 let cx = inner_area.x + line_num_width as u16 + cursor_visible_col as u16;
1356 let cy = inner_area.y + display_row;
1357 if cx < inner_area.x + inner_area.width && cy < inner_area.y + inner_area.height
1358 {
1359 cursor_screen_pos = Some((cx, cy));
1360 }
1361 } else if cursor_visible_col <= visible_char_count {
1362 let before: String = visible_chars.chars().take(cursor_visible_col).collect();
1364 let cursor_char: String = visible_chars
1365 .chars()
1366 .skip(cursor_visible_col)
1367 .take(1)
1368 .collect();
1369 let after: String =
1370 visible_chars.chars().skip(cursor_visible_col + 1).collect();
1371
1372 if !before.is_empty() {
1373 spans.push(Span::styled(before, line_style));
1374 }
1375
1376 let cursor_style = Style::default()
1377 .fg(self.style.cursor_fg)
1378 .bg(self.style.text_fg);
1379 let cursor_display = if cursor_char.is_empty() {
1380 " "
1381 } else {
1382 &cursor_char
1383 };
1384 spans.push(Span::styled(cursor_display.to_string(), cursor_style));
1385
1386 if !after.is_empty() {
1387 spans.push(Span::styled(after, line_style));
1388 }
1389 } else {
1390 spans.push(Span::styled(visible_chars, line_style));
1391 }
1392 } else {
1393 spans.push(Span::styled(visible_chars, line_style));
1394 }
1395
1396 display_lines.push(Line::from(spans));
1397 }
1398 } if display_lines.is_empty() && state.focused {
1402 let mut spans = Vec::new();
1403 if self.style.show_line_numbers {
1404 let line_num = format!("{:>width$} ", 1, width = line_num_width.saturating_sub(2));
1405 spans.push(Span::styled(
1406 line_num,
1407 Style::default().fg(self.style.line_number_fg),
1408 ));
1409 }
1410 if use_terminal_cursor {
1411 spans.push(Span::styled(" ", Style::default().fg(self.style.text_fg)));
1412 let cx = inner_area.x + line_num_width as u16;
1413 let cy = inner_area.y;
1414 cursor_screen_pos = Some((cx, cy));
1415 } else {
1416 let cursor_style = Style::default()
1417 .fg(self.style.cursor_fg)
1418 .bg(self.style.text_fg);
1419 spans.push(Span::styled(" ", cursor_style));
1420 }
1421 display_lines.push(Line::from(spans));
1422 }
1423
1424 let paragraph = Paragraph::new(display_lines);
1425
1426 if let Some(block) = block {
1427 frame.render_widget(block, area);
1428 }
1429 frame.render_widget(paragraph, inner_area);
1430
1431 TextAreaRender {
1432 click_region: ClickRegion::new(area, TextAreaAction::Focus),
1433 cursor_position: cursor_screen_pos,
1434 }
1435 }
1436}
1437
1438#[cfg(test)]
1439mod tests {
1440 use super::*;
1441
1442 #[test]
1447 fn test_state_default() {
1448 let state = TextAreaState::default();
1449 assert_eq!(state.lines.len(), 1);
1450 assert!(state.lines[0].is_empty());
1451 assert_eq!(state.cursor_line, 0);
1452 assert_eq!(state.cursor_col, 0);
1453 assert!(!state.focused);
1454 assert!(state.enabled);
1455 }
1456
1457 #[test]
1458 fn test_state_new_single_line() {
1459 let state = TextAreaState::new("Hello");
1460 assert_eq!(state.lines.len(), 1);
1461 assert_eq!(state.lines[0], "Hello");
1462 assert_eq!(state.cursor_line, 0);
1463 assert_eq!(state.cursor_col, 0);
1464 }
1465
1466 #[test]
1467 fn test_state_new_multi_line() {
1468 let state = TextAreaState::new("Hello\nWorld");
1469 assert_eq!(state.lines.len(), 2);
1470 assert_eq!(state.lines[0], "Hello");
1471 assert_eq!(state.lines[1], "World");
1472 assert_eq!(state.cursor_line, 0);
1473 assert_eq!(state.cursor_col, 0);
1474 }
1475
1476 #[test]
1477 fn test_state_new_empty() {
1478 let state = TextAreaState::new("");
1479 assert_eq!(state.lines.len(), 1);
1480 assert!(state.lines[0].is_empty());
1481 assert_eq!(state.cursor_line, 0);
1482 assert_eq!(state.cursor_col, 0);
1483 }
1484
1485 #[test]
1486 fn test_state_empty() {
1487 let state = TextAreaState::empty();
1488 assert!(state.is_empty());
1489 }
1490
1491 #[test]
1496 fn test_insert_char() {
1497 let mut state = TextAreaState::new("Hello");
1498 state.move_to_end();
1499 state.insert_char('!');
1500 assert_eq!(state.lines[0], "Hello!");
1501 assert_eq!(state.cursor_col, 6);
1502 }
1503
1504 #[test]
1505 fn test_insert_char_middle() {
1506 let mut state = TextAreaState::new("Hllo");
1507 state.cursor_col = 1;
1508 state.insert_char('e');
1509 assert_eq!(state.lines[0], "Hello");
1510 assert_eq!(state.cursor_col, 2);
1511 }
1512
1513 #[test]
1514 fn test_insert_str_single_line() {
1515 let mut state = TextAreaState::new("Hello");
1516 state.move_to_end();
1517 state.insert_str(" World");
1518 assert_eq!(state.lines[0], "Hello World");
1519 }
1520
1521 #[test]
1522 fn test_insert_str_multi_line() {
1523 let mut state = TextAreaState::new("Hello");
1524 state.move_to_end();
1525 state.insert_str(" World\nNew Line");
1526 assert_eq!(state.lines.len(), 2);
1527 assert_eq!(state.lines[0], "Hello World");
1528 assert_eq!(state.lines[1], "New Line");
1529 }
1530
1531 #[test]
1532 fn test_insert_newline() {
1533 let mut state = TextAreaState::new("HelloWorld");
1534 state.cursor_col = 5;
1535 state.insert_newline();
1536 assert_eq!(state.lines.len(), 2);
1537 assert_eq!(state.lines[0], "Hello");
1538 assert_eq!(state.lines[1], "World");
1539 assert_eq!(state.cursor_line, 1);
1540 assert_eq!(state.cursor_col, 0);
1541 }
1542
1543 #[test]
1544 fn test_insert_newline_at_start() {
1545 let mut state = TextAreaState::new("Hello");
1546 state.insert_newline();
1547 assert_eq!(state.lines.len(), 2);
1548 assert_eq!(state.lines[0], "");
1549 assert_eq!(state.lines[1], "Hello");
1550 }
1551
1552 #[test]
1553 fn test_insert_newline_at_end() {
1554 let mut state = TextAreaState::new("Hello");
1555 state.move_to_end();
1556 state.insert_newline();
1557 assert_eq!(state.lines.len(), 2);
1558 assert_eq!(state.lines[0], "Hello");
1559 assert_eq!(state.lines[1], "");
1560 }
1561
1562 #[test]
1563 fn test_insert_tab_spaces() {
1564 let mut state = TextAreaState::empty();
1565 state.tab_config = TabConfig::Spaces(4);
1566 state.insert_tab();
1567 assert_eq!(state.lines[0], " ");
1568 }
1569
1570 #[test]
1571 fn test_insert_tab_literal() {
1572 let mut state = TextAreaState::empty();
1573 state.tab_config = TabConfig::Literal;
1574 state.insert_tab();
1575 assert_eq!(state.lines[0], "\t");
1576 }
1577
1578 #[test]
1583 fn test_delete_char_backward() {
1584 let mut state = TextAreaState::new("Hello");
1585 state.move_to_end();
1586 assert!(state.delete_char_backward());
1587 assert_eq!(state.lines[0], "Hell");
1588 assert_eq!(state.cursor_col, 4);
1589 }
1590
1591 #[test]
1592 fn test_delete_char_backward_at_start() {
1593 let mut state = TextAreaState::new("Hello");
1594 assert!(!state.delete_char_backward());
1596 assert_eq!(state.lines[0], "Hello");
1597 }
1598
1599 #[test]
1600 fn test_delete_char_backward_merges_lines() {
1601 let mut state = TextAreaState::new("Hello\nWorld");
1602 state.cursor_line = 1;
1603 state.cursor_col = 0;
1604 assert!(state.delete_char_backward());
1605 assert_eq!(state.lines.len(), 1);
1606 assert_eq!(state.lines[0], "HelloWorld");
1607 assert_eq!(state.cursor_col, 5);
1608 }
1609
1610 #[test]
1611 fn test_delete_char_forward() {
1612 let mut state = TextAreaState::new("Hello");
1613 state.cursor_col = 0;
1614 assert!(state.delete_char_forward());
1615 assert_eq!(state.lines[0], "ello");
1616 }
1617
1618 #[test]
1619 fn test_delete_char_forward_at_end() {
1620 let mut state = TextAreaState::new("Hello");
1621 state.move_to_end();
1622 assert!(!state.delete_char_forward());
1623 assert_eq!(state.lines[0], "Hello");
1624 }
1625
1626 #[test]
1627 fn test_delete_char_forward_merges_lines() {
1628 let mut state = TextAreaState::new("Hello\nWorld");
1629 state.cursor_line = 0;
1630 state.cursor_col = 5;
1631 assert!(state.delete_char_forward());
1632 assert_eq!(state.lines.len(), 1);
1633 assert_eq!(state.lines[0], "HelloWorld");
1634 }
1635
1636 #[test]
1637 fn test_delete_word_backward() {
1638 let mut state = TextAreaState::new("Hello World");
1639 state.move_to_end();
1640 assert!(state.delete_word_backward());
1641 assert_eq!(state.lines[0], "Hello ");
1642 }
1643
1644 #[test]
1645 fn test_delete_word_backward_from_start() {
1646 let mut state = TextAreaState::new("Hello");
1647 assert!(!state.delete_word_backward());
1649 }
1650
1651 #[test]
1652 fn test_delete_line() {
1653 let mut state = TextAreaState::new("Line 1\nLine 2\nLine 3");
1654 state.cursor_line = 1;
1655 state.cursor_col = 0;
1656 state.delete_line();
1657 assert_eq!(state.lines.len(), 2);
1658 assert_eq!(state.lines[0], "Line 1");
1659 assert_eq!(state.lines[1], "Line 3");
1660 }
1661
1662 #[test]
1663 fn test_delete_line_single() {
1664 let mut state = TextAreaState::new("Hello");
1665 state.delete_line();
1666 assert_eq!(state.lines.len(), 1);
1667 assert!(state.lines[0].is_empty());
1668 }
1669
1670 #[test]
1671 fn test_delete_to_line_start() {
1672 let mut state = TextAreaState::new("Hello World");
1673 state.cursor_col = 6;
1674 state.delete_to_line_start();
1675 assert_eq!(state.lines[0], "World");
1676 assert_eq!(state.cursor_col, 0);
1677 }
1678
1679 #[test]
1680 fn test_delete_to_line_end() {
1681 let mut state = TextAreaState::new("Hello World");
1682 state.cursor_col = 5;
1683 state.delete_to_line_end();
1684 assert_eq!(state.lines[0], "Hello");
1685 }
1686
1687 #[test]
1692 fn test_move_left() {
1693 let mut state = TextAreaState::new("Hello");
1694 state.move_to_end();
1695 state.move_left();
1696 assert_eq!(state.cursor_col, 4);
1697 }
1698
1699 #[test]
1700 fn test_move_left_wraps_line() {
1701 let mut state = TextAreaState::new("Hello\nWorld");
1702 state.cursor_line = 1;
1703 state.cursor_col = 0;
1704 state.move_left();
1705 assert_eq!(state.cursor_line, 0);
1706 assert_eq!(state.cursor_col, 5);
1707 }
1708
1709 #[test]
1710 fn test_move_left_at_start() {
1711 let mut state = TextAreaState::new("Hello");
1712 state.cursor_col = 0;
1713 state.move_left();
1714 assert_eq!(state.cursor_col, 0);
1715 }
1716
1717 #[test]
1718 fn test_move_right() {
1719 let mut state = TextAreaState::new("Hello");
1720 state.cursor_col = 0;
1721 state.move_right();
1722 assert_eq!(state.cursor_col, 1);
1723 }
1724
1725 #[test]
1726 fn test_move_right_wraps_line() {
1727 let mut state = TextAreaState::new("Hello\nWorld");
1728 state.cursor_line = 0;
1729 state.cursor_col = 5;
1730 state.move_right();
1731 assert_eq!(state.cursor_line, 1);
1732 assert_eq!(state.cursor_col, 0);
1733 }
1734
1735 #[test]
1736 fn test_move_right_at_end() {
1737 let mut state = TextAreaState::new("Hello");
1738 state.move_to_end();
1739 state.move_right();
1740 assert_eq!(state.cursor_col, 5); }
1742
1743 #[test]
1744 fn test_move_line_start() {
1745 let mut state = TextAreaState::new("Hello");
1746 state.move_line_start();
1747 assert_eq!(state.cursor_col, 0);
1748 }
1749
1750 #[test]
1751 fn test_move_line_end() {
1752 let mut state = TextAreaState::new("Hello");
1753 state.cursor_col = 0;
1754 state.move_line_end();
1755 assert_eq!(state.cursor_col, 5);
1756 }
1757
1758 #[test]
1759 fn test_move_up() {
1760 let mut state = TextAreaState::new("Line 1\nLine 2\nLine 3");
1761 state.cursor_line = 2; state.move_up();
1763 assert_eq!(state.cursor_line, 1);
1764 }
1765
1766 #[test]
1767 fn test_move_up_clamps_column() {
1768 let mut state = TextAreaState::new("AB\nCDEFG");
1769 state.cursor_line = 1; state.cursor_col = 5;
1771 state.move_up();
1772 assert_eq!(state.cursor_line, 0);
1773 assert_eq!(state.cursor_col, 2); }
1775
1776 #[test]
1777 fn test_move_down() {
1778 let mut state = TextAreaState::new("Line 1\nLine 2\nLine 3");
1779 state.cursor_line = 0;
1780 state.move_down();
1781 assert_eq!(state.cursor_line, 1);
1782 }
1783
1784 #[test]
1785 fn test_move_down_at_last_line() {
1786 let mut state = TextAreaState::new("Hello");
1787 state.move_down();
1788 assert_eq!(state.cursor_line, 0);
1789 }
1790
1791 #[test]
1792 fn test_move_word_left() {
1793 let mut state = TextAreaState::new("Hello World Test");
1794 state.move_to_end(); state.move_word_left();
1796 assert_eq!(state.cursor_col, 12); }
1798
1799 #[test]
1800 fn test_move_word_right() {
1801 let mut state = TextAreaState::new("Hello World Test");
1802 state.cursor_col = 0;
1803 state.move_word_right();
1804 assert_eq!(state.cursor_col, 6); }
1806
1807 #[test]
1808 fn test_move_page_up() {
1809 let mut state = TextAreaState::new("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
1810 state.visible_height = 3;
1811 state.cursor_line = 9; state.move_page_up();
1813 assert_eq!(state.cursor_line, 6);
1814 }
1815
1816 #[test]
1817 fn test_move_page_down() {
1818 let mut state = TextAreaState::new("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
1819 state.cursor_line = 0;
1820 state.visible_height = 3;
1821 state.move_page_down();
1822 assert_eq!(state.cursor_line, 3);
1823 }
1824
1825 #[test]
1826 fn test_move_to_start() {
1827 let mut state = TextAreaState::new("Hello\nWorld");
1828 state.move_to_start();
1829 assert_eq!(state.cursor_line, 0);
1830 assert_eq!(state.cursor_col, 0);
1831 }
1832
1833 #[test]
1834 fn test_move_to_end() {
1835 let mut state = TextAreaState::new("Hello\nWorld");
1836 state.cursor_line = 0;
1837 state.cursor_col = 0;
1838 state.move_to_end();
1839 assert_eq!(state.cursor_line, 1);
1840 assert_eq!(state.cursor_col, 5);
1841 }
1842
1843 #[test]
1848 fn test_text() {
1849 let state = TextAreaState::new("Hello\nWorld");
1850 assert_eq!(state.text(), "Hello\nWorld");
1851 }
1852
1853 #[test]
1854 fn test_set_text() {
1855 let mut state = TextAreaState::new("Old");
1856 state.set_text("New\nContent");
1857 assert_eq!(state.lines.len(), 2);
1858 assert_eq!(state.lines[0], "New");
1859 assert_eq!(state.lines[1], "Content");
1860 assert_eq!(state.cursor_line, 1);
1861 assert_eq!(state.cursor_col, 7);
1862 }
1863
1864 #[test]
1865 fn test_clear() {
1866 let mut state = TextAreaState::new("Hello\nWorld");
1867 state.clear();
1868 assert!(state.is_empty());
1869 assert_eq!(state.cursor_line, 0);
1870 assert_eq!(state.cursor_col, 0);
1871 }
1872
1873 #[test]
1874 fn test_line_count() {
1875 let state = TextAreaState::new("A\nB\nC");
1876 assert_eq!(state.line_count(), 3);
1877 }
1878
1879 #[test]
1880 fn test_current_line() {
1881 let mut state = TextAreaState::new("Hello\nWorld");
1882 state.cursor_line = 0;
1883 assert_eq!(state.current_line(), "Hello");
1884 }
1885
1886 #[test]
1887 fn test_is_empty() {
1888 let state = TextAreaState::empty();
1889 assert!(state.is_empty());
1890
1891 let state = TextAreaState::new("Hi");
1892 assert!(!state.is_empty());
1893 }
1894
1895 #[test]
1896 fn test_len() {
1897 let state = TextAreaState::new("Hi\nWorld");
1898 assert_eq!(state.len(), 8);
1900 }
1901
1902 #[test]
1903 fn test_text_before_after_cursor() {
1904 let mut state = TextAreaState::new("Hello World");
1905 state.cursor_col = 5;
1906 assert_eq!(state.text_before_cursor(), "Hello");
1907 assert_eq!(state.text_after_cursor(), " World");
1908 }
1909
1910 #[test]
1915 fn test_unicode_handling() {
1916 let mut state = TextAreaState::new("你好");
1917 state.move_to_end();
1918 assert_eq!(state.cursor_col, 2);
1919
1920 state.move_left();
1921 assert_eq!(state.cursor_col, 1);
1922
1923 state.insert_char('世');
1924 assert_eq!(state.lines[0], "你世好");
1925 }
1926
1927 #[test]
1928 fn test_emoji_handling() {
1929 let mut state = TextAreaState::new("Hi 👋");
1930 assert_eq!(state.len(), 4);
1931
1932 state.move_to_end();
1933 state.delete_char_backward();
1934 assert_eq!(state.lines[0], "Hi ");
1935 }
1936
1937 #[test]
1942 fn test_disabled_no_insert() {
1943 let mut state = TextAreaState::new("Hello");
1944 state.enabled = false;
1945 state.insert_char('!');
1946 assert_eq!(state.lines[0], "Hello");
1947 }
1948
1949 #[test]
1950 fn test_disabled_no_delete() {
1951 let mut state = TextAreaState::new("Hello");
1952 state.enabled = false;
1953 assert!(!state.delete_char_backward());
1954 assert_eq!(state.lines[0], "Hello");
1955 }
1956
1957 #[test]
1958 fn test_disabled_no_newline() {
1959 let mut state = TextAreaState::new("Hello");
1960 state.enabled = false;
1961 state.insert_newline();
1962 assert_eq!(state.lines.len(), 1);
1963 }
1964
1965 #[test]
1970 fn test_scroll_to_cursor_down() {
1971 let mut state = TextAreaState::new("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
1972 state.visible_height = 3;
1973 state.cursor_line = 8;
1974 state.scroll_to_cursor();
1975 assert_eq!(state.scroll_y, 6);
1976 }
1977
1978 #[test]
1979 fn test_scroll_to_cursor_up() {
1980 let mut state = TextAreaState::new("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
1981 state.visible_height = 3;
1982 state.scroll_y = 5;
1983 state.cursor_line = 2;
1984 state.scroll_to_cursor();
1985 assert_eq!(state.scroll_y, 2);
1986 }
1987
1988 #[test]
1989 fn test_scroll_up() {
1990 let mut state = TextAreaState::new("1\n2\n3");
1991 state.scroll_y = 2;
1992 state.scroll_up();
1993 assert_eq!(state.scroll_y, 1);
1994 }
1995
1996 #[test]
1997 fn test_scroll_down() {
1998 let mut state = TextAreaState::new("1\n2\n3\n4\n5");
1999 state.visible_height = 2;
2000 state.scroll_down();
2001 assert_eq!(state.scroll_y, 1);
2002 }
2003
2004 #[test]
2009 fn test_style_default() {
2010 let style = TextAreaStyle::default();
2011 assert_eq!(style.focused_border, Color::Yellow);
2012 assert_eq!(style.text_fg, Color::White);
2013 assert!(!style.show_line_numbers);
2014 }
2015
2016 #[test]
2017 fn test_style_builder() {
2018 let style = TextAreaStyle::default()
2019 .focused_border(Color::Cyan)
2020 .text_fg(Color::Green)
2021 .show_line_numbers(true);
2022
2023 assert_eq!(style.focused_border, Color::Cyan);
2024 assert_eq!(style.text_fg, Color::Green);
2025 assert!(style.show_line_numbers);
2026 }
2027
2028 #[test]
2033 fn test_tab_config_default() {
2034 let config = TabConfig::default();
2035 assert_eq!(config, TabConfig::Spaces(4));
2036 }
2037
2038 #[test]
2039 fn test_with_tab_config() {
2040 let state = TextAreaState::empty().with_tab_config(TabConfig::Spaces(2));
2041 assert_eq!(state.tab_config, TabConfig::Spaces(2));
2042 }
2043
2044 #[test]
2049 fn test_delete_word_forward() {
2050 let mut state = TextAreaState::new("Hello World Test");
2051 state.cursor_col = 0;
2052 assert!(state.delete_word_forward());
2053 assert_eq!(state.lines[0], "World Test");
2054 assert_eq!(state.cursor_col, 0);
2055 }
2056
2057 #[test]
2058 fn test_delete_word_forward_mid_word() {
2059 let mut state = TextAreaState::new("Hello World");
2060 state.cursor_col = 3; assert!(state.delete_word_forward());
2062 assert_eq!(state.lines[0], "HelWorld");
2063 }
2064
2065 #[test]
2066 fn test_delete_word_forward_at_end() {
2067 let mut state = TextAreaState::new("Hello");
2068 state.move_to_end();
2069 assert!(!state.delete_word_forward());
2070 assert_eq!(state.lines[0], "Hello");
2071 }
2072
2073 #[test]
2074 fn test_delete_word_forward_merges_lines() {
2075 let mut state = TextAreaState::new("Hello\nWorld");
2076 state.cursor_col = 5; assert!(state.delete_word_forward());
2078 assert_eq!(state.lines.len(), 1);
2079 assert_eq!(state.lines[0], "HelloWorld");
2080 }
2081
2082 #[test]
2083 fn test_cursor_mode_default() {
2084 assert_eq!(CursorMode::default(), CursorMode::Block);
2085 }
2086
2087 #[test]
2088 fn test_scroll_mode_default() {
2089 assert_eq!(ScrollMode::default(), ScrollMode::Minimal);
2090 }
2091
2092 #[test]
2093 fn test_style_cursor_mode() {
2094 let style = TextAreaStyle::default().cursor_mode(CursorMode::Terminal);
2095 assert_eq!(style.cursor_mode, CursorMode::Terminal);
2096 }
2097
2098 #[test]
2099 fn test_style_scroll_mode() {
2100 let style = TextAreaStyle::default().scroll_mode(ScrollMode::CenterTracking);
2101 assert_eq!(style.scroll_mode, ScrollMode::CenterTracking);
2102 }
2103
2104 #[test]
2105 fn test_textarea_title_builder() {
2106 let textarea = TextArea::new().title(Line::from("My Title"));
2107 assert!(textarea.title.is_some());
2108 }
2109
2110 #[test]
2111 fn test_textarea_border_color_builder() {
2112 let textarea = TextArea::new().border_color(Color::Red);
2113 assert_eq!(textarea.border_color_override, Some(Color::Red));
2114 }
2115
2116 #[test]
2117 fn test_textarea_content_lines_builder() {
2118 let lines = vec![Line::from("test")];
2119 let textarea = TextArea::new().content_lines(lines);
2120 assert!(textarea.content_lines.is_some());
2121 }
2122}