minui 0.7.0

A minimalist framework for building terminal UIs in Rust.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
//! # Input Widgets
//!
//! This module provides interactive input widgets for building terminal UIs.
//!
//! ## Implemented (first pass)
//! - [`TextInput`] single-line input with:
//!   - cursor movement (left/right)
//!   - selection (mouse drag + shift+arrows where available)
//!   - copy/cut/paste (best-effort: uses `KeybindAction` + `Event::Paste`)
//!   - horizontal scrolling to keep caret visible
//!   - placeholder text
//!   - basic border rendering (optional)
//!
//! ## Notes / limitations
//! - Unicode handling is pragmatic: cursor/selection operate on `char` boundaries.
//! - Terminal cell width for rendering uses `minui::text` helpers (so wide chars are less likely
//!   to corrupt layout), but mapping from char-index to cell columns is still approximate.
//! - Shift+arrow support uses modifier-aware events when available. For compatibility, the widget
//!   also normalizes `Event::KeyWithModifiers` via `Event::as_legacy_key_event()`.
//!
//! ## Typical usage
//!
//! ```rust,ignore
//! use minui::prelude::*;
//!
//! struct State {
//!     input: TextInputState,
//! }
//!
//! let mut app = App::new(State { input: TextInputState::new() })?;
//!
//! app.run(
//!     |state, event| {
//!         match event {
//!             Event::Character('q') => false,
//!             _ => {
//!                 state.input.handle_event(event);
//!                 true
//!             }
//!         }
//!     },
//!     |state, window| {
//!         TextInput::new().with_width(30).draw(window, &mut state.input)?;
//!         window.end_frame()?;
//!         Ok(())
//!     },
//! )?;
//! # Ok::<(), minui::Error>(())
//! ```

use crate::input::KeybindAction;
use crate::text::{
    TabPolicy, byte_index_for_char_index, cell_column_for_char_index, cell_width_char,
    char_index_from_cell_column, clip_to_cells,
};
use crate::widgets::WidgetArea;
use crate::window::CursorSpec;
use crate::{Color, ColorPair, Event, InteractionCache, InteractionId, Result, Window};

/// Persistent state for a [`TextInput`].
///
/// This is owned by the application (or a form model). The widget borrows it mutably
/// during `draw()` and `handle_event()`.
#[derive(Debug, Clone)]
pub struct TextInputState {
    text: String,
    cursor: usize,                   // caret index in chars (0..=len_chars)
    selection_anchor: Option<usize>, // char index where selection started
    view_col: u16,                   // horizontal scroll offset in terminal cells
    focused: bool,

    /// Last-known layout (absolute terminal coordinates), captured during `TextInput::draw`.
    ///
    /// This is intentionally exposed so apps can do simple hit-testing and event routing
    /// without needing a full framework-level focus/router system.
    pub last_x: u16,
    /// Last-known layout (absolute terminal coordinates), captured during `TextInput::draw`.
    pub last_y: u16,
    /// Last-known layout (width in terminal cells), captured during `TextInput::draw`.
    pub last_w: u16,
}

impl Default for TextInputState {
    fn default() -> Self {
        Self::new()
    }
}

impl TextInputState {
    /// Creates an empty input state.
    pub fn new() -> Self {
        Self {
            text: String::new(),
            cursor: 0,
            selection_anchor: None,
            view_col: 0,
            focused: false,
            last_x: 0,
            last_y: 0,
            last_w: 0,
        }
    }

    /// Returns the current text.
    pub fn text(&self) -> &str {
        &self.text
    }

    /// Sets the text, resetting cursor/selection.
    pub fn set_text(&mut self, text: impl Into<String>) {
        self.text = text.into();
        self.cursor = self.len_chars();
        self.selection_anchor = None;
        self.view_col = 0;
    }

    /// Clears all text.
    pub fn clear(&mut self) {
        self.text.clear();
        self.cursor = 0;
        self.selection_anchor = None;
        self.view_col = 0;
    }

    /// Returns whether the input is focused (eligible to receive keystrokes).
    pub fn is_focused(&self) -> bool {
        self.focused
    }

    /// Sets focus.
    pub fn set_focused(&mut self, focused: bool) {
        self.focused = focused;
        if !focused {
            self.selection_anchor = None;
        }
    }

