Skip to main content

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)]
524#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
525mod tests {
526    use super::*;
527    use presentar_core::Widget;
528
529    // =========================================================================
530    // Message Tests - TESTS FIRST
531    // =========================================================================
532
533    #[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    // =========================================================================
550    // TextInput Construction Tests - TESTS FIRST
551    // =========================================================================
552
553    #[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    // =========================================================================
592    // TextInput Value Tests - TESTS FIRST
593    // =========================================================================
594
595    #[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); // At end
612    }
613
614    // =========================================================================
615    // TextInput Display Tests - TESTS FIRST
616    // =========================================================================
617
618    #[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    // =========================================================================
631    // TextInput Editing Tests - TESTS FIRST
632    // =========================================================================
633
634    #[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    // =========================================================================
685    // TextInput Cursor Movement Tests - TESTS FIRST
686    // =========================================================================
687
688    #[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); // Stay at 0
701    }
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); // Stay at end
716    }
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    // =========================================================================
734    // TextInput Widget Trait Tests - TESTS FIRST
735    // =========================================================================
736
737    #[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    // =========================================================================
782    // TextInput Color Tests - TESTS FIRST
783    // =========================================================================
784
785    #[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    // =========================================================================
800    // TextInput Focus Tests - TESTS FIRST
801    // =========================================================================
802
803    #[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; // Force after value set
821        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    // =========================================================================
835    // Event Handling Tests - TESTS FIRST
836    // =========================================================================
837
838    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()); // No message for focus
849    }
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; // Start at 0
896        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
897
898        // Click to focus should move cursor to end
899        input.event(&Event::MouseDown {
900            position: Point::new(100.0, 15.0),
901            button: MouseButton::Left,
902        });
903        assert_eq!(input.cursor, 5); // Moved to end of "hello"
904    }
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        // Not focused
927
928        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()); // No text change
972    }
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        // Not focused
1028
1029        let result = input.event(&Event::key_down(Key::Backspace));
1030        assert_eq!(input.get_value(), "hello"); // Unchanged
1031        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; // Force focused
1062
1063        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        // 1. Click to focus
1087        input.event(&Event::MouseDown {
1088            position: Point::new(100.0, 15.0),
1089            button: MouseButton::Left,
1090        });
1091        assert!(input.focused);
1092
1093        // 2. Type "Hello"
1094        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        // 3. Backspace
1101        input.event(&Event::key_down(Key::Backspace));
1102        assert_eq!(input.get_value(), "Hell");
1103        assert_eq!(input.cursor, 4);
1104
1105        // 4. Navigate home
1106        input.event(&Event::key_down(Key::Home));
1107        assert_eq!(input.cursor, 0);
1108
1109        // 5. Type "Say "
1110        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        // 6. Navigate end
1117        input.event(&Event::key_down(Key::End));
1118        assert_eq!(input.cursor, 8);
1119
1120        // 7. Type "o"
1121        input.event(&Event::TextInput {
1122            text: "o".to_string(),
1123        });
1124        assert_eq!(input.get_value(), "Say Hello");
1125
1126        // 8. Submit
1127        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        // 9. Click outside to unfocus
1132        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        // Left boundary
1147        input.event(&Event::key_down(Key::Left));
1148        input.event(&Event::key_down(Key::Left));
1149        input.event(&Event::key_down(Key::Left)); // Should stay at 0
1150        assert_eq!(input.cursor, 0);
1151
1152        // Right boundary
1153        input.event(&Event::key_down(Key::End));
1154        input.event(&Event::key_down(Key::Right)); // Should stay at end
1155        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"); // Unchanged
1167        assert!(result.is_none()); // No change message
1168    }
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"); // Unchanged
1179        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        // Type beyond max length
1189        input.event(&Event::TextInput {
1190            text: "hello world".to_string(),
1191        });
1192        assert_eq!(input.get_value(), "hello"); // Truncated to 5
1193    }
1194}