1#![forbid(unsafe_code)]
2
3use ftui_core::event::{Event, KeyCode, KeyEvent, KeyEventKind, Modifiers};
9use ftui_core::geometry::Rect;
10use ftui_render::cell::{Cell, CellContent};
11use ftui_render::frame::Frame;
12use ftui_style::Style;
13use ftui_text::grapheme_width;
14use unicode_segmentation::UnicodeSegmentation;
15
16use crate::Widget;
17use crate::undo_support::{TextEditOperation, TextInputUndoExt, UndoSupport, UndoWidgetId};
18
19#[derive(Debug, Clone, Default)]
21pub struct TextInput {
22 undo_id: UndoWidgetId,
24 value: String,
26 cursor: usize,
28 scroll_cells: std::cell::Cell<usize>,
30 selection_anchor: Option<usize>,
32 placeholder: String,
34 mask_char: Option<char>,
36 max_length: Option<usize>,
38 style: Style,
40 cursor_style: Style,
42 placeholder_style: Style,
44 selection_style: Style,
46 focused: bool,
48}
49
50impl TextInput {
51 pub fn new() -> Self {
53 Self::default()
54 }
55
56 #[must_use]
60 pub fn with_value(mut self, value: impl Into<String>) -> Self {
61 self.value = value.into();
62 self.cursor = self.value.graphemes(true).count();
63 self.selection_anchor = None;
64 self
65 }
66
67 #[must_use]
69 pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
70 self.placeholder = placeholder.into();
71 self
72 }
73
74 #[must_use]
76 pub fn with_mask(mut self, mask: char) -> Self {
77 self.mask_char = Some(mask);
78 self
79 }
80
81 #[must_use]
83 pub fn with_max_length(mut self, max: usize) -> Self {
84 self.max_length = Some(max);
85 self
86 }
87
88 #[must_use]
90 pub fn with_style(mut self, style: Style) -> Self {
91 self.style = style;
92 self
93 }
94
95 #[must_use]
97 pub fn with_cursor_style(mut self, style: Style) -> Self {
98 self.cursor_style = style;
99 self
100 }
101
102 #[must_use]
104 pub fn with_placeholder_style(mut self, style: Style) -> Self {
105 self.placeholder_style = style;
106 self
107 }
108
109 #[must_use]
111 pub fn with_selection_style(mut self, style: Style) -> Self {
112 self.selection_style = style;
113 self
114 }
115
116 #[must_use]
118 pub fn with_focused(mut self, focused: bool) -> Self {
119 self.focused = focused;
120 self
121 }
122
123 pub fn value(&self) -> &str {
127 &self.value
128 }
129
130 pub fn set_value(&mut self, value: impl Into<String>) {
132 self.value = value.into();
133 let max = self.grapheme_count();
134 self.cursor = self.cursor.min(max);
135 self.scroll_cells.set(0);
136 self.selection_anchor = None;
137 }
138
139 pub fn clear(&mut self) {
141 self.value.clear();
142 self.cursor = 0;
143 self.scroll_cells.set(0);
144 self.selection_anchor = None;
145 }
146
147 #[inline]
149 pub fn cursor(&self) -> usize {
150 self.cursor
151 }
152
153 #[inline]
155 pub fn focused(&self) -> bool {
156 self.focused
157 }
158
159 pub fn set_focused(&mut self, focused: bool) {
161 self.focused = focused;
162 }
163
164 pub fn cursor_position(&self, area: Rect) -> (u16, u16) {
169 let cursor_visual = self.cursor_visual_pos();
170 let effective_scroll = self.effective_scroll(area.width as usize);
171 let rel_x = cursor_visual.saturating_sub(effective_scroll);
172 let x = area
173 .x
174 .saturating_add(rel_x as u16)
175 .min(area.right().saturating_sub(1));
176 (x, area.y)
177 }
178
179 #[must_use]
181 pub fn selected_text(&self) -> Option<&str> {
182 let anchor = self.selection_anchor?;
183 let (start, end) = self.selection_range(anchor);
184 let byte_start = self.grapheme_byte_offset(start);
185 let byte_end = self.grapheme_byte_offset(end);
186 Some(&self.value[byte_start..byte_end])
187 }
188
189 pub fn handle_event(&mut self, event: &Event) -> bool {
195 match event {
196 Event::Key(key)
197 if key.kind == KeyEventKind::Press || key.kind == KeyEventKind::Repeat =>
198 {
199 self.handle_key(key)
200 }
201 Event::Paste(paste) => {
202 self.delete_selection();
203 self.insert_text(&paste.text);
204 true
205 }
206 _ => false,
207 }
208 }
209
210 fn handle_key(&mut self, key: &KeyEvent) -> bool {
211 let ctrl = key.modifiers.contains(Modifiers::CTRL);
212 let shift = key.modifiers.contains(Modifiers::SHIFT);
213
214 match key.code {
215 KeyCode::Char(c) if !ctrl => {
216 self.delete_selection();
217 self.insert_char(c);
218 true
219 }
220 KeyCode::Char('a') if ctrl => {
222 self.select_all();
223 true
224 }
225 KeyCode::Char('w') if ctrl => {
227 self.delete_word_back();
228 true
229 }
230 KeyCode::Backspace => {
231 if self.selection_anchor.is_some() {
232 self.delete_selection();
233 } else if ctrl {
234 self.delete_word_back();
235 } else {
236 self.delete_char_back();
237 }
238 true
239 }
240 KeyCode::Delete => {
241 if self.selection_anchor.is_some() {
242 self.delete_selection();
243 } else if ctrl {
244 self.delete_word_forward();
245 } else {
246 self.delete_char_forward();
247 }
248 true
249 }
250 KeyCode::Left => {
251 if ctrl {
252 self.move_cursor_word_left(shift);
253 } else if shift {
254 self.move_cursor_left_select();
255 } else {
256 self.move_cursor_left();
257 }
258 true
259 }
260 KeyCode::Right => {
261 if ctrl {
262 self.move_cursor_word_right(shift);
263 } else if shift {
264 self.move_cursor_right_select();
265 } else {
266 self.move_cursor_right();
267 }
268 true
269 }
270 KeyCode::Home => {
271 if shift {
272 self.ensure_selection_anchor();
273 } else {
274 self.selection_anchor = None;
275 }
276 self.cursor = 0;
277 self.scroll_cells.set(0);
278 true
279 }
280 KeyCode::End => {
281 if shift {
282 self.ensure_selection_anchor();
283 } else {
284 self.selection_anchor = None;
285 }
286 self.cursor = self.grapheme_count();
287 true
288 }
289 _ => false,
290 }
291 }
292
293 pub fn insert_text(&mut self, text: &str) {
303 let clean_text: String = text
305 .chars()
306 .map(|c| {
307 if c == '\n' || c == '\r' || c == '\t' {
308 ' '
309 } else {
310 c
311 }
312 })
313 .filter(|c| !c.is_control())
314 .collect();
315
316 if clean_text.is_empty() {
317 return;
318 }
319
320 let current_count = self.grapheme_count();
321 let old_cursor = self.cursor;
322 let avail = if let Some(max) = self.max_length {
323 if current_count >= max {
324 1
326 } else {
327 max - current_count
328 }
329 } else {
330 usize::MAX
331 };
332
333 let new_graphemes = clean_text.graphemes(true).count();
335 let to_insert = if new_graphemes > avail {
336 let end_byte = clean_text
338 .grapheme_indices(true)
339 .map(|(i, _)| i)
340 .nth(avail)
341 .unwrap_or(clean_text.len());
342 &clean_text[..end_byte]
343 } else {
344 clean_text.as_str()
345 };
346
347 if to_insert.is_empty() {
348 return;
349 }
350
351 let byte_offset = self.grapheme_byte_offset(self.cursor);
352 self.value.insert_str(byte_offset, to_insert);
353
354 let new_total = self.grapheme_count();
356 if let Some(max) = self.max_length
357 && new_total > max
358 {
359 self.value.drain(byte_offset..byte_offset + to_insert.len());
361 return;
362 }
363
364 let gc = self.grapheme_count();
365 let delta = gc.saturating_sub(current_count);
366 self.cursor = (old_cursor + delta).min(gc);
367 }
368
369 fn insert_char(&mut self, c: char) {
370 if c.is_control() {
372 return;
373 }
374
375 let old_count = self.grapheme_count();
376 let byte_offset = self.grapheme_byte_offset(self.cursor);
377 self.value.insert(byte_offset, c);
378
379 let new_count = self.grapheme_count();
380
381 if let Some(max) = self.max_length
383 && new_count > max
384 {
385 let char_len = c.len_utf8();
387 self.value.drain(byte_offset..byte_offset + char_len);
388 return;
389 }
390
391 if new_count > old_count {
395 self.cursor += 1;
396 }
397 }
398
399 fn delete_char_back(&mut self) {
400 if self.cursor > 0 {
401 let byte_start = self.grapheme_byte_offset(self.cursor - 1);
402 let byte_end = self.grapheme_byte_offset(self.cursor);
403 self.value.drain(byte_start..byte_end);
404 self.cursor -= 1;
405 }
406 }
407
408 fn delete_char_forward(&mut self) {
409 let count = self.grapheme_count();
410 if self.cursor < count {
411 let byte_start = self.grapheme_byte_offset(self.cursor);
412 let byte_end = self.grapheme_byte_offset(self.cursor + 1);
413 self.value.drain(byte_start..byte_end);
414 }
415 }
416
417 fn delete_word_back(&mut self) {
418 let old_cursor = self.cursor;
419 self.move_cursor_word_left(false);
420 let new_cursor = self.cursor;
421 if new_cursor < old_cursor {
422 let byte_start = self.grapheme_byte_offset(new_cursor);
423 let byte_end = self.grapheme_byte_offset(old_cursor);
424 self.value.drain(byte_start..byte_end);
425 }
426 }
427
428 fn delete_word_forward(&mut self) {
429 let old_cursor = self.cursor;
430 self.move_cursor_word_right(false);
432 let new_cursor = self.cursor;
433 self.cursor = old_cursor;
435
436 if new_cursor > old_cursor {
437 let byte_start = self.grapheme_byte_offset(old_cursor);
438 let byte_end = self.grapheme_byte_offset(new_cursor);
439 self.value.drain(byte_start..byte_end);
440 }
441 }
442
443 pub fn select_all(&mut self) {
447 self.selection_anchor = Some(0);
448 self.cursor = self.grapheme_count();
449 }
450
451 fn delete_selection(&mut self) {
453 if let Some(anchor) = self.selection_anchor.take() {
454 let (start, end) = self.selection_range(anchor);
455 let byte_start = self.grapheme_byte_offset(start);
456 let byte_end = self.grapheme_byte_offset(end);
457 self.value.drain(byte_start..byte_end);
458 self.cursor = start;
459 }
460 }
461
462 fn ensure_selection_anchor(&mut self) {
463 if self.selection_anchor.is_none() {
464 self.selection_anchor = Some(self.cursor);
465 }
466 }
467
468 fn selection_range(&self, anchor: usize) -> (usize, usize) {
469 if anchor <= self.cursor {
470 (anchor, self.cursor)
471 } else {
472 (self.cursor, anchor)
473 }
474 }
475
476 fn is_in_selection(&self, grapheme_idx: usize) -> bool {
477 if let Some(anchor) = self.selection_anchor {
478 let (start, end) = self.selection_range(anchor);
479 grapheme_idx >= start && grapheme_idx < end
480 } else {
481 false
482 }
483 }
484
485 fn move_cursor_left(&mut self) {
488 if let Some(anchor) = self.selection_anchor.take() {
489 self.cursor = self.cursor.min(anchor);
490 } else if self.cursor > 0 {
491 self.cursor -= 1;
492 }
493 }
494
495 fn move_cursor_right(&mut self) {
496 if let Some(anchor) = self.selection_anchor.take() {
497 self.cursor = self.cursor.max(anchor);
498 } else if self.cursor < self.grapheme_count() {
499 self.cursor += 1;
500 }
501 }
502
503 fn move_cursor_left_select(&mut self) {
504 self.ensure_selection_anchor();
505 if self.cursor > 0 {
506 self.cursor -= 1;
507 }
508 }
509
510 fn move_cursor_right_select(&mut self) {
511 self.ensure_selection_anchor();
512 if self.cursor < self.grapheme_count() {
513 self.cursor += 1;
514 }
515 }
516
517 fn get_grapheme_class(g: &str) -> u8 {
518 if g.chars().all(char::is_whitespace) {
519 0
520 } else if g.chars().any(char::is_alphanumeric) {
521 1
522 } else {
523 2
524 }
525 }
526
527 fn move_cursor_word_left(&mut self, select: bool) {
528 if select {
529 self.ensure_selection_anchor();
530 } else {
531 self.selection_anchor = None;
532 }
533
534 if self.cursor == 0 {
535 return;
536 }
537
538 let graphemes: Vec<&str> = self.value.graphemes(true).collect();
539 let mut pos = self.cursor;
540
541 while pos > 0 && Self::get_grapheme_class(graphemes[pos - 1]) == 0 {
543 pos -= 1;
544 }
545
546 while pos > 0 && Self::get_grapheme_class(graphemes[pos - 1]) == 2 {
548 pos -= 1;
549 }
550
551 while pos > 0 && Self::get_grapheme_class(graphemes[pos - 1]) == 1 {
553 pos -= 1;
554 }
555
556 self.cursor = pos;
557 }
558
559 fn move_cursor_word_right(&mut self, select: bool) {
560 if select {
561 self.ensure_selection_anchor();
562 } else {
563 self.selection_anchor = None;
564 }
565
566 let graphemes: Vec<&str> = self.value.graphemes(true).collect();
567 let max = graphemes.len();
568
569 if self.cursor >= max {
570 return;
571 }
572
573 let mut pos = self.cursor;
574
575 while pos < max && Self::get_grapheme_class(graphemes[pos]) == 1 {
577 pos += 1;
578 }
579
580 while pos < max && Self::get_grapheme_class(graphemes[pos]) != 1 {
582 pos += 1;
583 }
584
585 self.cursor = pos;
586 }
587
588 fn grapheme_count(&self) -> usize {
591 self.value.graphemes(true).count()
592 }
593
594 fn grapheme_byte_offset(&self, grapheme_idx: usize) -> usize {
595 self.value
596 .grapheme_indices(true)
597 .nth(grapheme_idx)
598 .map(|(i, _)| i)
599 .unwrap_or(self.value.len())
600 }
601
602 fn grapheme_width(&self, g: &str) -> usize {
603 if let Some(mask) = self.mask_char {
604 let mut buf = [0u8; 4];
605 let mask_str = mask.encode_utf8(&mut buf);
606 grapheme_width(mask_str)
607 } else {
608 grapheme_width(g)
609 }
610 }
611
612 fn prev_grapheme_width(&self) -> usize {
613 if self.cursor == 0 {
614 return 0;
615 }
616 self.value
617 .graphemes(true)
618 .nth(self.cursor - 1)
619 .map(|g| self.grapheme_width(g))
620 .unwrap_or(0)
621 }
622
623 fn cursor_visual_pos(&self) -> usize {
624 if self.value.is_empty() {
625 return 0;
626 }
627 self.value
628 .graphemes(true)
629 .take(self.cursor)
630 .map(|g| self.grapheme_width(g))
631 .sum()
632 }
633
634 fn effective_scroll(&self, viewport_width: usize) -> usize {
635 let cursor_visual = self.cursor_visual_pos();
636 let mut scroll = self.scroll_cells.get();
637 if cursor_visual < scroll {
638 scroll = cursor_visual;
639 }
640 if cursor_visual >= scroll + viewport_width {
641 let candidate_scroll = cursor_visual - viewport_width + 1;
642 let prev_width = self.prev_grapheme_width();
645 let max_scroll_for_prev = cursor_visual.saturating_sub(prev_width);
646
647 scroll = candidate_scroll.min(max_scroll_for_prev);
648 }
649 self.scroll_cells.set(scroll);
650 scroll
651 }
652}
653
654impl Widget for TextInput {
655 fn render(&self, area: Rect, frame: &mut Frame) {
656 #[cfg(feature = "tracing")]
657 let _span = tracing::debug_span!(
658 "widget_render",
659 widget = "TextInput",
660 x = area.x,
661 y = area.y,
662 w = area.width,
663 h = area.height
664 )
665 .entered();
666
667 if area.width < 1 || area.height < 1 {
668 return;
669 }
670
671 let deg = frame.buffer.degradation;
672
673 if deg.apply_styling() {
677 crate::set_style_area(&mut frame.buffer, area, self.style);
678 }
679
680 let graphemes: Vec<&str> = self.value.graphemes(true).collect();
681 let show_placeholder = self.value.is_empty() && !self.placeholder.is_empty();
682
683 let viewport_width = area.width as usize;
684 let cursor_visual_pos = self.cursor_visual_pos();
685 let effective_scroll = self.effective_scroll(viewport_width);
686
687 let mut visual_x: usize = 0;
689 let y = area.y;
690
691 if show_placeholder {
692 let placeholder_style = if deg.apply_styling() {
693 self.placeholder_style
694 } else {
695 Style::default()
696 };
697 for g in self.placeholder.graphemes(true) {
698 let w = self.grapheme_width(g);
699 if w == 0 {
700 continue;
701 }
702
703 if visual_x + w <= effective_scroll {
705 visual_x += w;
706 continue;
707 }
708
709 if visual_x < effective_scroll {
711 visual_x += w;
712 continue;
713 }
714
715 let rel_x = visual_x - effective_scroll;
716
717 if rel_x >= viewport_width {
719 break;
720 }
721
722 if rel_x + w > viewport_width {
724 break;
725 }
726
727 let mut cell = if g.chars().count() > 1 || w > 1 {
728 let id = frame.intern_with_width(g, w as u8);
729 Cell::new(CellContent::from_grapheme(id))
730 } else if let Some(c) = g.chars().next() {
731 Cell::from_char(c)
732 } else {
733 visual_x += w;
734 continue;
735 };
736 crate::apply_style(&mut cell, placeholder_style);
737
738 frame
739 .buffer
740 .set(area.x.saturating_add(rel_x as u16), y, cell);
741 visual_x += w;
742 }
743 } else {
744 for (gi, g) in graphemes.iter().enumerate() {
745 let w = self.grapheme_width(g);
746 if w == 0 {
747 continue;
748 }
749
750 if visual_x + w <= effective_scroll {
752 visual_x += w;
753 continue;
754 }
755
756 if visual_x < effective_scroll {
758 visual_x += w;
759 continue;
760 }
761
762 let rel_x = visual_x - effective_scroll;
763
764 if rel_x >= viewport_width {
766 break;
767 }
768
769 if rel_x + w > viewport_width {
771 break;
772 }
773
774 let cell_style = if !deg.apply_styling() {
775 Style::default()
776 } else if self.is_in_selection(gi) {
777 self.selection_style
778 } else {
779 self.style
780 };
781
782 let mut cell = if let Some(mask) = self.mask_char {
783 Cell::from_char(mask)
784 } else if g.chars().count() > 1 || w > 1 {
785 let id = frame.intern_with_width(g, w as u8);
786 Cell::new(CellContent::from_grapheme(id))
787 } else {
788 Cell::from_char(g.chars().next().unwrap_or(' '))
789 };
790 crate::apply_style(&mut cell, cell_style);
791
792 frame
793 .buffer
794 .set(area.x.saturating_add(rel_x as u16), y, cell);
795 visual_x += w;
796 }
797 }
798
799 if self.focused {
800 let cursor_rel_x = cursor_visual_pos.saturating_sub(effective_scroll);
802 if cursor_rel_x < viewport_width {
803 let cursor_screen_x = area.x.saturating_add(cursor_rel_x as u16);
804 if let Some(cell) = frame.buffer.get_mut(cursor_screen_x, y) {
805 if !deg.apply_styling() {
806 use ftui_render::cell::StyleFlags;
808 let current_flags = cell.attrs.flags();
809 let new_flags = current_flags ^ StyleFlags::REVERSE;
810 cell.attrs = cell.attrs.with_flags(new_flags);
811 } else if self.cursor_style.is_empty() {
812 use ftui_render::cell::StyleFlags;
814 let current_flags = cell.attrs.flags();
815 let new_flags = current_flags ^ StyleFlags::REVERSE;
816 cell.attrs = cell.attrs.with_flags(new_flags);
817 } else {
818 crate::apply_style(cell, self.cursor_style);
819 }
820 }
821 }
822
823 frame.set_cursor(Some(self.cursor_position(area)));
824 frame.set_cursor_visible(true);
825 }
826 }
827
828 fn is_essential(&self) -> bool {
829 true
830 }
831}
832
833#[derive(Debug, Clone)]
839pub struct TextInputSnapshot {
840 value: String,
841 cursor: usize,
842 selection_anchor: Option<usize>,
843}
844
845impl UndoSupport for TextInput {
846 fn undo_widget_id(&self) -> UndoWidgetId {
847 self.undo_id
848 }
849
850 fn create_snapshot(&self) -> Box<dyn std::any::Any + Send> {
851 Box::new(TextInputSnapshot {
852 value: self.value.clone(),
853 cursor: self.cursor,
854 selection_anchor: self.selection_anchor,
855 })
856 }
857
858 fn restore_snapshot(&mut self, snapshot: &dyn std::any::Any) -> bool {
859 if let Some(snap) = snapshot.downcast_ref::<TextInputSnapshot>() {
860 self.value = snap.value.clone();
861 self.cursor = snap.cursor;
862 self.selection_anchor = snap.selection_anchor;
863 self.scroll_cells.set(0); true
865 } else {
866 false
867 }
868 }
869}
870
871impl TextInputUndoExt for TextInput {
872 fn text_value(&self) -> &str {
873 &self.value
874 }
875
876 fn set_text_value(&mut self, value: &str) {
877 self.value = value.to_string();
878 let max = self.grapheme_count();
879 self.cursor = self.cursor.min(max);
880 self.selection_anchor = None;
881 }
882
883 fn cursor_position(&self) -> usize {
884 self.cursor
885 }
886
887 fn set_cursor_position(&mut self, pos: usize) {
888 let max = self.grapheme_count();
889 self.cursor = pos.min(max);
890 }
891
892 fn insert_text_at(&mut self, position: usize, text: &str) {
893 let byte_offset = self.grapheme_byte_offset(position);
894 self.value.insert_str(byte_offset, text);
895 let inserted_graphemes = text.graphemes(true).count();
896 if self.cursor >= position {
897 self.cursor += inserted_graphemes;
898 }
899 }
900
901 fn delete_text_range(&mut self, start: usize, end: usize) {
902 if start >= end {
903 return;
904 }
905 let byte_start = self.grapheme_byte_offset(start);
906 let byte_end = self.grapheme_byte_offset(end);
907 self.value.drain(byte_start..byte_end);
908 let deleted_count = end - start;
909 if self.cursor > end {
910 self.cursor -= deleted_count;
911 } else if self.cursor > start {
912 self.cursor = start;
913 }
914 }
915}
916
917impl TextInput {
918 #[must_use]
943 pub fn create_text_edit_command(
944 &self,
945 operation: TextEditOperation,
946 ) -> Option<crate::undo_support::WidgetTextEditCmd> {
947 Some(crate::undo_support::WidgetTextEditCmd::new(
948 self.undo_id,
949 operation,
950 ))
951 }
952
953 #[must_use]
957 pub fn undo_id(&self) -> UndoWidgetId {
958 self.undo_id
959 }
960}
961
962#[cfg(test)]
963mod tests {
964 use super::*;
965
966 #[allow(dead_code)]
967 fn cell_at(frame: &Frame, x: u16, y: u16) -> Cell {
968 frame
969 .buffer
970 .get(x, y)
971 .copied()
972 .unwrap_or_else(|| panic!("test cell should exist at ({x},{y})"))
973 }
974
975 #[allow(dead_code)]
976 #[test]
977 fn test_empty_input() {
978 let input = TextInput::new();
979 assert!(input.value().is_empty());
980 assert_eq!(input.cursor(), 0);
981 assert!(input.selected_text().is_none());
982 }
983
984 #[test]
985 fn test_with_value() {
986 let mut input = TextInput::new().with_value("hello");
987 input.set_focused(true);
988 assert_eq!(input.value(), "hello");
989 assert_eq!(input.cursor(), 5);
990 }
991
992 #[test]
993 fn test_set_value() {
994 let mut input = TextInput::new().with_value("hello world");
995 input.cursor = 11;
996 input.set_value("hi");
997 assert_eq!(input.value(), "hi");
998 assert_eq!(input.cursor(), 2);
999 }
1000
1001 #[test]
1002 fn test_clear() {
1003 let mut input = TextInput::new().with_value("hello");
1004 input.set_focused(true);
1005 input.clear();
1006 assert!(input.value().is_empty());
1007 assert_eq!(input.cursor(), 0);
1008 }
1009
1010 #[test]
1011 fn test_insert_char() {
1012 let mut input = TextInput::new();
1013 input.insert_char('a');
1014 input.insert_char('b');
1015 input.insert_char('c');
1016 assert_eq!(input.value(), "abc");
1017 assert_eq!(input.cursor(), 3);
1018 }
1019
1020 #[test]
1021 fn test_insert_char_mid() {
1022 let mut input = TextInput::new().with_value("ac");
1023 input.cursor = 1;
1024 input.insert_char('b');
1025 assert_eq!(input.value(), "abc");
1026 assert_eq!(input.cursor(), 2);
1027 }
1028
1029 #[test]
1030 fn test_max_length() {
1031 let mut input = TextInput::new().with_max_length(3);
1032 for c in "abcdef".chars() {
1033 input.insert_char(c);
1034 }
1035 assert_eq!(input.value(), "abc");
1036 assert_eq!(input.cursor(), 3);
1037 }
1038
1039 #[test]
1040 fn test_delete_char_back() {
1041 let mut input = TextInput::new().with_value("hello");
1042 input.delete_char_back();
1043 assert_eq!(input.value(), "hell");
1044 assert_eq!(input.cursor(), 4);
1045 }
1046
1047 #[test]
1048 fn test_delete_char_back_at_start() {
1049 let mut input = TextInput::new().with_value("hello");
1050 input.cursor = 0;
1051 input.delete_char_back();
1052 assert_eq!(input.value(), "hello");
1053 }
1054
1055 #[test]
1056 fn test_delete_char_forward() {
1057 let mut input = TextInput::new().with_value("hello");
1058 input.cursor = 0;
1059 input.delete_char_forward();
1060 assert_eq!(input.value(), "ello");
1061 assert_eq!(input.cursor(), 0);
1062 }
1063
1064 #[test]
1065 fn test_delete_char_forward_at_end() {
1066 let mut input = TextInput::new().with_value("hello");
1067 input.delete_char_forward();
1068 assert_eq!(input.value(), "hello");
1069 }
1070
1071 #[test]
1072 fn test_cursor_left_right() {
1073 let mut input = TextInput::new().with_value("hello");
1074 assert_eq!(input.cursor(), 5);
1075 input.move_cursor_left();
1076 assert_eq!(input.cursor(), 4);
1077 input.move_cursor_left();
1078 assert_eq!(input.cursor(), 3);
1079 input.move_cursor_right();
1080 assert_eq!(input.cursor(), 4);
1081 }
1082
1083 #[test]
1084 fn test_cursor_bounds() {
1085 let mut input = TextInput::new().with_value("hi");
1086 input.cursor = 0;
1087 input.move_cursor_left();
1088 assert_eq!(input.cursor(), 0);
1089 input.cursor = 2;
1090 input.move_cursor_right();
1091 assert_eq!(input.cursor(), 2);
1092 }
1093
1094 #[test]
1095 fn test_word_movement_left() {
1096 let mut input = TextInput::new().with_value("hello world test");
1097 input.move_cursor_word_left(false);
1100 assert_eq!(input.cursor(), 12); input.move_cursor_word_left(false);
1103 assert_eq!(input.cursor(), 6); input.move_cursor_word_left(false);
1106 assert_eq!(input.cursor(), 0); }
1108
1109 #[test]
1110 fn test_word_movement_right() {
1111 let mut input = TextInput::new().with_value("hello world test");
1112 input.cursor = 0;
1113 input.move_cursor_word_right(false);
1116 assert_eq!(input.cursor(), 6); input.move_cursor_word_right(false);
1119 assert_eq!(input.cursor(), 12); input.move_cursor_word_right(false);
1122 assert_eq!(input.cursor(), 16); }
1124
1125 #[test]
1126 fn test_word_movement_skips_punctuation() {
1127 let mut input = TextInput::new().with_value("hello, world");
1128 input.cursor = 0;
1129 input.move_cursor_word_right(false);
1132 assert_eq!(input.cursor(), 7); input.move_cursor_word_left(false);
1135 assert_eq!(input.cursor(), 0); }
1137
1138 #[test]
1139 fn test_delete_word_back() {
1140 let mut input = TextInput::new().with_value("hello world");
1141 input.delete_word_back();
1143 assert_eq!(input.value(), "hello "); input.delete_word_back();
1147 assert_eq!(input.value(), ""); }
1149
1150 #[test]
1151 fn test_delete_word_forward() {
1152 let mut input = TextInput::new().with_value("hello world");
1153 input.cursor = 0;
1154 input.delete_word_forward();
1156 assert_eq!(input.value(), "world"); input.delete_word_forward();
1159 assert_eq!(input.value(), ""); }
1161
1162 #[test]
1163 fn test_select_all() {
1164 let mut input = TextInput::new().with_value("hello");
1165 input.select_all();
1166 assert_eq!(input.selected_text(), Some("hello"));
1167 }
1168
1169 #[test]
1170 fn test_delete_selection() {
1171 let mut input = TextInput::new().with_value("hello world");
1172 input.selection_anchor = Some(0);
1173 input.cursor = 5;
1174 input.delete_selection();
1175 assert_eq!(input.value(), " world");
1176 assert_eq!(input.cursor(), 0);
1177 }
1178
1179 #[test]
1180 fn test_insert_replaces_selection() {
1181 let mut input = TextInput::new().with_value("hello");
1182 input.select_all();
1183 input.delete_selection();
1184 input.insert_char('x');
1185 assert_eq!(input.value(), "x");
1186 }
1187
1188 #[test]
1189 fn test_unicode_grapheme_handling() {
1190 let mut input = TextInput::new();
1191 input.set_value("café");
1192 assert_eq!(input.grapheme_count(), 4);
1193 input.cursor = 4;
1194 input.delete_char_back();
1195 assert_eq!(input.value(), "caf");
1196 }
1197
1198 #[test]
1199 fn test_multi_codepoint_grapheme_cursor_movement() {
1200 let mut input = TextInput::new().with_value("a👩💻b");
1201 assert_eq!(input.grapheme_count(), 3);
1202 assert_eq!(input.cursor(), 3);
1203
1204 input.move_cursor_left();
1205 assert_eq!(input.cursor(), 2);
1206 input.move_cursor_left();
1207 assert_eq!(input.cursor(), 1);
1208 input.move_cursor_left();
1209 assert_eq!(input.cursor(), 0);
1210
1211 input.move_cursor_right();
1212 assert_eq!(input.cursor(), 1);
1213 input.move_cursor_right();
1214 assert_eq!(input.cursor(), 2);
1215 input.move_cursor_right();
1216 assert_eq!(input.cursor(), 3);
1217 }
1218
1219 #[test]
1220 fn test_delete_back_multi_codepoint_grapheme() {
1221 let mut input = TextInput::new().with_value("a👩💻b");
1222 input.cursor = 2; input.delete_char_back();
1224 assert_eq!(input.value(), "ab");
1225 assert_eq!(input.cursor(), 1);
1226 assert_eq!(input.grapheme_count(), 2);
1227 }
1228
1229 #[test]
1230 fn test_handle_event_char() {
1231 let mut input = TextInput::new();
1232 let event = Event::Key(KeyEvent::new(KeyCode::Char('a')));
1233 assert!(input.handle_event(&event));
1234 assert_eq!(input.value(), "a");
1235 }
1236
1237 #[test]
1238 fn test_handle_event_backspace() {
1239 let mut input = TextInput::new().with_value("ab");
1240 let event = Event::Key(KeyEvent::new(KeyCode::Backspace));
1241 assert!(input.handle_event(&event));
1242 assert_eq!(input.value(), "a");
1243 }
1244
1245 #[test]
1246 fn test_handle_event_ctrl_a() {
1247 let mut input = TextInput::new().with_value("hello");
1248 let event = Event::Key(KeyEvent::new(KeyCode::Char('a')).with_modifiers(Modifiers::CTRL));
1249 assert!(input.handle_event(&event));
1250 assert_eq!(input.selected_text(), Some("hello"));
1251 }
1252
1253 #[test]
1254 fn test_handle_event_ctrl_backspace() {
1255 let mut input = TextInput::new().with_value("hello world");
1256 let event = Event::Key(KeyEvent::new(KeyCode::Backspace).with_modifiers(Modifiers::CTRL));
1257 assert!(input.handle_event(&event));
1258 assert_eq!(input.value(), "hello ");
1259 }
1260
1261 #[test]
1262 fn test_handle_event_home_end() {
1263 let mut input = TextInput::new().with_value("hello");
1264 input.cursor = 3;
1265 let home = Event::Key(KeyEvent::new(KeyCode::Home));
1266 assert!(input.handle_event(&home));
1267 assert_eq!(input.cursor(), 0);
1268 let end = Event::Key(KeyEvent::new(KeyCode::End));
1269 assert!(input.handle_event(&end));
1270 assert_eq!(input.cursor(), 5);
1271 }
1272
1273 #[test]
1274 fn test_shift_left_creates_selection() {
1275 let mut input = TextInput::new().with_value("hello");
1276 let event = Event::Key(KeyEvent::new(KeyCode::Left).with_modifiers(Modifiers::SHIFT));
1277 assert!(input.handle_event(&event));
1278 assert_eq!(input.cursor(), 4);
1279 assert_eq!(input.selection_anchor, Some(5));
1280 assert_eq!(input.selected_text(), Some("o"));
1281 }
1282
1283 #[test]
1284 fn test_cursor_position() {
1285 let input = TextInput::new().with_value("hello");
1286 let area = Rect::new(10, 5, 20, 1);
1287 let (x, y) = input.cursor_position(area);
1288 assert_eq!(x, 15);
1289 assert_eq!(y, 5);
1290 }
1291
1292 #[test]
1293 fn test_cursor_position_empty() {
1294 let input = TextInput::new();
1295 let area = Rect::new(0, 0, 80, 1);
1296 let (x, y) = input.cursor_position(area);
1297 assert_eq!(x, 0);
1298 assert_eq!(y, 0);
1299 }
1300
1301 #[test]
1302 fn test_password_mask() {
1303 let input = TextInput::new().with_mask('*').with_value("secret");
1304 assert_eq!(input.value(), "secret");
1305 assert_eq!(input.cursor_visual_pos(), 6);
1306 }
1307
1308 #[test]
1309 fn test_render_basic() {
1310 use ftui_render::frame::Frame;
1311 use ftui_render::grapheme_pool::GraphemePool;
1312
1313 let input = TextInput::new().with_value("hi");
1314 let area = Rect::new(0, 0, 10, 1);
1315 let mut pool = GraphemePool::new();
1316 let mut frame = Frame::new(10, 1, &mut pool);
1317 input.render(area, &mut frame);
1318 let cell_h = cell_at(&frame, 0, 0);
1319 assert_eq!(cell_h.content.as_char(), Some('h'));
1320 let cell_i = cell_at(&frame, 1, 0);
1321 assert_eq!(cell_i.content.as_char(), Some('i'));
1322 }
1323
1324 #[test]
1325 fn test_render_sets_cursor_when_focused() {
1326 use ftui_render::frame::Frame;
1327 use ftui_render::grapheme_pool::GraphemePool;
1328
1329 let input = TextInput::new().with_value("hi").with_focused(true);
1330 let area = Rect::new(0, 0, 10, 1);
1331 let mut pool = GraphemePool::new();
1332 let mut frame = Frame::new(10, 1, &mut pool);
1333 input.render(area, &mut frame);
1334
1335 assert_eq!(frame.cursor_position, Some((2, 0)));
1336 assert!(frame.cursor_visible);
1337 }
1338
1339 #[test]
1340 fn test_render_does_not_set_cursor_when_unfocused() {
1341 use ftui_render::frame::Frame;
1342 use ftui_render::grapheme_pool::GraphemePool;
1343
1344 let input = TextInput::new().with_value("hi");
1345 let area = Rect::new(0, 0, 10, 1);
1346 let mut pool = GraphemePool::new();
1347 let mut frame = Frame::new(10, 1, &mut pool);
1348 input.render(area, &mut frame);
1349
1350 assert!(frame.cursor_position.is_none());
1351 }
1352
1353 #[test]
1354 fn test_render_grapheme_uses_pool() {
1355 use ftui_render::frame::Frame;
1356 use ftui_render::grapheme_pool::GraphemePool;
1357
1358 let grapheme = "👩💻";
1359 let input = TextInput::new().with_value(grapheme);
1360 let area = Rect::new(0, 0, 6, 1);
1361 let mut pool = GraphemePool::new();
1362 let mut frame = Frame::new(6, 1, &mut pool);
1363 input.render(area, &mut frame);
1364
1365 let cell = cell_at(&frame, 0, 0);
1366 assert!(cell.content.is_grapheme());
1367 let width = grapheme_width(grapheme);
1368 if width > 1 {
1369 assert!(cell_at(&frame, 1, 0).is_continuation());
1370 }
1371 }
1372
1373 #[test]
1374 fn test_left_collapses_selection() {
1375 let mut input = TextInput::new().with_value("hello");
1376 input.selection_anchor = Some(1);
1377 input.cursor = 4;
1378 input.move_cursor_left();
1379 assert_eq!(input.cursor(), 1);
1380 assert!(input.selection_anchor.is_none());
1381 }
1382
1383 #[test]
1384 fn test_right_collapses_selection() {
1385 let mut input = TextInput::new().with_value("hello");
1386 input.selection_anchor = Some(1);
1387 input.cursor = 4;
1388 input.move_cursor_right();
1389 assert_eq!(input.cursor(), 4);
1390 assert!(input.selection_anchor.is_none());
1391 }
1392
1393 #[test]
1394 fn test_render_sets_frame_cursor() {
1395 use ftui_render::frame::Frame;
1396 use ftui_render::grapheme_pool::GraphemePool;
1397
1398 let input = TextInput::new().with_value("hello").with_focused(true);
1399 let area = Rect::new(5, 3, 20, 1);
1400 let mut pool = GraphemePool::new();
1401 let mut frame = Frame::new(30, 10, &mut pool);
1402 input.render(area, &mut frame);
1403
1404 assert_eq!(frame.cursor_position, Some((10, 3)));
1408 }
1409
1410 #[test]
1411 fn test_render_cursor_mid_text() {
1412 use ftui_render::frame::Frame;
1413 use ftui_render::grapheme_pool::GraphemePool;
1414
1415 let mut input = TextInput::new().with_value("hello").with_focused(true);
1416 input.cursor = 2; let area = Rect::new(0, 0, 20, 1);
1418 let mut pool = GraphemePool::new();
1419 let mut frame = Frame::new(20, 1, &mut pool);
1420 input.render(area, &mut frame);
1421
1422 assert_eq!(frame.cursor_position, Some((2, 0)));
1424 }
1425
1426 #[test]
1431 fn test_undo_widget_id_is_stable() {
1432 let input = TextInput::new();
1433 let id1 = input.undo_id();
1434 let id2 = input.undo_id();
1435 assert_eq!(id1, id2);
1436 }
1437
1438 #[test]
1439 fn test_undo_widget_id_unique_per_instance() {
1440 let input1 = TextInput::new();
1441 let input2 = TextInput::new();
1442 assert_ne!(input1.undo_id(), input2.undo_id());
1443 }
1444
1445 #[test]
1446 fn test_snapshot_and_restore() {
1447 let mut input = TextInput::new().with_value("hello");
1448 input.cursor = 3;
1449 input.selection_anchor = Some(1);
1450
1451 let snapshot = input.create_snapshot();
1452
1453 input.set_value("world");
1455 input.cursor = 5;
1456 input.selection_anchor = None;
1457
1458 assert_eq!(input.value(), "world");
1459 assert_eq!(input.cursor(), 5);
1460
1461 assert!(input.restore_snapshot(snapshot.as_ref()));
1463 assert_eq!(input.value(), "hello");
1464 assert_eq!(input.cursor(), 3);
1465 assert_eq!(input.selection_anchor, Some(1));
1466 }
1467
1468 #[test]
1469 fn test_text_input_undo_ext_insert() {
1470 let mut input = TextInput::new().with_value("hello");
1471 input.cursor = 2;
1472
1473 input.insert_text_at(2, " world");
1474 assert_eq!(input.value(), "he worldllo");
1476 assert_eq!(input.cursor(), 8); }
1478
1479 #[test]
1480 fn test_text_input_undo_ext_delete() {
1481 let mut input = TextInput::new().with_value("hello world");
1482 input.cursor = 8;
1483
1484 input.delete_text_range(5, 11); assert_eq!(input.value(), "hello");
1486 assert_eq!(input.cursor(), 5); }
1488
1489 #[test]
1490 fn test_create_text_edit_command() {
1491 let input = TextInput::new().with_value("hello");
1492 let cmd = input.create_text_edit_command(TextEditOperation::Insert {
1493 position: 0,
1494 text: "hi".to_string(),
1495 });
1496 assert!(cmd.is_some());
1497 let cmd = cmd.expect("test command should exist");
1498 assert_eq!(cmd.widget_id(), input.undo_id());
1499 assert_eq!(cmd.description(), "Insert text");
1500 }
1501
1502 #[test]
1503 fn test_paste_bulk_insert() {
1504 let mut input = TextInput::new().with_value("hello");
1505 input.cursor = 5;
1506 let event = Event::Paste(ftui_core::event::PasteEvent::bracketed(" world"));
1507 assert!(input.handle_event(&event));
1508 assert_eq!(input.value(), "hello world");
1509 assert_eq!(input.cursor(), 11);
1510 }
1511
1512 #[test]
1513 fn test_paste_multi_grapheme_sequence() {
1514 let mut input = TextInput::new().with_value("hi");
1515 input.cursor = 2;
1516 let event = Event::Paste(ftui_core::event::PasteEvent::new("👩💻🔥", false));
1517 assert!(input.handle_event(&event));
1518 assert_eq!(input.value(), "hi👩💻🔥");
1519 assert_eq!(input.cursor(), 4);
1520 }
1521
1522 #[test]
1523 fn test_paste_max_length() {
1524 let mut input = TextInput::new().with_value("abc").with_max_length(5);
1525 input.cursor = 3;
1526 let event = Event::Paste(ftui_core::event::PasteEvent::bracketed("def"));
1528 assert!(input.handle_event(&event));
1529 assert_eq!(input.value(), "abcde");
1530 assert_eq!(input.cursor(), 5);
1531 }
1532
1533 #[test]
1534 fn test_paste_combining_merge() {
1535 let mut input = TextInput::new().with_value("e");
1536 input.cursor = 1;
1537 let event = Event::Paste(ftui_core::event::PasteEvent::bracketed("\u{0301}"));
1540 assert!(input.handle_event(&event));
1541 assert_eq!(input.value(), "e\u{0301}");
1542 assert_eq!(input.grapheme_count(), 1);
1543 assert_eq!(input.cursor(), 1);
1544 }
1545
1546 #[test]
1547 fn test_paste_combining_merge_mid_string() {
1548 let mut input = TextInput::new().with_value("ab");
1549 input.cursor = 1; let event = Event::Paste(ftui_core::event::PasteEvent::bracketed("\u{0301}"));
1551 assert!(input.handle_event(&event));
1552 assert_eq!(input.value(), "a\u{0301}b");
1553 assert_eq!(input.grapheme_count(), 2);
1554 assert_eq!(input.cursor(), 1);
1555 }
1556
1557 #[test]
1558 fn test_wide_char_scroll_visibility() {
1559 use ftui_render::frame::Frame;
1560 use ftui_render::grapheme_pool::GraphemePool;
1561
1562 let wide_char = "\u{3000}"; let mut input = TextInput::new().with_value(wide_char).with_focused(true);
1564 input.cursor = 1; let area = Rect::new(0, 0, 2, 1);
1572 let mut pool = GraphemePool::new();
1573 let mut frame = Frame::new(2, 1, &mut pool);
1574 input.render(area, &mut frame);
1575
1576 let cell = cell_at(&frame, 0, 0);
1577 assert!(!cell.is_empty(), "Wide char should be visible");
1579 }
1580}