    /// Returns current cursor index (in chars).
    pub fn cursor(&self) -> usize {
        self.cursor
    }

    /// Returns selection bounds as (start, end) in char indices, if any selection exists.
    pub fn selection(&self) -> Option<(usize, usize)> {
        let a = self.selection_anchor?;
        if a == self.cursor {
            return None;
        }
        Some((a.min(self.cursor), a.max(self.cursor)))
    }

    /// Clears selection.
    pub fn clear_selection(&mut self) {
        self.selection_anchor = None;
    }

    /// Selects all text.
    pub fn select_all(&mut self) {
        let len = self.len_chars();
        self.selection_anchor = Some(0);
        self.cursor = len;
    }

    /// Returns true if there is any selected range.
    pub fn has_selection(&self) -> bool {
        self.selection().is_some()
    }

    /// Deletes selected text if present. Returns true if deletion occurred.
    pub fn delete_selection(&mut self) -> bool {
        let Some((start, end)) = self.selection() else {
            return false;
        };
        self.delete_range_chars(start, end);
        self.cursor = start;
        self.selection_anchor = None;
        true
    }

    /// Inserts a char at the cursor (replacing selection if present).
    pub fn insert_char(&mut self, ch: char) {
        if self.delete_selection() {
            // selection removed, cursor already positioned
        }
        self.insert_str_at_cursor(&ch.to_string());
    }

    /// Inserts a string at the cursor (replacing selection if present).
    pub fn insert_str(&mut self, s: &str) {
        if s.is_empty() {
            return;
        }
        if self.delete_selection() {
            // selection removed
        }
        self.insert_str_at_cursor(s);
    }

    /// Backspace: delete char before cursor, or selection if present.
    pub fn backspace(&mut self) {
        if self.delete_selection() {
            return;
        }
        if self.cursor == 0 {
            return;
        }
        let start = self.cursor.saturating_sub(1);
        let end = self.cursor;
        self.delete_range_chars(start, end);
        self.cursor = start;
    }

    /// Delete: delete char at cursor, or selection if present.
    pub fn delete_forward(&mut self) {
        if self.delete_selection() {
            return;
        }
        let len = self.len_chars();
        if self.cursor >= len {
            return;
        }
        self.delete_range_chars(self.cursor, self.cursor + 1);
    }

    /// Move cursor left. If `selecting` is true, extends selection.
    pub fn move_left(&mut self, selecting: bool) {
        self.begin_or_clear_selection(selecting);
        self.cursor = self.cursor.saturating_sub(1);
        if !selecting {
            self.selection_anchor = None;
        }
    }

    /// Move cursor right. If `selecting` is true, extends selection.
    pub fn move_right(&mut self, selecting: bool) {
        self.begin_or_clear_selection(selecting);
        let len = self.len_chars();
        self.cursor = (self.cursor + 1).min(len);
        if !selecting {
            self.selection_anchor = None;
        }
    }

    /// Move cursor to start. If `selecting` is true, extends selection.
    pub fn move_home(&mut self, selecting: bool) {
        self.begin_or_clear_selection(selecting);
        self.cursor = 0;
        if !selecting {
            self.selection_anchor = None;
        }
    }

    /// Move cursor to end. If `selecting` is true, extends selection.
    pub fn move_end(&mut self, selecting: bool) {
        self.begin_or_clear_selection(selecting);
        self.cursor = self.len_chars();
        if !selecting {
            self.selection_anchor = None;
        }
    }

    /// Copies the selected text into an internal string and returns it.
    /// (Clipboard integration is out of scope for a pure terminal framework.)
    pub fn copy_selection(&self) -> Option<String> {
        let (start, end) = self.selection()?;
        Some(self.slice_chars(start, end))
    }

    /// Cuts selection and returns removed text.
    pub fn cut_selection(&mut self) -> Option<String> {
        let (start, end) = self.selection()?;
        let cut = self.slice_chars(start, end);
        self.delete_range_chars(start, end);
        self.cursor = start;
        self.selection_anchor = None;
        Some(cut)
    }

