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)]
85pub struct TextAreaState {
86 pub lines: Vec<String>,
88 pub cursor_line: usize,
90 pub cursor_col: usize,
92 pub scroll_y: usize,
94 pub scroll_x: usize,
96 pub visible_height: usize,
98 pub focused: bool,
100 pub enabled: bool,
102 pub tab_config: TabConfig,
104}
105
106impl Default for TextAreaState {
107 fn default() -> Self {
108 Self {
109 lines: vec![String::new()],
110 cursor_line: 0,
111 cursor_col: 0,
112 scroll_y: 0,
113 scroll_x: 0,
114 visible_height: 0,
115 focused: false,
116 enabled: true,
117 tab_config: TabConfig::default(),
118 }
119 }
120}
121
122impl TextAreaState {
123 pub fn new(text: impl Into<String>) -> Self {
127 let text = text.into();
128 let lines: Vec<String> = if text.is_empty() {
129 vec![String::new()]
130 } else {
131 text.lines().map(|s| s.to_string()).collect()
132 };
133 let lines = if lines.is_empty() {
135 vec![String::new()]
136 } else {
137 lines
138 };
139
140 Self {
141 lines,
142 cursor_line: 0,
143 cursor_col: 0,
144 scroll_y: 0,
145 scroll_x: 0,
146 visible_height: 0,
147 focused: false,
148 enabled: true,
149 tab_config: TabConfig::default(),
150 }
151 }
152
153 pub fn empty() -> Self {
155 Self::default()
156 }
157
158 pub fn with_tab_config(mut self, config: TabConfig) -> Self {
160 self.tab_config = config;
161 self
162 }
163
164 pub fn insert_char(&mut self, c: char) {
170 if !self.enabled {
171 return;
172 }
173 let byte_pos = char_to_byte_index(&self.lines[self.cursor_line], self.cursor_col);
174 self.lines[self.cursor_line].insert(byte_pos, c);
175 self.cursor_col += 1;
176 }
177
178 pub fn insert_str(&mut self, s: &str) {
180 if !self.enabled {
181 return;
182 }
183 for c in s.chars() {
184 if c == '\n' {
185 self.insert_newline();
186 } else if c != '\r' {
187 self.insert_char(c);
188 }
189 }
190 }
191
192 pub fn insert_newline(&mut self) {
194 if !self.enabled {
195 return;
196 }
197
198 let byte_pos = char_to_byte_index(&self.lines[self.cursor_line], self.cursor_col);
199
200 let rest = self.lines[self.cursor_line][byte_pos..].to_string();
202 self.lines[self.cursor_line].truncate(byte_pos);
203
204 self.cursor_line += 1;
206 self.lines.insert(self.cursor_line, rest);
207 self.cursor_col = 0;
208
209 self.ensure_cursor_visible();
210 }
211
212 pub fn insert_tab(&mut self) {
214 if !self.enabled {
215 return;
216 }
217 match self.tab_config {
218 TabConfig::Spaces(n) => {
219 for _ in 0..n {
220 self.insert_char(' ');
221 }
222 }
223 TabConfig::Literal => {
224 self.insert_char('\t');
225 }
226 }
227 }
228
229 pub fn delete_char_backward(&mut self) -> bool {
238 if !self.enabled {
239 return false;
240 }
241
242 if self.cursor_col > 0 {
243 self.cursor_col -= 1;
245 let byte_pos = char_to_byte_index(&self.lines[self.cursor_line], self.cursor_col);
246 if let Some(c) = self.lines[self.cursor_line][byte_pos..].chars().next() {
247 self.lines[self.cursor_line]
248 .replace_range(byte_pos..byte_pos + c.len_utf8(), "");
249 return true;
250 }
251 } else if self.cursor_line > 0 {
252 let current_line = self.lines.remove(self.cursor_line);
254 self.cursor_line -= 1;
255 self.cursor_col = self.lines[self.cursor_line].chars().count();
256 self.lines[self.cursor_line].push_str(¤t_line);
257 self.ensure_cursor_visible();
258 return true;
259 }
260 false
261 }
262
263 pub fn delete_char_forward(&mut self) -> bool {
268 if !self.enabled {
269 return false;
270 }
271
272 let line_len = self.lines[self.cursor_line].chars().count();
273
274 if self.cursor_col < line_len {
275 let byte_pos = char_to_byte_index(&self.lines[self.cursor_line], self.cursor_col);
277 if let Some(c) = self.lines[self.cursor_line][byte_pos..].chars().next() {
278 self.lines[self.cursor_line]
279 .replace_range(byte_pos..byte_pos + c.len_utf8(), "");
280 return true;
281 }
282 } else if self.cursor_line + 1 < self.lines.len() {
283 let next_line = self.lines.remove(self.cursor_line + 1);
285 self.lines[self.cursor_line].push_str(&next_line);
286 return true;
287 }
288 false
289 }
290
291 pub fn delete_word_backward(&mut self) -> bool {
295 if !self.enabled || (self.cursor_col == 0 && self.cursor_line == 0) {
296 return false;
297 }
298
299 if self.cursor_col == 0 {
301 return self.delete_char_backward();
302 }
303
304 let start_col = self.cursor_col;
305 let line = &self.lines[self.cursor_line];
306
307 while self.cursor_col > 0 {
309 if let Some(c) = char_at(line, self.cursor_col - 1) {
310 if c.is_whitespace() {
311 self.cursor_col -= 1;
312 } else {
313 break;
314 }
315 } else {
316 break;
317 }
318 }
319
320 while self.cursor_col > 0 {
322 if let Some(c) = char_at(&self.lines[self.cursor_line], self.cursor_col - 1)
323 {
324 if !c.is_whitespace() {
325 self.delete_char_backward();
326 } else {
327 break;
328 }
329 } else {
330 break;
331 }
332 }
333
334 start_col != self.cursor_col
335 }
336
337 pub fn delete_line(&mut self) {
341 if !self.enabled {
342 return;
343 }
344
345 if self.lines.len() == 1 {
346 self.lines[0].clear();
347 self.cursor_col = 0;
348 } else {
349 self.lines.remove(self.cursor_line);
350 if self.cursor_line >= self.lines.len() {
351 self.cursor_line = self.lines.len().saturating_sub(1);
352 }
353 let new_line_len = self.lines[self.cursor_line].chars().count();
355 self.cursor_col = self.cursor_col.min(new_line_len);
356 }
357 self.ensure_cursor_visible();
358 }
359
360 pub fn delete_to_line_start(&mut self) {
362 if !self.enabled || self.cursor_col == 0 {
363 return;
364 }
365
366 let line = &self.lines[self.cursor_line];
367 let byte_pos = char_to_byte_index(line, self.cursor_col);
368 self.lines[self.cursor_line] = line[byte_pos..].to_string();
369 self.cursor_col = 0;
370 }
371
372 pub fn delete_to_line_end(&mut self) {
374 if !self.enabled {
375 return;
376 }
377
378 let line = &self.lines[self.cursor_line];
379 let byte_pos = char_to_byte_index(line, self.cursor_col);
380 self.lines[self.cursor_line] = line[..byte_pos].to_string();
381 }
382
383 pub fn move_left(&mut self) {
391 if self.cursor_col > 0 {
392 self.cursor_col -= 1;
393 } else if self.cursor_line > 0 {
394 self.cursor_line -= 1;
395 self.cursor_col = self.lines[self.cursor_line].chars().count();
396 self.ensure_cursor_visible();
397 }
398 }
399
400 pub fn move_right(&mut self) {
404 let line_len = self.lines[self.cursor_line].chars().count();
405 if self.cursor_col < line_len {
406 self.cursor_col += 1;
407 } else if self.cursor_line + 1 < self.lines.len() {
408 self.cursor_line += 1;
409 self.cursor_col = 0;
410 self.ensure_cursor_visible();
411 }
412 }
413
414 pub fn move_line_start(&mut self) {
416 self.cursor_col = 0;
417 }
418
419 pub fn move_line_end(&mut self) {
421 self.cursor_col = self.lines[self.cursor_line].chars().count();
422 }
423
424 pub fn move_word_left(&mut self) {
426 if self.cursor_col == 0 {
427 if self.cursor_line > 0 {
428 self.cursor_line -= 1;
429 self.cursor_col = self.lines[self.cursor_line].chars().count();
430 self.ensure_cursor_visible();
431 }
432 return;
433 }
434
435 let line = &self.lines[self.cursor_line];
436
437 while self.cursor_col > 0 {
439 if let Some(c) = char_at(line, self.cursor_col - 1) {
440 if c.is_whitespace() {
441 self.cursor_col -= 1;
442 } else {
443 break;
444 }
445 } else {
446 break;
447 }
448 }
449
450 while self.cursor_col > 0 {
452 if let Some(c) = char_at(line, self.cursor_col - 1) {
453 if !c.is_whitespace() {
454 self.cursor_col -= 1;
455 } else {
456 break;
457 }
458 } else {
459 break;
460 }
461 }
462 }
463
464 pub fn move_word_right(&mut self) {
466 let line = &self.lines[self.cursor_line];
467 let line_len = line.chars().count();
468
469 if self.cursor_col >= line_len {
470 if self.cursor_line + 1 < self.lines.len() {
471 self.cursor_line += 1;
472 self.cursor_col = 0;
473 self.ensure_cursor_visible();
474 }
475 return;
476 }
477
478 while self.cursor_col < line_len {
480 if let Some(c) = char_at(&self.lines[self.cursor_line], self.cursor_col) {
481 if !c.is_whitespace() {
482 self.cursor_col += 1;
483 } else {
484 break;
485 }
486 } else {
487 break;
488 }
489 }
490
491 let line_len = self.lines[self.cursor_line].chars().count();
493 while self.cursor_col < line_len {
494 if let Some(c) = char_at(&self.lines[self.cursor_line], self.cursor_col) {
495 if c.is_whitespace() {
496 self.cursor_col += 1;
497 } else {
498 break;
499 }
500 } else {
501 break;
502 }
503 }
504 }
505
506 pub fn move_up(&mut self) {
512 if self.cursor_line > 0 {
513 self.cursor_line -= 1;
514 let new_line_len = self.lines[self.cursor_line].chars().count();
516 self.cursor_col = self.cursor_col.min(new_line_len);
517 self.ensure_cursor_visible();
518 }
519 }
520
521 pub fn move_down(&mut self) {
523 if self.cursor_line + 1 < self.lines.len() {
524 self.cursor_line += 1;
525 let new_line_len = self.lines[self.cursor_line].chars().count();
527 self.cursor_col = self.cursor_col.min(new_line_len);
528 self.ensure_cursor_visible();
529 }
530 }
531
532 pub fn move_page_up(&mut self) {
534 let page_size = self.visible_height.max(1);
535 if self.cursor_line >= page_size {
536 self.cursor_line -= page_size;
537 } else {
538 self.cursor_line = 0;
539 }
540 let new_line_len = self.lines[self.cursor_line].chars().count();
542 self.cursor_col = self.cursor_col.min(new_line_len);
543 self.ensure_cursor_visible();
544 }
545
546 pub fn move_page_down(&mut self) {
548 let page_size = self.visible_height.max(1);
549 let max_line = self.lines.len().saturating_sub(1);
550 self.cursor_line = (self.cursor_line + page_size).min(max_line);
551 let new_line_len = self.lines[self.cursor_line].chars().count();
553 self.cursor_col = self.cursor_col.min(new_line_len);
554 self.ensure_cursor_visible();
555 }
556
557 pub fn move_to_start(&mut self) {
559 self.cursor_line = 0;
560 self.cursor_col = 0;
561 self.ensure_cursor_visible();
562 }
563
564 pub fn move_to_end(&mut self) {
566 self.cursor_line = self.lines.len().saturating_sub(1);
567 self.cursor_col = self.lines[self.cursor_line].chars().count();
568 self.ensure_cursor_visible();
569 }
570
571 pub fn scroll_to_cursor(&mut self) {
577 if self.cursor_line < self.scroll_y {
579 self.scroll_y = self.cursor_line;
580 } else if self.visible_height > 0 && self.cursor_line >= self.scroll_y + self.visible_height
581 {
582 self.scroll_y = self.cursor_line - self.visible_height + 1;
583 }
584 }
585
586 pub fn ensure_cursor_visible(&mut self) {
588 self.scroll_to_cursor();
589 }
590
591 pub fn scroll_up(&mut self) {
593 self.scroll_y = self.scroll_y.saturating_sub(1);
594 }
595
596 pub fn scroll_down(&mut self) {
598 let max_scroll = self.lines.len().saturating_sub(self.visible_height.max(1));
599 if self.scroll_y < max_scroll {
600 self.scroll_y += 1;
601 }
602 }
603
604 pub fn scroll_left(&mut self) {
606 self.scroll_x = self.scroll_x.saturating_sub(4);
607 }
608
609 pub fn scroll_right(&mut self) {
611 self.scroll_x += 4;
612 }
613
614 pub fn text(&self) -> String {
620 self.lines.join("\n")
621 }
622
623 pub fn set_text(&mut self, text: impl Into<String>) {
627 let text = text.into();
628 self.lines = if text.is_empty() {
629 vec![String::new()]
630 } else {
631 text.lines().map(|s| s.to_string()).collect()
632 };
633 if self.lines.is_empty() {
634 self.lines.push(String::new());
635 }
636 self.cursor_line = self.lines.len().saturating_sub(1);
637 self.cursor_col = self.lines[self.cursor_line].chars().count();
638 self.scroll_y = 0;
639 self.scroll_x = 0;
640 }
641
642 pub fn clear(&mut self) {
644 self.lines = vec![String::new()];
645 self.cursor_line = 0;
646 self.cursor_col = 0;
647 self.scroll_y = 0;
648 self.scroll_x = 0;
649 }
650
651 pub fn line_count(&self) -> usize {
653 self.lines.len()
654 }
655
656 pub fn current_line(&self) -> &str {
658 &self.lines[self.cursor_line]
659 }
660
661 pub fn is_empty(&self) -> bool {
663 self.lines.len() == 1 && self.lines[0].is_empty()
664 }
665
666 pub fn len(&self) -> usize {
668 let line_chars: usize = self.lines.iter().map(|l| l.chars().count()).sum();
669 let newlines = self.lines.len().saturating_sub(1);
670 line_chars + newlines
671 }
672
673 pub fn text_before_cursor(&self) -> &str {
675 let line = &self.lines[self.cursor_line];
676 let byte_pos = char_to_byte_index(line, self.cursor_col);
677 &line[..byte_pos]
678 }
679
680 pub fn text_after_cursor(&self) -> &str {
682 let line = &self.lines[self.cursor_line];
683 let byte_pos = char_to_byte_index(line, self.cursor_col);
684 &line[byte_pos..]
685 }
686}
687
688#[derive(Debug, Clone)]
690pub struct TextAreaStyle {
691 pub focused_border: Color,
693 pub unfocused_border: Color,
695 pub disabled_border: Color,
697 pub text_fg: Color,
699 pub cursor_fg: Color,
701 pub placeholder_fg: Color,
703 pub line_number_fg: Color,
705 pub current_line_bg: Option<Color>,
707 pub show_line_numbers: bool,
709}
710
711impl Default for TextAreaStyle {
712 fn default() -> Self {
713 Self {
714 focused_border: Color::Yellow,
715 unfocused_border: Color::Gray,
716 disabled_border: Color::DarkGray,
717 text_fg: Color::White,
718 cursor_fg: Color::Yellow,
719 placeholder_fg: Color::DarkGray,
720 line_number_fg: Color::DarkGray,
721 current_line_bg: None,
722 show_line_numbers: false,
723 }
724 }
725}
726
727impl TextAreaStyle {
728 pub fn focused_border(mut self, color: Color) -> Self {
730 self.focused_border = color;
731 self
732 }
733
734 pub fn unfocused_border(mut self, color: Color) -> Self {
736 self.unfocused_border = color;
737 self
738 }
739
740 pub fn disabled_border(mut self, color: Color) -> Self {
742 self.disabled_border = color;
743 self
744 }
745
746 pub fn text_fg(mut self, color: Color) -> Self {
748 self.text_fg = color;
749 self
750 }
751
752 pub fn cursor_fg(mut self, color: Color) -> Self {
754 self.cursor_fg = color;
755 self
756 }
757
758 pub fn placeholder_fg(mut self, color: Color) -> Self {
760 self.placeholder_fg = color;
761 self
762 }
763
764 pub fn line_number_fg(mut self, color: Color) -> Self {
766 self.line_number_fg = color;
767 self
768 }
769
770 pub fn current_line_bg(mut self, color: Option<Color>) -> Self {
772 self.current_line_bg = color;
773 self
774 }
775
776 pub fn show_line_numbers(mut self, show: bool) -> Self {
778 self.show_line_numbers = show;
779 self
780 }
781}
782
783pub struct TextArea<'a> {
787 label: Option<&'a str>,
788 placeholder: Option<&'a str>,
789 style: TextAreaStyle,
790 focus_id: FocusId,
791 with_border: bool,
792 wrap_mode: WrapMode,
793}
794
795impl TextArea<'_> {
796 pub fn new() -> Self {
798 Self {
799 label: None,
800 placeholder: None,
801 style: TextAreaStyle::default(),
802 focus_id: FocusId::default(),
803 with_border: true,
804 wrap_mode: WrapMode::default(),
805 }
806 }
807}
808
809impl Default for TextArea<'_> {
810 fn default() -> Self {
811 Self::new()
812 }
813}
814
815impl<'a> TextArea<'a> {
816
817 pub fn label(mut self, label: &'a str) -> Self {
819 self.label = Some(label);
820 self
821 }
822
823 pub fn placeholder(mut self, placeholder: &'a str) -> Self {
825 self.placeholder = Some(placeholder);
826 self
827 }
828
829 pub fn style(mut self, style: TextAreaStyle) -> Self {
831 self.style = style;
832 self
833 }
834
835 pub fn focus_id(mut self, id: FocusId) -> Self {
837 self.focus_id = id;
838 self
839 }
840
841 pub fn with_border(mut self, with_border: bool) -> Self {
843 self.with_border = with_border;
844 self
845 }
846
847 pub fn wrap_mode(mut self, mode: WrapMode) -> Self {
849 self.wrap_mode = mode;
850 self
851 }
852
853 pub fn render_stateful(
855 self,
856 frame: &mut Frame,
857 area: Rect,
858 state: &mut TextAreaState,
859 ) -> ClickRegion<TextAreaAction> {
860 let border_color = if !state.enabled {
861 self.style.disabled_border
862 } else if state.focused {
863 self.style.focused_border
864 } else {
865 self.style.unfocused_border
866 };
867
868 let block = if self.with_border {
869 let mut block = Block::default()
870 .borders(Borders::ALL)
871 .border_style(Style::default().fg(border_color));
872 if let Some(label) = self.label {
873 block = block.title(format!(" {} ", label));
874 }
875 Some(block)
876 } else {
877 None
878 };
879
880 let inner_area = if let Some(ref b) = block {
881 b.inner(area)
882 } else {
883 area
884 };
885
886 state.visible_height = inner_area.height as usize;
888
889 let line_num_width = if self.style.show_line_numbers {
891 let max_line = state.lines.len();
892 let digits = max_line.to_string().len();
893 digits + 2 } else {
895 0
896 };
897
898 let content_width = (inner_area.width as usize).saturating_sub(line_num_width);
900
901 if state.is_empty() && !state.focused {
903 if let Some(placeholder) = self.placeholder {
904 let display_line = Line::from(Span::styled(
905 placeholder,
906 Style::default().fg(self.style.placeholder_fg),
907 ));
908 let paragraph = Paragraph::new(display_line);
909
910 if let Some(block) = block {
911 frame.render_widget(block, area);
912 }
913 frame.render_widget(paragraph, inner_area);
914 return ClickRegion::new(area, TextAreaAction::Focus);
915 }
916 }
917
918 let start_line = state.scroll_y;
920 let end_line = (start_line + state.visible_height).min(state.lines.len());
921
922 let mut display_lines: Vec<Line> = Vec::new();
923
924 for line_idx in start_line..end_line {
925 let line = &state.lines[line_idx];
926 let is_cursor_line = line_idx == state.cursor_line;
927
928 let chars: Vec<char> = line.chars().collect();
930 let visible_chars: String = chars
931 .iter()
932 .skip(state.scroll_x)
933 .take(content_width)
934 .collect();
935
936 let mut spans = Vec::new();
937
938 if self.style.show_line_numbers {
940 let line_num = format!(
941 "{:>width$} ",
942 line_idx + 1,
943 width = line_num_width.saturating_sub(2)
944 );
945 spans.push(Span::styled(
946 line_num,
947 Style::default().fg(self.style.line_number_fg),
948 ));
949 }
950
951 let line_style = if is_cursor_line {
953 if let Some(bg) = self.style.current_line_bg {
954 Style::default().fg(self.style.text_fg).bg(bg)
955 } else {
956 Style::default().fg(self.style.text_fg)
957 }
958 } else {
959 Style::default().fg(self.style.text_fg)
960 };
961
962 if is_cursor_line && state.focused {
964 let cursor_visible_col =
966 state.cursor_col.saturating_sub(state.scroll_x);
967
968 let visible_char_count = visible_chars.chars().count();
969
970 if cursor_visible_col <= visible_char_count {
971 let before: String = visible_chars.chars().take(cursor_visible_col).collect();
972 let cursor_char: String = visible_chars
973 .chars()
974 .skip(cursor_visible_col)
975 .take(1)
976 .collect();
977 let after: String = visible_chars.chars().skip(cursor_visible_col + 1).collect();
978
979 if !before.is_empty() {
980 spans.push(Span::styled(before, line_style));
981 }
982
983 let cursor_style = Style::default()
985 .fg(self.style.cursor_fg)
986 .bg(self.style.text_fg);
987 let cursor_display = if cursor_char.is_empty() { " " } else { &cursor_char };
988 spans.push(Span::styled(cursor_display.to_string(), cursor_style));
989
990 if !after.is_empty() {
991 spans.push(Span::styled(after, line_style));
992 }
993 } else {
994 spans.push(Span::styled(visible_chars, line_style));
996 }
997 } else {
998 spans.push(Span::styled(visible_chars, line_style));
999 }
1000
1001 display_lines.push(Line::from(spans));
1002 }
1003
1004 if display_lines.is_empty() && state.focused {
1006 let mut spans = Vec::new();
1007 if self.style.show_line_numbers {
1008 let line_num = format!("{:>width$} ", 1, width = line_num_width.saturating_sub(2));
1009 spans.push(Span::styled(
1010 line_num,
1011 Style::default().fg(self.style.line_number_fg),
1012 ));
1013 }
1014 let cursor_style = Style::default()
1016 .fg(self.style.cursor_fg)
1017 .bg(self.style.text_fg);
1018 spans.push(Span::styled(" ", cursor_style));
1019 display_lines.push(Line::from(spans));
1020 }
1021
1022 let paragraph = Paragraph::new(display_lines);
1023
1024 if let Some(block) = block {
1025 frame.render_widget(block, area);
1026 }
1027 frame.render_widget(paragraph, inner_area);
1028
1029 ClickRegion::new(area, TextAreaAction::Focus)
1030 }
1031}
1032
1033#[cfg(test)]
1034mod tests {
1035 use super::*;
1036
1037 #[test]
1042 fn test_state_default() {
1043 let state = TextAreaState::default();
1044 assert_eq!(state.lines.len(), 1);
1045 assert!(state.lines[0].is_empty());
1046 assert_eq!(state.cursor_line, 0);
1047 assert_eq!(state.cursor_col, 0);
1048 assert!(!state.focused);
1049 assert!(state.enabled);
1050 }
1051
1052 #[test]
1053 fn test_state_new_single_line() {
1054 let state = TextAreaState::new("Hello");
1055 assert_eq!(state.lines.len(), 1);
1056 assert_eq!(state.lines[0], "Hello");
1057 assert_eq!(state.cursor_line, 0);
1058 assert_eq!(state.cursor_col, 0);
1059 }
1060
1061 #[test]
1062 fn test_state_new_multi_line() {
1063 let state = TextAreaState::new("Hello\nWorld");
1064 assert_eq!(state.lines.len(), 2);
1065 assert_eq!(state.lines[0], "Hello");
1066 assert_eq!(state.lines[1], "World");
1067 assert_eq!(state.cursor_line, 0);
1068 assert_eq!(state.cursor_col, 0);
1069 }
1070
1071 #[test]
1072 fn test_state_new_empty() {
1073 let state = TextAreaState::new("");
1074 assert_eq!(state.lines.len(), 1);
1075 assert!(state.lines[0].is_empty());
1076 assert_eq!(state.cursor_line, 0);
1077 assert_eq!(state.cursor_col, 0);
1078 }
1079
1080 #[test]
1081 fn test_state_empty() {
1082 let state = TextAreaState::empty();
1083 assert!(state.is_empty());
1084 }
1085
1086 #[test]
1091 fn test_insert_char() {
1092 let mut state = TextAreaState::new("Hello");
1093 state.move_to_end();
1094 state.insert_char('!');
1095 assert_eq!(state.lines[0], "Hello!");
1096 assert_eq!(state.cursor_col, 6);
1097 }
1098
1099 #[test]
1100 fn test_insert_char_middle() {
1101 let mut state = TextAreaState::new("Hllo");
1102 state.cursor_col = 1;
1103 state.insert_char('e');
1104 assert_eq!(state.lines[0], "Hello");
1105 assert_eq!(state.cursor_col, 2);
1106 }
1107
1108 #[test]
1109 fn test_insert_str_single_line() {
1110 let mut state = TextAreaState::new("Hello");
1111 state.move_to_end();
1112 state.insert_str(" World");
1113 assert_eq!(state.lines[0], "Hello World");
1114 }
1115
1116 #[test]
1117 fn test_insert_str_multi_line() {
1118 let mut state = TextAreaState::new("Hello");
1119 state.move_to_end();
1120 state.insert_str(" World\nNew Line");
1121 assert_eq!(state.lines.len(), 2);
1122 assert_eq!(state.lines[0], "Hello World");
1123 assert_eq!(state.lines[1], "New Line");
1124 }
1125
1126 #[test]
1127 fn test_insert_newline() {
1128 let mut state = TextAreaState::new("HelloWorld");
1129 state.cursor_col = 5;
1130 state.insert_newline();
1131 assert_eq!(state.lines.len(), 2);
1132 assert_eq!(state.lines[0], "Hello");
1133 assert_eq!(state.lines[1], "World");
1134 assert_eq!(state.cursor_line, 1);
1135 assert_eq!(state.cursor_col, 0);
1136 }
1137
1138 #[test]
1139 fn test_insert_newline_at_start() {
1140 let mut state = TextAreaState::new("Hello");
1141 state.insert_newline();
1142 assert_eq!(state.lines.len(), 2);
1143 assert_eq!(state.lines[0], "");
1144 assert_eq!(state.lines[1], "Hello");
1145 }
1146
1147 #[test]
1148 fn test_insert_newline_at_end() {
1149 let mut state = TextAreaState::new("Hello");
1150 state.move_to_end();
1151 state.insert_newline();
1152 assert_eq!(state.lines.len(), 2);
1153 assert_eq!(state.lines[0], "Hello");
1154 assert_eq!(state.lines[1], "");
1155 }
1156
1157 #[test]
1158 fn test_insert_tab_spaces() {
1159 let mut state = TextAreaState::empty();
1160 state.tab_config = TabConfig::Spaces(4);
1161 state.insert_tab();
1162 assert_eq!(state.lines[0], " ");
1163 }
1164
1165 #[test]
1166 fn test_insert_tab_literal() {
1167 let mut state = TextAreaState::empty();
1168 state.tab_config = TabConfig::Literal;
1169 state.insert_tab();
1170 assert_eq!(state.lines[0], "\t");
1171 }
1172
1173 #[test]
1178 fn test_delete_char_backward() {
1179 let mut state = TextAreaState::new("Hello");
1180 state.move_to_end();
1181 assert!(state.delete_char_backward());
1182 assert_eq!(state.lines[0], "Hell");
1183 assert_eq!(state.cursor_col, 4);
1184 }
1185
1186 #[test]
1187 fn test_delete_char_backward_at_start() {
1188 let mut state = TextAreaState::new("Hello");
1189 assert!(!state.delete_char_backward());
1191 assert_eq!(state.lines[0], "Hello");
1192 }
1193
1194 #[test]
1195 fn test_delete_char_backward_merges_lines() {
1196 let mut state = TextAreaState::new("Hello\nWorld");
1197 state.cursor_line = 1;
1198 state.cursor_col = 0;
1199 assert!(state.delete_char_backward());
1200 assert_eq!(state.lines.len(), 1);
1201 assert_eq!(state.lines[0], "HelloWorld");
1202 assert_eq!(state.cursor_col, 5);
1203 }
1204
1205 #[test]
1206 fn test_delete_char_forward() {
1207 let mut state = TextAreaState::new("Hello");
1208 state.cursor_col = 0;
1209 assert!(state.delete_char_forward());
1210 assert_eq!(state.lines[0], "ello");
1211 }
1212
1213 #[test]
1214 fn test_delete_char_forward_at_end() {
1215 let mut state = TextAreaState::new("Hello");
1216 state.move_to_end();
1217 assert!(!state.delete_char_forward());
1218 assert_eq!(state.lines[0], "Hello");
1219 }
1220
1221 #[test]
1222 fn test_delete_char_forward_merges_lines() {
1223 let mut state = TextAreaState::new("Hello\nWorld");
1224 state.cursor_line = 0;
1225 state.cursor_col = 5;
1226 assert!(state.delete_char_forward());
1227 assert_eq!(state.lines.len(), 1);
1228 assert_eq!(state.lines[0], "HelloWorld");
1229 }
1230
1231 #[test]
1232 fn test_delete_word_backward() {
1233 let mut state = TextAreaState::new("Hello World");
1234 state.move_to_end();
1235 assert!(state.delete_word_backward());
1236 assert_eq!(state.lines[0], "Hello ");
1237 }
1238
1239 #[test]
1240 fn test_delete_word_backward_from_start() {
1241 let mut state = TextAreaState::new("Hello");
1242 assert!(!state.delete_word_backward());
1244 }
1245
1246 #[test]
1247 fn test_delete_line() {
1248 let mut state = TextAreaState::new("Line 1\nLine 2\nLine 3");
1249 state.cursor_line = 1;
1250 state.cursor_col = 0;
1251 state.delete_line();
1252 assert_eq!(state.lines.len(), 2);
1253 assert_eq!(state.lines[0], "Line 1");
1254 assert_eq!(state.lines[1], "Line 3");
1255 }
1256
1257 #[test]
1258 fn test_delete_line_single() {
1259 let mut state = TextAreaState::new("Hello");
1260 state.delete_line();
1261 assert_eq!(state.lines.len(), 1);
1262 assert!(state.lines[0].is_empty());
1263 }
1264
1265 #[test]
1266 fn test_delete_to_line_start() {
1267 let mut state = TextAreaState::new("Hello World");
1268 state.cursor_col = 6;
1269 state.delete_to_line_start();
1270 assert_eq!(state.lines[0], "World");
1271 assert_eq!(state.cursor_col, 0);
1272 }
1273
1274 #[test]
1275 fn test_delete_to_line_end() {
1276 let mut state = TextAreaState::new("Hello World");
1277 state.cursor_col = 5;
1278 state.delete_to_line_end();
1279 assert_eq!(state.lines[0], "Hello");
1280 }
1281
1282 #[test]
1287 fn test_move_left() {
1288 let mut state = TextAreaState::new("Hello");
1289 state.move_to_end();
1290 state.move_left();
1291 assert_eq!(state.cursor_col, 4);
1292 }
1293
1294 #[test]
1295 fn test_move_left_wraps_line() {
1296 let mut state = TextAreaState::new("Hello\nWorld");
1297 state.cursor_line = 1;
1298 state.cursor_col = 0;
1299 state.move_left();
1300 assert_eq!(state.cursor_line, 0);
1301 assert_eq!(state.cursor_col, 5);
1302 }
1303
1304 #[test]
1305 fn test_move_left_at_start() {
1306 let mut state = TextAreaState::new("Hello");
1307 state.cursor_col = 0;
1308 state.move_left();
1309 assert_eq!(state.cursor_col, 0);
1310 }
1311
1312 #[test]
1313 fn test_move_right() {
1314 let mut state = TextAreaState::new("Hello");
1315 state.cursor_col = 0;
1316 state.move_right();
1317 assert_eq!(state.cursor_col, 1);
1318 }
1319
1320 #[test]
1321 fn test_move_right_wraps_line() {
1322 let mut state = TextAreaState::new("Hello\nWorld");
1323 state.cursor_line = 0;
1324 state.cursor_col = 5;
1325 state.move_right();
1326 assert_eq!(state.cursor_line, 1);
1327 assert_eq!(state.cursor_col, 0);
1328 }
1329
1330 #[test]
1331 fn test_move_right_at_end() {
1332 let mut state = TextAreaState::new("Hello");
1333 state.move_to_end();
1334 state.move_right();
1335 assert_eq!(state.cursor_col, 5); }
1337
1338 #[test]
1339 fn test_move_line_start() {
1340 let mut state = TextAreaState::new("Hello");
1341 state.move_line_start();
1342 assert_eq!(state.cursor_col, 0);
1343 }
1344
1345 #[test]
1346 fn test_move_line_end() {
1347 let mut state = TextAreaState::new("Hello");
1348 state.cursor_col = 0;
1349 state.move_line_end();
1350 assert_eq!(state.cursor_col, 5);
1351 }
1352
1353 #[test]
1354 fn test_move_up() {
1355 let mut state = TextAreaState::new("Line 1\nLine 2\nLine 3");
1356 state.cursor_line = 2; state.move_up();
1358 assert_eq!(state.cursor_line, 1);
1359 }
1360
1361 #[test]
1362 fn test_move_up_clamps_column() {
1363 let mut state = TextAreaState::new("AB\nCDEFG");
1364 state.cursor_line = 1; state.cursor_col = 5;
1366 state.move_up();
1367 assert_eq!(state.cursor_line, 0);
1368 assert_eq!(state.cursor_col, 2); }
1370
1371 #[test]
1372 fn test_move_down() {
1373 let mut state = TextAreaState::new("Line 1\nLine 2\nLine 3");
1374 state.cursor_line = 0;
1375 state.move_down();
1376 assert_eq!(state.cursor_line, 1);
1377 }
1378
1379 #[test]
1380 fn test_move_down_at_last_line() {
1381 let mut state = TextAreaState::new("Hello");
1382 state.move_down();
1383 assert_eq!(state.cursor_line, 0);
1384 }
1385
1386 #[test]
1387 fn test_move_word_left() {
1388 let mut state = TextAreaState::new("Hello World Test");
1389 state.move_to_end(); state.move_word_left();
1391 assert_eq!(state.cursor_col, 12); }
1393
1394 #[test]
1395 fn test_move_word_right() {
1396 let mut state = TextAreaState::new("Hello World Test");
1397 state.cursor_col = 0;
1398 state.move_word_right();
1399 assert_eq!(state.cursor_col, 6); }
1401
1402 #[test]
1403 fn test_move_page_up() {
1404 let mut state = TextAreaState::new("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
1405 state.visible_height = 3;
1406 state.cursor_line = 9; state.move_page_up();
1408 assert_eq!(state.cursor_line, 6);
1409 }
1410
1411 #[test]
1412 fn test_move_page_down() {
1413 let mut state = TextAreaState::new("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
1414 state.cursor_line = 0;
1415 state.visible_height = 3;
1416 state.move_page_down();
1417 assert_eq!(state.cursor_line, 3);
1418 }
1419
1420 #[test]
1421 fn test_move_to_start() {
1422 let mut state = TextAreaState::new("Hello\nWorld");
1423 state.move_to_start();
1424 assert_eq!(state.cursor_line, 0);
1425 assert_eq!(state.cursor_col, 0);
1426 }
1427
1428 #[test]
1429 fn test_move_to_end() {
1430 let mut state = TextAreaState::new("Hello\nWorld");
1431 state.cursor_line = 0;
1432 state.cursor_col = 0;
1433 state.move_to_end();
1434 assert_eq!(state.cursor_line, 1);
1435 assert_eq!(state.cursor_col, 5);
1436 }
1437
1438 #[test]
1443 fn test_text() {
1444 let state = TextAreaState::new("Hello\nWorld");
1445 assert_eq!(state.text(), "Hello\nWorld");
1446 }
1447
1448 #[test]
1449 fn test_set_text() {
1450 let mut state = TextAreaState::new("Old");
1451 state.set_text("New\nContent");
1452 assert_eq!(state.lines.len(), 2);
1453 assert_eq!(state.lines[0], "New");
1454 assert_eq!(state.lines[1], "Content");
1455 assert_eq!(state.cursor_line, 1);
1456 assert_eq!(state.cursor_col, 7);
1457 }
1458
1459 #[test]
1460 fn test_clear() {
1461 let mut state = TextAreaState::new("Hello\nWorld");
1462 state.clear();
1463 assert!(state.is_empty());
1464 assert_eq!(state.cursor_line, 0);
1465 assert_eq!(state.cursor_col, 0);
1466 }
1467
1468 #[test]
1469 fn test_line_count() {
1470 let state = TextAreaState::new("A\nB\nC");
1471 assert_eq!(state.line_count(), 3);
1472 }
1473
1474 #[test]
1475 fn test_current_line() {
1476 let mut state = TextAreaState::new("Hello\nWorld");
1477 state.cursor_line = 0;
1478 assert_eq!(state.current_line(), "Hello");
1479 }
1480
1481 #[test]
1482 fn test_is_empty() {
1483 let state = TextAreaState::empty();
1484 assert!(state.is_empty());
1485
1486 let state = TextAreaState::new("Hi");
1487 assert!(!state.is_empty());
1488 }
1489
1490 #[test]
1491 fn test_len() {
1492 let state = TextAreaState::new("Hi\nWorld");
1493 assert_eq!(state.len(), 8);
1495 }
1496
1497 #[test]
1498 fn test_text_before_after_cursor() {
1499 let mut state = TextAreaState::new("Hello World");
1500 state.cursor_col = 5;
1501 assert_eq!(state.text_before_cursor(), "Hello");
1502 assert_eq!(state.text_after_cursor(), " World");
1503 }
1504
1505 #[test]
1510 fn test_unicode_handling() {
1511 let mut state = TextAreaState::new("你好");
1512 state.move_to_end();
1513 assert_eq!(state.cursor_col, 2);
1514
1515 state.move_left();
1516 assert_eq!(state.cursor_col, 1);
1517
1518 state.insert_char('世');
1519 assert_eq!(state.lines[0], "你世好");
1520 }
1521
1522 #[test]
1523 fn test_emoji_handling() {
1524 let mut state = TextAreaState::new("Hi 👋");
1525 assert_eq!(state.len(), 4);
1526
1527 state.move_to_end();
1528 state.delete_char_backward();
1529 assert_eq!(state.lines[0], "Hi ");
1530 }
1531
1532 #[test]
1537 fn test_disabled_no_insert() {
1538 let mut state = TextAreaState::new("Hello");
1539 state.enabled = false;
1540 state.insert_char('!');
1541 assert_eq!(state.lines[0], "Hello");
1542 }
1543
1544 #[test]
1545 fn test_disabled_no_delete() {
1546 let mut state = TextAreaState::new("Hello");
1547 state.enabled = false;
1548 assert!(!state.delete_char_backward());
1549 assert_eq!(state.lines[0], "Hello");
1550 }
1551
1552 #[test]
1553 fn test_disabled_no_newline() {
1554 let mut state = TextAreaState::new("Hello");
1555 state.enabled = false;
1556 state.insert_newline();
1557 assert_eq!(state.lines.len(), 1);
1558 }
1559
1560 #[test]
1565 fn test_scroll_to_cursor_down() {
1566 let mut state = TextAreaState::new("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
1567 state.visible_height = 3;
1568 state.cursor_line = 8;
1569 state.scroll_to_cursor();
1570 assert_eq!(state.scroll_y, 6);
1571 }
1572
1573 #[test]
1574 fn test_scroll_to_cursor_up() {
1575 let mut state = TextAreaState::new("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
1576 state.visible_height = 3;
1577 state.scroll_y = 5;
1578 state.cursor_line = 2;
1579 state.scroll_to_cursor();
1580 assert_eq!(state.scroll_y, 2);
1581 }
1582
1583 #[test]
1584 fn test_scroll_up() {
1585 let mut state = TextAreaState::new("1\n2\n3");
1586 state.scroll_y = 2;
1587 state.scroll_up();
1588 assert_eq!(state.scroll_y, 1);
1589 }
1590
1591 #[test]
1592 fn test_scroll_down() {
1593 let mut state = TextAreaState::new("1\n2\n3\n4\n5");
1594 state.visible_height = 2;
1595 state.scroll_down();
1596 assert_eq!(state.scroll_y, 1);
1597 }
1598
1599 #[test]
1604 fn test_style_default() {
1605 let style = TextAreaStyle::default();
1606 assert_eq!(style.focused_border, Color::Yellow);
1607 assert_eq!(style.text_fg, Color::White);
1608 assert!(!style.show_line_numbers);
1609 }
1610
1611 #[test]
1612 fn test_style_builder() {
1613 let style = TextAreaStyle::default()
1614 .focused_border(Color::Cyan)
1615 .text_fg(Color::Green)
1616 .show_line_numbers(true);
1617
1618 assert_eq!(style.focused_border, Color::Cyan);
1619 assert_eq!(style.text_fg, Color::Green);
1620 assert!(style.show_line_numbers);
1621 }
1622
1623 #[test]
1628 fn test_tab_config_default() {
1629 let config = TabConfig::default();
1630 assert_eq!(config, TabConfig::Spaces(4));
1631 }
1632
1633 #[test]
1634 fn test_with_tab_config() {
1635 let state = TextAreaState::empty().with_tab_config(TabConfig::Spaces(2));
1636 assert_eq!(state.tab_config, TabConfig::Spaces(2));
1637 }
1638}