1use presentar_core::{
7 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event, Key,
8 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
9};
10use std::any::Any;
11use std::time::Duration;
12
13#[derive(Debug, Clone)]
15pub struct TextInput {
16 text: String,
18 cursor: usize,
20 selection: Option<(usize, usize)>,
22 placeholder: String,
24 mask: Option<char>,
26 max_length: Option<usize>,
28 scroll_offset: usize,
30 focused: bool,
32 text_color: Color,
34 cursor_color: Color,
36 selection_color: Color,
38 placeholder_color: Color,
40 bounds: Rect,
42}
43
44impl Default for TextInput {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl TextInput {
51 #[must_use]
53 pub fn new() -> Self {
54 Self {
55 text: String::new(),
56 cursor: 0,
57 selection: None,
58 placeholder: String::new(),
59 mask: None,
60 max_length: None,
61 scroll_offset: 0,
62 focused: false,
63 text_color: Color::WHITE,
64 cursor_color: Color::new(0.8, 0.8, 0.8, 1.0),
65 selection_color: Color::new(0.3, 0.5, 0.8, 0.5),
66 placeholder_color: Color::new(0.5, 0.5, 0.5, 1.0),
67 bounds: Rect::default(),
68 }
69 }
70
71 #[must_use]
73 pub fn with_text(mut self, text: impl Into<String>) -> Self {
74 self.text = text.into();
75 self.cursor = self.text.chars().count();
76 self
77 }
78
79 #[must_use]
81 pub fn with_placeholder(mut self, text: impl Into<String>) -> Self {
82 self.placeholder = text.into();
83 self
84 }
85
86 #[must_use]
88 pub fn with_mask(mut self, ch: char) -> Self {
89 self.mask = Some(ch);
90 self
91 }
92
93 #[must_use]
95 pub fn with_max_length(mut self, len: usize) -> Self {
96 self.max_length = Some(len);
97 self
98 }
99
100 #[must_use]
102 pub fn with_focused(mut self, focused: bool) -> Self {
103 self.focused = focused;
104 self
105 }
106
107 #[must_use]
109 pub fn with_text_color(mut self, color: Color) -> Self {
110 self.text_color = color;
111 self
112 }
113
114 #[must_use]
116 pub fn with_cursor_color(mut self, color: Color) -> Self {
117 self.cursor_color = color;
118 self
119 }
120
121 #[must_use]
125 pub fn text(&self) -> &str {
126 &self.text
127 }
128
129 #[must_use]
131 pub fn cursor(&self) -> usize {
132 self.cursor
133 }
134
135 #[must_use]
137 pub fn is_empty(&self) -> bool {
138 self.text.is_empty()
139 }
140
141 #[must_use]
143 pub fn len(&self) -> usize {
144 self.text.chars().count()
145 }
146
147 #[must_use]
149 pub fn is_focused(&self) -> bool {
150 self.focused
151 }
152
153 #[must_use]
155 pub fn has_selection(&self) -> bool {
156 self.selection.is_some()
157 }
158
159 #[must_use]
161 pub fn selection(&self) -> Option<(usize, usize)> {
162 self.selection
163 }
164
165 pub fn set_text(&mut self, text: impl Into<String>) {
169 self.text = text.into();
170 let len = self.text.chars().count();
171 if let Some(max) = self.max_length {
172 if len > max {
173 self.text = self.text.chars().take(max).collect();
174 }
175 }
176 self.cursor = self.text.chars().count();
177 self.selection = None;
178 self.adjust_scroll();
179 }
180
181 pub fn insert(&mut self, ch: char) {
183 if let Some(max) = self.max_length {
184 if self.len() >= max {
185 return;
186 }
187 }
188 self.delete_selection();
189 let byte_pos = self.cursor_byte_pos();
190 self.text.insert(byte_pos, ch);
191 self.cursor += 1;
192 self.adjust_scroll();
193 }
194
195 pub fn insert_str(&mut self, s: &str) {
197 self.delete_selection();
198 let to_insert: String = if let Some(max) = self.max_length {
199 let available = max.saturating_sub(self.len());
200 s.chars().take(available).collect()
201 } else {
202 s.to_string()
203 };
204 let byte_pos = self.cursor_byte_pos();
205 self.text.insert_str(byte_pos, &to_insert);
206 self.cursor += to_insert.chars().count();
207 self.adjust_scroll();
208 }
209
210 pub fn delete(&mut self) {
212 if self.selection.is_some() {
213 self.delete_selection();
214 return;
215 }
216 let len = self.len();
217 if self.cursor < len {
218 let byte_pos = self.cursor_byte_pos();
219 let next_byte = self.char_byte_pos(self.cursor + 1);
220 self.text.drain(byte_pos..next_byte);
221 }
222 }
223
224 pub fn backspace(&mut self) {
226 if self.selection.is_some() {
227 self.delete_selection();
228 return;
229 }
230 if self.cursor > 0 {
231 self.cursor -= 1;
232 let byte_pos = self.cursor_byte_pos();
233 let next_byte = self.char_byte_pos(self.cursor + 1);
234 self.text.drain(byte_pos..next_byte);
235 self.adjust_scroll();
236 }
237 }
238
239 pub fn delete_word(&mut self) {
241 if self.selection.is_some() {
242 self.delete_selection();
243 return;
244 }
245 let start = self.cursor;
246 self.move_word_right();
247 let end = self.cursor;
248 if end > start {
249 let start_byte = self.char_byte_pos(start);
250 let end_byte = self.char_byte_pos(end);
251 self.text.drain(start_byte..end_byte);
252 self.cursor = start;
253 }
254 }
255
256 pub fn delete_to_end(&mut self) {
258 let byte_pos = self.cursor_byte_pos();
259 self.text.truncate(byte_pos);
260 }
261
262 pub fn delete_line(&mut self) {
264 self.text.clear();
265 self.cursor = 0;
266 self.selection = None;
267 self.scroll_offset = 0;
268 }
269
270 pub fn move_left(&mut self) {
274 self.selection = None;
275 if self.cursor > 0 {
276 self.cursor -= 1;
277 self.adjust_scroll();
278 }
279 }
280
281 pub fn move_right(&mut self) {
283 self.selection = None;
284 let len = self.len();
285 if self.cursor < len {
286 self.cursor += 1;
287 self.adjust_scroll();
288 }
289 }
290
291 pub fn move_word_left(&mut self) {
293 self.selection = None;
294 if self.cursor == 0 {
295 return;
296 }
297 let chars: Vec<char> = self.text.chars().collect();
298 let mut pos = self.cursor - 1;
299 while pos > 0 && chars[pos].is_whitespace() {
301 pos -= 1;
302 }
303 while pos > 0 && !chars[pos - 1].is_whitespace() {
305 pos -= 1;
306 }
307 self.cursor = pos;
308 self.adjust_scroll();
309 }
310
311 pub fn move_word_right(&mut self) {
313 self.selection = None;
314 let chars: Vec<char> = self.text.chars().collect();
315 let len = chars.len();
316 if self.cursor >= len {
317 return;
318 }
319 let mut pos = self.cursor;
320 while pos < len && !chars[pos].is_whitespace() {
322 pos += 1;
323 }
324 while pos < len && chars[pos].is_whitespace() {
326 pos += 1;
327 }
328 self.cursor = pos;
329 self.adjust_scroll();
330 }
331
332 pub fn move_home(&mut self) {
334 self.selection = None;
335 self.cursor = 0;
336 self.scroll_offset = 0;
337 }
338
339 pub fn move_end(&mut self) {
341 self.selection = None;
342 self.cursor = self.len();
343 self.adjust_scroll();
344 }
345
346 pub fn select_all(&mut self) {
350 let len = self.len();
351 if len > 0 {
352 self.selection = Some((0, len));
353 self.cursor = len;
354 }
355 }
356
357 pub fn select_word(&mut self) {
359 let chars: Vec<char> = self.text.chars().collect();
360 let len = chars.len();
361 if len == 0 || self.cursor > len {
362 return;
363 }
364 let pos = self.cursor.min(len.saturating_sub(1));
365 let mut start = pos;
366 let mut end = pos;
367 while start > 0 && !chars[start - 1].is_whitespace() {
369 start -= 1;
370 }
371 while end < len && !chars[end].is_whitespace() {
373 end += 1;
374 }
375 if start < end {
376 self.selection = Some((start, end));
377 self.cursor = end;
378 }
379 }
380
381 pub fn extend_selection_left(&mut self) {
383 if self.cursor == 0 {
384 return;
385 }
386 let new_cursor = self.cursor - 1;
387 match self.selection {
388 Some((start, end)) => {
389 if self.cursor == start {
390 self.selection = Some((new_cursor, end));
391 } else {
392 self.selection = Some((start, new_cursor));
393 }
394 }
395 None => {
396 self.selection = Some((new_cursor, self.cursor));
397 }
398 }
399 self.cursor = new_cursor;
400 self.adjust_scroll();
401 }
402
403 pub fn extend_selection_right(&mut self) {
405 let len = self.len();
406 if self.cursor >= len {
407 return;
408 }
409 let new_cursor = self.cursor + 1;
410 match self.selection {
411 Some((start, end)) => {
412 if self.cursor == end {
413 self.selection = Some((start, new_cursor));
414 } else {
415 self.selection = Some((new_cursor, end));
416 }
417 }
418 None => {
419 self.selection = Some((self.cursor, new_cursor));
420 }
421 }
422 self.cursor = new_cursor;
423 self.adjust_scroll();
424 }
425
426 pub fn clear_selection(&mut self) {
428 self.selection = None;
429 }
430
431 #[must_use]
433 pub fn selected_text(&self) -> Option<String> {
434 self.selection.map(|(start, end)| {
435 let (start, end) = (start.min(end), start.max(end));
436 self.text.chars().skip(start).take(end - start).collect()
437 })
438 }
439
440 pub fn delete_selection(&mut self) {
442 if let Some((start, end)) = self.selection.take() {
443 let (start, end) = (start.min(end), start.max(end));
444 let start_byte = self.char_byte_pos(start);
445 let end_byte = self.char_byte_pos(end);
446 self.text.drain(start_byte..end_byte);
447 self.cursor = start;
448 self.adjust_scroll();
449 }
450 }
451
452 #[must_use]
456 pub fn copy(&self) -> Option<String> {
457 self.selected_text()
458 }
459
460 pub fn cut(&mut self) -> Option<String> {
462 let text = self.selected_text();
463 self.delete_selection();
464 text
465 }
466
467 pub fn paste(&mut self, text: &str) {
469 self.delete_selection();
470 self.insert_str(text);
471 }
472
473 pub fn focus(&mut self) {
477 self.focused = true;
478 }
479
480 pub fn blur(&mut self) {
482 self.focused = false;
483 self.selection = None;
484 }
485
486 fn char_byte_pos(&self, char_idx: usize) -> usize {
490 self.text
491 .char_indices()
492 .nth(char_idx)
493 .map_or(self.text.len(), |(i, _)| i)
494 }
495
496 fn cursor_byte_pos(&self) -> usize {
498 self.char_byte_pos(self.cursor)
499 }
500
501 fn adjust_scroll(&mut self) {
503 let visible_width = self.bounds.width as usize;
504 if visible_width == 0 {
505 return;
506 }
507 if self.cursor < self.scroll_offset {
508 self.scroll_offset = self.cursor;
509 } else if self.cursor >= self.scroll_offset + visible_width {
510 self.scroll_offset = self.cursor.saturating_sub(visible_width - 1);
511 }
512 }
513
514 fn display_text(&self) -> String {
516 if let Some(mask) = self.mask {
517 std::iter::repeat_n(mask, self.len()).collect()
518 } else {
519 self.text.clone()
520 }
521 }
522}
523
524impl Brick for TextInput {
525 fn brick_name(&self) -> &'static str {
526 "text_input"
527 }
528
529 fn assertions(&self) -> &[BrickAssertion] {
530 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
531 ASSERTIONS
532 }
533
534 fn budget(&self) -> BrickBudget {
535 BrickBudget::uniform(8)
536 }
537
538 fn verify(&self) -> BrickVerification {
539 BrickVerification {
540 passed: self.assertions().to_vec(),
541 failed: vec![],
542 verification_time: Duration::from_micros(5),
543 }
544 }
545
546 fn to_html(&self) -> String {
547 String::new()
548 }
549
550 fn to_css(&self) -> String {
551 String::new()
552 }
553}
554
555impl Widget for TextInput {
556 fn type_id(&self) -> TypeId {
557 TypeId::of::<Self>()
558 }
559
560 fn measure(&self, constraints: Constraints) -> Size {
561 let width = constraints.max_width.clamp(5.0, 40.0);
562 Size::new(width, 1.0)
563 }
564
565 fn layout(&mut self, bounds: Rect) -> LayoutResult {
566 self.bounds = bounds;
567 self.adjust_scroll();
568 LayoutResult {
569 size: Size::new(bounds.width, bounds.height),
570 }
571 }
572
573 fn paint(&self, canvas: &mut dyn Canvas) {
574 let width = self.bounds.width as usize;
575 if width == 0 {
576 return;
577 }
578
579 let display = if self.is_empty() && !self.focused {
581 &self.placeholder
582 } else {
583 &self.display_text()
584 };
585
586 let text_color = if self.is_empty() && !self.focused {
587 self.placeholder_color
588 } else {
589 self.text_color
590 };
591
592 let chars: Vec<char> = display.chars().collect();
594 let visible_start = self.scroll_offset;
595 let visible_end = (visible_start + width).min(chars.len());
596
597 for (i, char_idx) in (visible_start..visible_end).enumerate() {
598 let ch = chars[char_idx];
599 let x = self.bounds.x + i as f32;
600
601 let in_selection = self.selection.is_some_and(|(start, end)| {
603 let (s, e) = (start.min(end), start.max(end));
604 char_idx >= s && char_idx < e
605 });
606
607 if in_selection {
609 canvas.fill_rect(Rect::new(x, self.bounds.y, 1.0, 1.0), self.selection_color);
610 }
611
612 let style = TextStyle {
613 color: text_color,
614 ..Default::default()
615 };
616 canvas.draw_text(&ch.to_string(), Point::new(x, self.bounds.y), &style);
617 }
618
619 if self.focused
621 && self.cursor >= self.scroll_offset
622 && self.cursor < self.scroll_offset + width
623 {
624 let cursor_x = self.bounds.x + (self.cursor - self.scroll_offset) as f32;
625 canvas.fill_rect(
626 Rect::new(cursor_x, self.bounds.y, 0.1, 1.0),
627 self.cursor_color,
628 );
629 }
630 }
631
632 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
633 if !self.focused {
634 return None;
635 }
636
637 match event {
638 Event::KeyDown { key, .. } => match key {
639 Key::Backspace => {
640 self.backspace();
641 }
642 Key::Delete => {
643 self.delete();
644 }
645 Key::Left => {
646 self.move_left();
647 }
648 Key::Right => {
649 self.move_right();
650 }
651 Key::Home => {
652 self.move_home();
653 }
654 Key::End => {
655 self.move_end();
656 }
657 _ => {}
658 },
659 Event::TextInput { text } => {
660 for ch in text.chars() {
661 self.insert(ch);
662 }
663 }
664 _ => {}
665 }
666
667 None
668 }
669
670 fn children(&self) -> &[Box<dyn Widget>] {
671 &[]
672 }
673
674 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
675 &mut []
676 }
677}
678
679#[cfg(test)]
680mod tests {
681 use super::*;
682
683 struct MockCanvas {
684 texts: Vec<(String, Point)>,
685 rects: Vec<Rect>,
686 }
687
688 impl MockCanvas {
689 fn new() -> Self {
690 Self {
691 texts: vec![],
692 rects: vec![],
693 }
694 }
695 }
696
697 impl Canvas for MockCanvas {
698 fn fill_rect(&mut self, rect: Rect, _color: Color) {
699 self.rects.push(rect);
700 }
701 fn stroke_rect(&mut self, _rect: Rect, _color: Color, _width: f32) {}
702 fn draw_text(&mut self, text: &str, position: Point, _style: &TextStyle) {
703 self.texts.push((text.to_string(), position));
704 }
705 fn draw_line(&mut self, _from: Point, _to: Point, _color: Color, _width: f32) {}
706 fn fill_circle(&mut self, _center: Point, _radius: f32, _color: Color) {}
707 fn stroke_circle(&mut self, _center: Point, _radius: f32, _color: Color, _width: f32) {}
708 fn fill_arc(&mut self, _c: Point, _r: f32, _s: f32, _e: f32, _color: Color) {}
709 fn draw_path(&mut self, _points: &[Point], _color: Color, _width: f32) {}
710 fn fill_polygon(&mut self, _points: &[Point], _color: Color) {}
711 fn push_clip(&mut self, _rect: Rect) {}
712 fn pop_clip(&mut self) {}
713 fn push_transform(&mut self, _transform: presentar_core::Transform2D) {}
714 fn pop_transform(&mut self) {}
715 }
716
717 #[test]
722 fn test_new() {
723 let input = TextInput::new();
724 assert!(input.is_empty());
725 assert_eq!(input.cursor(), 0);
726 assert!(!input.is_focused());
727 }
728
729 #[test]
730 fn test_default() {
731 let input = TextInput::default();
732 assert!(input.is_empty());
733 }
734
735 #[test]
736 fn test_with_text() {
737 let input = TextInput::new().with_text("hello");
738 assert_eq!(input.text(), "hello");
739 assert_eq!(input.cursor(), 5);
740 }
741
742 #[test]
743 fn test_with_placeholder() {
744 let input = TextInput::new().with_placeholder("Enter text...");
745 assert_eq!(input.placeholder, "Enter text...");
746 }
747
748 #[test]
749 fn test_with_mask() {
750 let input = TextInput::new().with_mask('*');
751 assert_eq!(input.mask, Some('*'));
752 }
753
754 #[test]
755 fn test_with_max_length() {
756 let input = TextInput::new().with_max_length(10);
757 assert_eq!(input.max_length, Some(10));
758 }
759
760 #[test]
761 fn test_with_focused() {
762 let input = TextInput::new().with_focused(true);
763 assert!(input.is_focused());
764 }
765
766 #[test]
767 fn test_with_text_color() {
768 let input = TextInput::new().with_text_color(Color::RED);
769 assert_eq!(input.text_color, Color::RED);
770 }
771
772 #[test]
773 fn test_with_cursor_color() {
774 let input = TextInput::new().with_cursor_color(Color::GREEN);
775 assert_eq!(input.cursor_color, Color::GREEN);
776 }
777
778 #[test]
783 fn test_len() {
784 let input = TextInput::new().with_text("hello");
785 assert_eq!(input.len(), 5);
786 }
787
788 #[test]
789 fn test_len_unicode() {
790 let input = TextInput::new().with_text("héllo");
791 assert_eq!(input.len(), 5);
792 }
793
794 #[test]
795 fn test_is_empty_true() {
796 let input = TextInput::new();
797 assert!(input.is_empty());
798 }
799
800 #[test]
801 fn test_is_empty_false() {
802 let input = TextInput::new().with_text("x");
803 assert!(!input.is_empty());
804 }
805
806 #[test]
811 fn test_set_text() {
812 let mut input = TextInput::new();
813 input.set_text("hello");
814 assert_eq!(input.text(), "hello");
815 assert_eq!(input.cursor(), 5);
816 }
817
818 #[test]
819 fn test_set_text_clears_selection() {
820 let mut input = TextInput::new().with_text("hello");
821 input.select_all();
822 input.set_text("world");
823 assert!(!input.has_selection());
824 }
825
826 #[test]
827 fn test_set_text_respects_max_length() {
828 let mut input = TextInput::new().with_max_length(3);
829 input.set_text("hello");
830 assert_eq!(input.text(), "hel");
831 }
832
833 #[test]
834 fn test_insert() {
835 let mut input = TextInput::new();
836 input.insert('a');
837 input.insert('b');
838 assert_eq!(input.text(), "ab");
839 assert_eq!(input.cursor(), 2);
840 }
841
842 #[test]
843 fn test_insert_respects_max_length() {
844 let mut input = TextInput::new().with_max_length(2);
845 input.insert('a');
846 input.insert('b');
847 input.insert('c');
848 assert_eq!(input.text(), "ab");
849 }
850
851 #[test]
852 fn test_insert_deletes_selection() {
853 let mut input = TextInput::new().with_text("hello");
854 input.select_all();
855 input.insert('X');
856 assert_eq!(input.text(), "X");
857 }
858
859 #[test]
860 fn test_insert_str() {
861 let mut input = TextInput::new();
862 input.insert_str("hello");
863 assert_eq!(input.text(), "hello");
864 assert_eq!(input.cursor(), 5);
865 }
866
867 #[test]
868 fn test_insert_str_respects_max_length() {
869 let mut input = TextInput::new().with_max_length(3);
870 input.insert_str("hello");
871 assert_eq!(input.text(), "hel");
872 }
873
874 #[test]
875 fn test_delete() {
876 let mut input = TextInput::new().with_text("hello");
877 input.cursor = 2;
878 input.delete();
879 assert_eq!(input.text(), "helo");
880 }
881
882 #[test]
883 fn test_delete_at_end() {
884 let mut input = TextInput::new().with_text("hello");
885 input.delete();
886 assert_eq!(input.text(), "hello");
887 }
888
889 #[test]
890 fn test_delete_selection() {
891 let mut input = TextInput::new().with_text("hello");
892 input.select_all();
893 input.delete();
894 assert!(input.is_empty());
895 }
896
897 #[test]
898 fn test_backspace() {
899 let mut input = TextInput::new().with_text("hello");
900 input.backspace();
901 assert_eq!(input.text(), "hell");
902 assert_eq!(input.cursor(), 4);
903 }
904
905 #[test]
906 fn test_backspace_at_start() {
907 let mut input = TextInput::new().with_text("hello");
908 input.cursor = 0;
909 input.backspace();
910 assert_eq!(input.text(), "hello");
911 }
912
913 #[test]
914 fn test_delete_word() {
915 let mut input = TextInput::new().with_text("hello world");
916 input.cursor = 0;
917 input.delete_word();
918 assert_eq!(input.text(), "world");
919 }
920
921 #[test]
922 fn test_delete_to_end() {
923 let mut input = TextInput::new().with_text("hello world");
924 input.cursor = 6;
925 input.delete_to_end();
926 assert_eq!(input.text(), "hello ");
927 }
928
929 #[test]
930 fn test_delete_line() {
931 let mut input = TextInput::new().with_text("hello world");
932 input.delete_line();
933 assert!(input.is_empty());
934 assert_eq!(input.cursor(), 0);
935 }
936
937 #[test]
942 fn test_move_left() {
943 let mut input = TextInput::new().with_text("hello");
944 input.move_left();
945 assert_eq!(input.cursor(), 4);
946 }
947
948 #[test]
949 fn test_move_left_at_start() {
950 let mut input = TextInput::new().with_text("hello");
951 input.cursor = 0;
952 input.move_left();
953 assert_eq!(input.cursor(), 0);
954 }
955
956 #[test]
957 fn test_move_left_clears_selection() {
958 let mut input = TextInput::new().with_text("hello");
959 input.select_all();
960 input.move_left();
961 assert!(!input.has_selection());
962 }
963
964 #[test]
965 fn test_move_right() {
966 let mut input = TextInput::new().with_text("hello");
967 input.cursor = 0;
968 input.move_right();
969 assert_eq!(input.cursor(), 1);
970 }
971
972 #[test]
973 fn test_move_right_at_end() {
974 let mut input = TextInput::new().with_text("hello");
975 input.move_right();
976 assert_eq!(input.cursor(), 5);
977 }
978
979 #[test]
980 fn test_move_word_left() {
981 let mut input = TextInput::new().with_text("hello world");
982 input.move_word_left();
983 assert_eq!(input.cursor(), 6);
984 }
985
986 #[test]
987 fn test_move_word_right() {
988 let mut input = TextInput::new().with_text("hello world");
989 input.cursor = 0;
990 input.move_word_right();
991 assert_eq!(input.cursor(), 6);
992 }
993
994 #[test]
995 fn test_move_home() {
996 let mut input = TextInput::new().with_text("hello");
997 input.move_home();
998 assert_eq!(input.cursor(), 0);
999 }
1000
1001 #[test]
1002 fn test_move_end() {
1003 let mut input = TextInput::new().with_text("hello");
1004 input.cursor = 0;
1005 input.move_end();
1006 assert_eq!(input.cursor(), 5);
1007 }
1008
1009 #[test]
1014 fn test_select_all() {
1015 let mut input = TextInput::new().with_text("hello");
1016 input.select_all();
1017 assert_eq!(input.selection(), Some((0, 5)));
1018 }
1019
1020 #[test]
1021 fn test_select_all_empty() {
1022 let mut input = TextInput::new();
1023 input.select_all();
1024 assert!(!input.has_selection());
1025 }
1026
1027 #[test]
1028 fn test_select_word() {
1029 let mut input = TextInput::new().with_text("hello world");
1030 input.cursor = 2;
1031 input.select_word();
1032 assert_eq!(input.selection(), Some((0, 5)));
1033 }
1034
1035 #[test]
1036 fn test_selected_text() {
1037 let mut input = TextInput::new().with_text("hello");
1038 input.select_all();
1039 assert_eq!(input.selected_text(), Some("hello".to_string()));
1040 }
1041
1042 #[test]
1043 fn test_selected_text_none() {
1044 let input = TextInput::new().with_text("hello");
1045 assert_eq!(input.selected_text(), None);
1046 }
1047
1048 #[test]
1049 fn test_extend_selection_left() {
1050 let mut input = TextInput::new().with_text("hello");
1051 input.extend_selection_left();
1052 assert_eq!(input.selection(), Some((4, 5)));
1053 assert_eq!(input.cursor(), 4);
1054 }
1055
1056 #[test]
1057 fn test_extend_selection_right() {
1058 let mut input = TextInput::new().with_text("hello");
1059 input.cursor = 0;
1060 input.extend_selection_right();
1061 assert_eq!(input.selection(), Some((0, 1)));
1062 assert_eq!(input.cursor(), 1);
1063 }
1064
1065 #[test]
1066 fn test_clear_selection() {
1067 let mut input = TextInput::new().with_text("hello");
1068 input.select_all();
1069 input.clear_selection();
1070 assert!(!input.has_selection());
1071 }
1072
1073 #[test]
1078 fn test_copy() {
1079 let mut input = TextInput::new().with_text("hello");
1080 input.select_all();
1081 assert_eq!(input.copy(), Some("hello".to_string()));
1082 assert!(input.has_selection()); }
1084
1085 #[test]
1086 fn test_cut() {
1087 let mut input = TextInput::new().with_text("hello");
1088 input.select_all();
1089 assert_eq!(input.cut(), Some("hello".to_string()));
1090 assert!(input.is_empty());
1091 }
1092
1093 #[test]
1094 fn test_paste() {
1095 let mut input = TextInput::new();
1096 input.paste("hello");
1097 assert_eq!(input.text(), "hello");
1098 }
1099
1100 #[test]
1101 fn test_paste_replaces_selection() {
1102 let mut input = TextInput::new().with_text("hello");
1103 input.select_all();
1104 input.paste("world");
1105 assert_eq!(input.text(), "world");
1106 }
1107
1108 #[test]
1113 fn test_focus() {
1114 let mut input = TextInput::new();
1115 input.focus();
1116 assert!(input.is_focused());
1117 }
1118
1119 #[test]
1120 fn test_blur() {
1121 let mut input = TextInput::new().with_focused(true);
1122 input.select_all();
1123 input.blur();
1124 assert!(!input.is_focused());
1125 assert!(!input.has_selection());
1126 }
1127
1128 #[test]
1133 fn test_display_text_normal() {
1134 let input = TextInput::new().with_text("hello");
1135 assert_eq!(input.display_text(), "hello");
1136 }
1137
1138 #[test]
1139 fn test_display_text_masked() {
1140 let input = TextInput::new().with_text("hello").with_mask('*');
1141 assert_eq!(input.display_text(), "*****");
1142 }
1143
1144 #[test]
1149 fn test_brick_name() {
1150 let input = TextInput::new();
1151 assert_eq!(input.brick_name(), "text_input");
1152 }
1153
1154 #[test]
1155 fn test_assertions_not_empty() {
1156 let input = TextInput::new();
1157 assert!(!input.assertions().is_empty());
1158 }
1159
1160 #[test]
1161 fn test_budget() {
1162 let input = TextInput::new();
1163 assert!(input.budget().paint_ms > 0);
1164 }
1165
1166 #[test]
1167 fn test_verify() {
1168 let input = TextInput::new();
1169 assert!(input.verify().is_valid());
1170 }
1171
1172 #[test]
1173 fn test_to_html() {
1174 let input = TextInput::new();
1175 assert!(input.to_html().is_empty());
1176 }
1177
1178 #[test]
1179 fn test_to_css() {
1180 let input = TextInput::new();
1181 assert!(input.to_css().is_empty());
1182 }
1183
1184 #[test]
1189 fn test_type_id() {
1190 let input = TextInput::new();
1191 assert_eq!(Widget::type_id(&input), TypeId::of::<TextInput>());
1192 }
1193
1194 #[test]
1195 fn test_measure() {
1196 let input = TextInput::new();
1197 let size = input.measure(Constraints::loose(Size::new(100.0, 100.0)));
1198 assert!(size.width >= 5.0);
1199 assert_eq!(size.height, 1.0);
1200 }
1201
1202 #[test]
1203 fn test_layout() {
1204 let mut input = TextInput::new();
1205 let bounds = Rect::new(0.0, 0.0, 20.0, 1.0);
1206 let result = input.layout(bounds);
1207 assert_eq!(result.size.width, 20.0);
1208 assert_eq!(input.bounds, bounds);
1209 }
1210
1211 #[test]
1212 fn test_children() {
1213 let input = TextInput::new();
1214 assert!(input.children().is_empty());
1215 }
1216
1217 #[test]
1218 fn test_children_mut() {
1219 let mut input = TextInput::new();
1220 assert!(input.children_mut().is_empty());
1221 }
1222
1223 #[test]
1228 fn test_paint_empty() {
1229 let mut input = TextInput::new();
1230 input.bounds = Rect::new(0.0, 0.0, 10.0, 1.0);
1231 let mut canvas = MockCanvas::new();
1232 input.paint(&mut canvas);
1233 }
1235
1236 #[test]
1237 fn test_paint_with_text() {
1238 let mut input = TextInput::new().with_text("hello");
1239 input.bounds = Rect::new(0.0, 0.0, 10.0, 1.0);
1240 let mut canvas = MockCanvas::new();
1241 input.paint(&mut canvas);
1242 assert!(!canvas.texts.is_empty());
1243 }
1244
1245 #[test]
1246 fn test_paint_with_cursor() {
1247 let mut input = TextInput::new().with_text("hello").with_focused(true);
1248 input.bounds = Rect::new(0.0, 0.0, 10.0, 1.0);
1249 let mut canvas = MockCanvas::new();
1250 input.paint(&mut canvas);
1251 assert!(!canvas.rects.is_empty()); }
1253
1254 #[test]
1255 fn test_paint_with_selection() {
1256 let mut input = TextInput::new().with_text("hello").with_focused(true);
1257 input.select_all();
1258 input.bounds = Rect::new(0.0, 0.0, 10.0, 1.0);
1259 let mut canvas = MockCanvas::new();
1260 input.paint(&mut canvas);
1261 assert!(!canvas.rects.is_empty());
1263 }
1264
1265 #[test]
1266 fn test_paint_placeholder() {
1267 let mut input = TextInput::new().with_placeholder("Type here...");
1268 input.bounds = Rect::new(0.0, 0.0, 20.0, 1.0);
1269 let mut canvas = MockCanvas::new();
1270 input.paint(&mut canvas);
1271 assert!(!canvas.texts.is_empty());
1273 }
1274
1275 #[test]
1280 fn test_event_not_focused() {
1281 let mut input = TextInput::new();
1282 let event = Event::key_down(Key::Left);
1283 assert!(input.event(&event).is_none());
1284 }
1286
1287 #[test]
1288 fn test_event_backspace() {
1289 let mut input = TextInput::new().with_text("hello").with_focused(true);
1290 let event = Event::key_down(Key::Backspace);
1291 input.event(&event);
1292 assert_eq!(input.text(), "hell");
1293 }
1294
1295 #[test]
1296 fn test_event_delete() {
1297 let mut input = TextInput::new().with_text("hello").with_focused(true);
1298 input.cursor = 0;
1299 let event = Event::key_down(Key::Delete);
1300 input.event(&event);
1301 assert_eq!(input.text(), "ello");
1302 }
1303
1304 #[test]
1305 fn test_event_left() {
1306 let mut input = TextInput::new().with_text("hello").with_focused(true);
1307 let event = Event::key_down(Key::Left);
1308 input.event(&event);
1309 assert_eq!(input.cursor(), 4);
1310 }
1311
1312 #[test]
1313 fn test_event_right() {
1314 let mut input = TextInput::new().with_text("hello").with_focused(true);
1315 input.cursor = 0;
1316 let event = Event::key_down(Key::Right);
1317 input.event(&event);
1318 assert_eq!(input.cursor(), 1);
1319 }
1320
1321 #[test]
1322 fn test_event_home() {
1323 let mut input = TextInput::new().with_text("hello").with_focused(true);
1324 let event = Event::key_down(Key::Home);
1325 input.event(&event);
1326 assert_eq!(input.cursor(), 0);
1327 }
1328
1329 #[test]
1330 fn test_event_end() {
1331 let mut input = TextInput::new().with_text("hello").with_focused(true);
1332 input.cursor = 0;
1333 let event = Event::key_down(Key::End);
1334 input.event(&event);
1335 assert_eq!(input.cursor(), 5);
1336 }
1337
1338 #[test]
1339 fn test_event_text_input() {
1340 let mut input = TextInput::new().with_focused(true);
1341 let event = Event::TextInput {
1342 text: "hi".to_string(),
1343 };
1344 input.event(&event);
1345 assert_eq!(input.text(), "hi");
1346 }
1347
1348 #[test]
1353 fn test_unicode_insert() {
1354 let mut input = TextInput::new();
1355 input.insert('é');
1356 input.insert('ñ');
1357 assert_eq!(input.text(), "éñ");
1358 assert_eq!(input.len(), 2);
1359 }
1360
1361 #[test]
1362 fn test_unicode_backspace() {
1363 let mut input = TextInput::new().with_text("héllo");
1364 input.backspace();
1365 assert_eq!(input.text(), "héll");
1366 }
1367
1368 #[test]
1369 fn test_unicode_cursor() {
1370 let mut input = TextInput::new().with_text("héllo");
1371 input.cursor = 2;
1372 input.insert('X');
1373 assert_eq!(input.text(), "héXllo");
1374 }
1375
1376 #[test]
1381 fn test_scroll_on_insert() {
1382 let mut input = TextInput::new();
1383 input.bounds = Rect::new(0.0, 0.0, 5.0, 1.0);
1384 for c in "hello world".chars() {
1385 input.insert(c);
1386 }
1387 assert!(input.scroll_offset > 0);
1389 }
1390
1391 #[test]
1392 fn test_scroll_on_move_home() {
1393 let mut input = TextInput::new().with_text("hello world");
1394 input.bounds = Rect::new(0.0, 0.0, 5.0, 1.0);
1395 input.scroll_offset = 6;
1396 input.move_home();
1397 assert_eq!(input.scroll_offset, 0);
1398 }
1399}