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)]
524#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
525mod tests {
526 use super::*;
527 use presentar_core::Widget;
528
529 #[test]
534 fn test_text_changed_message() {
535 let msg = TextChanged {
536 value: "hello".to_string(),
537 };
538 assert_eq!(msg.value, "hello");
539 }
540
541 #[test]
542 fn test_text_submitted_message() {
543 let msg = TextSubmitted {
544 value: "world".to_string(),
545 };
546 assert_eq!(msg.value, "world");
547 }
548
549 #[test]
554 fn test_text_input_new() {
555 let input = TextInput::new();
556 assert!(input.get_value().is_empty());
557 assert!(input.get_placeholder().is_empty());
558 assert!(input.is_empty());
559 assert!(!input.disabled);
560 assert!(!input.obscure);
561 }
562
563 #[test]
564 fn test_text_input_default() {
565 let input = TextInput::default();
566 assert!(input.is_empty());
567 }
568
569 #[test]
570 fn test_text_input_builder() {
571 let input = TextInput::new()
572 .value("hello")
573 .placeholder("Enter text...")
574 .disabled(true)
575 .obscure(true)
576 .max_length(20)
577 .padding(10.0)
578 .min_width(200.0)
579 .with_test_id("my-input")
580 .with_accessible_name("Email");
581
582 assert_eq!(input.get_value(), "hello");
583 assert_eq!(input.get_placeholder(), "Enter text...");
584 assert!(input.disabled);
585 assert!(input.obscure);
586 assert_eq!(input.max_length, 20);
587 assert_eq!(Widget::test_id(&input), Some("my-input"));
588 assert_eq!(input.accessible_name(), Some("Email"));
589 }
590
591 #[test]
596 fn test_text_input_value() {
597 let input = TextInput::new().value("test");
598 assert_eq!(input.get_value(), "test");
599 assert!(!input.is_empty());
600 }
601
602 #[test]
603 fn test_text_input_max_length_truncate() {
604 let input = TextInput::new().max_length(5).value("hello world");
605 assert_eq!(input.get_value(), "hello");
606 }
607
608 #[test]
609 fn test_text_input_cursor_position() {
610 let input = TextInput::new().value("hello");
611 assert_eq!(input.cursor_position(), 5); }
613
614 #[test]
619 fn test_text_input_display_normal() {
620 let input = TextInput::new().value("password");
621 assert_eq!(input.display_text(), "password");
622 }
623
624 #[test]
625 fn test_text_input_display_obscured() {
626 let input = TextInput::new().value("secret").obscure(true);
627 assert_eq!(input.display_text(), "••••••");
628 }
629
630 #[test]
635 fn test_text_input_insert() {
636 let mut input = TextInput::new().value("hlo");
637 input.cursor = 1;
638 input.insert_text("el");
639 assert_eq!(input.get_value(), "hello");
640 assert_eq!(input.cursor_position(), 3);
641 }
642
643 #[test]
644 fn test_text_input_insert_respects_max_length() {
645 let mut input = TextInput::new().max_length(5).value("abc");
646 input.insert_text("defgh");
647 assert_eq!(input.get_value(), "abcde");
648 }
649
650 #[test]
651 fn test_text_input_backspace() {
652 let mut input = TextInput::new().value("hello");
653 input.backspace();
654 assert_eq!(input.get_value(), "hell");
655 assert_eq!(input.cursor_position(), 4);
656 }
657
658 #[test]
659 fn test_text_input_backspace_at_start() {
660 let mut input = TextInput::new().value("hello");
661 input.cursor = 0;
662 let changed = input.backspace();
663 assert!(!changed);
664 assert_eq!(input.get_value(), "hello");
665 }
666
667 #[test]
668 fn test_text_input_delete() {
669 let mut input = TextInput::new().value("hello");
670 input.cursor = 0;
671 input.delete();
672 assert_eq!(input.get_value(), "ello");
673 assert_eq!(input.cursor_position(), 0);
674 }
675
676 #[test]
677 fn test_text_input_delete_at_end() {
678 let mut input = TextInput::new().value("hello");
679 let changed = input.delete();
680 assert!(!changed);
681 assert_eq!(input.get_value(), "hello");
682 }
683
684 #[test]
689 fn test_text_input_move_left() {
690 let mut input = TextInput::new().value("hello");
691 input.move_left();
692 assert_eq!(input.cursor_position(), 4);
693 }
694
695 #[test]
696 fn test_text_input_move_left_at_start() {
697 let mut input = TextInput::new().value("hello");
698 input.cursor = 0;
699 input.move_left();
700 assert_eq!(input.cursor_position(), 0); }
702
703 #[test]
704 fn test_text_input_move_right() {
705 let mut input = TextInput::new().value("hello");
706 input.cursor = 2;
707 input.move_right();
708 assert_eq!(input.cursor_position(), 3);
709 }
710
711 #[test]
712 fn test_text_input_move_right_at_end() {
713 let mut input = TextInput::new().value("hello");
714 input.move_right();
715 assert_eq!(input.cursor_position(), 5); }
717
718 #[test]
719 fn test_text_input_move_home() {
720 let mut input = TextInput::new().value("hello");
721 input.move_home();
722 assert_eq!(input.cursor_position(), 0);
723 }
724
725 #[test]
726 fn test_text_input_move_end() {
727 let mut input = TextInput::new().value("hello");
728 input.cursor = 0;
729 input.move_end();
730 assert_eq!(input.cursor_position(), 5);
731 }
732
733 #[test]
738 fn test_text_input_type_id() {
739 let input = TextInput::new();
740 assert_eq!(Widget::type_id(&input), TypeId::of::<TextInput>());
741 }
742
743 #[test]
744 fn test_text_input_measure() {
745 let input = TextInput::new();
746 let size = input.measure(Constraints::loose(Size::new(400.0, 100.0)));
747 assert!(size.width >= 100.0);
748 assert!(size.height > 0.0);
749 }
750
751 #[test]
752 fn test_text_input_is_interactive() {
753 let input = TextInput::new();
754 assert!(input.is_interactive());
755
756 let input = TextInput::new().disabled(true);
757 assert!(!input.is_interactive());
758 }
759
760 #[test]
761 fn test_text_input_is_focusable() {
762 let input = TextInput::new();
763 assert!(input.is_focusable());
764
765 let input = TextInput::new().disabled(true);
766 assert!(!input.is_focusable());
767 }
768
769 #[test]
770 fn test_text_input_accessible_role() {
771 let input = TextInput::new();
772 assert_eq!(input.accessible_role(), AccessibleRole::TextInput);
773 }
774
775 #[test]
776 fn test_text_input_children() {
777 let input = TextInput::new();
778 assert!(input.children().is_empty());
779 }
780
781 #[test]
786 fn test_text_input_colors() {
787 let input = TextInput::new()
788 .background_color(Color::RED)
789 .border_color(Color::GREEN)
790 .focus_border_color(Color::BLUE)
791 .placeholder_color(Color::YELLOW);
792
793 assert_eq!(input.background_color, Color::RED);
794 assert_eq!(input.border_color, Color::GREEN);
795 assert_eq!(input.focus_border_color, Color::BLUE);
796 assert_eq!(input.placeholder_color, Color::YELLOW);
797 }
798
799 #[test]
804 fn test_text_input_focus_state() {
805 let input = TextInput::new();
806 assert!(!input.is_focused());
807 }
808
809 #[test]
810 fn test_text_input_disabled_no_insert() {
811 let mut input = TextInput::new().disabled(true);
812 let changed = input.insert_text("test");
813 assert!(!changed);
814 assert!(input.is_empty());
815 }
816
817 #[test]
818 fn test_text_input_disabled_no_backspace() {
819 let mut input = TextInput::new().value("test").disabled(true);
820 input.disabled = true; let changed = input.backspace();
822 assert!(!changed);
823 }
824
825 #[test]
826 fn test_text_input_disabled_no_delete() {
827 let mut input = TextInput::new().value("test").disabled(true);
828 input.disabled = true;
829 input.cursor = 0;
830 let changed = input.delete();
831 assert!(!changed);
832 }
833
834 use presentar_core::{Key, MouseButton, Point};
839
840 #[test]
841 fn test_text_input_event_focus_in() {
842 let mut input = TextInput::new();
843 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
844
845 assert!(!input.focused);
846 let result = input.event(&Event::FocusIn);
847 assert!(input.focused);
848 assert!(result.is_none()); }
850
851 #[test]
852 fn test_text_input_event_focus_out() {
853 let mut input = TextInput::new();
854 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
855
856 input.event(&Event::FocusIn);
857 assert!(input.focused);
858
859 let result = input.event(&Event::FocusOut);
860 assert!(!input.focused);
861 assert!(result.is_none());
862 }
863
864 #[test]
865 fn test_text_input_event_mouse_down_inside_focuses() {
866 let mut input = TextInput::new();
867 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
868
869 assert!(!input.focused);
870 let result = input.event(&Event::MouseDown {
871 position: Point::new(100.0, 15.0),
872 button: MouseButton::Left,
873 });
874 assert!(input.focused);
875 assert!(result.is_none());
876 }
877
878 #[test]
879 fn test_text_input_event_mouse_down_outside_unfocuses() {
880 let mut input = TextInput::new();
881 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
882 input.focused = true;
883
884 let result = input.event(&Event::MouseDown {
885 position: Point::new(300.0, 15.0),
886 button: MouseButton::Left,
887 });
888 assert!(!input.focused);
889 assert!(result.is_none());
890 }
891
892 #[test]
893 fn test_text_input_event_mouse_down_sets_cursor() {
894 let mut input = TextInput::new().value("hello");
895 input.cursor = 0; input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
897
898 input.event(&Event::MouseDown {
900 position: Point::new(100.0, 15.0),
901 button: MouseButton::Left,
902 });
903 assert_eq!(input.cursor, 5); }
905
906 #[test]
907 fn test_text_input_event_text_input_when_focused() {
908 let mut input = TextInput::new();
909 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
910 input.event(&Event::FocusIn);
911
912 let result = input.event(&Event::TextInput {
913 text: "hello".to_string(),
914 });
915 assert_eq!(input.get_value(), "hello");
916 assert!(result.is_some());
917
918 let msg = result.unwrap().downcast::<TextChanged>().unwrap();
919 assert_eq!(msg.value, "hello");
920 }
921
922 #[test]
923 fn test_text_input_event_text_input_when_not_focused() {
924 let mut input = TextInput::new();
925 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
926 let result = input.event(&Event::TextInput {
929 text: "hello".to_string(),
930 });
931 assert!(input.get_value().is_empty());
932 assert!(result.is_none());
933 }
934
935 #[test]
936 fn test_text_input_event_key_backspace() {
937 let mut input = TextInput::new().value("hello");
938 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
939 input.event(&Event::FocusIn);
940 input.cursor = 5;
941
942 let result = input.event(&Event::key_down(Key::Backspace));
943 assert_eq!(input.get_value(), "hell");
944 assert!(result.is_some());
945
946 let msg = result.unwrap().downcast::<TextChanged>().unwrap();
947 assert_eq!(msg.value, "hell");
948 }
949
950 #[test]
951 fn test_text_input_event_key_delete() {
952 let mut input = TextInput::new().value("hello");
953 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
954 input.event(&Event::FocusIn);
955 input.cursor = 0;
956
957 let result = input.event(&Event::key_down(Key::Delete));
958 assert_eq!(input.get_value(), "ello");
959 assert!(result.is_some());
960 }
961
962 #[test]
963 fn test_text_input_event_key_left() {
964 let mut input = TextInput::new().value("hello");
965 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
966 input.event(&Event::FocusIn);
967 input.cursor = 3;
968
969 let result = input.event(&Event::key_down(Key::Left));
970 assert_eq!(input.cursor, 2);
971 assert!(result.is_none()); }
973
974 #[test]
975 fn test_text_input_event_key_right() {
976 let mut input = TextInput::new().value("hello");
977 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
978 input.event(&Event::FocusIn);
979 input.cursor = 2;
980
981 let result = input.event(&Event::key_down(Key::Right));
982 assert_eq!(input.cursor, 3);
983 assert!(result.is_none());
984 }
985
986 #[test]
987 fn test_text_input_event_key_home() {
988 let mut input = TextInput::new().value("hello");
989 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
990 input.event(&Event::FocusIn);
991 input.cursor = 5;
992
993 let result = input.event(&Event::key_down(Key::Home));
994 assert_eq!(input.cursor, 0);
995 assert!(result.is_none());
996 }
997
998 #[test]
999 fn test_text_input_event_key_end() {
1000 let mut input = TextInput::new().value("hello");
1001 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1002 input.event(&Event::FocusIn);
1003 input.cursor = 0;
1004
1005 let result = input.event(&Event::key_down(Key::End));
1006 assert_eq!(input.cursor, 5);
1007 assert!(result.is_none());
1008 }
1009
1010 #[test]
1011 fn test_text_input_event_key_enter_submits() {
1012 let mut input = TextInput::new().value("hello");
1013 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1014 input.event(&Event::FocusIn);
1015
1016 let result = input.event(&Event::key_down(Key::Enter));
1017 assert!(result.is_some());
1018
1019 let msg = result.unwrap().downcast::<TextSubmitted>().unwrap();
1020 assert_eq!(msg.value, "hello");
1021 }
1022
1023 #[test]
1024 fn test_text_input_event_key_when_not_focused() {
1025 let mut input = TextInput::new().value("hello");
1026 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1027 let result = input.event(&Event::key_down(Key::Backspace));
1030 assert_eq!(input.get_value(), "hello"); assert!(result.is_none());
1032 }
1033
1034 #[test]
1035 fn test_text_input_event_disabled_blocks_focus() {
1036 let mut input = TextInput::new().disabled(true);
1037 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1038
1039 let result = input.event(&Event::FocusIn);
1040 assert!(!input.focused);
1041 assert!(result.is_none());
1042 }
1043
1044 #[test]
1045 fn test_text_input_event_disabled_blocks_mouse_down() {
1046 let mut input = TextInput::new().disabled(true);
1047 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1048
1049 let result = input.event(&Event::MouseDown {
1050 position: Point::new(100.0, 15.0),
1051 button: MouseButton::Left,
1052 });
1053 assert!(!input.focused);
1054 assert!(result.is_none());
1055 }
1056
1057 #[test]
1058 fn test_text_input_event_disabled_blocks_text_input() {
1059 let mut input = TextInput::new().disabled(true);
1060 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1061 input.focused = true; let result = input.event(&Event::TextInput {
1064 text: "hello".to_string(),
1065 });
1066 assert!(input.get_value().is_empty());
1067 assert!(result.is_none());
1068 }
1069
1070 #[test]
1071 fn test_text_input_event_disabled_blocks_key_down() {
1072 let mut input = TextInput::new().value("hello").disabled(true);
1073 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1074 input.focused = true;
1075
1076 let result = input.event(&Event::key_down(Key::Backspace));
1077 assert_eq!(input.get_value(), "hello");
1078 assert!(result.is_none());
1079 }
1080
1081 #[test]
1082 fn test_text_input_event_full_typing_flow() {
1083 let mut input = TextInput::new();
1084 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1085
1086 input.event(&Event::MouseDown {
1088 position: Point::new(100.0, 15.0),
1089 button: MouseButton::Left,
1090 });
1091 assert!(input.focused);
1092
1093 input.event(&Event::TextInput {
1095 text: "Hello".to_string(),
1096 });
1097 assert_eq!(input.get_value(), "Hello");
1098 assert_eq!(input.cursor, 5);
1099
1100 input.event(&Event::key_down(Key::Backspace));
1102 assert_eq!(input.get_value(), "Hell");
1103 assert_eq!(input.cursor, 4);
1104
1105 input.event(&Event::key_down(Key::Home));
1107 assert_eq!(input.cursor, 0);
1108
1109 input.event(&Event::TextInput {
1111 text: "Say ".to_string(),
1112 });
1113 assert_eq!(input.get_value(), "Say Hell");
1114 assert_eq!(input.cursor, 4);
1115
1116 input.event(&Event::key_down(Key::End));
1118 assert_eq!(input.cursor, 8);
1119
1120 input.event(&Event::TextInput {
1122 text: "o".to_string(),
1123 });
1124 assert_eq!(input.get_value(), "Say Hello");
1125
1126 let result = input.event(&Event::key_down(Key::Enter));
1128 let msg = result.unwrap().downcast::<TextSubmitted>().unwrap();
1129 assert_eq!(msg.value, "Say Hello");
1130
1131 input.event(&Event::MouseDown {
1133 position: Point::new(300.0, 15.0),
1134 button: MouseButton::Left,
1135 });
1136 assert!(!input.focused);
1137 }
1138
1139 #[test]
1140 fn test_text_input_event_cursor_navigation() {
1141 let mut input = TextInput::new().value("abcde");
1142 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1143 input.event(&Event::FocusIn);
1144 input.cursor = 2;
1145
1146 input.event(&Event::key_down(Key::Left));
1148 input.event(&Event::key_down(Key::Left));
1149 input.event(&Event::key_down(Key::Left)); assert_eq!(input.cursor, 0);
1151
1152 input.event(&Event::key_down(Key::End));
1154 input.event(&Event::key_down(Key::Right)); assert_eq!(input.cursor, 5);
1156 }
1157
1158 #[test]
1159 fn test_text_input_event_backspace_at_start() {
1160 let mut input = TextInput::new().value("hello");
1161 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1162 input.event(&Event::FocusIn);
1163 input.cursor = 0;
1164
1165 let result = input.event(&Event::key_down(Key::Backspace));
1166 assert_eq!(input.get_value(), "hello"); assert!(result.is_none()); }
1169
1170 #[test]
1171 fn test_text_input_event_delete_at_end() {
1172 let mut input = TextInput::new().value("hello");
1173 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1174 input.event(&Event::FocusIn);
1175 input.cursor = 5;
1176
1177 let result = input.event(&Event::key_down(Key::Delete));
1178 assert_eq!(input.get_value(), "hello"); assert!(result.is_none());
1180 }
1181
1182 #[test]
1183 fn test_text_input_event_max_length_enforced() {
1184 let mut input = TextInput::new().max_length(5);
1185 input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1186 input.event(&Event::FocusIn);
1187
1188 input.event(&Event::TextInput {
1190 text: "hello world".to_string(),
1191 });
1192 assert_eq!(input.get_value(), "hello"); }
1194}