presentar_widgets/
text_input.rs

1//! `TextInput` widget for text entry.
2
3use 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/// Message emitted when text changes.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct TextChanged {
15    /// The new text value
16    pub value: String,
17}
18
19/// Message emitted when Enter is pressed.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct TextSubmitted {
22    /// The submitted text value
23    pub value: String,
24}
25
26/// `TextInput` widget for text entry.
27#[derive(Serialize, Deserialize)]
28pub struct TextInput {
29    /// Current text value
30    value: String,
31    /// Placeholder text
32    placeholder: String,
33    /// Whether the input is disabled
34    disabled: bool,
35    /// Whether to obscure text (password mode)
36    obscure: bool,
37    /// Maximum length (0 = unlimited)
38    max_length: usize,
39    /// Text style
40    text_style: TextStyle,
41    /// Placeholder text color
42    placeholder_color: Color,
43    /// Background color
44    background_color: Color,
45    /// Border color
46    border_color: Color,
47    /// Focused border color
48    focus_border_color: Color,
49    /// Padding
50    padding: f32,
51    /// Minimum width
52    min_width: f32,
53    /// Test ID
54    test_id_value: Option<String>,
55    /// Accessible name
56    accessible_name_value: Option<String>,
57    /// Cached bounds
58    #[serde(skip)]
59    bounds: Rect,
60    /// Whether focused
61    #[serde(skip)]
62    focused: bool,
63    /// Cursor position (character index)
64    #[serde(skip)]
65    cursor: usize,
66}
67
68impl Default for TextInput {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74impl TextInput {
75    /// Create a new text input.
76    #[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    /// Set the current value.
100    #[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    /// Set placeholder text.
111    #[must_use]
112    pub fn placeholder(mut self, text: impl Into<String>) -> Self {
113        self.placeholder = text.into();
114        self
115    }
116
117    /// Set disabled state.
118    #[must_use]
119    pub const fn disabled(mut self, disabled: bool) -> Self {
120        self.disabled = disabled;
121        self
122    }
123
124    /// Set password mode.
125    #[must_use]
126    pub const fn obscure(mut self, obscure: bool) -> Self {
127        self.obscure = obscure;
128        self
129    }
130
131    /// Set maximum length.
132    #[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    /// Set text style.
143    #[must_use]
144    pub const fn text_style(mut self, style: TextStyle) -> Self {
145        self.text_style = style;
146        self
147    }
148
149    /// Set placeholder color.
150    #[must_use]
151    pub const fn placeholder_color(mut self, color: Color) -> Self {
152        self.placeholder_color = color;
153        self
154    }
155
156    /// Set background color.
157    #[must_use]
158    pub const fn background_color(mut self, color: Color) -> Self {
159        self.background_color = color;
160        self
161    }
162
163    /// Set border color.
164    #[must_use]
165    pub const fn border_color(mut self, color: Color) -> Self {
166        self.border_color = color;
167        self
168    }
169
170    /// Set focus border color.
171    #[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    /// Set padding.
178    #[must_use]
179    pub fn padding(mut self, padding: f32) -> Self {
180        self.padding = padding.max(0.0);
181        self
182    }
183
184    /// Set minimum width.
185    #[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    /// Set test ID.
192    #[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    /// Set accessible name.
199    #[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    /// Get current value.
206    #[must_use]
207    pub fn get_value(&self) -> &str {
208        &self.value
209    }
210
211    /// Get placeholder.
212    #[must_use]
213    pub fn get_placeholder(&self) -> &str {
214        &self.placeholder
215    }
216
217    /// Check if empty.
218    #[must_use]
219    pub fn is_empty(&self) -> bool {
220        self.value.is_empty()
221    }
222
223    /// Get cursor position.
224    #[must_use]
225    pub const fn cursor_position(&self) -> usize {
226        self.cursor
227    }
228
229    /// Check if focused.
230    #[must_use]
231    pub const fn is_focused(&self) -> bool {
232        self.focused
233    }
234
235    /// Get display text (obscured if password mode).
236    #[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    /// Insert text at cursor.
246    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    /// Delete character before cursor.
264    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    /// Delete character at cursor.
274    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    /// Move cursor left.
283    fn move_left(&mut self) {
284        if self.cursor > 0 {
285            self.cursor -= 1;
286        }
287    }
288
289    /// Move cursor right.
290    fn move_right(&mut self) {
291        if self.cursor < self.value.len() {
292            self.cursor += 1;
293        }
294    }
295
296    /// Move cursor to start.
297    fn move_home(&mut self) {
298        self.cursor = 0;
299    }
300
301    /// Move cursor to end.
302    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        // Draw background
327        canvas.fill_rect(self.bounds, self.background_color);
328
329        // Draw border
330        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        // Draw text or placeholder
338        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            // Draw placeholder
344            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            // Draw actual text
349            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
441// PROBAR-SPEC-009: Brick Architecture - Tests define interface
442impl 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), // WCAG AA for text input
451        ]
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        // Verify contrast ratio
463        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        // Latency assertion always passes at verification time
474        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    // =========================================================================
529    // Message Tests - TESTS FIRST
530    // =========================================================================
531
532    #[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    // =========================================================================
549    // TextInput Construction Tests - TESTS FIRST
550    // =========================================================================
551
552    #[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    // =========================================================================
591    // TextInput Value Tests - TESTS FIRST
592    // =========================================================================
593
594    #[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); // At end
611    }
612
613    // =========================================================================
614    // TextInput Display Tests - TESTS FIRST
615    // =========================================================================
616
617    #[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    // =========================================================================
630    // TextInput Editing Tests - TESTS FIRST
631    // =========================================================================
632
633    #[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    // =========================================================================
684    // TextInput Cursor Movement Tests - TESTS FIRST
685    // =========================================================================
686
687    #[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); // Stay at 0
700    }
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); // Stay at end
715    }
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    // =========================================================================
733    // TextInput Widget Trait Tests - TESTS FIRST
734    // =========================================================================
735
736    #[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    // =========================================================================
781    // TextInput Color Tests - TESTS FIRST
782    // =========================================================================
783
784    #[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    // =========================================================================
799    // TextInput Focus Tests - TESTS FIRST
800    // =========================================================================
801
802    #[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; // Force after value set
820        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    // =========================================================================
834    // Event Handling Tests - TESTS FIRST
835    // =========================================================================
836
837    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()); // No message for focus
848    }
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; // Start at 0
895        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
896
897        // Click to focus should move cursor to end
898        input.event(&Event::MouseDown {
899            position: Point::new(100.0, 15.0),
900            button: MouseButton::Left,
901        });
902        assert_eq!(input.cursor, 5); // Moved to end of "hello"
903    }
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        // Not focused
926
927        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()); // No text change
973    }
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        // Not focused
1029
1030        let result = input.event(&Event::KeyDown {
1031            key: Key::Backspace,
1032        });
1033        assert_eq!(input.get_value(), "hello"); // Unchanged
1034        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; // Force focused
1065
1066        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        // 1. Click to focus
1092        input.event(&Event::MouseDown {
1093            position: Point::new(100.0, 15.0),
1094            button: MouseButton::Left,
1095        });
1096        assert!(input.focused);
1097
1098        // 2. Type "Hello"
1099        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        // 3. Backspace
1106        input.event(&Event::KeyDown {
1107            key: Key::Backspace,
1108        });
1109        assert_eq!(input.get_value(), "Hell");
1110        assert_eq!(input.cursor, 4);
1111
1112        // 4. Navigate home
1113        input.event(&Event::KeyDown { key: Key::Home });
1114        assert_eq!(input.cursor, 0);
1115
1116        // 5. Type "Say "
1117        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        // 6. Navigate end
1124        input.event(&Event::KeyDown { key: Key::End });
1125        assert_eq!(input.cursor, 8);
1126
1127        // 7. Type "o"
1128        input.event(&Event::TextInput {
1129            text: "o".to_string(),
1130        });
1131        assert_eq!(input.get_value(), "Say Hello");
1132
1133        // 8. Submit
1134        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        // 9. Click outside to unfocus
1139        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        // Left boundary
1154        input.event(&Event::KeyDown { key: Key::Left });
1155        input.event(&Event::KeyDown { key: Key::Left });
1156        input.event(&Event::KeyDown { key: Key::Left }); // Should stay at 0
1157        assert_eq!(input.cursor, 0);
1158
1159        // Right boundary
1160        input.event(&Event::KeyDown { key: Key::End });
1161        input.event(&Event::KeyDown { key: Key::Right }); // Should stay at end
1162        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"); // Unchanged
1176        assert!(result.is_none()); // No change message
1177    }
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"); // Unchanged
1188        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        // Type beyond max length
1198        input.event(&Event::TextInput {
1199            text: "hello world".to_string(),
1200        });
1201        assert_eq!(input.get_value(), "hello"); // Truncated to 5
1202    }
1203}