Skip to main content

presentar_terminal/widgets/
text_input.rs

1//! `TextInput` widget with full editing capabilities.
2//!
3//! Provides a text input field with cursor, selection, and editing.
4//! Based on btop filter input patterns.
5
6use presentar_core::{
7    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event, Key,
8    LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
9};
10use std::any::Any;
11use std::time::Duration;
12
13/// Text input widget with cursor and editing support.
14#[derive(Debug, Clone)]
15pub struct TextInput {
16    /// Current text content.
17    text: String,
18    /// Cursor position (character index, not byte).
19    cursor: usize,
20    /// Selection range (start, end) if active. Both are character indices.
21    selection: Option<(usize, usize)>,
22    /// Placeholder text when empty.
23    placeholder: String,
24    /// Input mask character (e.g., '*' for password).
25    mask: Option<char>,
26    /// Maximum length (None = unlimited).
27    max_length: Option<usize>,
28    /// Horizontal scroll offset (character index).
29    scroll_offset: usize,
30    /// Is the input focused.
31    focused: bool,
32    /// Text color.
33    text_color: Color,
34    /// Cursor color.
35    cursor_color: Color,
36    /// Selection background color.
37    selection_color: Color,
38    /// Placeholder color.
39    placeholder_color: Color,
40    /// Cached bounds.
41    bounds: Rect,
42}
43
44impl Default for TextInput {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50impl TextInput {
51    /// Create a new text input.
52    #[must_use]
53    pub fn new() -> Self {
54        Self {
55            text: String::new(),
56            cursor: 0,
57            selection: None,
58            placeholder: String::new(),
59            mask: None,
60            max_length: None,
61            scroll_offset: 0,
62            focused: false,
63            text_color: Color::WHITE,
64            cursor_color: Color::new(0.8, 0.8, 0.8, 1.0),
65            selection_color: Color::new(0.3, 0.5, 0.8, 0.5),
66            placeholder_color: Color::new(0.5, 0.5, 0.5, 1.0),
67            bounds: Rect::default(),
68        }
69    }
70
71    /// Set initial text.
72    #[must_use]
73    pub fn with_text(mut self, text: impl Into<String>) -> Self {
74        self.text = text.into();
75        self.cursor = self.text.chars().count();
76        self
77    }
78
79    /// Set placeholder text.
80    #[must_use]
81    pub fn with_placeholder(mut self, text: impl Into<String>) -> Self {
82        self.placeholder = text.into();
83        self
84    }
85
86    /// Set mask character for password input.
87    #[must_use]
88    pub fn with_mask(mut self, ch: char) -> Self {
89        self.mask = Some(ch);
90        self
91    }
92
93    /// Set maximum length.
94    #[must_use]
95    pub fn with_max_length(mut self, len: usize) -> Self {
96        self.max_length = Some(len);
97        self
98    }
99
100    /// Set focused state.
101    #[must_use]
102    pub fn with_focused(mut self, focused: bool) -> Self {
103        self.focused = focused;
104        self
105    }
106
107    /// Set text color.
108    #[must_use]
109    pub fn with_text_color(mut self, color: Color) -> Self {
110        self.text_color = color;
111        self
112    }
113
114    /// Set cursor color.
115    #[must_use]
116    pub fn with_cursor_color(mut self, color: Color) -> Self {
117        self.cursor_color = color;
118        self
119    }
120
121    // ================= State Getters =================
122
123    /// Get the current text.
124    #[must_use]
125    pub fn text(&self) -> &str {
126        &self.text
127    }
128
129    /// Get cursor position (character index).
130    #[must_use]
131    pub fn cursor(&self) -> usize {
132        self.cursor
133    }
134
135    /// Check if input is empty.
136    #[must_use]
137    pub fn is_empty(&self) -> bool {
138        self.text.is_empty()
139    }
140
141    /// Get text length in characters.
142    #[must_use]
143    pub fn len(&self) -> usize {
144        self.text.chars().count()
145    }
146
147    /// Check if input is focused.
148    #[must_use]
149    pub fn is_focused(&self) -> bool {
150        self.focused
151    }
152
153    /// Check if there's an active selection.
154    #[must_use]
155    pub fn has_selection(&self) -> bool {
156        self.selection.is_some()
157    }
158
159    /// Get selection range if any.
160    #[must_use]
161    pub fn selection(&self) -> Option<(usize, usize)> {
162        self.selection
163    }
164
165    // ================= Text Modification =================
166
167    /// Set the text content.
168    pub fn set_text(&mut self, text: impl Into<String>) {
169        self.text = text.into();
170        let len = self.text.chars().count();
171        if let Some(max) = self.max_length {
172            if len > max {
173                self.text = self.text.chars().take(max).collect();
174            }
175        }
176        self.cursor = self.text.chars().count();
177        self.selection = None;
178        self.adjust_scroll();
179    }
180
181    /// Insert a character at cursor.
182    pub fn insert(&mut self, ch: char) {
183        if let Some(max) = self.max_length {
184            if self.len() >= max {
185                return;
186            }
187        }
188        self.delete_selection();
189        let byte_pos = self.cursor_byte_pos();
190        self.text.insert(byte_pos, ch);
191        self.cursor += 1;
192        self.adjust_scroll();
193    }
194
195    /// Insert a string at cursor.
196    pub fn insert_str(&mut self, s: &str) {
197        self.delete_selection();
198        let to_insert: String = if let Some(max) = self.max_length {
199            let available = max.saturating_sub(self.len());
200            s.chars().take(available).collect()
201        } else {
202            s.to_string()
203        };
204        let byte_pos = self.cursor_byte_pos();
205        self.text.insert_str(byte_pos, &to_insert);
206        self.cursor += to_insert.chars().count();
207        self.adjust_scroll();
208    }
209
210    /// Delete character at cursor (Delete key).
211    pub fn delete(&mut self) {
212        if self.selection.is_some() {
213            self.delete_selection();
214            return;
215        }
216        let len = self.len();
217        if self.cursor < len {
218            let byte_pos = self.cursor_byte_pos();
219            let next_byte = self.char_byte_pos(self.cursor + 1);
220            self.text.drain(byte_pos..next_byte);
221        }
222    }
223
224    /// Delete character before cursor (Backspace key).
225    pub fn backspace(&mut self) {
226        if self.selection.is_some() {
227            self.delete_selection();
228            return;
229        }
230        if self.cursor > 0 {
231            self.cursor -= 1;
232            let byte_pos = self.cursor_byte_pos();
233            let next_byte = self.char_byte_pos(self.cursor + 1);
234            self.text.drain(byte_pos..next_byte);
235            self.adjust_scroll();
236        }
237    }
238
239    /// Delete word at/after cursor.
240    pub fn delete_word(&mut self) {
241        if self.selection.is_some() {
242            self.delete_selection();
243            return;
244        }
245        let start = self.cursor;
246        self.move_word_right();
247        let end = self.cursor;
248        if end > start {
249            let start_byte = self.char_byte_pos(start);
250            let end_byte = self.char_byte_pos(end);
251            self.text.drain(start_byte..end_byte);
252            self.cursor = start;
253        }
254    }
255
256    /// Delete from cursor to end of line.
257    pub fn delete_to_end(&mut self) {
258        let byte_pos = self.cursor_byte_pos();
259        self.text.truncate(byte_pos);
260    }
261
262    /// Delete entire line (clear all text).
263    pub fn delete_line(&mut self) {
264        self.text.clear();
265        self.cursor = 0;
266        self.selection = None;
267        self.scroll_offset = 0;
268    }
269
270    // ================= Cursor Movement =================
271
272    /// Move cursor left.
273    pub fn move_left(&mut self) {
274        self.selection = None;
275        if self.cursor > 0 {
276            self.cursor -= 1;
277            self.adjust_scroll();
278        }
279    }
280
281    /// Move cursor right.
282    pub fn move_right(&mut self) {
283        self.selection = None;
284        let len = self.len();
285        if self.cursor < len {
286            self.cursor += 1;
287            self.adjust_scroll();
288        }
289    }
290
291    /// Move cursor to start of previous word.
292    pub fn move_word_left(&mut self) {
293        self.selection = None;
294        if self.cursor == 0 {
295            return;
296        }
297        let chars: Vec<char> = self.text.chars().collect();
298        let mut pos = self.cursor - 1;
299        // Skip whitespace
300        while pos > 0 && chars[pos].is_whitespace() {
301            pos -= 1;
302        }
303        // Skip word characters
304        while pos > 0 && !chars[pos - 1].is_whitespace() {
305            pos -= 1;
306        }
307        self.cursor = pos;
308        self.adjust_scroll();
309    }
310
311    /// Move cursor to end of next word.
312    pub fn move_word_right(&mut self) {
313        self.selection = None;
314        let chars: Vec<char> = self.text.chars().collect();
315        let len = chars.len();
316        if self.cursor >= len {
317            return;
318        }
319        let mut pos = self.cursor;
320        // Skip word characters
321        while pos < len && !chars[pos].is_whitespace() {
322            pos += 1;
323        }
324        // Skip whitespace
325        while pos < len && chars[pos].is_whitespace() {
326            pos += 1;
327        }
328        self.cursor = pos;
329        self.adjust_scroll();
330    }
331
332    /// Move cursor to start (Home).
333    pub fn move_home(&mut self) {
334        self.selection = None;
335        self.cursor = 0;
336        self.scroll_offset = 0;
337    }
338
339    /// Move cursor to end (End).
340    pub fn move_end(&mut self) {
341        self.selection = None;
342        self.cursor = self.len();
343        self.adjust_scroll();
344    }
345
346    // ================= Selection =================
347
348    /// Select all text.
349    pub fn select_all(&mut self) {
350        let len = self.len();
351        if len > 0 {
352            self.selection = Some((0, len));
353            self.cursor = len;
354        }
355    }
356
357    /// Select word at cursor.
358    pub fn select_word(&mut self) {
359        let chars: Vec<char> = self.text.chars().collect();
360        let len = chars.len();
361        if len == 0 || self.cursor > len {
362            return;
363        }
364        let pos = self.cursor.min(len.saturating_sub(1));
365        let mut start = pos;
366        let mut end = pos;
367        // Expand backward
368        while start > 0 && !chars[start - 1].is_whitespace() {
369            start -= 1;
370        }
371        // Expand forward
372        while end < len && !chars[end].is_whitespace() {
373            end += 1;
374        }
375        if start < end {
376            self.selection = Some((start, end));
377            self.cursor = end;
378        }
379    }
380
381    /// Extend selection left.
382    pub fn extend_selection_left(&mut self) {
383        if self.cursor == 0 {
384            return;
385        }
386        let new_cursor = self.cursor - 1;
387        match self.selection {
388            Some((start, end)) => {
389                if self.cursor == start {
390                    self.selection = Some((new_cursor, end));
391                } else {
392                    self.selection = Some((start, new_cursor));
393                }
394            }
395            None => {
396                self.selection = Some((new_cursor, self.cursor));
397            }
398        }
399        self.cursor = new_cursor;
400        self.adjust_scroll();
401    }
402
403    /// Extend selection right.
404    pub fn extend_selection_right(&mut self) {
405        let len = self.len();
406        if self.cursor >= len {
407            return;
408        }
409        let new_cursor = self.cursor + 1;
410        match self.selection {
411            Some((start, end)) => {
412                if self.cursor == end {
413                    self.selection = Some((start, new_cursor));
414                } else {
415                    self.selection = Some((new_cursor, end));
416                }
417            }
418            None => {
419                self.selection = Some((self.cursor, new_cursor));
420            }
421        }
422        self.cursor = new_cursor;
423        self.adjust_scroll();
424    }
425
426    /// Clear selection.
427    pub fn clear_selection(&mut self) {
428        self.selection = None;
429    }
430
431    /// Get selected text.
432    #[must_use]
433    pub fn selected_text(&self) -> Option<String> {
434        self.selection.map(|(start, end)| {
435            let (start, end) = (start.min(end), start.max(end));
436            self.text.chars().skip(start).take(end - start).collect()
437        })
438    }
439
440    /// Delete selected text.
441    pub fn delete_selection(&mut self) {
442        if let Some((start, end)) = self.selection.take() {
443            let (start, end) = (start.min(end), start.max(end));
444            let start_byte = self.char_byte_pos(start);
445            let end_byte = self.char_byte_pos(end);
446            self.text.drain(start_byte..end_byte);
447            self.cursor = start;
448            self.adjust_scroll();
449        }
450    }
451
452    // ================= Clipboard =================
453
454    /// Copy selected text (returns text for external clipboard).
455    #[must_use]
456    pub fn copy(&self) -> Option<String> {
457        self.selected_text()
458    }
459
460    /// Cut selected text (returns text for external clipboard).
461    pub fn cut(&mut self) -> Option<String> {
462        let text = self.selected_text();
463        self.delete_selection();
464        text
465    }
466
467    /// Paste text (caller provides from external clipboard).
468    pub fn paste(&mut self, text: &str) {
469        self.delete_selection();
470        self.insert_str(text);
471    }
472
473    // ================= Focus =================
474
475    /// Set focused state.
476    pub fn focus(&mut self) {
477        self.focused = true;
478    }
479
480    /// Remove focus.
481    pub fn blur(&mut self) {
482        self.focused = false;
483        self.selection = None;
484    }
485
486    // ================= Internal Helpers =================
487
488    /// Get byte position for character index.
489    fn char_byte_pos(&self, char_idx: usize) -> usize {
490        self.text
491            .char_indices()
492            .nth(char_idx)
493            .map_or(self.text.len(), |(i, _)| i)
494    }
495
496    /// Get byte position of cursor.
497    fn cursor_byte_pos(&self) -> usize {
498        self.char_byte_pos(self.cursor)
499    }
500
501    /// Adjust scroll offset to keep cursor visible.
502    fn adjust_scroll(&mut self) {
503        let visible_width = self.bounds.width as usize;
504        if visible_width == 0 {
505            return;
506        }
507        if self.cursor < self.scroll_offset {
508            self.scroll_offset = self.cursor;
509        } else if self.cursor >= self.scroll_offset + visible_width {
510            self.scroll_offset = self.cursor.saturating_sub(visible_width - 1);
511        }
512    }
513
514    /// Get display text (masked if mask is set).
515    fn display_text(&self) -> String {
516        if let Some(mask) = self.mask {
517            std::iter::repeat_n(mask, self.len()).collect()
518        } else {
519            self.text.clone()
520        }
521    }
522}
523
524impl Brick for TextInput {
525    fn brick_name(&self) -> &'static str {
526        "text_input"
527    }
528
529    fn assertions(&self) -> &[BrickAssertion] {
530        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
531        ASSERTIONS
532    }
533
534    fn budget(&self) -> BrickBudget {
535        BrickBudget::uniform(8)
536    }
537
538    fn verify(&self) -> BrickVerification {
539        BrickVerification {
540            passed: self.assertions().to_vec(),
541            failed: vec![],
542            verification_time: Duration::from_micros(5),
543        }
544    }
545
546    fn to_html(&self) -> String {
547        String::new()
548    }
549
550    fn to_css(&self) -> String {
551        String::new()
552    }
553}
554
555impl Widget for TextInput {
556    fn type_id(&self) -> TypeId {
557        TypeId::of::<Self>()
558    }
559
560    fn measure(&self, constraints: Constraints) -> Size {
561        let width = constraints.max_width.clamp(5.0, 40.0);
562        Size::new(width, 1.0)
563    }
564
565    fn layout(&mut self, bounds: Rect) -> LayoutResult {
566        self.bounds = bounds;
567        self.adjust_scroll();
568        LayoutResult {
569            size: Size::new(bounds.width, bounds.height),
570        }
571    }
572
573    fn paint(&self, canvas: &mut dyn Canvas) {
574        let width = self.bounds.width as usize;
575        if width == 0 {
576            return;
577        }
578
579        // Get display text (masked or actual)
580        let display = if self.is_empty() && !self.focused {
581            &self.placeholder
582        } else {
583            &self.display_text()
584        };
585
586        let text_color = if self.is_empty() && !self.focused {
587            self.placeholder_color
588        } else {
589            self.text_color
590        };
591
592        // Draw visible portion
593        let chars: Vec<char> = display.chars().collect();
594        let visible_start = self.scroll_offset;
595        let visible_end = (visible_start + width).min(chars.len());
596
597        for (i, char_idx) in (visible_start..visible_end).enumerate() {
598            let ch = chars[char_idx];
599            let x = self.bounds.x + i as f32;
600
601            // Check if in selection
602            let in_selection = self.selection.is_some_and(|(start, end)| {
603                let (s, e) = (start.min(end), start.max(end));
604                char_idx >= s && char_idx < e
605            });
606
607            // Draw selection background
608            if in_selection {
609                canvas.fill_rect(Rect::new(x, self.bounds.y, 1.0, 1.0), self.selection_color);
610            }
611
612            let style = TextStyle {
613                color: text_color,
614                ..Default::default()
615            };
616            canvas.draw_text(&ch.to_string(), Point::new(x, self.bounds.y), &style);
617        }
618
619        // Draw cursor if focused
620        if self.focused
621            && self.cursor >= self.scroll_offset
622            && self.cursor < self.scroll_offset + width
623        {
624            let cursor_x = self.bounds.x + (self.cursor - self.scroll_offset) as f32;
625            canvas.fill_rect(
626                Rect::new(cursor_x, self.bounds.y, 0.1, 1.0),
627                self.cursor_color,
628            );
629        }
630    }
631
632    fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
633        if !self.focused {
634            return None;
635        }
636
637        match event {
638            Event::KeyDown { key, .. } => match key {
639                Key::Backspace => {
640                    self.backspace();
641                }
642                Key::Delete => {
643                    self.delete();
644                }
645                Key::Left => {
646                    self.move_left();
647                }
648                Key::Right => {
649                    self.move_right();
650                }
651                Key::Home => {
652                    self.move_home();
653                }
654                Key::End => {
655                    self.move_end();
656                }
657                _ => {}
658            },
659            Event::TextInput { text } => {
660                for ch in text.chars() {
661                    self.insert(ch);
662                }
663            }
664            _ => {}
665        }
666
667        None
668    }
669
670    fn children(&self) -> &[Box<dyn Widget>] {
671        &[]
672    }
673
674    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
675        &mut []
676    }
677}
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682
683    struct MockCanvas {
684        texts: Vec<(String, Point)>,
685        rects: Vec<Rect>,
686    }
687
688    impl MockCanvas {
689        fn new() -> Self {
690            Self {
691                texts: vec![],
692                rects: vec![],
693            }
694        }
695    }
696
697    impl Canvas for MockCanvas {
698        fn fill_rect(&mut self, rect: Rect, _color: Color) {
699            self.rects.push(rect);
700        }
701        fn stroke_rect(&mut self, _rect: Rect, _color: Color, _width: f32) {}
702        fn draw_text(&mut self, text: &str, position: Point, _style: &TextStyle) {
703            self.texts.push((text.to_string(), position));
704        }
705        fn draw_line(&mut self, _from: Point, _to: Point, _color: Color, _width: f32) {}
706        fn fill_circle(&mut self, _center: Point, _radius: f32, _color: Color) {}
707        fn stroke_circle(&mut self, _center: Point, _radius: f32, _color: Color, _width: f32) {}
708        fn fill_arc(&mut self, _c: Point, _r: f32, _s: f32, _e: f32, _color: Color) {}
709        fn draw_path(&mut self, _points: &[Point], _color: Color, _width: f32) {}
710        fn fill_polygon(&mut self, _points: &[Point], _color: Color) {}
711        fn push_clip(&mut self, _rect: Rect) {}
712        fn pop_clip(&mut self) {}
713        fn push_transform(&mut self, _transform: presentar_core::Transform2D) {}
714        fn pop_transform(&mut self) {}
715    }
716
717    // =====================================================
718    // Construction Tests
719    // =====================================================
720
721    #[test]
722    fn test_new() {
723        let input = TextInput::new();
724        assert!(input.is_empty());
725        assert_eq!(input.cursor(), 0);
726        assert!(!input.is_focused());
727    }
728
729    #[test]
730    fn test_default() {
731        let input = TextInput::default();
732        assert!(input.is_empty());
733    }
734
735    #[test]
736    fn test_with_text() {
737        let input = TextInput::new().with_text("hello");
738        assert_eq!(input.text(), "hello");
739        assert_eq!(input.cursor(), 5);
740    }
741
742    #[test]
743    fn test_with_placeholder() {
744        let input = TextInput::new().with_placeholder("Enter text...");
745        assert_eq!(input.placeholder, "Enter text...");
746    }
747
748    #[test]
749    fn test_with_mask() {
750        let input = TextInput::new().with_mask('*');
751        assert_eq!(input.mask, Some('*'));
752    }
753
754    #[test]
755    fn test_with_max_length() {
756        let input = TextInput::new().with_max_length(10);
757        assert_eq!(input.max_length, Some(10));
758    }
759
760    #[test]
761    fn test_with_focused() {
762        let input = TextInput::new().with_focused(true);
763        assert!(input.is_focused());
764    }
765
766    #[test]
767    fn test_with_text_color() {
768        let input = TextInput::new().with_text_color(Color::RED);
769        assert_eq!(input.text_color, Color::RED);
770    }
771
772    #[test]
773    fn test_with_cursor_color() {
774        let input = TextInput::new().with_cursor_color(Color::GREEN);
775        assert_eq!(input.cursor_color, Color::GREEN);
776    }
777
778    // =====================================================
779    // State Getter Tests
780    // =====================================================
781
782    #[test]
783    fn test_len() {
784        let input = TextInput::new().with_text("hello");
785        assert_eq!(input.len(), 5);
786    }
787
788    #[test]
789    fn test_len_unicode() {
790        let input = TextInput::new().with_text("héllo");
791        assert_eq!(input.len(), 5);
792    }
793
794    #[test]
795    fn test_is_empty_true() {
796        let input = TextInput::new();
797        assert!(input.is_empty());
798    }
799
800    #[test]
801    fn test_is_empty_false() {
802        let input = TextInput::new().with_text("x");
803        assert!(!input.is_empty());
804    }
805
806    // =====================================================
807    // Text Modification Tests
808    // =====================================================
809
810    #[test]
811    fn test_set_text() {
812        let mut input = TextInput::new();
813        input.set_text("hello");
814        assert_eq!(input.text(), "hello");
815        assert_eq!(input.cursor(), 5);
816    }
817
818    #[test]
819    fn test_set_text_clears_selection() {
820        let mut input = TextInput::new().with_text("hello");
821        input.select_all();
822        input.set_text("world");
823        assert!(!input.has_selection());
824    }
825
826    #[test]
827    fn test_set_text_respects_max_length() {
828        let mut input = TextInput::new().with_max_length(3);
829        input.set_text("hello");
830        assert_eq!(input.text(), "hel");
831    }
832
833    #[test]
834    fn test_insert() {
835        let mut input = TextInput::new();
836        input.insert('a');
837        input.insert('b');
838        assert_eq!(input.text(), "ab");
839        assert_eq!(input.cursor(), 2);
840    }
841
842    #[test]
843    fn test_insert_respects_max_length() {
844        let mut input = TextInput::new().with_max_length(2);
845        input.insert('a');
846        input.insert('b');
847        input.insert('c');
848        assert_eq!(input.text(), "ab");
849    }
850
851    #[test]
852    fn test_insert_deletes_selection() {
853        let mut input = TextInput::new().with_text("hello");
854        input.select_all();
855        input.insert('X');
856        assert_eq!(input.text(), "X");
857    }
858
859    #[test]
860    fn test_insert_str() {
861        let mut input = TextInput::new();
862        input.insert_str("hello");
863        assert_eq!(input.text(), "hello");
864        assert_eq!(input.cursor(), 5);
865    }
866
867    #[test]
868    fn test_insert_str_respects_max_length() {
869        let mut input = TextInput::new().with_max_length(3);
870        input.insert_str("hello");
871        assert_eq!(input.text(), "hel");
872    }
873
874    #[test]
875    fn test_delete() {
876        let mut input = TextInput::new().with_text("hello");
877        input.cursor = 2;
878        input.delete();
879        assert_eq!(input.text(), "helo");
880    }
881
882    #[test]
883    fn test_delete_at_end() {
884        let mut input = TextInput::new().with_text("hello");
885        input.delete();
886        assert_eq!(input.text(), "hello");
887    }
888
889    #[test]
890    fn test_delete_selection() {
891        let mut input = TextInput::new().with_text("hello");
892        input.select_all();
893        input.delete();
894        assert!(input.is_empty());
895    }
896
897    #[test]
898    fn test_backspace() {
899        let mut input = TextInput::new().with_text("hello");
900        input.backspace();
901        assert_eq!(input.text(), "hell");
902        assert_eq!(input.cursor(), 4);
903    }
904
905    #[test]
906    fn test_backspace_at_start() {
907        let mut input = TextInput::new().with_text("hello");
908        input.cursor = 0;
909        input.backspace();
910        assert_eq!(input.text(), "hello");
911    }
912
913    #[test]
914    fn test_delete_word() {
915        let mut input = TextInput::new().with_text("hello world");
916        input.cursor = 0;
917        input.delete_word();
918        assert_eq!(input.text(), "world");
919    }
920
921    #[test]
922    fn test_delete_to_end() {
923        let mut input = TextInput::new().with_text("hello world");
924        input.cursor = 6;
925        input.delete_to_end();
926        assert_eq!(input.text(), "hello ");
927    }
928
929    #[test]
930    fn test_delete_line() {
931        let mut input = TextInput::new().with_text("hello world");
932        input.delete_line();
933        assert!(input.is_empty());
934        assert_eq!(input.cursor(), 0);
935    }
936
937    // =====================================================
938    // Cursor Movement Tests
939    // =====================================================
940
941    #[test]
942    fn test_move_left() {
943        let mut input = TextInput::new().with_text("hello");
944        input.move_left();
945        assert_eq!(input.cursor(), 4);
946    }
947
948    #[test]
949    fn test_move_left_at_start() {
950        let mut input = TextInput::new().with_text("hello");
951        input.cursor = 0;
952        input.move_left();
953        assert_eq!(input.cursor(), 0);
954    }
955
956    #[test]
957    fn test_move_left_clears_selection() {
958        let mut input = TextInput::new().with_text("hello");
959        input.select_all();
960        input.move_left();
961        assert!(!input.has_selection());
962    }
963
964    #[test]
965    fn test_move_right() {
966        let mut input = TextInput::new().with_text("hello");
967        input.cursor = 0;
968        input.move_right();
969        assert_eq!(input.cursor(), 1);
970    }
971
972    #[test]
973    fn test_move_right_at_end() {
974        let mut input = TextInput::new().with_text("hello");
975        input.move_right();
976        assert_eq!(input.cursor(), 5);
977    }
978
979    #[test]
980    fn test_move_word_left() {
981        let mut input = TextInput::new().with_text("hello world");
982        input.move_word_left();
983        assert_eq!(input.cursor(), 6);
984    }
985
986    #[test]
987    fn test_move_word_right() {
988        let mut input = TextInput::new().with_text("hello world");
989        input.cursor = 0;
990        input.move_word_right();
991        assert_eq!(input.cursor(), 6);
992    }
993
994    #[test]
995    fn test_move_home() {
996        let mut input = TextInput::new().with_text("hello");
997        input.move_home();
998        assert_eq!(input.cursor(), 0);
999    }
1000
1001    #[test]
1002    fn test_move_end() {
1003        let mut input = TextInput::new().with_text("hello");
1004        input.cursor = 0;
1005        input.move_end();
1006        assert_eq!(input.cursor(), 5);
1007    }
1008
1009    // =====================================================
1010    // Selection Tests
1011    // =====================================================
1012
1013    #[test]
1014    fn test_select_all() {
1015        let mut input = TextInput::new().with_text("hello");
1016        input.select_all();
1017        assert_eq!(input.selection(), Some((0, 5)));
1018    }
1019
1020    #[test]
1021    fn test_select_all_empty() {
1022        let mut input = TextInput::new();
1023        input.select_all();
1024        assert!(!input.has_selection());
1025    }
1026
1027    #[test]
1028    fn test_select_word() {
1029        let mut input = TextInput::new().with_text("hello world");
1030        input.cursor = 2;
1031        input.select_word();
1032        assert_eq!(input.selection(), Some((0, 5)));
1033    }
1034
1035    #[test]
1036    fn test_selected_text() {
1037        let mut input = TextInput::new().with_text("hello");
1038        input.select_all();
1039        assert_eq!(input.selected_text(), Some("hello".to_string()));
1040    }
1041
1042    #[test]
1043    fn test_selected_text_none() {
1044        let input = TextInput::new().with_text("hello");
1045        assert_eq!(input.selected_text(), None);
1046    }
1047
1048    #[test]
1049    fn test_extend_selection_left() {
1050        let mut input = TextInput::new().with_text("hello");
1051        input.extend_selection_left();
1052        assert_eq!(input.selection(), Some((4, 5)));
1053        assert_eq!(input.cursor(), 4);
1054    }
1055
1056    #[test]
1057    fn test_extend_selection_right() {
1058        let mut input = TextInput::new().with_text("hello");
1059        input.cursor = 0;
1060        input.extend_selection_right();
1061        assert_eq!(input.selection(), Some((0, 1)));
1062        assert_eq!(input.cursor(), 1);
1063    }
1064
1065    #[test]
1066    fn test_clear_selection() {
1067        let mut input = TextInput::new().with_text("hello");
1068        input.select_all();
1069        input.clear_selection();
1070        assert!(!input.has_selection());
1071    }
1072
1073    // =====================================================
1074    // Clipboard Tests
1075    // =====================================================
1076
1077    #[test]
1078    fn test_copy() {
1079        let mut input = TextInput::new().with_text("hello");
1080        input.select_all();
1081        assert_eq!(input.copy(), Some("hello".to_string()));
1082        assert!(input.has_selection()); // Selection preserved
1083    }
1084
1085    #[test]
1086    fn test_cut() {
1087        let mut input = TextInput::new().with_text("hello");
1088        input.select_all();
1089        assert_eq!(input.cut(), Some("hello".to_string()));
1090        assert!(input.is_empty());
1091    }
1092
1093    #[test]
1094    fn test_paste() {
1095        let mut input = TextInput::new();
1096        input.paste("hello");
1097        assert_eq!(input.text(), "hello");
1098    }
1099
1100    #[test]
1101    fn test_paste_replaces_selection() {
1102        let mut input = TextInput::new().with_text("hello");
1103        input.select_all();
1104        input.paste("world");
1105        assert_eq!(input.text(), "world");
1106    }
1107
1108    // =====================================================
1109    // Focus Tests
1110    // =====================================================
1111
1112    #[test]
1113    fn test_focus() {
1114        let mut input = TextInput::new();
1115        input.focus();
1116        assert!(input.is_focused());
1117    }
1118
1119    #[test]
1120    fn test_blur() {
1121        let mut input = TextInput::new().with_focused(true);
1122        input.select_all();
1123        input.blur();
1124        assert!(!input.is_focused());
1125        assert!(!input.has_selection());
1126    }
1127
1128    // =====================================================
1129    // Display Text Tests
1130    // =====================================================
1131
1132    #[test]
1133    fn test_display_text_normal() {
1134        let input = TextInput::new().with_text("hello");
1135        assert_eq!(input.display_text(), "hello");
1136    }
1137
1138    #[test]
1139    fn test_display_text_masked() {
1140        let input = TextInput::new().with_text("hello").with_mask('*');
1141        assert_eq!(input.display_text(), "*****");
1142    }
1143
1144    // =====================================================
1145    // Brick Trait Tests
1146    // =====================================================
1147
1148    #[test]
1149    fn test_brick_name() {
1150        let input = TextInput::new();
1151        assert_eq!(input.brick_name(), "text_input");
1152    }
1153
1154    #[test]
1155    fn test_assertions_not_empty() {
1156        let input = TextInput::new();
1157        assert!(!input.assertions().is_empty());
1158    }
1159
1160    #[test]
1161    fn test_budget() {
1162        let input = TextInput::new();
1163        assert!(input.budget().paint_ms > 0);
1164    }
1165
1166    #[test]
1167    fn test_verify() {
1168        let input = TextInput::new();
1169        assert!(input.verify().is_valid());
1170    }
1171
1172    #[test]
1173    fn test_to_html() {
1174        let input = TextInput::new();
1175        assert!(input.to_html().is_empty());
1176    }
1177
1178    #[test]
1179    fn test_to_css() {
1180        let input = TextInput::new();
1181        assert!(input.to_css().is_empty());
1182    }
1183
1184    // =====================================================
1185    // Widget Trait Tests
1186    // =====================================================
1187
1188    #[test]
1189    fn test_type_id() {
1190        let input = TextInput::new();
1191        assert_eq!(Widget::type_id(&input), TypeId::of::<TextInput>());
1192    }
1193
1194    #[test]
1195    fn test_measure() {
1196        let input = TextInput::new();
1197        let size = input.measure(Constraints::loose(Size::new(100.0, 100.0)));
1198        assert!(size.width >= 5.0);
1199        assert_eq!(size.height, 1.0);
1200    }
1201
1202    #[test]
1203    fn test_layout() {
1204        let mut input = TextInput::new();
1205        let bounds = Rect::new(0.0, 0.0, 20.0, 1.0);
1206        let result = input.layout(bounds);
1207        assert_eq!(result.size.width, 20.0);
1208        assert_eq!(input.bounds, bounds);
1209    }
1210
1211    #[test]
1212    fn test_children() {
1213        let input = TextInput::new();
1214        assert!(input.children().is_empty());
1215    }
1216
1217    #[test]
1218    fn test_children_mut() {
1219        let mut input = TextInput::new();
1220        assert!(input.children_mut().is_empty());
1221    }
1222
1223    // =====================================================
1224    // Paint Tests
1225    // =====================================================
1226
1227    #[test]
1228    fn test_paint_empty() {
1229        let mut input = TextInput::new();
1230        input.bounds = Rect::new(0.0, 0.0, 10.0, 1.0);
1231        let mut canvas = MockCanvas::new();
1232        input.paint(&mut canvas);
1233        // Should handle empty gracefully
1234    }
1235
1236    #[test]
1237    fn test_paint_with_text() {
1238        let mut input = TextInput::new().with_text("hello");
1239        input.bounds = Rect::new(0.0, 0.0, 10.0, 1.0);
1240        let mut canvas = MockCanvas::new();
1241        input.paint(&mut canvas);
1242        assert!(!canvas.texts.is_empty());
1243    }
1244
1245    #[test]
1246    fn test_paint_with_cursor() {
1247        let mut input = TextInput::new().with_text("hello").with_focused(true);
1248        input.bounds = Rect::new(0.0, 0.0, 10.0, 1.0);
1249        let mut canvas = MockCanvas::new();
1250        input.paint(&mut canvas);
1251        assert!(!canvas.rects.is_empty()); // Cursor rect
1252    }
1253
1254    #[test]
1255    fn test_paint_with_selection() {
1256        let mut input = TextInput::new().with_text("hello").with_focused(true);
1257        input.select_all();
1258        input.bounds = Rect::new(0.0, 0.0, 10.0, 1.0);
1259        let mut canvas = MockCanvas::new();
1260        input.paint(&mut canvas);
1261        // Selection backgrounds
1262        assert!(!canvas.rects.is_empty());
1263    }
1264
1265    #[test]
1266    fn test_paint_placeholder() {
1267        let mut input = TextInput::new().with_placeholder("Type here...");
1268        input.bounds = Rect::new(0.0, 0.0, 20.0, 1.0);
1269        let mut canvas = MockCanvas::new();
1270        input.paint(&mut canvas);
1271        // Should show placeholder
1272        assert!(!canvas.texts.is_empty());
1273    }
1274
1275    // =====================================================
1276    // Event Tests
1277    // =====================================================
1278
1279    #[test]
1280    fn test_event_not_focused() {
1281        let mut input = TextInput::new();
1282        let event = Event::key_down(Key::Left);
1283        assert!(input.event(&event).is_none());
1284        // Should not process event when not focused
1285    }
1286
1287    #[test]
1288    fn test_event_backspace() {
1289        let mut input = TextInput::new().with_text("hello").with_focused(true);
1290        let event = Event::key_down(Key::Backspace);
1291        input.event(&event);
1292        assert_eq!(input.text(), "hell");
1293    }
1294
1295    #[test]
1296    fn test_event_delete() {
1297        let mut input = TextInput::new().with_text("hello").with_focused(true);
1298        input.cursor = 0;
1299        let event = Event::key_down(Key::Delete);
1300        input.event(&event);
1301        assert_eq!(input.text(), "ello");
1302    }
1303
1304    #[test]
1305    fn test_event_left() {
1306        let mut input = TextInput::new().with_text("hello").with_focused(true);
1307        let event = Event::key_down(Key::Left);
1308        input.event(&event);
1309        assert_eq!(input.cursor(), 4);
1310    }
1311
1312    #[test]
1313    fn test_event_right() {
1314        let mut input = TextInput::new().with_text("hello").with_focused(true);
1315        input.cursor = 0;
1316        let event = Event::key_down(Key::Right);
1317        input.event(&event);
1318        assert_eq!(input.cursor(), 1);
1319    }
1320
1321    #[test]
1322    fn test_event_home() {
1323        let mut input = TextInput::new().with_text("hello").with_focused(true);
1324        let event = Event::key_down(Key::Home);
1325        input.event(&event);
1326        assert_eq!(input.cursor(), 0);
1327    }
1328
1329    #[test]
1330    fn test_event_end() {
1331        let mut input = TextInput::new().with_text("hello").with_focused(true);
1332        input.cursor = 0;
1333        let event = Event::key_down(Key::End);
1334        input.event(&event);
1335        assert_eq!(input.cursor(), 5);
1336    }
1337
1338    #[test]
1339    fn test_event_text_input() {
1340        let mut input = TextInput::new().with_focused(true);
1341        let event = Event::TextInput {
1342            text: "hi".to_string(),
1343        };
1344        input.event(&event);
1345        assert_eq!(input.text(), "hi");
1346    }
1347
1348    // =====================================================
1349    // Unicode Tests
1350    // =====================================================
1351
1352    #[test]
1353    fn test_unicode_insert() {
1354        let mut input = TextInput::new();
1355        input.insert('é');
1356        input.insert('ñ');
1357        assert_eq!(input.text(), "éñ");
1358        assert_eq!(input.len(), 2);
1359    }
1360
1361    #[test]
1362    fn test_unicode_backspace() {
1363        let mut input = TextInput::new().with_text("héllo");
1364        input.backspace();
1365        assert_eq!(input.text(), "héll");
1366    }
1367
1368    #[test]
1369    fn test_unicode_cursor() {
1370        let mut input = TextInput::new().with_text("héllo");
1371        input.cursor = 2;
1372        input.insert('X');
1373        assert_eq!(input.text(), "héXllo");
1374    }
1375
1376    // =====================================================
1377    // Scroll Offset Tests
1378    // =====================================================
1379
1380    #[test]
1381    fn test_scroll_on_insert() {
1382        let mut input = TextInput::new();
1383        input.bounds = Rect::new(0.0, 0.0, 5.0, 1.0);
1384        for c in "hello world".chars() {
1385            input.insert(c);
1386        }
1387        // Scroll offset should adjust to keep cursor visible
1388        assert!(input.scroll_offset > 0);
1389    }
1390
1391    #[test]
1392    fn test_scroll_on_move_home() {
1393        let mut input = TextInput::new().with_text("hello world");
1394        input.bounds = Rect::new(0.0, 0.0, 5.0, 1.0);
1395        input.scroll_offset = 6;
1396        input.move_home();
1397        assert_eq!(input.scroll_offset, 0);
1398    }
1399}