1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult, TextStyle},
5 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event, Key,
6 Rect, Size, TypeId, Widget,
7};
8use serde::{Deserialize, Serialize};
9use std::any::Any;
10use std::time::Duration;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct TextChanged {
15 pub value: String,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct TextSubmitted {
22 pub value: String,
24}
25
26#[derive(Serialize, Deserialize)]
28pub struct TextInput {
29 value: String,
31 placeholder: String,
33 disabled: bool,
35 obscure: bool,
37 max_length: usize,
39 text_style: TextStyle,
41 placeholder_color: Color,
43 background_color: Color,
45 border_color: Color,
47 focus_border_color: Color,
49 padding: f32,
51 min_width: f32,
53 test_id_value: Option<String>,
55 accessible_name_value: Option<String>,
57 #[serde(skip)]
59 bounds: Rect,
60 #[serde(skip)]
62 focused: bool,
63 #[serde(skip)]
65 cursor: usize,
66}
67
68impl Default for TextInput {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74impl TextInput {
75 #[must_use]
77 pub fn new() -> Self {
78 Self {
79 value: String::new(),
80 placeholder: String::new(),
81 disabled: false,
82 obscure: false,
83 max_length: 0,
84 text_style: TextStyle::default(),
85 placeholder_color: Color::new(0.6, 0.6, 0.6, 1.0),
86 background_color: Color::WHITE,
87 border_color: Color::new(0.8, 0.8, 0.8, 1.0),
88 focus_border_color: Color::new(0.2, 0.6, 1.0, 1.0),
89 padding: 8.0,
90 min_width: 100.0,
91 test_id_value: None,
92 accessible_name_value: None,
93 bounds: Rect::default(),
94 focused: false,
95 cursor: 0,
96 }
97 }
98
99 #[must_use]
101 pub fn value(mut self, value: impl Into<String>) -> Self {
102 self.value = value.into();
103 if self.max_length > 0 && self.value.len() > self.max_length {
104 self.value.truncate(self.max_length);
105 }
106 self.cursor = self.value.len();
107 self
108 }
109
110 #[must_use]
112 pub fn placeholder(mut self, text: impl Into<String>) -> Self {
113 self.placeholder = text.into();
114 self
115 }
116
117 #[must_use]
119 pub const fn disabled(mut self, disabled: bool) -> Self {
120 self.disabled = disabled;
121 self
122 }
123
124 #[must_use]
126 pub const fn obscure(mut self, obscure: bool) -> Self {
127 self.obscure = obscure;
128 self
129 }
130
131 #[must_use]
133 pub fn max_length(mut self, max: usize) -> Self {
134 self.max_length = max;
135 if max > 0 && self.value.len() > max {
136 self.value.truncate(max);
137 self.cursor = self.cursor.min(max);
138 }
139 self
140 }
141
142 #[must_use]
144 pub const fn text_style(mut self, style: TextStyle) -> Self {
145 self.text_style = style;
146 self
147 }
148
149 #[must_use]
151 pub const fn placeholder_color(mut self, color: Color) -> Self {
152 self.placeholder_color = color;
153 self
154 }
155
156 #[must_use]
158 pub const fn background_color(mut self, color: Color) -> Self {
159 self.background_color = color;
160 self
161 }
162
163 #[must_use]
165 pub const fn border_color(mut self, color: Color) -> Self {
166 self.border_color = color;
167 self
168 }
169
170 #[must_use]
172 pub const fn focus_border_color(mut self, color: Color) -> Self {
173 self.focus_border_color = color;
174 self
175 }
176
177 #[must_use]
179 pub fn padding(mut self, padding: f32) -> Self {
180 self.padding = padding.max(0.0);
181 self
182 }
183
184 #[must_use]
186 pub fn min_width(mut self, width: f32) -> Self {
187 self.min_width = width.max(0.0);
188 self
189 }
190
191 #[must_use]
193 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
194 self.test_id_value = Some(id.into());
195 self
196 }
197
198 #[must_use]
200 pub fn with_accessible_name(mut self, name: impl Into<String>) -> Self {
201 self.accessible_name_value = Some(name.into());
202 self
203 }
204
205 #[must_use]
207 pub fn get_value(&self) -> &str {
208 &self.value
209 }
210
211 #[must_use]
213 pub fn get_placeholder(&self) -> &str {
214 &self.placeholder
215 }
216
217 #[must_use]
219 pub fn is_empty(&self) -> bool {
220 self.value.is_empty()
221 }
222
223 #[must_use]
225 pub const fn cursor_position(&self) -> usize {
226 self.cursor
227 }
228
229 #[must_use]
231 pub const fn is_focused(&self) -> bool {
232 self.focused
233 }
234
235 #[must_use]
237 pub fn display_text(&self) -> String {
238 if self.obscure {
239 "•".repeat(self.value.len())
240 } else {
241 self.value.clone()
242 }
243 }
244
245 fn insert_text(&mut self, text: &str) -> bool {
247 if self.disabled {
248 return false;
249 }
250
251 let mut changed = false;
252 for c in text.chars() {
253 if self.max_length > 0 && self.value.len() >= self.max_length {
254 break;
255 }
256 self.value.insert(self.cursor, c);
257 self.cursor += 1;
258 changed = true;
259 }
260 changed
261 }
262
263 fn backspace(&mut self) -> bool {
265 if self.disabled || self.cursor == 0 {
266 return false;
267 }
268 self.cursor -= 1;
269 self.value.remove(self.cursor);
270 true
271 }
272
273 fn delete(&mut self) -> bool {
275 if self.disabled || self.cursor >= self.value.len() {
276 return false;
277 }
278 self.value.remove(self.cursor);
279 true
280 }
281
282 fn move_left(&mut self) {
284 if self.cursor > 0 {
285 self.cursor -= 1;
286 }
287 }
288
289 fn move_right(&mut self) {
291 if self.cursor < self.value.len() {
292 self.cursor += 1;
293 }
294 }
295
296 fn move_home(&mut self) {
298 self.cursor = 0;
299 }
300
301 fn move_end(&mut self) {
303 self.cursor = self.value.len();
304 }
305}
306
307impl Widget for TextInput {
308 fn type_id(&self) -> TypeId {
309 TypeId::of::<Self>()
310 }
311
312 fn measure(&self, constraints: Constraints) -> Size {
313 let height = 2.0f32.mul_add(self.padding, self.text_style.size);
314 let width = self.min_width.max(constraints.min_width);
315 constraints.constrain(Size::new(width, height))
316 }
317
318 fn layout(&mut self, bounds: Rect) -> LayoutResult {
319 self.bounds = bounds;
320 LayoutResult {
321 size: bounds.size(),
322 }
323 }
324
325 fn paint(&self, canvas: &mut dyn Canvas) {
326 canvas.fill_rect(self.bounds, self.background_color);
328
329 let border_color = if self.focused {
331 self.focus_border_color
332 } else {
333 self.border_color
334 };
335 canvas.stroke_rect(self.bounds, border_color, 1.0);
336
337 let text_x = self.bounds.x + self.padding;
339 let text_y = self.bounds.y + self.padding;
340 let position = presentar_core::Point::new(text_x, text_y);
341
342 if self.value.is_empty() {
343 let mut placeholder_style = self.text_style.clone();
345 placeholder_style.color = self.placeholder_color;
346 canvas.draw_text(&self.placeholder, position, &placeholder_style);
347 } else {
348 let display = self.display_text();
350 canvas.draw_text(&display, position, &self.text_style);
351 }
352 }
353
354 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
355 if self.disabled {
356 return None;
357 }
358
359 match event {
360 Event::MouseDown { position, .. } => {
361 let was_focused = self.focused;
362 self.focused = self.bounds.contains_point(position);
363 if self.focused && !was_focused {
364 self.cursor = self.value.len();
365 }
366 }
367 Event::FocusIn => {
368 self.focused = true;
369 }
370 Event::FocusOut => {
371 self.focused = false;
372 }
373 Event::TextInput { text } if self.focused => {
374 if self.insert_text(text) {
375 return Some(Box::new(TextChanged {
376 value: self.value.clone(),
377 }));
378 }
379 }
380 Event::KeyDown { key } if self.focused => match key {
381 Key::Backspace => {
382 if self.backspace() {
383 return Some(Box::new(TextChanged {
384 value: self.value.clone(),
385 }));
386 }
387 }
388 Key::Delete => {
389 if self.delete() {
390 return Some(Box::new(TextChanged {
391 value: self.value.clone(),
392 }));
393 }
394 }
395 Key::Left => self.move_left(),
396 Key::Right => self.move_right(),
397 Key::Home => self.move_home(),
398 Key::End => self.move_end(),
399 Key::Enter => {
400 return Some(Box::new(TextSubmitted {
401 value: self.value.clone(),
402 }));
403 }
404 _ => {}
405 },
406 _ => {}
407 }
408
409 None
410 }
411
412 fn children(&self) -> &[Box<dyn Widget>] {
413 &[]
414 }
415
416 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
417 &mut []
418 }
419
420 fn is_interactive(&self) -> bool {
421 !self.disabled
422 }
423
424 fn is_focusable(&self) -> bool {
425 !self.disabled
426 }
427
428 fn accessible_name(&self) -> Option<&str> {
429 self.accessible_name_value.as_deref()
430 }
431
432 fn accessible_role(&self) -> AccessibleRole {
433 AccessibleRole::TextInput
434 }
435
436 fn test_id(&self) -> Option<&str> {
437 self.test_id_value.as_deref()
438 }
439}
440
441impl Brick for TextInput {
443 fn brick_name(&self) -> &'static str {
444 "TextInput"
445 }
446
447 fn assertions(&self) -> &[BrickAssertion] {
448 &[
449 BrickAssertion::MaxLatencyMs(16),
450 BrickAssertion::ContrastRatio(4.5), ]
452 }
453
454 fn budget(&self) -> BrickBudget {
455 BrickBudget::uniform(16)
456 }
457
458 fn verify(&self) -> BrickVerification {
459 let mut passed = Vec::new();
460 let mut failed = Vec::new();
461
462 let contrast = self.background_color.contrast_ratio(&self.text_style.color);
464 if contrast >= 4.5 {
465 passed.push(BrickAssertion::ContrastRatio(4.5));
466 } else {
467 failed.push((
468 BrickAssertion::ContrastRatio(4.5),
469 format!("Contrast ratio {contrast:.2}:1 < 4.5:1"),
470 ));
471 }
472
473 passed.push(BrickAssertion::MaxLatencyMs(16));
475
476 BrickVerification {
477 passed,
478 failed,
479 verification_time: Duration::from_micros(10),
480 }
481 }
482
483 fn to_html(&self) -> String {
484 let test_id = self.test_id_value.as_deref().unwrap_or("text-input");
485 let disabled = if self.disabled { " disabled" } else { "" };
486 let input_type = if self.obscure { "password" } else { "text" };
487 let aria_label = self.accessible_name_value.as_deref().unwrap_or("");
488 format!(
489 r#"<input type="{}" class="brick-textinput" data-testid="{}" placeholder="{}" value="{}" aria-label="{}"{} />"#,
490 input_type, test_id, self.placeholder, self.value, aria_label, disabled
491 )
492 }
493
494 fn to_css(&self) -> String {
495 format!(
496 r".brick-textinput {{
497 background: {};
498 color: {};
499 border: 1px solid {};
500 padding: {}px;
501 min-width: {}px;
502 font-size: {}px;
503}}
504.brick-textinput:focus {{
505 border-color: {};
506 outline: none;
507}}
508.brick-textinput:disabled {{
509 opacity: 0.5;
510 cursor: not-allowed;
511}}",
512 self.background_color.to_hex(),
513 self.text_style.color.to_hex(),
514 self.border_color.to_hex(),
515 self.padding,
516 self.min_width,
517 self.text_style.size,
518 self.focus_border_color.to_hex(),
519 )
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use presentar_core::Widget;
527
528 #[test]
533 fn test_text_changed_message() {
534 let msg = TextChanged {
535 value: "hello".to_string(),
536 };
537 assert_eq!(msg.value, "hello");
538 }
539
540 #[test]
541 fn test_text_submitted_message() {
542 let msg = TextSubmitted {
543 value: "world".to_string(),
544 };
545 assert_eq!(msg.value, "world");
546 }
547
548 #[test]
553 fn test_text_input_new() {
554 let input = TextInput::new();
555 assert!(input.get_value().is_empty());
556 assert!(input.get_placeholder().is_empty());
557 assert!(input.is_empty());
558 assert!(!input.disabled);
559 assert!(!input.obscure);
560 }
561
562 #[test]
563 fn test_text_input_default() {
564 let input = TextInput::default();
565 assert!(input.is_empty());
566 }
567
568 #[test]
569 fn test_text_input_builder() {
570 let input = TextInput::new()
571 .value("hello")
572 .placeholder("Enter text...")
573 .disabled(true)
574 .obscure(true)
575 .max_length(20)
576 .padding(10.0)
577 .min_width(200.0)
578 .with_test_id("my-input")
579 .with_accessible_name("Email");
580
581 assert_eq!(input.get_value(), "hello");
582 assert_eq!(input.get_placeholder(), "Enter text...");
583 assert!(input.disabled);
584 assert!(input.obscure);
585 assert_eq!(input.max_length, 20);
586 assert_eq!(Widget::test_id(&input), Some("my-input"));
587 assert_eq!(input.accessible_name(), Some("Email"));
588 }
589
590 #[test]
595 fn test_text_input_value() {
596 let input = TextInput::new().value("test");
597 assert_eq!(input.get_value(), "test");
598 assert!(!input.is_empty());
599 }
600
601 #[test]
602 fn test_text_input_max_length_truncate() {
603 let input = TextInput::new().max_length(5).value("hello world");
604 assert_eq!(input.get_value(), "hello");
605 }
606
607 #[test]
608 fn test_text_input_cursor_position() {
609 let input = TextInput::new().value("hello");
610 assert_eq!(input.cursor_position(), 5); }
612
613 #[test]
618 fn test_text_input_display_normal() {
619 let input = TextInput::new().value("password");
620 assert_eq!(input.display_text(), "password");
621 }
622
623 #[test]
624 fn test_text_input_display_obscured() {
625 let input = TextInput::new().value("secret").obscure(true);
626 assert_eq!(input.display_text(), "••••••");
627 }
628
629 #[test]
634 fn test_text_input_insert() {
635 let mut input = TextInput::new().value("hlo");
636 input.cursor = 1;
637 input.insert_text("el");
638 assert_eq!(input.get_value(), "hello");
639 assert_eq!(input.cursor_position(), 3);
640 }
641
642 #[test]
643 fn test_text_input_insert_respects_max_length() {
644 let mut input = TextInput::new().max_length(5).value("abc");
645 input.insert_text("defgh");
646 assert_eq!(input.get_value(), "abcde");
647 }
648
649 #[test]
650 fn test_text_input_backspace() {
651 let mut input = TextInput::new().value("hello");
652 input.backspace();
653 assert_eq!(input.get_value(), "hell");
654 assert_eq!(input.cursor_position(), 4);
655 }
656
657 #[test]
658 fn test_text_input_backspace_at_start() {
659 let mut input = TextInput::new().value("hello");
660 input.cursor = 0;
661 let changed = input.backspace();
662 assert!(!changed);
663 assert_eq!(input.get_value(), "hello");
664 }
665
666 #[test]
667 fn test_text_input_delete() {
668 let mut input = TextInput::new().value("hello");
669 input.cursor = 0;
670 input.delete();
671 assert_eq!(input.get_value(), "ello");
672 assert_eq!(input.cursor_position(), 0);
673 }
674
675 #[test]
676 fn test_text_input_delete_at_end() {
677 let mut input = TextInput::new().value("hello");
678 let changed = input.delete();
679 assert!(!changed);
680 assert_eq!(input.get_value(), "hello");
681 }
682
683 #[test]
688 fn test_text_input_move_left() {
689 let mut input = TextInput::new().value("hello");
690 input.move_left();
691 assert_eq!(input.cursor_position(), 4);
692 }
693
694 #[test]
695 fn test_text_input_move_left_at_start() {
696 let mut input = TextInput::new().value("hello");
697 input.cursor = 0;
698 input.move_left();
699 assert_eq!(input.cursor_position(), 0); }
701
702 #[test]
703 fn test_text_input_move_right() {
704 let mut input = TextInput::new().value("hello");
705 input.cursor = 2;
706 input.move_right();
707 assert_eq!(input.cursor_position(), 3);
708 }
709
710 #[test]
711 fn test_text_input_move_right_at_end() {
712 let mut input = TextInput::new().value("hello");
713 input.move_right();
714 assert_eq!(input.cursor_position(), 5); }
716
717 #[test]
718 fn test_text_input_move_home() {
719 let mut input = TextInput::new().value("hello");
720 input.move_home();
721 assert_eq!(input.cursor_position(), 0);
722 }
723
724 #[test]
725 fn test_text_input_move_end() {
726 let mut input = TextInput::new().value("hello");
727 input.cursor = 0;
728 input.move_end();
729 assert_eq!(input.cursor_position(), 5);
730 }
731
732 #[test]
737 fn test_text_input_type_id() {
738 let input = TextInput::new();
739 assert_eq!(Widget::type_id(&input), TypeId::of::<TextInput>());
740 }
741
742 #[test]
743 fn test_text_input_measure() {
744 let input = TextInput::new();
745 let size = input.measure(Constraints::loose(Size::new(400.0, 100.0)));
746 assert!(size.width >= 100.0);
747 assert!(size.height > 0.0);
748 }
749
750 #[test]
751 fn test_text_input_is_interactive() {
752 let input = TextInput::new();
753 assert!(input.is_interactive());
754
755 let input = TextInput::new().disabled(true);
756 assert!(!input.is_interactive());
757 }
758
759 #[test]
760 fn test_text_input_is_focusable() {
761 let input = TextInput::new();
762 assert!(input.is_focusable());
763
764 let input = TextInput::new().disabled(true);
765 assert!(!input.is_focusable());
766 }
767
768 #[test]
769 fn test_text_input_accessible_role() {
770 let input = TextInput::new();
771 assert_eq!(input.accessible_role(), AccessibleRole::TextInput);
772 }
773
774 #[test]
775 fn test_text_input_children() {
776 let input = TextInput::new();
777 assert!(input.children().is_empty());
778 }
779
780 #[test]
785 fn test_text_input_colors() {
786 let input = TextInput::new()
787 .background_color(Color::RED)
788 .border_color(Color::GREEN)
789 .focus_border_color(Color::BLUE)
790 .placeholder_color(Color::YELLOW);
791
792 assert_eq!(input.background_color, Color::RED);
793 assert_eq!(input.border_color, Color::GREEN);
794 assert_eq!(input.focus_border_color, Color::BLUE);
795 assert_eq!(input.placeholder_color, Color::YELLOW);
796 }
797
798 #[test]
803 fn test_text_input_focus_state() {
804 let input = TextInput::new();
805 assert!(!input.is_focused());
806 }
807
808 #[test]
809 fn test_text_input_disabled_no_insert() {
810 let mut input = TextInput::new().disabled(true);
811 let changed = input.insert_text("test");
812 assert!(!changed);
813 assert!(input.is_empty());
814 }
815
816 #[test]
817 fn test_text_input_disabled_no_backspace() {
818 let mut input = TextInput::new().value("test").disabled(true);
819 input.disabled = true; let changed = input.backspace();
821 assert!(!changed);
822 }
823
824 #[test]
825 fn test_text_input_disabled_no_delete() {
826 let mut input = TextInput::new().value("test").disabled(true);
827 input.disabled = true;
828 input.cursor = 0;
829 let changed = input.delete();
830 assert!(!changed);
831 }
832
833 use presentar_core::{Key, MouseButton, Point};
838
839 #[test]
840 fn test_text_input_event_focus_in() {
841 let mut input = TextInput::new();
842 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
843
844 assert!(!input.focused);
845 let result = input.event(&Event::FocusIn);
846 assert!(input.focused);
847 assert!(result.is_none()); }
849
850 #[test]
851 fn test_text_input_event_focus_out() {
852 let mut input = TextInput::new();
853 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
854
855 input.event(&Event::FocusIn);
856 assert!(input.focused);
857
858 let result = input.event(&Event::FocusOut);
859 assert!(!input.focused);
860 assert!(result.is_none());
861 }
862
863 #[test]
864 fn test_text_input_event_mouse_down_inside_focuses() {
865 let mut input = TextInput::new();
866 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
867
868 assert!(!input.focused);
869 let result = input.event(&Event::MouseDown {
870 position: Point::new(100.0, 15.0),
871 button: MouseButton::Left,
872 });
873 assert!(input.focused);
874 assert!(result.is_none());
875 }
876
877 #[test]
878 fn test_text_input_event_mouse_down_outside_unfocuses() {
879 let mut input = TextInput::new();
880 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
881 input.focused = true;
882
883 let result = input.event(&Event::MouseDown {
884 position: Point::new(300.0, 15.0),
885 button: MouseButton::Left,
886 });
887 assert!(!input.focused);
888 assert!(result.is_none());
889 }
890
891 #[test]
892 fn test_text_input_event_mouse_down_sets_cursor() {
893 let mut input = TextInput::new().value("hello");
894 input.cursor = 0; input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
896
897 input.event(&Event::MouseDown {
899 position: Point::new(100.0, 15.0),
900 button: MouseButton::Left,
901 });
902 assert_eq!(input.cursor, 5); }
904
905 #[test]
906 fn test_text_input_event_text_input_when_focused() {
907 let mut input = TextInput::new();
908 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
909 input.event(&Event::FocusIn);
910
911 let result = input.event(&Event::TextInput {
912 text: "hello".to_string(),
913 });
914 assert_eq!(input.get_value(), "hello");
915 assert!(result.is_some());
916
917 let msg = result.unwrap().downcast::<TextChanged>().unwrap();
918 assert_eq!(msg.value, "hello");
919 }
920
921 #[test]
922 fn test_text_input_event_text_input_when_not_focused() {
923 let mut input = TextInput::new();
924 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
925 let result = input.event(&Event::TextInput {
928 text: "hello".to_string(),
929 });
930 assert!(input.get_value().is_empty());
931 assert!(result.is_none());
932 }
933
934 #[test]
935 fn test_text_input_event_key_backspace() {
936 let mut input = TextInput::new().value("hello");
937 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
938 input.event(&Event::FocusIn);
939 input.cursor = 5;
940
941 let result = input.event(&Event::KeyDown {
942 key: Key::Backspace,
943 });
944 assert_eq!(input.get_value(), "hell");
945 assert!(result.is_some());
946
947 let msg = result.unwrap().downcast::<TextChanged>().unwrap();
948 assert_eq!(msg.value, "hell");
949 }
950
951 #[test]
952 fn test_text_input_event_key_delete() {
953 let mut input = TextInput::new().value("hello");
954 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
955 input.event(&Event::FocusIn);
956 input.cursor = 0;
957
958 let result = input.event(&Event::KeyDown { key: Key::Delete });
959 assert_eq!(input.get_value(), "ello");
960 assert!(result.is_some());
961 }
962
963 #[test]
964 fn test_text_input_event_key_left() {
965 let mut input = TextInput::new().value("hello");
966 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
967 input.event(&Event::FocusIn);
968 input.cursor = 3;
969
970 let result = input.event(&Event::KeyDown { key: Key::Left });
971 assert_eq!(input.cursor, 2);
972 assert!(result.is_none()); }
974
975 #[test]
976 fn test_text_input_event_key_right() {
977 let mut input = TextInput::new().value("hello");
978 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
979 input.event(&Event::FocusIn);
980 input.cursor = 2;
981
982 let result = input.event(&Event::KeyDown { key: Key::Right });
983 assert_eq!(input.cursor, 3);
984 assert!(result.is_none());
985 }
986
987 #[test]
988 fn test_text_input_event_key_home() {
989 let mut input = TextInput::new().value("hello");
990 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
991 input.event(&Event::FocusIn);
992 input.cursor = 5;
993
994 let result = input.event(&Event::KeyDown { key: Key::Home });
995 assert_eq!(input.cursor, 0);
996 assert!(result.is_none());
997 }
998
999 #[test]
1000 fn test_text_input_event_key_end() {
1001 let mut input = TextInput::new().value("hello");
1002 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1003 input.event(&Event::FocusIn);
1004 input.cursor = 0;
1005
1006 let result = input.event(&Event::KeyDown { key: Key::End });
1007 assert_eq!(input.cursor, 5);
1008 assert!(result.is_none());
1009 }
1010
1011 #[test]
1012 fn test_text_input_event_key_enter_submits() {
1013 let mut input = TextInput::new().value("hello");
1014 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1015 input.event(&Event::FocusIn);
1016
1017 let result = input.event(&Event::KeyDown { key: Key::Enter });
1018 assert!(result.is_some());
1019
1020 let msg = result.unwrap().downcast::<TextSubmitted>().unwrap();
1021 assert_eq!(msg.value, "hello");
1022 }
1023
1024 #[test]
1025 fn test_text_input_event_key_when_not_focused() {
1026 let mut input = TextInput::new().value("hello");
1027 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1028 let result = input.event(&Event::KeyDown {
1031 key: Key::Backspace,
1032 });
1033 assert_eq!(input.get_value(), "hello"); assert!(result.is_none());
1035 }
1036
1037 #[test]
1038 fn test_text_input_event_disabled_blocks_focus() {
1039 let mut input = TextInput::new().disabled(true);
1040 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1041
1042 let result = input.event(&Event::FocusIn);
1043 assert!(!input.focused);
1044 assert!(result.is_none());
1045 }
1046
1047 #[test]
1048 fn test_text_input_event_disabled_blocks_mouse_down() {
1049 let mut input = TextInput::new().disabled(true);
1050 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1051
1052 let result = input.event(&Event::MouseDown {
1053 position: Point::new(100.0, 15.0),
1054 button: MouseButton::Left,
1055 });
1056 assert!(!input.focused);
1057 assert!(result.is_none());
1058 }
1059
1060 #[test]
1061 fn test_text_input_event_disabled_blocks_text_input() {
1062 let mut input = TextInput::new().disabled(true);
1063 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1064 input.focused = true; let result = input.event(&Event::TextInput {
1067 text: "hello".to_string(),
1068 });
1069 assert!(input.get_value().is_empty());
1070 assert!(result.is_none());
1071 }
1072
1073 #[test]
1074 fn test_text_input_event_disabled_blocks_key_down() {
1075 let mut input = TextInput::new().value("hello").disabled(true);
1076 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1077 input.focused = true;
1078
1079 let result = input.event(&Event::KeyDown {
1080 key: Key::Backspace,
1081 });
1082 assert_eq!(input.get_value(), "hello");
1083 assert!(result.is_none());
1084 }
1085
1086 #[test]
1087 fn test_text_input_event_full_typing_flow() {
1088 let mut input = TextInput::new();
1089 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1090
1091 input.event(&Event::MouseDown {
1093 position: Point::new(100.0, 15.0),
1094 button: MouseButton::Left,
1095 });
1096 assert!(input.focused);
1097
1098 input.event(&Event::TextInput {
1100 text: "Hello".to_string(),
1101 });
1102 assert_eq!(input.get_value(), "Hello");
1103 assert_eq!(input.cursor, 5);
1104
1105 input.event(&Event::KeyDown {
1107 key: Key::Backspace,
1108 });
1109 assert_eq!(input.get_value(), "Hell");
1110 assert_eq!(input.cursor, 4);
1111
1112 input.event(&Event::KeyDown { key: Key::Home });
1114 assert_eq!(input.cursor, 0);
1115
1116 input.event(&Event::TextInput {
1118 text: "Say ".to_string(),
1119 });
1120 assert_eq!(input.get_value(), "Say Hell");
1121 assert_eq!(input.cursor, 4);
1122
1123 input.event(&Event::KeyDown { key: Key::End });
1125 assert_eq!(input.cursor, 8);
1126
1127 input.event(&Event::TextInput {
1129 text: "o".to_string(),
1130 });
1131 assert_eq!(input.get_value(), "Say Hello");
1132
1133 let result = input.event(&Event::KeyDown { key: Key::Enter });
1135 let msg = result.unwrap().downcast::<TextSubmitted>().unwrap();
1136 assert_eq!(msg.value, "Say Hello");
1137
1138 input.event(&Event::MouseDown {
1140 position: Point::new(300.0, 15.0),
1141 button: MouseButton::Left,
1142 });
1143 assert!(!input.focused);
1144 }
1145
1146 #[test]
1147 fn test_text_input_event_cursor_navigation() {
1148 let mut input = TextInput::new().value("abcde");
1149 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1150 input.event(&Event::FocusIn);
1151 input.cursor = 2;
1152
1153 input.event(&Event::KeyDown { key: Key::Left });
1155 input.event(&Event::KeyDown { key: Key::Left });
1156 input.event(&Event::KeyDown { key: Key::Left }); assert_eq!(input.cursor, 0);
1158
1159 input.event(&Event::KeyDown { key: Key::End });
1161 input.event(&Event::KeyDown { key: Key::Right }); assert_eq!(input.cursor, 5);
1163 }
1164
1165 #[test]
1166 fn test_text_input_event_backspace_at_start() {
1167 let mut input = TextInput::new().value("hello");
1168 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1169 input.event(&Event::FocusIn);
1170 input.cursor = 0;
1171
1172 let result = input.event(&Event::KeyDown {
1173 key: Key::Backspace,
1174 });
1175 assert_eq!(input.get_value(), "hello"); assert!(result.is_none()); }
1178
1179 #[test]
1180 fn test_text_input_event_delete_at_end() {
1181 let mut input = TextInput::new().value("hello");
1182 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1183 input.event(&Event::FocusIn);
1184 input.cursor = 5;
1185
1186 let result = input.event(&Event::KeyDown { key: Key::Delete });
1187 assert_eq!(input.get_value(), "hello"); assert!(result.is_none());
1189 }
1190
1191 #[test]
1192 fn test_text_input_event_max_length_enforced() {
1193 let mut input = TextInput::new().max_length(5);
1194 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1195 input.event(&Event::FocusIn);
1196
1197 input.event(&Event::TextInput {
1199 text: "hello world".to_string(),
1200 });
1201 assert_eq!(input.get_value(), "hello"); }
1203}