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