    /// Handles a MinUI event to mutate the input state.
    ///
    /// Returns true if the event was consumed.
    ///
    /// This is a first-pass event model:
    /// - it consumes most typing and navigation keys when focused
    /// - it uses `KeybindAction::{Copy,Cut,Paste,SelectAll}` when emitted by MinUI
    ///
    /// If you want more sophisticated routing/focus, do it at the app level and call
    /// the state methods directly.
    pub fn handle_event(&mut self, event: Event) -> bool {
        if !self.focused {
            // Still allow click-to-focus behavior if the app wants to route that here later.
            return false;
        }

        // Modifier-aware keyboard events are now the default input path in MinUI.
        //
        // This widget is intentionally implemented in terms of the legacy `Event::*` variants,
        // so we normalize at the boundary:
        // - If `event` is `KeyWithModifiers`, we can honor Shift-selection for left/right.
        // - Then we convert to the closest legacy key event via `as_legacy_key_event()`.
        if let Event::KeyWithModifiers(k) = &event {
            if k.mods.shift {
                match k.key {
                    crate::KeyKind::Left => {
                        self.move_left(true);
                        return true;
                    }
                    crate::KeyKind::Right => {
                        self.move_right(true);
                        return true;
                    }
                    _ => {}
                }
            }
        }

        let event = event.as_legacy_key_event().unwrap_or(event);

        match event {
            Event::Character(c) => {
                // Ignore control chars in raw mode; framework should map those to keybinds.
                if !c.is_control() {
                    self.insert_char(c);
                }
                true
            }
            Event::Paste(text) => {
                self.insert_str(&text);
                true
            }
            Event::Backspace => {
                self.backspace();
                true
            }
            Event::Delete => {
                self.delete_forward();
                true
            }
            Event::KeyLeft => {
                self.move_left(false);
                true
            }
            Event::KeyRight => {
                self.move_right(false);
                true
            }
            Event::KeyUp | Event::KeyDown => {
                // Single-line: ignore
                false
            }
            Event::Enter => {
                // App decides what Enter means.
                false
            }
            Event::Escape => {
                // Clear selection on escape (common behavior).
                self.clear_selection();
                true
            }
            Event::Keybind(action) => match action {
                KeybindAction::SelectAll => {
                    self.select_all();
                    true
                }
                KeybindAction::Copy => {
                    // App can read copy_selection() and write to OS clipboard if desired.
                    self.copy_selection();
                    true
                }
                KeybindAction::Cut => {
                    self.cut_selection();
                    true
                }
                KeybindAction::Paste => {
                    // Real paste should arrive as Event::Paste when bracketed paste works.
                    // If user pressed Ctrl+V and terminal doesn't send paste events,
                    // apps can choose to integrate a clipboard provider and call insert_str().
                    true
                }
                _ => false,
            },
            _ => false,
        }
    }

    /// Call this from the app when a mouse click occurs inside the input region.
    ///
    /// `x` is absolute terminal column.
    pub fn click_set_cursor(&mut self, x: u16) {
        let local_x = x.saturating_sub(self.last_x);
        let idx = self.char_index_from_cell_column(local_x.saturating_add(self.view_col));
        self.cursor = idx;
        self.selection_anchor = None;
    }

    /// Call this from the app when a mouse drag occurs.
    ///
    /// This method is defensive: it clamps the provided x coordinate to the last-known
    /// input bounds so dragging outside the field doesn't cause erratic selection behavior.
    ///
    /// If selection hasn't started, it starts it at the original cursor position.
    pub fn drag_select_to(&mut self, x: u16) {
        // Clamp to the input's last-known bounds.
        // This prevents selection math from "running away" when the cursor leaves the field
        // while the mouse is still held down.
        let clamped_x = if self.last_w == 0 {
            self.last_x
        } else {
            let min_x = self.last_x;
            let max_x_inclusive = self.last_x.saturating_add(self.last_w.saturating_sub(1));
            x.clamp(min_x, max_x_inclusive)
        };

        let local_x = clamped_x.saturating_sub(self.last_x);
        let idx = self.char_index_from_cell_column(local_x.saturating_add(self.view_col));

        if self.selection_anchor.is_none() {
            self.selection_anchor = Some(self.cursor);
        }
        self.cursor = idx;
    }

