presentar_widgets/
text_input.rs

1//! `TextInput` widget for text entry.
2
3use presentar_core::{
4    widget::{AccessibleRole, LayoutResult, TextStyle},
5    Canvas, Color, Constraints, Event, Key, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9
10/// Message emitted when text changes.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct TextChanged {
13    /// The new text value
14    pub value: String,
15}
16
17/// Message emitted when Enter is pressed.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct TextSubmitted {
20    /// The submitted text value
21    pub value: String,
22}
23
24/// `TextInput` widget for text entry.
25#[derive(Serialize, Deserialize)]
26pub struct TextInput {
27    /// Current text value
28    value: String,
29    /// Placeholder text
30    placeholder: String,
31    /// Whether the input is disabled
32    disabled: bool,
33    /// Whether to obscure text (password mode)
34    obscure: bool,
35    /// Maximum length (0 = unlimited)
36    max_length: usize,
37    /// Text style
38    text_style: TextStyle,
39    /// Placeholder text color
40    placeholder_color: Color,
41    /// Background color
42    background_color: Color,
43    /// Border color
44    border_color: Color,
45    /// Focused border color
46    focus_border_color: Color,
47    /// Padding
48    padding: f32,
49    /// Minimum width
50    min_width: f32,
51    /// Test ID
52    test_id_value: Option<String>,
53    /// Accessible name
54    accessible_name_value: Option<String>,
55    /// Cached bounds
56    #[serde(skip)]
57    bounds: Rect,
58    /// Whether focused
59    #[serde(skip)]
60    focused: bool,
61    /// Cursor position (character index)
62    #[serde(skip)]
63    cursor: usize,
64}
65
66impl Default for TextInput {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72impl TextInput {
73    /// Create a new text input.
74    #[must_use]
75    pub fn new() -> Self {
76        Self {
77            value: String::new(),
78            placeholder: String::new(),
79            disabled: false,
80            obscure: false,
81            max_length: 0,
82            text_style: TextStyle::default(),
83            placeholder_color: Color::new(0.6, 0.6, 0.6, 1.0),
84            background_color: Color::WHITE,
85            border_color: Color::new(0.8, 0.8, 0.8, 1.0),
86            focus_border_color: Color::new(0.2, 0.6, 1.0, 1.0),
87            padding: 8.0,
88            min_width: 100.0,
89            test_id_value: None,
90            accessible_name_value: None,
91            bounds: Rect::default(),
92            focused: false,
93            cursor: 0,
94        }
95    }
96
97    /// Set the current value.
98    #[must_use]
99    pub fn value(mut self, value: impl Into<String>) -> Self {
100        self.value = value.into();
101        if self.max_length > 0 && self.value.len() > self.max_length {
102            self.value.truncate(self.max_length);
103        }
104        self.cursor = self.value.len();
105        self
106    }
107
108    /// Set placeholder text.
109    #[must_use]
110    pub fn placeholder(mut self, text: impl Into<String>) -> Self {
111        self.placeholder = text.into();
112        self
113    }
114
115    /// Set disabled state.
116    #[must_use]
117    pub const fn disabled(mut self, disabled: bool) -> Self {
118        self.disabled = disabled;
119        self
120    }
121
122    /// Set password mode.
123    #[must_use]
124    pub const fn obscure(mut self, obscure: bool) -> Self {
125        self.obscure = obscure;
126        self
127    }
128
129    /// Set maximum length.
130    #[must_use]
131    pub fn max_length(mut self, max: usize) -> Self {
132        self.max_length = max;
133        if max > 0 && self.value.len() > max {
134            self.value.truncate(max);
135            self.cursor = self.cursor.min(max);
136        }
137        self
138    }
139
140    /// Set text style.
141    #[must_use]
142    pub const fn text_style(mut self, style: TextStyle) -> Self {
143        self.text_style = style;
144        self
145    }
146
147    /// Set placeholder color.
148    #[must_use]
149    pub const fn placeholder_color(mut self, color: Color) -> Self {
150        self.placeholder_color = color;
151        self
152    }
153
154    /// Set background color.
155    #[must_use]
156    pub const fn background_color(mut self, color: Color) -> Self {
157        self.background_color = color;
158        self
159    }
160
161    /// Set border color.
162    #[must_use]
163    pub const fn border_color(mut self, color: Color) -> Self {
164        self.border_color = color;
165        self
166    }
167
168    /// Set focus border color.
169    #[must_use]
170    pub const fn focus_border_color(mut self, color: Color) -> Self {
171        self.focus_border_color = color;
172        self
173    }
174
175    /// Set padding.
176    #[must_use]
177    pub fn padding(mut self, padding: f32) -> Self {
178        self.padding = padding.max(0.0);
179        self
180    }
181
182    /// Set minimum width.
183    #[must_use]
184    pub fn min_width(mut self, width: f32) -> Self {
185        self.min_width = width.max(0.0);
186        self
187    }
188
189    /// Set test ID.
190    #[must_use]
191    pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
192        self.test_id_value = Some(id.into());
193        self
194    }
195
196    /// Set accessible name.
197    #[must_use]
198    pub fn with_accessible_name(mut self, name: impl Into<String>) -> Self {
199        self.accessible_name_value = Some(name.into());
200        self
201    }
202
203    /// Get current value.
204    #[must_use]
205    pub fn get_value(&self) -> &str {
206        &self.value
207    }
208
209    /// Get placeholder.
210    #[must_use]
211    pub fn get_placeholder(&self) -> &str {
212        &self.placeholder
213    }
214
215    /// Check if empty.
216    #[must_use]
217    pub fn is_empty(&self) -> bool {
218        self.value.is_empty()
219    }
220
221    /// Get cursor position.
222    #[must_use]
223    pub const fn cursor_position(&self) -> usize {
224        self.cursor
225    }
226
227    /// Check if focused.
228    #[must_use]
229    pub const fn is_focused(&self) -> bool {
230        self.focused
231    }
232
233    /// Get display text (obscured if password mode).
234    #[must_use]
235    pub fn display_text(&self) -> String {
236        if self.obscure {
237            "•".repeat(self.value.len())
238        } else {
239            self.value.clone()
240        }
241    }
242
243    /// Insert text at cursor.
244    fn insert_text(&mut self, text: &str) -> bool {
245        if self.disabled {
246            return false;
247        }
248
249        let mut changed = false;
250        for c in text.chars() {
251            if self.max_length > 0 && self.value.len() >= self.max_length {
252                break;
253            }
254            self.value.insert(self.cursor, c);
255            self.cursor += 1;
256            changed = true;
257        }
258        changed
259    }
260
261    /// Delete character before cursor.
262    fn backspace(&mut self) -> bool {
263        if self.disabled || self.cursor == 0 {
264            return false;
265        }
266        self.cursor -= 1;
267        self.value.remove(self.cursor);
268        true
269    }
270
271    /// Delete character at cursor.
272    fn delete(&mut self) -> bool {
273        if self.disabled || self.cursor >= self.value.len() {
274            return false;
275        }
276        self.value.remove(self.cursor);
277        true
278    }
279
280    /// Move cursor left.
281    fn move_left(&mut self) {
282        if self.cursor > 0 {
283            self.cursor -= 1;
284        }
285    }
286
287    /// Move cursor right.
288    fn move_right(&mut self) {
289        if self.cursor < self.value.len() {
290            self.cursor += 1;
291        }
292    }
293
294    /// Move cursor to start.
295    fn move_home(&mut self) {
296        self.cursor = 0;
297    }
298
299    /// Move cursor to end.
300    fn move_end(&mut self) {
301        self.cursor = self.value.len();
302    }
303}
304
305impl Widget for TextInput {
306    fn type_id(&self) -> TypeId {
307        TypeId::of::<Self>()
308    }
309
310    fn measure(&self, constraints: Constraints) -> Size {
311        let height = 2.0f32.mul_add(self.padding, self.text_style.size);
312        let width = self.min_width.max(constraints.min_width);
313        constraints.constrain(Size::new(width, height))
314    }
315
316    fn layout(&mut self, bounds: Rect) -> LayoutResult {
317        self.bounds = bounds;
318        LayoutResult {
319            size: bounds.size(),
320        }
321    }
322
323    fn paint(&self, canvas: &mut dyn Canvas) {
324        // Draw background
325        canvas.fill_rect(self.bounds, self.background_color);
326
327        // Draw border
328        let border_color = if self.focused {
329            self.focus_border_color
330        } else {
331            self.border_color
332        };
333        canvas.stroke_rect(self.bounds, border_color, 1.0);
334
335        // Draw text or placeholder
336        let text_x = self.bounds.x + self.padding;
337        let text_y = self.bounds.y + self.padding;
338        let position = presentar_core::Point::new(text_x, text_y);
339
340        if self.value.is_empty() {
341            // Draw placeholder
342            let mut placeholder_style = self.text_style.clone();
343            placeholder_style.color = self.placeholder_color;
344            canvas.draw_text(&self.placeholder, position, &placeholder_style);
345        } else {
346            // Draw actual text
347            let display = self.display_text();
348            canvas.draw_text(&display, position, &self.text_style);
349        }
350    }
351
352    fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
353        if self.disabled {
354            return None;
355        }
356
357        match event {
358            Event::MouseDown { position, .. } => {
359                let was_focused = self.focused;
360                self.focused = self.bounds.contains_point(position);
361                if self.focused && !was_focused {
362                    self.cursor = self.value.len();
363                }
364            }
365            Event::FocusIn => {
366                self.focused = true;
367            }
368            Event::FocusOut => {
369                self.focused = false;
370            }
371            Event::TextInput { text } if self.focused => {
372                if self.insert_text(text) {
373                    return Some(Box::new(TextChanged {
374                        value: self.value.clone(),
375                    }));
376                }
377            }
378            Event::KeyDown { key } if self.focused => match key {
379                Key::Backspace => {
380                    if self.backspace() {
381                        return Some(Box::new(TextChanged {
382                            value: self.value.clone(),
383                        }));
384                    }
385                }
386                Key::Delete => {
387                    if self.delete() {
388                        return Some(Box::new(TextChanged {
389                            value: self.value.clone(),
390                        }));
391                    }
392                }
393                Key::Left => self.move_left(),
394                Key::Right => self.move_right(),
395                Key::Home => self.move_home(),
396                Key::End => self.move_end(),
397                Key::Enter => {
398                    return Some(Box::new(TextSubmitted {
399                        value: self.value.clone(),
400                    }));
401                }
402                _ => {}
403            },
404            _ => {}
405        }
406
407        None
408    }
409
410    fn children(&self) -> &[Box<dyn Widget>] {
411        &[]
412    }
413
414    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
415        &mut []
416    }
417
418    fn is_interactive(&self) -> bool {
419        !self.disabled
420    }
421
422    fn is_focusable(&self) -> bool {
423        !self.disabled
424    }
425
426    fn accessible_name(&self) -> Option<&str> {
427        self.accessible_name_value.as_deref()
428    }
429
430    fn accessible_role(&self) -> AccessibleRole {
431        AccessibleRole::TextInput
432    }
433
434    fn test_id(&self) -> Option<&str> {
435        self.test_id_value.as_deref()
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442    use presentar_core::Widget;
443
444    // =========================================================================
445    // Message Tests - TESTS FIRST
446    // =========================================================================
447
448    #[test]
449    fn test_text_changed_message() {
450        let msg = TextChanged {
451            value: "hello".to_string(),
452        };
453        assert_eq!(msg.value, "hello");
454    }
455
456    #[test]
457    fn test_text_submitted_message() {
458        let msg = TextSubmitted {
459            value: "world".to_string(),
460        };
461        assert_eq!(msg.value, "world");
462    }
463
464    // =========================================================================
465    // TextInput Construction Tests - TESTS FIRST
466    // =========================================================================
467
468    #[test]
469    fn test_text_input_new() {
470        let input = TextInput::new();
471        assert!(input.get_value().is_empty());
472        assert!(input.get_placeholder().is_empty());
473        assert!(input.is_empty());
474        assert!(!input.disabled);
475        assert!(!input.obscure);
476    }
477
478    #[test]
479    fn test_text_input_default() {
480        let input = TextInput::default();
481        assert!(input.is_empty());
482    }
483
484    #[test]
485    fn test_text_input_builder() {
486        let input = TextInput::new()
487            .value("hello")
488            .placeholder("Enter text...")
489            .disabled(true)
490            .obscure(true)
491            .max_length(20)
492            .padding(10.0)
493            .min_width(200.0)
494            .with_test_id("my-input")
495            .with_accessible_name("Email");
496
497        assert_eq!(input.get_value(), "hello");
498        assert_eq!(input.get_placeholder(), "Enter text...");
499        assert!(input.disabled);
500        assert!(input.obscure);
501        assert_eq!(input.max_length, 20);
502        assert_eq!(Widget::test_id(&input), Some("my-input"));
503        assert_eq!(input.accessible_name(), Some("Email"));
504    }
505
506    // =========================================================================
507    // TextInput Value Tests - TESTS FIRST
508    // =========================================================================
509
510    #[test]
511    fn test_text_input_value() {
512        let input = TextInput::new().value("test");
513        assert_eq!(input.get_value(), "test");
514        assert!(!input.is_empty());
515    }
516
517    #[test]
518    fn test_text_input_max_length_truncate() {
519        let input = TextInput::new().max_length(5).value("hello world");
520        assert_eq!(input.get_value(), "hello");
521    }
522
523    #[test]
524    fn test_text_input_cursor_position() {
525        let input = TextInput::new().value("hello");
526        assert_eq!(input.cursor_position(), 5); // At end
527    }
528
529    // =========================================================================
530    // TextInput Display Tests - TESTS FIRST
531    // =========================================================================
532
533    #[test]
534    fn test_text_input_display_normal() {
535        let input = TextInput::new().value("password");
536        assert_eq!(input.display_text(), "password");
537    }
538
539    #[test]
540    fn test_text_input_display_obscured() {
541        let input = TextInput::new().value("secret").obscure(true);
542        assert_eq!(input.display_text(), "••••••");
543    }
544
545    // =========================================================================
546    // TextInput Editing Tests - TESTS FIRST
547    // =========================================================================
548
549    #[test]
550    fn test_text_input_insert() {
551        let mut input = TextInput::new().value("hlo");
552        input.cursor = 1;
553        input.insert_text("el");
554        assert_eq!(input.get_value(), "hello");
555        assert_eq!(input.cursor_position(), 3);
556    }
557
558    #[test]
559    fn test_text_input_insert_respects_max_length() {
560        let mut input = TextInput::new().max_length(5).value("abc");
561        input.insert_text("defgh");
562        assert_eq!(input.get_value(), "abcde");
563    }
564
565    #[test]
566    fn test_text_input_backspace() {
567        let mut input = TextInput::new().value("hello");
568        input.backspace();
569        assert_eq!(input.get_value(), "hell");
570        assert_eq!(input.cursor_position(), 4);
571    }
572
573    #[test]
574    fn test_text_input_backspace_at_start() {
575        let mut input = TextInput::new().value("hello");
576        input.cursor = 0;
577        let changed = input.backspace();
578        assert!(!changed);
579        assert_eq!(input.get_value(), "hello");
580    }
581
582    #[test]
583    fn test_text_input_delete() {
584        let mut input = TextInput::new().value("hello");
585        input.cursor = 0;
586        input.delete();
587        assert_eq!(input.get_value(), "ello");
588        assert_eq!(input.cursor_position(), 0);
589    }
590
591    #[test]
592    fn test_text_input_delete_at_end() {
593        let mut input = TextInput::new().value("hello");
594        let changed = input.delete();
595        assert!(!changed);
596        assert_eq!(input.get_value(), "hello");
597    }
598
599    // =========================================================================
600    // TextInput Cursor Movement Tests - TESTS FIRST
601    // =========================================================================
602
603    #[test]
604    fn test_text_input_move_left() {
605        let mut input = TextInput::new().value("hello");
606        input.move_left();
607        assert_eq!(input.cursor_position(), 4);
608    }
609
610    #[test]
611    fn test_text_input_move_left_at_start() {
612        let mut input = TextInput::new().value("hello");
613        input.cursor = 0;
614        input.move_left();
615        assert_eq!(input.cursor_position(), 0); // Stay at 0
616    }
617
618    #[test]
619    fn test_text_input_move_right() {
620        let mut input = TextInput::new().value("hello");
621        input.cursor = 2;
622        input.move_right();
623        assert_eq!(input.cursor_position(), 3);
624    }
625
626    #[test]
627    fn test_text_input_move_right_at_end() {
628        let mut input = TextInput::new().value("hello");
629        input.move_right();
630        assert_eq!(input.cursor_position(), 5); // Stay at end
631    }
632
633    #[test]
634    fn test_text_input_move_home() {
635        let mut input = TextInput::new().value("hello");
636        input.move_home();
637        assert_eq!(input.cursor_position(), 0);
638    }
639
640    #[test]
641    fn test_text_input_move_end() {
642        let mut input = TextInput::new().value("hello");
643        input.cursor = 0;
644        input.move_end();
645        assert_eq!(input.cursor_position(), 5);
646    }
647
648    // =========================================================================
649    // TextInput Widget Trait Tests - TESTS FIRST
650    // =========================================================================
651
652    #[test]
653    fn test_text_input_type_id() {
654        let input = TextInput::new();
655        assert_eq!(Widget::type_id(&input), TypeId::of::<TextInput>());
656    }
657
658    #[test]
659    fn test_text_input_measure() {
660        let input = TextInput::new();
661        let size = input.measure(Constraints::loose(Size::new(400.0, 100.0)));
662        assert!(size.width >= 100.0);
663        assert!(size.height > 0.0);
664    }
665
666    #[test]
667    fn test_text_input_is_interactive() {
668        let input = TextInput::new();
669        assert!(input.is_interactive());
670
671        let input = TextInput::new().disabled(true);
672        assert!(!input.is_interactive());
673    }
674
675    #[test]
676    fn test_text_input_is_focusable() {
677        let input = TextInput::new();
678        assert!(input.is_focusable());
679
680        let input = TextInput::new().disabled(true);
681        assert!(!input.is_focusable());
682    }
683
684    #[test]
685    fn test_text_input_accessible_role() {
686        let input = TextInput::new();
687        assert_eq!(input.accessible_role(), AccessibleRole::TextInput);
688    }
689
690    #[test]
691    fn test_text_input_children() {
692        let input = TextInput::new();
693        assert!(input.children().is_empty());
694    }
695
696    // =========================================================================
697    // TextInput Color Tests - TESTS FIRST
698    // =========================================================================
699
700    #[test]
701    fn test_text_input_colors() {
702        let input = TextInput::new()
703            .background_color(Color::RED)
704            .border_color(Color::GREEN)
705            .focus_border_color(Color::BLUE)
706            .placeholder_color(Color::YELLOW);
707
708        assert_eq!(input.background_color, Color::RED);
709        assert_eq!(input.border_color, Color::GREEN);
710        assert_eq!(input.focus_border_color, Color::BLUE);
711        assert_eq!(input.placeholder_color, Color::YELLOW);
712    }
713
714    // =========================================================================
715    // TextInput Focus Tests - TESTS FIRST
716    // =========================================================================
717
718    #[test]
719    fn test_text_input_focus_state() {
720        let input = TextInput::new();
721        assert!(!input.is_focused());
722    }
723
724    #[test]
725    fn test_text_input_disabled_no_insert() {
726        let mut input = TextInput::new().disabled(true);
727        let changed = input.insert_text("test");
728        assert!(!changed);
729        assert!(input.is_empty());
730    }
731
732    #[test]
733    fn test_text_input_disabled_no_backspace() {
734        let mut input = TextInput::new().value("test").disabled(true);
735        input.disabled = true; // Force after value set
736        let changed = input.backspace();
737        assert!(!changed);
738    }
739
740    #[test]
741    fn test_text_input_disabled_no_delete() {
742        let mut input = TextInput::new().value("test").disabled(true);
743        input.disabled = true;
744        input.cursor = 0;
745        let changed = input.delete();
746        assert!(!changed);
747    }
748
749    // =========================================================================
750    // Event Handling Tests - TESTS FIRST
751    // =========================================================================
752
753    use presentar_core::{Key, MouseButton, Point};
754
755    #[test]
756    fn test_text_input_event_focus_in() {
757        let mut input = TextInput::new();
758        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
759
760        assert!(!input.focused);
761        let result = input.event(&Event::FocusIn);
762        assert!(input.focused);
763        assert!(result.is_none()); // No message for focus
764    }
765
766    #[test]
767    fn test_text_input_event_focus_out() {
768        let mut input = TextInput::new();
769        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
770
771        input.event(&Event::FocusIn);
772        assert!(input.focused);
773
774        let result = input.event(&Event::FocusOut);
775        assert!(!input.focused);
776        assert!(result.is_none());
777    }
778
779    #[test]
780    fn test_text_input_event_mouse_down_inside_focuses() {
781        let mut input = TextInput::new();
782        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
783
784        assert!(!input.focused);
785        let result = input.event(&Event::MouseDown {
786            position: Point::new(100.0, 15.0),
787            button: MouseButton::Left,
788        });
789        assert!(input.focused);
790        assert!(result.is_none());
791    }
792
793    #[test]
794    fn test_text_input_event_mouse_down_outside_unfocuses() {
795        let mut input = TextInput::new();
796        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
797        input.focused = true;
798
799        let result = input.event(&Event::MouseDown {
800            position: Point::new(300.0, 15.0),
801            button: MouseButton::Left,
802        });
803        assert!(!input.focused);
804        assert!(result.is_none());
805    }
806
807    #[test]
808    fn test_text_input_event_mouse_down_sets_cursor() {
809        let mut input = TextInput::new().value("hello");
810        input.cursor = 0; // Start at 0
811        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
812
813        // Click to focus should move cursor to end
814        input.event(&Event::MouseDown {
815            position: Point::new(100.0, 15.0),
816            button: MouseButton::Left,
817        });
818        assert_eq!(input.cursor, 5); // Moved to end of "hello"
819    }
820
821    #[test]
822    fn test_text_input_event_text_input_when_focused() {
823        let mut input = TextInput::new();
824        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
825        input.event(&Event::FocusIn);
826
827        let result = input.event(&Event::TextInput {
828            text: "hello".to_string(),
829        });
830        assert_eq!(input.get_value(), "hello");
831        assert!(result.is_some());
832
833        let msg = result.unwrap().downcast::<TextChanged>().unwrap();
834        assert_eq!(msg.value, "hello");
835    }
836
837    #[test]
838    fn test_text_input_event_text_input_when_not_focused() {
839        let mut input = TextInput::new();
840        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
841        // Not focused
842
843        let result = input.event(&Event::TextInput {
844            text: "hello".to_string(),
845        });
846        assert!(input.get_value().is_empty());
847        assert!(result.is_none());
848    }
849
850    #[test]
851    fn test_text_input_event_key_backspace() {
852        let mut input = TextInput::new().value("hello");
853        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
854        input.event(&Event::FocusIn);
855        input.cursor = 5;
856
857        let result = input.event(&Event::KeyDown {
858            key: Key::Backspace,
859        });
860        assert_eq!(input.get_value(), "hell");
861        assert!(result.is_some());
862
863        let msg = result.unwrap().downcast::<TextChanged>().unwrap();
864        assert_eq!(msg.value, "hell");
865    }
866
867    #[test]
868    fn test_text_input_event_key_delete() {
869        let mut input = TextInput::new().value("hello");
870        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
871        input.event(&Event::FocusIn);
872        input.cursor = 0;
873
874        let result = input.event(&Event::KeyDown { key: Key::Delete });
875        assert_eq!(input.get_value(), "ello");
876        assert!(result.is_some());
877    }
878
879    #[test]
880    fn test_text_input_event_key_left() {
881        let mut input = TextInput::new().value("hello");
882        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
883        input.event(&Event::FocusIn);
884        input.cursor = 3;
885
886        let result = input.event(&Event::KeyDown { key: Key::Left });
887        assert_eq!(input.cursor, 2);
888        assert!(result.is_none()); // No text change
889    }
890
891    #[test]
892    fn test_text_input_event_key_right() {
893        let mut input = TextInput::new().value("hello");
894        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
895        input.event(&Event::FocusIn);
896        input.cursor = 2;
897
898        let result = input.event(&Event::KeyDown { key: Key::Right });
899        assert_eq!(input.cursor, 3);
900        assert!(result.is_none());
901    }
902
903    #[test]
904    fn test_text_input_event_key_home() {
905        let mut input = TextInput::new().value("hello");
906        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
907        input.event(&Event::FocusIn);
908        input.cursor = 5;
909
910        let result = input.event(&Event::KeyDown { key: Key::Home });
911        assert_eq!(input.cursor, 0);
912        assert!(result.is_none());
913    }
914
915    #[test]
916    fn test_text_input_event_key_end() {
917        let mut input = TextInput::new().value("hello");
918        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
919        input.event(&Event::FocusIn);
920        input.cursor = 0;
921
922        let result = input.event(&Event::KeyDown { key: Key::End });
923        assert_eq!(input.cursor, 5);
924        assert!(result.is_none());
925    }
926
927    #[test]
928    fn test_text_input_event_key_enter_submits() {
929        let mut input = TextInput::new().value("hello");
930        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
931        input.event(&Event::FocusIn);
932
933        let result = input.event(&Event::KeyDown { key: Key::Enter });
934        assert!(result.is_some());
935
936        let msg = result.unwrap().downcast::<TextSubmitted>().unwrap();
937        assert_eq!(msg.value, "hello");
938    }
939
940    #[test]
941    fn test_text_input_event_key_when_not_focused() {
942        let mut input = TextInput::new().value("hello");
943        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
944        // Not focused
945
946        let result = input.event(&Event::KeyDown {
947            key: Key::Backspace,
948        });
949        assert_eq!(input.get_value(), "hello"); // Unchanged
950        assert!(result.is_none());
951    }
952
953    #[test]
954    fn test_text_input_event_disabled_blocks_focus() {
955        let mut input = TextInput::new().disabled(true);
956        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
957
958        let result = input.event(&Event::FocusIn);
959        assert!(!input.focused);
960        assert!(result.is_none());
961    }
962
963    #[test]
964    fn test_text_input_event_disabled_blocks_mouse_down() {
965        let mut input = TextInput::new().disabled(true);
966        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
967
968        let result = input.event(&Event::MouseDown {
969            position: Point::new(100.0, 15.0),
970            button: MouseButton::Left,
971        });
972        assert!(!input.focused);
973        assert!(result.is_none());
974    }
975
976    #[test]
977    fn test_text_input_event_disabled_blocks_text_input() {
978        let mut input = TextInput::new().disabled(true);
979        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
980        input.focused = true; // Force focused
981
982        let result = input.event(&Event::TextInput {
983            text: "hello".to_string(),
984        });
985        assert!(input.get_value().is_empty());
986        assert!(result.is_none());
987    }
988
989    #[test]
990    fn test_text_input_event_disabled_blocks_key_down() {
991        let mut input = TextInput::new().value("hello").disabled(true);
992        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
993        input.focused = true;
994
995        let result = input.event(&Event::KeyDown {
996            key: Key::Backspace,
997        });
998        assert_eq!(input.get_value(), "hello");
999        assert!(result.is_none());
1000    }
1001
1002    #[test]
1003    fn test_text_input_event_full_typing_flow() {
1004        let mut input = TextInput::new();
1005        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1006
1007        // 1. Click to focus
1008        input.event(&Event::MouseDown {
1009            position: Point::new(100.0, 15.0),
1010            button: MouseButton::Left,
1011        });
1012        assert!(input.focused);
1013
1014        // 2. Type "Hello"
1015        input.event(&Event::TextInput {
1016            text: "Hello".to_string(),
1017        });
1018        assert_eq!(input.get_value(), "Hello");
1019        assert_eq!(input.cursor, 5);
1020
1021        // 3. Backspace
1022        input.event(&Event::KeyDown {
1023            key: Key::Backspace,
1024        });
1025        assert_eq!(input.get_value(), "Hell");
1026        assert_eq!(input.cursor, 4);
1027
1028        // 4. Navigate home
1029        input.event(&Event::KeyDown { key: Key::Home });
1030        assert_eq!(input.cursor, 0);
1031
1032        // 5. Type "Say "
1033        input.event(&Event::TextInput {
1034            text: "Say ".to_string(),
1035        });
1036        assert_eq!(input.get_value(), "Say Hell");
1037        assert_eq!(input.cursor, 4);
1038
1039        // 6. Navigate end
1040        input.event(&Event::KeyDown { key: Key::End });
1041        assert_eq!(input.cursor, 8);
1042
1043        // 7. Type "o"
1044        input.event(&Event::TextInput {
1045            text: "o".to_string(),
1046        });
1047        assert_eq!(input.get_value(), "Say Hello");
1048
1049        // 8. Submit
1050        let result = input.event(&Event::KeyDown { key: Key::Enter });
1051        let msg = result.unwrap().downcast::<TextSubmitted>().unwrap();
1052        assert_eq!(msg.value, "Say Hello");
1053
1054        // 9. Click outside to unfocus
1055        input.event(&Event::MouseDown {
1056            position: Point::new(300.0, 15.0),
1057            button: MouseButton::Left,
1058        });
1059        assert!(!input.focused);
1060    }
1061
1062    #[test]
1063    fn test_text_input_event_cursor_navigation() {
1064        let mut input = TextInput::new().value("abcde");
1065        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1066        input.event(&Event::FocusIn);
1067        input.cursor = 2;
1068
1069        // Left boundary
1070        input.event(&Event::KeyDown { key: Key::Left });
1071        input.event(&Event::KeyDown { key: Key::Left });
1072        input.event(&Event::KeyDown { key: Key::Left }); // Should stay at 0
1073        assert_eq!(input.cursor, 0);
1074
1075        // Right boundary
1076        input.event(&Event::KeyDown { key: Key::End });
1077        input.event(&Event::KeyDown { key: Key::Right }); // Should stay at end
1078        assert_eq!(input.cursor, 5);
1079    }
1080
1081    #[test]
1082    fn test_text_input_event_backspace_at_start() {
1083        let mut input = TextInput::new().value("hello");
1084        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1085        input.event(&Event::FocusIn);
1086        input.cursor = 0;
1087
1088        let result = input.event(&Event::KeyDown {
1089            key: Key::Backspace,
1090        });
1091        assert_eq!(input.get_value(), "hello"); // Unchanged
1092        assert!(result.is_none()); // No change message
1093    }
1094
1095    #[test]
1096    fn test_text_input_event_delete_at_end() {
1097        let mut input = TextInput::new().value("hello");
1098        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1099        input.event(&Event::FocusIn);
1100        input.cursor = 5;
1101
1102        let result = input.event(&Event::KeyDown { key: Key::Delete });
1103        assert_eq!(input.get_value(), "hello"); // Unchanged
1104        assert!(result.is_none());
1105    }
1106
1107    #[test]
1108    fn test_text_input_event_max_length_enforced() {
1109        let mut input = TextInput::new().max_length(5);
1110        input.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
1111        input.event(&Event::FocusIn);
1112
1113        // Type beyond max length
1114        input.event(&Event::TextInput {
1115            text: "hello world".to_string(),
1116        });
1117        assert_eq!(input.get_value(), "hello"); // Truncated to 5
1118    }
1119}