返回 CodeWhale
text_input.rs
根目录 / vendor / codewhale-ratatui / src / components / text_input.rs
1 //! Package Input: a single-line text input.
2 //!
3 //! Three parts, each usable alone:
4 //!
5 //! - [`LineBuffer`]: the text model. One line, grapheme-aware (a combining
6 //! sequence, a CJK character or an emoji ZWJ sequence is one step for the
7 //! cursor and one delete), with display widths from `unicode-width`, the
8 //! editing operations a terminal field needs, and the horizontal scroll
9 //! window that keeps the cursor visible in a narrow field.
10 //! - [`TextInputState`]: a buffer plus whether it is a secret, and
11 //! [`TextInputState::handle_key`], which turns a key into a
12 //! [`TextInputOutcome`] the host reacts to.
13 //! - [`TextInput`]: paints a state: label, placeholder, a cursor cell, clip
14 //! marks where the text runs past the field, and a note underneath (an
15 //! error, a reason it is disabled, or help).
16 //!
17 //! Editing keys follow the Codewhale engine's composer and its automation
18 //! editor field (`crates/tui/src/tui/app/composer.rs`, `views/automations/
19 //! editor.rs`, `Hmbown/CodeWhale`): a *word* is a run of non-whitespace, so
20 //! `Ctrl+W` takes `/usr/local/bin` whole, as a shell's `unix-word-rubout`
21 //! does.
22 //!
23 //! | Key | Does |
24 //! |---|---|
25 //! | characters | insert (on Windows an AltGr chord inserts; `Alt+<non-ASCII>` inserts, as macOS Option does) |
26 //! | `Backspace`, `Ctrl+H` / `Delete` | delete the grapheme before / after the cursor |
27 //! | `Ctrl+W`, `Alt+Backspace`, `Ctrl+Backspace` | delete the word before the cursor |
28 //! | `Alt+D`, `Alt+Delete`, `Ctrl+Delete` | delete the word after the cursor |
29 //! | `Ctrl+U` / `Ctrl+K` | delete to the start / end of the line |
30 //! | `←` `→`, `Ctrl+B` `Ctrl+F` | move by grapheme |
31 //! | `Alt+←` `Alt+→`, `Ctrl+←` `Ctrl+→`, `Alt+B` `Alt+F` | move by word |
32 //! | `Home` `End`, `Ctrl+A` `Ctrl+E` | start / end of the line |
33 //! | `Enter` / `Esc` | [`TextInputOutcome::Submitted`] / [`TextInputOutcome::Cancelled`] |
34 //!
35 //! A secret field ([`TextInputState::secret`]) paints one mask cell per
36 //! grapheme, never the last character typed, and its word keys act on the
37 //! whole line, so the shape of the secret (where its spaces are) does not
38 //! leak through the cursor either.
39 //!
40 //! The kit paints the cursor itself, as a reversed cell, so a field works
41 //! with no hardware cursor. A host that wants the terminal's own cursor
42 //! calls [`TextInput::cursor_position`].
43
44 use std::{
45 borrow::Cow,
46 fmt,
47 sync::atomic::{AtomicUsize, Ordering},
48 };
49
50 use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
51 use ratatui::{
52 buffer::Buffer,
53 layout::{Position, Rect},
54 style::{Modifier, Style},
55 };
56 use unicode_segmentation::UnicodeSegmentation;
57
58 use crate::{Paint, Role, Theme, glyphs, keys, text};
59
60 /// The prompt mark of the focused field. Its ASCII form is `>`.
61 const PROMPT: &str = "›";
62 /// Cells the prompt and its space take before the text.
63 const PROMPT_CELLS: u16 = 2;
64 /// Shown in an empty, unfocused field that has no placeholder.
65 const EMPTY: &str = "—";
66 /// A secret's mask, one cell per grapheme. ASCII-safe terminals get `*`: the
67 /// charter's fallback for `•` is a lone `.`, which reads as punctuation.
68 const MASK: &str = "•";
69 const MASK_ASCII: &str = "*";
70 /// A field narrower than this has no room for clip marks.
71 const MIN_CELLS_FOR_MARKS: usize = 4;
72 /// Longest note (an error, a reason) under a field, in rows.
73 const MAX_NOTE_ROWS: usize = 3;
74
75 // ---------------------------------------------------------------------------
76 // LineBuffer
77 // ---------------------------------------------------------------------------
78
79 /// One line of text and a cursor, edited by grapheme.
80 ///
81 /// The cursor is always between graphemes (a combining sequence or an emoji
82 /// ZWJ sequence is never split). Text that goes in is made safe first
83 /// ([`LineBuffer::sanitize`]), so the buffer never holds a newline, a control
84 /// character, a bidi override or an invisible zero-width cluster.
85 ///
86 /// Every editing method returns whether it changed anything, so a host (or
87 /// [`TextInputState::handle_key`]) can tell an edit from a no-op at a
88 /// boundary.
89 #[derive(Clone, Debug, PartialEq, Eq)]
90 pub struct LineBuffer {
91 text: String,
92 /// Byte offset, always on a grapheme boundary.
93 cursor: usize,
94 /// Most graphemes the buffer will hold.
95 limit: usize,
96 }
97
98 impl Default for LineBuffer {
99 fn default() -> Self {
100 Self::new()
101 }
102 }
103
104 impl LineBuffer {
105 /// The default cap on a line, in graphemes: generous for a paste, and a
106 /// guard against one that is not.
107 pub const DEFAULT_LIMIT: usize = 65_536;
108
109 #[must_use]
110 pub const fn new() -> Self {
111 Self {
112 text: String::new(),
113 cursor: 0,
114 limit: Self::DEFAULT_LIMIT,
115 }
116 }
117
118 /// A buffer holding `text` (made safe, one line), cursor at the end.
119 #[must_use]
120 pub fn from_text(text: &str) -> Self {
121 let mut buffer = Self::new();
122 buffer.insert_str(text);
123 buffer
124 }
125
126 /// Cap the line at `graphemes`; text past the cap is cut now and refused
127 /// later.
128 #[must_use]
129 pub fn with_limit(mut self, graphemes: usize) -> Self {
130 self.limit = graphemes;
131 if self.len() > graphemes {
132 let cut = self
133 .text
134 .grapheme_indices(true)
135 .nth(graphemes)
136 .map_or(self.text.len(), |(at, _)| at);
137 self.text.truncate(cut);
138 self.cursor = self.cursor.min(cut);
139 }
140 self
141 }
142
143 /// `text` as a line: no control characters (so no newline, tab or escape),
144 /// no bidi overrides ([`text::display_safe`]), no line or paragraph
145 /// separators, and no cluster that draws nothing (a lone zero-width space
146 /// or joiner would be invisible text in a field). Combining marks and
147 /// ZWJ sequences that belong to a visible character stay.
148 #[must_use]
149 pub fn sanitize(text: &str) -> String {
150 text::display_safe(text)
151 .graphemes(true)
152 .filter(|g| !g.contains(['\u{2028}', '\u{2029}']) && text::width(g) > 0)
153 .collect()
154 }
155
156 #[must_use]
157 pub fn text(&self) -> &str {
158 &self.text
159 }
160
161 #[must_use]
162 pub fn is_empty(&self) -> bool {
163 self.text.is_empty()
164 }
165
166 /// Length in graphemes.
167 #[must_use]
168 pub fn len(&self) -> usize {
169 self.text.graphemes(true).count()
170 }
171
172 /// Display width of the whole line, in cells.
173 #[must_use]
174 pub fn width(&self) -> usize {
175 text::width(&self.text)
176 }
177
178 /// The cursor, as a grapheme index (`0..=len`).
179 #[must_use]
180 pub fn cursor(&self) -> usize {
181 self.text[..self.cursor].graphemes(true).count()
182 }
183
184 /// The cursor, as a byte offset into [`LineBuffer::text`].
185 #[must_use]
186 pub const fn cursor_byte(&self) -> usize {
187 self.cursor
188 }
189
190 /// Move the cursor to grapheme `index` (clamped to the end).
191 pub fn set_cursor(&mut self, index: usize) {
192 self.cursor = self
193 .text
194 .grapheme_indices(true)
195 .nth(index)
196 .map_or(self.text.len(), |(at, _)| at);
197 }
198
199 #[must_use]
200 pub fn before_cursor(&self) -> &str {
201 &self.text[..self.cursor]
202 }
203
204 #[must_use]
205 pub fn after_cursor(&self) -> &str {
206 &self.text[self.cursor..]
207 }
208
209 pub fn graphemes(&self) -> impl Iterator<Item = &str> {
210 self.text.graphemes(true)
211 }
212
213 /// Display width of each grapheme, or 1 each when `masked`.
214 #[must_use]
215 pub fn widths(&self, masked: bool) -> Vec<usize> {
216 self.graphemes()
217 .map(|g| if masked { 1 } else { text::width(g) })
218 .collect()
219 }
220
221 /// Replace the line (made safe), cursor at the end.
222 pub fn set_text(&mut self, text: &str) {
223 self.text.clear();
224 self.cursor = 0;
225 self.insert_str(text);
226 }
227
228 pub fn clear(&mut self) {
229 self.text.clear();
230 self.cursor = 0;
231 }
232
233 // -- inserting ----------------------------------------------------------
234
235 pub fn insert_char(&mut self, c: char) -> bool {
236 let mut utf8 = [0; 4];
237 self.insert_str(c.encode_utf8(&mut utf8))
238 }
239
240 /// Insert `text` at the cursor: a typed character, or a paste. The text
241 /// is made safe in context ([`LineBuffer::sanitize`]): a typed combining
242 /// mark or joiner may extend a visible grapheme already in the line.
243 /// A pasted newline is dropped, not turned into a space, and a paste
244 /// that would pass the limit is cut without removing existing text.
245 pub fn insert_str(&mut self, text: &str) -> bool {
246 let safe = text::display_safe(text);
247 if safe.is_empty() {
248 return false;
249 }
250 let prefix = &self.text[..self.cursor];
251 let clean_prefix = Self::sanitize(&format!("{prefix}{safe}"));
252 // The stored prefix is already safe. Sanitize input beside it so
253 // extensions survive while invisible-only clusters do not consume
254 // the paste budget or hide later visible text.
255 let clean = &clean_prefix[prefix.len()..];
256 // At each insertion boundary a cluster can join its neighbor. Keep
257 // two extra incoming clusters so extending a full line is possible;
258 // then enforce the cap on the combined, sanitized result.
259 let room = self.limit.saturating_sub(self.len()).saturating_add(2);
260 let mut end = clean
261 .grapheme_indices(true)
262 .nth(room)
263 .map_or(clean.len(), |(at, _)| at);
264 while end > 0 {
265 let insertion = &clean[..end];
266 let mut candidate = self.text.clone();
267 candidate.insert_str(self.cursor, insertion);
268 let cursor = Self::sanitize(&candidate[..self.cursor + insertion.len()]).len();
269 let candidate = Self::sanitize(&candidate);
270 if candidate.graphemes(true).count() <= self.limit {
271 if candidate == self.text {
272 return false;
273 }
274 self.text = candidate;
275 self.cursor = cursor;
276 self.snap_cursor();
277 return true;
278 }
279 end = insertion
280 .grapheme_indices(true)
281 .next_back()
282 .map_or(0, |(at, _)| at);
283 }
284 false
285 }
286
287 /// Typed text can join the grapheme after the cursor (`e` before a
288 /// combining mark): keep the cursor between graphemes.
289 fn snap_cursor(&mut self) {
290 let cursor = self.cursor.min(self.text.len());
291 self.cursor = self
292 .text
293 .grapheme_indices(true)
294 .map(|(at, _)| at)
295 .find(|at| *at >= cursor)
296 .unwrap_or(self.text.len());
297 }
298
299 // -- boundaries ---------------------------------------------------------
300
301 fn prev_boundary(&self, from: usize) -> usize {
302 self.text[..from]
303 .grapheme_indices(true)
304 .next_back()
305 .map_or(0, |(at, _)| at)
306 }
307
308 fn next_boundary(&self, from: usize) -> usize {
309 from + self.text[from..].graphemes(true).next().map_or(0, str::len)
310 }
311
312 /// Start of the word before `from`: skip whitespace, then the word.
313 fn word_start_before(&self, from: usize) -> usize {
314 let mut at = from;
315 let mut back = self.text[..from].grapheme_indices(true).rev().peekable();
316 while let Some((start, _)) = back.next_if(|(_, g)| is_space(g)) {
317 at = start;
318 }
319 while let Some((start, _)) = back.next_if(|(_, g)| !is_space(g)) {
320 at = start;
321 }
322 at
323 }
324
325 /// Start of the next word after `from`: skip the rest of this word, then
326 /// the whitespace. At the last word this is the end of the line.
327 fn next_word_start(&self, from: usize) -> usize {
328 let mut at = from;
329 let mut ahead = self.text[from..].grapheme_indices(true).peekable();
330 while let Some((start, g)) = ahead.next_if(|(_, g)| !is_space(g)) {
331 at = from + start + g.len();
332 }
333 while let Some((start, g)) = ahead.next_if(|(_, g)| is_space(g)) {
334 at = from + start + g.len();
335 }
336 at
337 }
338
339 /// End of the word at or after `from`: skip whitespace, then the word.
340 fn word_end_after(&self, from: usize) -> usize {
341 let mut at = from;
342 let mut ahead = self.text[from..].grapheme_indices(true).peekable();
343 while let Some((start, g)) = ahead.next_if(|(_, g)| is_space(g)) {
344 at = from + start + g.len();
345 }
346 while let Some((start, g)) = ahead.next_if(|(_, g)| !is_space(g)) {
347 at = from + start + g.len();
348 }
349 at
350 }
351
352 fn remove(&mut self, from: usize, to: usize) -> bool {
353 if from >= to {
354 return false;
355 }
356 self.text.replace_range(from..to, "");
357 self.cursor = from;
358 // Removing a separator can join neighboring regional indicators
359 // into one flag. The old byte boundary may now be inside a grapheme.
360 self.snap_cursor();
361 true
362 }
363
364 // -- deleting -----------------------------------------------------------
365
366 /// Delete the grapheme before the cursor (all of a combining or ZWJ
367 /// sequence).
368 pub fn backspace(&mut self) -> bool {
369 let from = self.prev_boundary(self.cursor);
370 self.remove(from, self.cursor)
371 }
372
373 /// Delete the grapheme after the cursor.
374 pub fn delete(&mut self) -> bool {
375 let to = self.next_boundary(self.cursor);
376 self.remove(self.cursor, to)
377 }
378
379 /// `Ctrl+W`, `Alt+Backspace`: delete back to the start of the word.
380 pub fn delete_word_back(&mut self) -> bool {
381 let from = self.word_start_before(self.cursor);
382 self.remove(from, self.cursor)
383 }
384
385 /// `Alt+D`: delete forward to the end of the next word.
386 pub fn delete_word_forward(&mut self) -> bool {
387 let to = self.word_end_after(self.cursor);
388 self.remove(self.cursor, to)
389 }
390
391 /// `Ctrl+U`: delete everything before the cursor.
392 pub fn kill_to_start(&mut self) -> bool {
393 self.remove(0, self.cursor)
394 }
395
396 /// `Ctrl+K`: delete everything after the cursor.
397 pub fn kill_to_end(&mut self) -> bool {
398 let end = self.text.len();
399 self.remove(self.cursor, end)
400 }
401
402 // -- moving -------------------------------------------------------------
403
404 fn move_to(&mut self, to: usize) -> bool {
405 let moved = to != self.cursor;
406 self.cursor = to;
407 moved
408 }
409
410 pub fn move_left(&mut self) -> bool {
411 let to = self.prev_boundary(self.cursor);
412 self.move_to(to)
413 }
414
415 pub fn move_right(&mut self) -> bool {
416 let to = self.next_boundary(self.cursor);
417 self.move_to(to)
418 }
419
420 /// To the start of the word before the cursor.
421 pub fn word_left(&mut self) -> bool {
422 let to = self.word_start_before(self.cursor);
423 self.move_to(to)
424 }
425
426 /// To the start of the next word (the end of the line after the last).
427 pub fn word_right(&mut self) -> bool {
428 let to = self.next_word_start(self.cursor);
429 self.move_to(to)
430 }
431
432 pub fn home(&mut self) -> bool {
433 self.move_to(0)
434 }
435
436 pub fn end(&mut self) -> bool {
437 let end = self.text.len();
438 self.move_to(end)
439 }
440
441 // -- scrolling ----------------------------------------------------------
442
443 /// The slice of the line to show in `avail` cells with the cursor in
444 /// view, scrolling as little as possible from `prev_start` (the first
445 /// grapheme shown last time). `masked` counts one cell per grapheme.
446 #[must_use]
447 pub fn window(&self, avail: usize, prev_start: usize, masked: bool) -> LineWindow {
448 line_window_of(&self.widths(masked), Some(self.cursor()), prev_start, avail)
449 }
450
451 /// The start of the line in `avail` cells, with no cursor: how an
452 /// unfocused field shows its text.
453 #[must_use]
454 pub fn head_window(&self, avail: usize, masked: bool) -> LineWindow {
455 line_window_of(&self.widths(masked), None, 0, avail)
456 }
457 }
458
459 fn is_space(grapheme: &str) -> bool {
460 grapheme.chars().all(char::is_whitespace)
461 }
462
463 /// Which graphemes of a line a field shows, and where the cursor lands.
464 ///
465 /// All indices are graphemes. `start..end` is what is drawn; when text is
466 /// cut off on a side a clip mark takes one cell there (none in a field too
467 /// narrow to spare it: `left_mark` and `right_mark` say which marks are
468 /// drawn). Whenever the field is wide enough for its cursor, `cursor_col`
469 /// is `Some`.
470 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
471 pub struct LineWindow {
472 pub start: usize,
473 pub end: usize,
474 /// Text before `start` is hidden.
475 pub left_clipped: bool,
476 /// Text from `end` on is hidden.
477 pub right_clipped: bool,
478 /// A clip mark is drawn in the first cell.
479 pub left_mark: bool,
480 /// A clip mark is drawn in the last cell.
481 pub right_mark: bool,
482 /// The cell, counted from the field's left edge, the cursor sits in; one
483 /// past the last grapheme when it is at the end of the line.
484 pub cursor_col: Option<usize>,
485 }
486
487 /// The window over graphemes of the given display `widths`: the pure core of
488 /// [`LineBuffer::window`]. With a `cursor` it scrolls as little as possible
489 /// from `prev_start` to keep it visible, and fills spare room on the left
490 /// rather than leave a gap after the text; without one it starts at 0.
491 #[must_use]
492 pub fn line_window_of(
493 widths: &[usize],
494 cursor: Option<usize>,
495 prev_start: usize,
496 avail: usize,
497 ) -> LineWindow {
498 let n = widths.len();
499 let mark = usize::from(avail >= MIN_CELLS_FOR_MARKS);
500 let span = |a: usize, b: usize| widths[a..b].iter().sum::<usize>();
501 let cursor = cursor.map(|c| c.min(n));
502 // At the end of the line the cursor is a cell past the last grapheme.
503 let cursor_cell = usize::from(cursor == Some(n));
504
505 let mut start = 0;
506 if let Some(c) = cursor {
507 // The leftmost start that still shows the cursor: walk back from it
508 // while it, what follows it up to a right mark, and the left mark fit.
509 let cursor_w = if c < n { widths[c] } else { 1 };
510 let right = if c + 1 < n { mark } else { 0 };
511 let mut lowest = c;
512 while lowest > 0 {
513 let left = usize::from(lowest - 1 > 0) * mark;
514 if span(lowest - 1, c) + cursor_w + right + left > avail {
515 break;
516 }
517 lowest -= 1;
518 }
519 start = prev_start.clamp(lowest, c);
520 // Spare room: if the whole tail fits, show more of the head. A cell
521 // stays reserved for a cursor at the end of the line even while the
522 // cursor is elsewhere, so the text does not shift by one every time
523 // the cursor crosses the end.
524 let tail_fits = |s: usize| span(s, n) + 1 + usize::from(s > 0) * mark <= avail;
525 if tail_fits(start) {
526 while start > 0 && tail_fits(start - 1) {
527 start -= 1;
528 }
529 }
530 }
531
532 let left = usize::from(start > 0) * mark;
533 let budget = avail.saturating_sub(left);
534 let (end, right_clipped) = if span(start, n) + cursor_cell <= budget {
535 (n, false)
536 } else {
537 let budget = budget.saturating_sub(mark);
538 let (mut end, mut used) = (start, 0);
539 while end < n && used + widths[end] <= budget {
540 used += widths[end];
541 end += 1;
542 }
543 (end, end < n)
544 };
545 let cursor_col = cursor.and_then(|c| {
546 let col = left + span(start, c.min(end).max(start));
547 let on_a_drawn_grapheme = c >= start && c < end;
548 let at_the_end = c == n && end == n && col < avail;
549 (on_a_drawn_grapheme || at_the_end).then_some(col)
550 });
551 LineWindow {
552 start,
553 end,
554 left_clipped: start > 0,
555 right_clipped,
556 left_mark: start > 0 && mark == 1,
557 right_mark: right_clipped && mark == 1,
558 cursor_col,
559 }
560 }
561
562 // ---------------------------------------------------------------------------
563 // TextInputState
564 // ---------------------------------------------------------------------------
565
566 /// What a key did to a [`TextInputState`]: the message a host reacts to.
567 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
568 pub enum TextInputOutcome {
569 /// The key means nothing to a field (or did nothing at a boundary: `←`
570 /// at the start, `Backspace` on an empty line); the host may use it.
571 Ignored,
572 /// The text or the cursor changed; repaint.
573 Changed,
574 /// Enter.
575 Submitted,
576 /// Esc.
577 Cancelled,
578 }
579
580 /// What [`TextInputState::apply`] did, a step finer than the outcome: the
581 /// form needs to tell an edit from a cursor move.
582 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
583 pub(super) enum KeyEffect {
584 Nothing,
585 Moved,
586 Edited,
587 Submit,
588 Cancel,
589 }
590
591 /// Where the field was last scrolled to, so scrolling is sticky: moving the
592 /// cursor left inside the window does not shift the text.
593 ///
594 /// [`TextInput`] paints from `&self` and records here what it painted. The
595 /// two cells are atomics so the state stays `Send + Sync`.
596 #[derive(Debug, Default)]
597 struct InputView {
598 start: AtomicUsize,
599 /// Cells the text area had last time; 0 until the first paint.
600 avail: AtomicUsize,
601 }
602
603 impl Clone for InputView {
604 fn clone(&self) -> Self {
605 Self {
606 start: AtomicUsize::new(self.start.load(Ordering::Relaxed)),
607 avail: AtomicUsize::new(self.avail.load(Ordering::Relaxed)),
608 }
609 }
610 }
611
612 /// The state of one text input: its [`LineBuffer`], whether it is a secret,
613 /// and where it is scrolled to. Keys go in through
614 /// [`TextInputState::handle_key`] and [`TextInputState::paste`].
615 ///
616 /// `Debug` never prints a secret's text.
617 #[derive(Clone, Default)]
618 pub struct TextInputState {
619 buffer: LineBuffer,
620 masked: bool,
621 view: InputView,
622 }
623
624 impl fmt::Debug for TextInputState {
625 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
626 let mut s = f.debug_struct("TextInputState");
627 if self.masked {
628 s.field("text", &"<hidden>");
629 } else {
630 s.field("text", &self.buffer.text())
631 .field("cursor", &self.buffer.cursor());
632 }
633 s.field("masked", &self.masked).finish()
634 }
635 }
636
637 impl TextInputState {
638 #[must_use]
639 pub fn new() -> Self {
640 Self::default()
641 }
642
643 /// A field holding `text`, cursor at the end.
644 #[must_use]
645 pub fn with_text(text: &str) -> Self {
646 Self {
647 buffer: LineBuffer::from_text(text),
648 ..Self::default()
649 }
650 }
651
652 /// A secret: painted as a mask, edited as one block.
653 #[must_use]
654 pub fn secret() -> Self {
655 Self {
656 masked: true,
657 ..Self::default()
658 }
659 }
660
661 /// Cap the line at `graphemes` ([`LineBuffer::with_limit`]).
662 #[must_use]
663 pub fn with_limit(mut self, graphemes: usize) -> Self {
664 self.buffer = self.buffer.with_limit(graphemes);
665 self
666 }
667
668 #[must_use]
669 pub const fn is_masked(&self) -> bool {
670 self.masked
671 }
672
673 #[must_use]
674 pub fn text(&self) -> &str {
675 self.buffer.text()
676 }
677
678 #[must_use]
679 pub fn is_empty(&self) -> bool {
680 self.buffer.is_empty()
681 }
682
683 /// The cursor, as a grapheme index.
684 #[must_use]
685 pub fn cursor(&self) -> usize {
686 self.buffer.cursor()
687 }
688
689 #[must_use]
690 pub const fn buffer(&self) -> &LineBuffer {
691 &self.buffer
692 }
693
694 /// Edit the buffer directly (a host's own shortcut, a completion). The
695 /// scroll catches up on the next key or paint.
696 pub const fn buffer_mut(&mut self) -> &mut LineBuffer {
697 &mut self.buffer
698 }
699
700 pub fn set_text(&mut self, text: &str) {
701 self.buffer.set_text(text);
702 self.follow();
703 }
704
705 pub fn clear(&mut self) {
706 self.buffer.clear();
707 self.view.start.store(0, Ordering::Relaxed);
708 }
709
710 /// Insert pasted text ([`LineBuffer::insert_str`]): one line, safe.
711 pub fn paste(&mut self, text: &str) -> TextInputOutcome {
712 if self.buffer.insert_str(text) {
713 self.follow();
714 TextInputOutcome::Changed
715 } else {
716 TextInputOutcome::Ignored
717 }
718 }
719
720 /// The window a field `avail` cells wide shows now.
721 #[must_use]
722 pub fn window(&self, avail: usize) -> LineWindow {
723 self.buffer
724 .window(avail, self.view.start.load(Ordering::Relaxed), self.masked)
725 }
726
727 /// Tell the state how many cells the text area has, and scroll the
728 /// cursor into it. [`TextInput`] does this each time it paints; a host
729 /// calls it only to scroll before the first paint, or after a resize.
730 pub fn scroll_into_view(&self, avail: usize) {
731 self.view.avail.store(avail, Ordering::Relaxed);
732 self.follow();
733 }
734
735 fn follow(&self) {
736 let avail = self.view.avail.load(Ordering::Relaxed);
737 if avail > 0 {
738 self.view
739 .start
740 .store(self.window(avail).start, Ordering::Relaxed);
741 }
742 }
743
744 /// Record what a paint showed, so the next key scrolls from there.
745 fn remember(&self, start: usize, avail: usize) {
746 self.view.start.store(start, Ordering::Relaxed);
747 self.view.avail.store(avail, Ordering::Relaxed);
748 }
749
750 /// Apply a key press and say what happened (see the module's key table).
751 /// Editing and movement accept held-key repeats. Enter/Esc require an
752 /// unmodified initial press. Releases and every key a field has no use
753 /// for (Tab, the arrows up and down, `Ctrl+C`, `Cmd` chords) are
754 /// [`TextInputOutcome::Ignored`].
755 pub fn handle_key(&mut self, key: KeyEvent) -> TextInputOutcome {
756 match self.apply(key) {
757 KeyEffect::Nothing => TextInputOutcome::Ignored,
758 KeyEffect::Moved | KeyEffect::Edited => TextInputOutcome::Changed,
759 KeyEffect::Submit => TextInputOutcome::Submitted,
760 KeyEffect::Cancel => TextInputOutcome::Cancelled,
761 }
762 }
763
764 pub(super) fn apply(&mut self, key: KeyEvent) -> KeyEffect {
765 if key.kind == KeyEventKind::Release {
766 return KeyEffect::Nothing;
767 }
768 let mods = key.modifiers;
769 // `Cmd` chords belong to the host.
770 if mods.contains(KeyModifiers::SUPER) {
771 return KeyEffect::Nothing;
772 }
773 let (ctrl, alt) = (
774 mods.contains(KeyModifiers::CONTROL),
775 mods.contains(KeyModifiers::ALT),
776 );
777 let word = ctrl || alt;
778 let effect = match key.code {
779 KeyCode::Enter | KeyCode::Esc => {
780 return if key.kind == KeyEventKind::Press && mods.is_empty() {
781 if key.code == KeyCode::Enter {
782 KeyEffect::Submit
783 } else {
784 KeyEffect::Cancel
785 }
786 } else {
787 KeyEffect::Nothing
788 };
789 }
790 KeyCode::Char(c) => {
791 if keys::is_ctrl_h_backspace(&key) {
792 self.edit(LineBuffer::backspace)
793 } else if ctrl && !alt {
794 match c.to_ascii_lowercase() {
795 'w' => self.edit_word_back(),
796 'u' => self.edit(LineBuffer::kill_to_start),
797 'k' => self.edit(LineBuffer::kill_to_end),
798 'a' => self.go(LineBuffer::home),
799 'e' => self.go(LineBuffer::end),
800 'b' => self.go(LineBuffer::move_left),
801 'f' => self.go(LineBuffer::move_right),
802 _ => KeyEffect::Nothing,
803 }
804 } else if alt && !ctrl {
805 match c {
806 _ if !c.is_ascii() => self.edit_char(c),
807 'b' | 'B' => self.go_word_left(),
808 'f' | 'F' => self.go_word_right(),
809 'd' | 'D' => self.edit_word_forward(),
810 _ => KeyEffect::Nothing,
811 }
812 } else if ctrl && alt {
813 // Windows reports AltGr as Ctrl+Alt.
814 if cfg!(windows) {
815 self.edit_char(c)
816 } else {
817 KeyEffect::Nothing
818 }
819 } else {
820 self.edit_char(c)
821 }
822 }
823 KeyCode::Backspace if word => self.edit_word_back(),
824 KeyCode::Backspace => self.edit(LineBuffer::backspace),
825 KeyCode::Delete if word => self.edit_word_forward(),
826 KeyCode::Delete => self.edit(LineBuffer::delete),
827 KeyCode::Left if word => self.go_word_left(),
828 KeyCode::Left => self.go(LineBuffer::move_left),
829 KeyCode::Right if word => self.go_word_right(),
830 KeyCode::Right => self.go(LineBuffer::move_right),
831 KeyCode::Home => self.go(LineBuffer::home),
832 KeyCode::End => self.go(LineBuffer::end),
833 _ => KeyEffect::Nothing,
834 };
835 if effect != KeyEffect::Nothing {
836 self.follow();
837 }
838 effect
839 }
840
841 fn edit(&mut self, op: fn(&mut LineBuffer) -> bool) -> KeyEffect {
842 if op(&mut self.buffer) {
843 KeyEffect::Edited
844 } else {
845 KeyEffect::Nothing
846 }
847 }
848
849 fn go(&mut self, op: fn(&mut LineBuffer) -> bool) -> KeyEffect {
850 if op(&mut self.buffer) {
851 KeyEffect::Moved
852 } else {
853 KeyEffect::Nothing
854 }
855 }
856
857 fn edit_char(&mut self, c: char) -> KeyEffect {
858 if self.buffer.insert_char(c) {
859 KeyEffect::Edited
860 } else {
861 KeyEffect::Nothing
862 }
863 }
864
865 // A secret has no visible words: its word keys act on the whole line.
866 fn edit_word_back(&mut self) -> KeyEffect {
867 self.edit(if self.masked {
868 LineBuffer::kill_to_start
869 } else {
870 LineBuffer::delete_word_back
871 })
872 }
873
874 fn edit_word_forward(&mut self) -> KeyEffect {
875 self.edit(if self.masked {
876 LineBuffer::kill_to_end
877 } else {
878 LineBuffer::delete_word_forward
879 })
880 }
881
882 fn go_word_left(&mut self) -> KeyEffect {
883 self.go(if self.masked {
884 LineBuffer::home
885 } else {
886 LineBuffer::word_left
887 })
888 }
889
890 fn go_word_right(&mut self) -> KeyEffect {
891 self.go(if self.masked {
892 LineBuffer::end
893 } else {
894 LineBuffer::word_right
895 })
896 }
897 }
898
899 // ---------------------------------------------------------------------------
900 // Painting: shared by TextInput and Form
901 // ---------------------------------------------------------------------------
902
903 /// The words a [`TextInput`] shows itself. English by default; the host
904 /// passes its own.
905 #[derive(Clone, Debug, PartialEq, Eq)]
906 pub struct TextInputWords {
907 /// Shown under a disabled field that gave no reason of its own.
908 pub disabled: Cow<'static, str>,
909 }
910
911 impl Default for TextInputWords {
912 fn default() -> Self {
913 Self {
914 disabled: Cow::Borrowed("Disabled"),
915 }
916 }
917 }
918
919 /// A line or more of words under a field, with the mark that gives them
920 /// their meaning.
921 pub(super) struct FieldNote {
922 pub mark: Option<(&'static str, Role)>,
923 pub text: String,
924 pub ink: Role,
925 }
926
927 impl FieldNote {
928 /// `✕ message`: the mark carries the state, the words stay body ink.
929 pub fn error(message: &str) -> Self {
930 Self {
931 mark: Some((glyphs::FAILED, Role::Danger)),
932 text: message.to_owned(),
933 ink: Role::Foreground,
934 }
935 }
936
937 /// `· reason`: a field that cannot be edited says why.
938 pub fn locked(reason: &str) -> Self {
939 Self {
940 mark: Some((glyphs::NEUTRAL, Role::Muted)),
941 text: reason.to_owned(),
942 ink: Role::Muted,
943 }
944 }
945
946 /// Plain muted guidance, aligned with the text.
947 pub fn help(text: &str) -> Self {
948 Self {
949 mark: None,
950 text: text.to_owned(),
951 ink: Role::Muted,
952 }
953 }
954 }
955
956 /// Break `text` into rows of at most `width` cells at spaces (a longer word
957 /// is cut between graphemes). Past `max_rows` the rest is joined onto the
958 /// last row, which the painter then cuts with `…`.
959 pub(super) fn wrap_field_note(text: &str, width: usize, max_rows: usize) -> Vec<String> {
960 let width = width.max(1);
961 let mut rows: Vec<String> = Vec::new();
962 let (mut row, mut used) = (String::new(), 0);
963 for word in text.split_whitespace() {
964 let w = text::width(word);
965 if w > width {
966 if !row.is_empty() {
967 rows.push(std::mem::take(&mut row));
968 }
969 used = 0;
970 for g in word.graphemes(true) {
971 let gw = text::width(g);
972 if used + gw > width && !row.is_empty() {
973 rows.push(std::mem::take(&mut row));
974 used = 0;
975 }
976 row.push_str(g);
977 used += gw;
978 }
979 } else if row.is_empty() {
980 row.push_str(word);
981 used = w;
982 } else if used + 1 + w <= width {
983 row.push(' ');
984 row.push_str(word);
985 used += 1 + w;
986 } else {
987 rows.push(std::mem::replace(&mut row, word.to_owned()));
988 used = w;
989 }
990 }
991 if !row.is_empty() {
992 rows.push(row);
993 }
994 if rows.len() > max_rows && max_rows > 0 {
995 let tail = rows.split_off(max_rows - 1).join(" ");
996 rows.push(tail);
997 }
998 rows
999 }
1000
1001 /// Everything around a field's text: label above, prompt and well, note
1002 /// below. The body is painted by the caller into the text area.
1003 pub(super) struct FieldChrome<'a> {
1004 pub label: Option<&'a str>,
1005 pub focused: bool,
1006 /// Disabled or read-only: no well, quiet ink.
1007 pub dimmed: bool,
1008 pub note: Option<FieldNote>,
1009 }
1010
1011 /// Where each part of a [`FieldChrome`] landed.
1012 pub(super) struct FieldPlaced {
1013 pub label: Option<Rect>,
1014 pub input: Rect,
1015 /// The text area of the input row, after the prompt.
1016 pub value: Rect,
1017 pub notes: Vec<(Rect, String)>,
1018 }
1019
1020 impl FieldChrome<'_> {
1021 fn note_rows(&self, width: u16) -> Vec<String> {
1022 self.note.as_ref().map_or_else(Vec::new, |note| {
1023 let width = usize::from(width.saturating_sub(PROMPT_CELLS));
1024 wrap_field_note(&text::display_safe(&note.text), width, MAX_NOTE_ROWS)
1025 })
1026 }
1027
1028 /// Rows this field wants at `width`: label, input, note.
1029 pub fn height(&self, width: u16) -> u16 {
1030 let rows = usize::from(self.label.is_some()) + 1 + self.note_rows(width).len();
1031 u16::try_from(rows).unwrap_or(u16::MAX)
1032 }
1033
1034 /// Lay the parts out in `area`. Too short: the note shrinks to one row
1035 /// (the first row of an error is the one that matters), then the label
1036 /// goes, then the note.
1037 pub fn place(&self, area: Rect) -> Option<FieldPlaced> {
1038 if area.is_empty() {
1039 return None;
1040 }
1041 let notes = self.note_rows(area.width);
1042 let rows = usize::from(area.height);
1043 let mut show_label = self.label.is_some();
1044 let mut shown_notes = notes.len();
1045 let need = |label: bool, notes: usize| usize::from(label) + 1 + notes;
1046 while need(show_label, shown_notes) > rows && shown_notes > 1 {
1047 shown_notes -= 1;
1048 }
1049 if need(show_label, shown_notes) > rows {
1050 show_label = false;
1051 }
1052 shown_notes = shown_notes.min(rows - 1);
1053
1054 let mut y = area.y;
1055 let row = |y: u16| Rect {
1056 y,
1057 height: 1,
1058 ..area
1059 };
1060 let label = show_label.then(|| {
1061 y += 1;
1062 row(y - 1)
1063 });
1064 let input = row(y);
1065 y += 1;
1066 let mut placed_notes = Vec::new();
1067 for text in notes.into_iter().take(shown_notes) {
1068 placed_notes.push((row(y), text));
1069 y += 1;
1070 }
1071 let value = Rect {
1072 x: input.x.saturating_add(PROMPT_CELLS.min(input.width)),
1073 width: input.width.saturating_sub(PROMPT_CELLS),
1074 ..input
1075 };
1076 Some(FieldPlaced {
1077 label,
1078 input,
1079 value,
1080 notes: placed_notes,
1081 })
1082 }
1083
1084 /// Paint everything but the text, then hand the text area to `body`.
1085 pub fn paint(
1086 &self,
1087 area: Rect,
1088 buf: &mut Buffer,
1089 theme: &Theme,
1090 body: impl FnOnce(Rect, &mut Buffer),
1091 ) {
1092 let area = area.intersection(buf.area);
1093 let Some(placed) = self.place(area) else {
1094 return;
1095 };
1096 let ascii = theme.ascii();
1097
1098 if let (Some(rect), Some(label)) = (placed.label, self.label) {
1099 let label = text::display_safe(label);
1100 let shown = text::truncate(&label, usize::from(rect.width), ascii);
1101 let style = if self.dimmed {
1102 theme.fg(Role::Muted).add_modifier(Modifier::DIM)
1103 } else if self.focused {
1104 theme.fg(Role::Foreground).add_modifier(Modifier::BOLD)
1105 } else {
1106 theme.fg(Role::Foreground)
1107 };
1108 buf.set_stringn(rect.x, rect.y, shown, usize::from(rect.width), style);
1109 }
1110
1111 // The well: where grounds paint it lifts the field; where they do
1112 // not, the prompt and the cursor carry focus.
1113 if !self.dimmed {
1114 let ground = if self.focused {
1115 Role::Selected
1116 } else {
1117 Role::Surface
1118 };
1119 buf.set_style(placed.input, theme.bg(ground));
1120 }
1121 if self.focused {
1122 buf.set_stringn(
1123 placed.input.x,
1124 placed.input.y,
1125 glyphs::pick(PROMPT, ascii),
1126 usize::from(placed.input.width),
1127 theme.fg(Role::Primary),
1128 );
1129 }
1130 if placed.value.width > 0 {
1131 body(placed.value, buf);
1132 }
1133
1134 if let Some(note) = &self.note {
1135 let width = usize::from(area.width.saturating_sub(PROMPT_CELLS));
1136 for (i, (rect, row)) in placed.notes.iter().enumerate() {
1137 if i == 0
1138 && let Some((mark, role)) = note.mark
1139 {
1140 buf.set_stringn(
1141 rect.x,
1142 rect.y,
1143 glyphs::pick(mark, ascii),
1144 usize::from(rect.width),
1145 theme.fg(role),
1146 );
1147 }
1148 let shown = text::truncate(row, width, ascii);
1149 buf.set_stringn(
1150 rect.x.saturating_add(PROMPT_CELLS),
1151 rect.y,
1152 shown,
1153 width,
1154 theme.fg(note.ink),
1155 );
1156 }
1157 }
1158 }
1159 }
1160
1161 /// The clip mark at the edge of a field's text.
1162 fn clip_mark(left: bool, ascii: bool) -> &'static str {
1163 match (ascii, left) {
1164 (false, _) => glyphs::ELLIPSIS,
1165 // A lone `.` at the edge of editable text reads as typed text.
1166 (true, true) => "<",
1167 (true, false) => ">",
1168 }
1169 }
1170
1171 /// Paint the text of a field into `area` (one row): the text or the mask, or
1172 /// the placeholder when empty; clip marks; the cursor cell when focused.
1173 pub(super) fn paint_input_value(
1174 state: &TextInputState,
1175 placeholder: Option<&str>,
1176 focused: bool,
1177 dimmed: bool,
1178 area: Rect,
1179 buf: &mut Buffer,
1180 theme: &Theme,
1181 ) {
1182 if area.is_empty() {
1183 return;
1184 }
1185 let ascii = theme.ascii();
1186 let avail = usize::from(area.width);
1187 let ink = if dimmed {
1188 theme.fg(Role::Muted).add_modifier(Modifier::DIM)
1189 } else {
1190 theme.fg(Role::Foreground)
1191 };
1192 let cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1193 let at = |col: usize| {
1194 area.x
1195 .saturating_add(u16::try_from(col).unwrap_or(u16::MAX))
1196 };
1197
1198 if state.is_empty() {
1199 let hint = theme.fg(if dimmed { Role::Dim } else { Role::Hint });
1200 let placeholder = text::display_safe(placeholder.unwrap_or_default());
1201 // An empty field with nothing to say still shows where it is: a dash
1202 // (the design's `—`), so it does not read as a gap in the form.
1203 let placeholder = if placeholder.is_empty() && !focused {
1204 Cow::Borrowed(glyphs::pick(EMPTY, ascii))
1205 } else {
1206 placeholder
1207 };
1208 let shown = text::truncate(&placeholder, avail, ascii);
1209 buf.set_stringn(area.x, area.y, &shown, avail, hint);
1210 if focused {
1211 // The cursor sits on the placeholder's first cell.
1212 let first = shown
1213 .graphemes(true)
1214 .next()
1215 .map_or(1, |g| text::width(g).max(1));
1216 let cell = Rect {
1217 width: u16::try_from(first.min(avail)).unwrap_or(1),
1218 height: 1,
1219 ..area
1220 };
1221 buf.set_style(cell, cursor_style);
1222 }
1223 return;
1224 }
1225
1226 let masked = state.is_masked();
1227 let window = if focused {
1228 let window = state.window(avail);
1229 state.remember(window.start, avail);
1230 window
1231 } else {
1232 state.buffer().head_window(avail, masked)
1233 };
1234 let mask = if ascii { MASK_ASCII } else { MASK };
1235 let mut col = usize::from(window.left_mark);
1236 for g in state
1237 .buffer()
1238 .graphemes()
1239 .skip(window.start)
1240 .take(window.end - window.start)
1241 {
1242 let (shown, w) = if masked {
1243 (mask, 1)
1244 } else {
1245 (g, text::width(g))
1246 };
1247 buf.set_stringn(at(col), area.y, shown, usize::from(area.width), ink);
1248 col += w;
1249 }
1250 let mark_style = theme.fg(Role::Muted);
1251 if window.left_mark {
1252 buf.set_stringn(area.x, area.y, clip_mark(true, ascii), 1, mark_style);
1253 }
1254 if window.right_mark {
1255 buf.set_stringn(
1256 at(avail - 1),
1257 area.y,
1258 clip_mark(false, ascii),
1259 1,
1260 mark_style,
1261 );
1262 }
1263 if focused && let Some(cursor_col) = window.cursor_col {
1264 let on = state.buffer().cursor();
1265 let w = if masked || on >= state.buffer().len() {
1266 1
1267 } else {
1268 text::width(state.buffer().graphemes().nth(on).unwrap_or(" ")).max(1)
1269 };
1270 let cell = Rect {
1271 x: at(cursor_col),
1272 width: u16::try_from(w.min(avail - cursor_col)).unwrap_or(1),
1273 height: 1,
1274 ..area
1275 };
1276 buf.set_style(cell, cursor_style);
1277 }
1278 }
1279
1280 // ---------------------------------------------------------------------------
1281 // TextInput
1282 // ---------------------------------------------------------------------------
1283
1284 /// Paints a [`TextInputState`]:
1285 ///
1286 /// ```text
1287 /// Port
1288 /// › 8080▏
1289 /// ✕ Enter a port from 1 to 65535.
1290 /// ```
1291 ///
1292 /// Every state has a mark and a word: focus is the `›` prompt and the cursor
1293 /// cell; an invalid field is `✕` and the reason; a disabled one is `·` and
1294 /// its reason (or [`TextInputWords::disabled`]); a secret is its mask.
1295 /// Where grounds paint, the field sits in a well (`Surface`, `Selected` when
1296 /// focused).
1297 #[derive(Clone, Debug)]
1298 pub struct TextInput<'a> {
1299 state: &'a TextInputState,
1300 label: Option<Cow<'static, str>>,
1301 placeholder: Option<Cow<'static, str>>,
1302 focused: bool,
1303 disabled: Option<Cow<'static, str>>,
1304 error: Option<Cow<'static, str>>,
1305 help: Option<Cow<'static, str>>,
1306 words: TextInputWords,
1307 }
1308
1309 impl<'a> TextInput<'a> {
1310 #[must_use]
1311 pub fn new(state: &'a TextInputState) -> Self {
1312 Self {
1313 state,
1314 label: None,
1315 placeholder: None,
1316 focused: false,
1317 disabled: None,
1318 error: None,
1319 help: None,
1320 words: TextInputWords::default(),
1321 }
1322 }
1323
1324 /// A line above the field naming it.
1325 #[must_use]
1326 pub fn label(mut self, label: impl Into<Cow<'static, str>>) -> Self {
1327 self.label = Some(label.into());
1328 self
1329 }
1330
1331 /// Hint ink shown while the field is empty.
1332 #[must_use]
1333 pub fn placeholder(mut self, placeholder: impl Into<Cow<'static, str>>) -> Self {
1334 self.placeholder = Some(placeholder.into());
1335 self
1336 }
1337
1338 #[must_use]
1339 pub const fn focused(mut self, focused: bool) -> Self {
1340 self.focused = focused;
1341 self
1342 }
1343
1344 /// Disable the field and say why. An empty reason shows
1345 /// [`TextInputWords::disabled`].
1346 #[must_use]
1347 pub fn disabled(mut self, reason: impl Into<Cow<'static, str>>) -> Self {
1348 self.disabled = Some(reason.into());
1349 self
1350 }
1351
1352 /// Mark the field invalid: `✕` and the reason under it.
1353 #[must_use]
1354 pub fn error(mut self, message: impl Into<Cow<'static, str>>) -> Self {
1355 self.error = Some(message.into());
1356 self
1357 }
1358
1359 /// Muted guidance under the field, shown while there is no error.
1360 #[must_use]
1361 pub fn help(mut self, help: impl Into<Cow<'static, str>>) -> Self {
1362 self.help = Some(help.into());
1363 self
1364 }
1365
1366 #[must_use]
1367 pub fn words(mut self, words: &TextInputWords) -> Self {
1368 self.words = words.clone();
1369 self
1370 }
1371
1372 fn chrome(&self) -> FieldChrome<'_> {
1373 let note = if let Some(error) = &self.error {
1374 Some(FieldNote::error(error))
1375 } else if let Some(reason) = &self.disabled {
1376 let reason = if reason.trim().is_empty() {
1377 &self.words.disabled
1378 } else {
1379 reason
1380 };
1381 Some(FieldNote::locked(reason))
1382 } else {
1383 self.help.as_deref().map(FieldNote::help)
1384 };
1385 FieldChrome {
1386 label: self.label.as_deref(),
1387 focused: self.focused && self.disabled.is_none(),
1388 dimmed: self.disabled.is_some(),
1389 note,
1390 }
1391 }
1392
1393 /// Where the terminal's own cursor goes when this field is painted in
1394 /// `area`: the cell the painted cursor is in. `None` unless focused and
1395 /// the cursor is visible.
1396 #[must_use]
1397 pub fn cursor_position(&self, area: Rect) -> Option<Position> {
1398 let chrome = self.chrome();
1399 if !chrome.focused {
1400 return None;
1401 }
1402 let placed = chrome.place(area)?;
1403 let col = if self.state.is_empty() {
1404 0
1405 } else {
1406 self.state
1407 .window(usize::from(placed.value.width))
1408 .cursor_col?
1409 };
1410 let x = placed
1411 .value
1412 .x
1413 .saturating_add(u16::try_from(col).unwrap_or(u16::MAX));
1414 (placed.value.width > 0 && x < placed.value.right())
1415 .then_some(Position::new(x, placed.value.y))
1416 }
1417 }
1418
1419 impl Paint for TextInput<'_> {
1420 fn paint(&self, area: Rect, buf: &mut Buffer, theme: &Theme) {
1421 let chrome = self.chrome();
1422 let (focused, dimmed) = (chrome.focused, chrome.dimmed);
1423 chrome.paint(area, buf, theme, |value, buf| {
1424 paint_input_value(
1425 self.state,
1426 self.placeholder.as_deref(),
1427 focused,
1428 dimmed,
1429 value,
1430 buf,
1431 theme,
1432 );
1433 });
1434 }
1435
1436 fn height(&self, width: u16, _theme: &Theme) -> u16 {
1437 self.chrome().height(width)
1438 }
1439 }
1440
1440 lines RUST