    /// Updates scroll offset so the caret is visible within `field_cells`.
    fn ensure_cursor_visible(&mut self, field_cells: u16) {
        if field_cells == 0 {
            self.view_col = 0;
            return;
        }

        let caret_col = self.cell_column_for_char_index(self.cursor);

        // Left clamp: if caret is left of viewport, scroll left.
        if caret_col < self.view_col {
            self.view_col = caret_col;
            return;
        }

        // Right clamp: if caret is past viewport end, scroll right.
        let viewport_end = self.view_col.saturating_add(field_cells.saturating_sub(1));
        if caret_col > viewport_end {
            self.view_col = caret_col.saturating_sub(field_cells.saturating_sub(1));
        }
    }

    fn begin_or_clear_selection(&mut self, selecting: bool) {
        if selecting {
            if self.selection_anchor.is_none() {
                self.selection_anchor = Some(self.cursor);
            }
        } else {
            self.selection_anchor = None;
        }
    }

    fn len_chars(&self) -> usize {
        self.text.chars().count()
    }

    fn insert_str_at_cursor(&mut self, s: &str) {
        let byte_idx = self.byte_index_for_char_index(self.cursor);
        self.text.insert_str(byte_idx, s);
        self.cursor += s.chars().count();
    }

    fn delete_range_chars(&mut self, start: usize, end: usize) {
        if start >= end {
            return;
        }
        let a = self.byte_index_for_char_index(start);
        let b = self.byte_index_for_char_index(end);
        self.text.replace_range(a..b, "");
    }

    fn slice_chars(&self, start: usize, end: usize) -> String {
        if start >= end {
            return String::new();
        }
        let a = self.byte_index_for_char_index(start);
        let b = self.byte_index_for_char_index(end);
        self.text[a..b].to_string()
    }

    fn byte_index_for_char_index(&self, char_idx: usize) -> usize {
        byte_index_for_char_index(&self.text, char_idx)
    }

    fn cell_column_for_char_index(&self, char_idx: usize) -> u16 {
        cell_column_for_char_index(&self.text, char_idx)
    }

    /// Best-effort mapping from cell column to char index.
    ///
    /// This walks the string accumulating cell widths. If the target column lands "inside"
    /// a wide char, we place the caret before that char.
    fn char_index_from_cell_column(&self, col: u16) -> usize {
        char_index_from_cell_column(&self.text, col)
    }
}

/// A single-line text input widget.
///
/// This widget is intentionally "immediate-mode friendly": you construct it each frame
/// with geometry/styling, and provide a mutable [`TextInputState`] that persists.
///
/// It does not own input focus globally; the app decides focus and routes events.
#[derive(Debug, Clone)]
pub struct TextInput {
    x: u16,
    y: u16,
    width: u16,

    placeholder: Option<String>,
    show_border: bool,

    // Styling
    text_color: ColorPair,
    placeholder_color: ColorPair,
    selection_color: ColorPair,
    border_color: ColorPair,
    cursor_color: Option<ColorPair>, // if set, draw a block cursor cell (optional)
}

impl Default for TextInput {
    fn default() -> Self {
        Self::new()
    }
}

impl TextInput {
    /// Creates a new input with default styling and zero geometry.
    pub fn new() -> Self {
        Self {
            x: 0,
            y: 0,
            width: 0,
            placeholder: None,
            show_border: false,

            text_color: ColorPair::new(Color::White, Color::Transparent),
            placeholder_color: ColorPair::new(Color::DarkGray, Color::Transparent),
            selection_color: ColorPair::new(Color::Black, Color::LightBlue),
            border_color: ColorPair::new(Color::LightGray, Color::Transparent),
            cursor_color: None,
        }
    }

    pub fn with_position(mut self, x: u16, y: u16) -> Self {
        self.x = x;
        self.y = y;
        self
    }

    pub fn with_width(mut self, width: u16) -> Self {
        self.width = width;
        self
    }

    pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
        self.placeholder = Some(placeholder.into());
        self
    }

    pub fn with_border(mut self, border: bool) -> Self {
        self.show_border = border;
        self
    }

    pub fn with_text_color(mut self, colors: ColorPair) -> Self {
        self.text_color = colors;
        self
    }

    pub fn with_placeholder_color(mut self, colors: ColorPair) -> Self {
        self.placeholder_color = colors;
        self
    }

    pub fn with_selection_color(mut self, colors: ColorPair) -> Self {
        self.selection_color = colors;
        self
    }

    pub fn with_border_color(mut self, colors: ColorPair) -> Self {
        self.border_color = colors;
        self
    }

    /// Optional "block cursor" style by drawing the cursor cell with an inverted-ish color.
    ///
    /// If not set, the widget uses the real terminal cursor via `Window::set_cursor_position`.
    pub fn with_cursor_cell_color(mut self, colors: ColorPair) -> Self {
        self.cursor_color = Some(colors);
        self
    }

    /// Draws the input at its configured position/width using `state`.
    ///
    /// This also:
    /// - caches geometry into the state for mouse helpers
    /// - updates horizontal scroll (`view_col`) to keep caret visible
    /// - places the real terminal cursor (recommended)
    /// Draws the input at its configured position/width using `state`.
    ///
    /// This also:
    /// - caches geometry into the state for mouse helpers
    /// - updates horizontal scroll (`view_col`) to keep caret visible
    /// - places the real terminal cursor (recommended)
    pub fn draw(&self, window: &mut dyn Window, state: &mut TextInputState) -> Result<()> {
        // Cache for mouse hit helpers.
        state.last_x = self.x;
        state.last_y = self.y;
        state.last_w = self.width;

        if self.width == 0 {
            return Ok(());
        }

        // Border consumes one cell on left+right; content is single-line, so height is 1.
        let (content_x, content_w) = if self.show_border {
            // Minimal "ASCII-ish" border: [ ... ]
            // We keep this very simple for a first pass.
            // You can wrap this inside a Container for richer borders.
            window.write_str_colored(self.y, self.x, "[", self.border_color)?;
            window.write_str_colored(
                self.y,
                self.x + self.width.saturating_sub(1),
                "]",
                self.border_color,
            )?;
            (self.x.saturating_add(1), self.width.saturating_sub(2))
        } else {
            (self.x, self.width)
        };

        // Clear the content area each frame (so deletions / scroll don't leave stale glyphs).
        if content_w > 0 {
            // Use fit-to-width by writing spaces.
            let spaces = " ".repeat(content_w as usize);
            window.write_str(self.y, content_x, &spaces)?;
        }

        let has_text = !state.text.is_empty();

        // Apply horizontal scroll so caret stays visible.
        state.ensure_cursor_visible(content_w.saturating_sub(1));

        // Clip to visible region based on view_col + width.
        // For first pass, we implement a simple "skip cells then take cells" using clipping twice.
        let left_skip = state.view_col;
        let visible = content_w;

        // Render placeholder vs text colors.
        if has_text {
            // Render with selection highlighting if present.
            self.draw_with_selection(window, state, content_x, content_w)?;
        } else {
            let display = self.placeholder.as_deref().unwrap_or_default();
            let after_skip = if left_skip == 0 {
                std::borrow::Cow::Borrowed(display)
            } else {
                // Clip to everything after skipping left cells:
                // This is O(n) but fine for small input fields.
                let mut acc: u16 = 0;
                let mut start_char = 0usize;
                for (i, ch) in display.chars().enumerate() {
                    let w = cell_width_char(ch);
                    if w == 0 {
                        continue;
                    }
                    if acc.saturating_add(w) > left_skip {
                        start_char = i;
                        break;
                    }
                    acc = acc.saturating_add(w);
                    start_char = i + 1;
                }
                std::borrow::Cow::Owned(display.chars().skip(start_char).collect::<String>())
            };

            let clipped = clip_to_cells(&after_skip, visible, TabPolicy::SingleCell);
            window.write_str_colored(self.y, content_x, &clipped, self.placeholder_color)?;
        }

        // Cursor: request a cursor state instead of moving/showing it immediately.
        //
        // IMPORTANT:
        // When multiple inputs are drawn in a frame, only the focused one should request a visible
        // cursor. The terminal applies the last request at `end_frame()`, which avoids flicker.
        if state.focused {
            let caret_col = state.cell_column_for_char_index(state.cursor);
            let caret_visible_col = caret_col.saturating_sub(state.view_col);
            let caret_x =
                content_x.saturating_add(caret_visible_col.min(content_w.saturating_sub(1)));

            window.request_cursor(CursorSpec {
                x: caret_x,
                y: self.y,
                visible: true,
            });

            // Optional: draw a cursor cell color if configured.
            // This is useful if you don't want to use the terminal cursor for some reason.
            if let Some(colors) = self.cursor_color {
                // Draw a block cursor by re-drawing the character under cursor with background.
                // We do NOT attempt to handle wide glyphs perfectly here.
                let ch = self
                    .char_at_cell_column(&state.text, caret_col)
                    .unwrap_or(' ');
                window.write_str_colored(self.y, caret_x, &ch.to_string(), colors)?;
            }
        }

        Ok(())
    }

    /// Draws the input and registers it into the given `InteractionCache` under `id`.
    ///
    /// This is an optional immediate-mode routing hook. It lets apps avoid duplicating geometry
    /// for hit-testing and focus routing.
    ///
    /// Registration behavior:
    /// - Always registers the widget's full area as `focusable`
    /// - Additionally registers it as `draggable` when the input is focused (for selection drags)
    ///
    /// Note: this does not mutate focus itself; focus policy remains app-owned.
    pub fn draw_with_id(
        &self,
        window: &mut dyn Window,
        state: &mut TextInputState,
        ui: &mut InteractionCache,
        id: InteractionId,
    ) -> Result<()> {
        let height: u16 = 1;
        let area = WidgetArea::new(self.x, self.y, self.width, height);

        ui.register_focusable(id, area);
        if state.is_focused() {
            ui.register_draggable(id, area);
        }

        self.draw(window, state)
    }

    fn draw_with_selection(
        &self,
        window: &mut dyn Window,
        state: &TextInputState,
        content_x: u16,
        content_w: u16,
    ) -> Result<()> {
        if content_w == 0 {
            return Ok(());
        }

        let visible_cells = content_w;
        let view_start = state.view_col;
        let view_end = state.view_col.saturating_add(visible_cells);

        let selection = state.selection();

        // Render by walking chars and deciding per-cell color.
        // First pass: we render as a string with per-char coloring by individual writes.
        // Not the most efficient, but acceptable for short input lines.
        let mut abs_col: u16 = 0;
        let mut run = String::new();
        let mut run_start_x: u16 = content_x;
        let mut run_color: Option<ColorPair> = None;

        for (i, ch) in state.text.chars().enumerate() {
            let w = cell_width_char(ch);
            if w == 0 {
                continue;
            }

            // abs_col is the cell column within the full line.
            let ch_start = abs_col;
            let ch_end = abs_col.saturating_add(w);

            // Skip if entirely left of viewport.
            if ch_end <= view_start {
                abs_col = ch_end;
                continue;
            }
            // Stop if beyond viewport.
            if ch_start >= view_end {
                break;
            }

            // Visible position in the field
            let vis_x = ch_start.saturating_sub(view_start);
            if vis_x >= visible_cells {
                break;
            }

            // Determine if this char is in selection range.
            let in_sel = selection.map(|(a, b)| i >= a && i < b).unwrap_or(false);

            let colors = if in_sel {
                self.selection_color
            } else {
                self.text_color
            };

            let draw_x = content_x + vis_x;
            if run_color != Some(colors) {
                if let Some(color) = run_color {
                    window.write_str_colored(state.last_y, run_start_x, &run, color)?;
                    run.clear();
                }
                run_start_x = draw_x;
                run_color = Some(colors);
            }

            run.push(ch);

            // Advance columns.
            abs_col = ch_end;
        }

        if let Some(color) = run_color {
            window.write_str_colored(state.last_y, run_start_x, &run, color)?;
        }

        Ok(())
    }

    /// Attempts to find the char occupying the given absolute cell column within `s`.
    ///
    /// Best-effort: if the column lands inside a wide char, returns that char.
    fn char_at_cell_column(&self, s: &str, col: u16) -> Option<char> {
        let mut acc: u16 = 0;
        for ch in s.chars() {
            let w = cell_width_char(ch);
            if w == 0 {
                continue;
            }
            let next = acc.saturating_add(w);
            if col < next {
                return Some(ch);
            }
            acc = next;
        }
        None
    }
}