1use std::fmt;
2
3use ratatui::{
4 buffer::Buffer,
5 layout::{Position, Rect},
6 style::{Modifier, Style},
7 widgets::Widget,
8};
9use unicode_segmentation::UnicodeSegmentation;
10
11use crate::key::Key;
12use crate::styles::{Styles, Tone};
13use crate::text::{self, ELLIPSIS, Pen};
14
15const MASK: &str = "•";
16const SHARE: usize = 4;
17const GAP: u16 = 1;
18const CARET: &str = " ";
19
20#[derive(Clone, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum Edit {
23 Pass,
24 Held,
25 Changed,
26 Submit(String),
27 Cancel,
28}
29
30impl fmt::Debug for Edit {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 Edit::Pass => f.write_str("Pass"),
34 Edit::Held => f.write_str("Held"),
35 Edit::Changed => f.write_str("Changed"),
36 Edit::Submit(value) => write!(f, "Submit(<{} bytes>)", value.len()),
37 Edit::Cancel => f.write_str("Cancel"),
38 }
39 }
40}
41
42#[derive(Clone, PartialEq, Eq)]
43pub struct Input {
44 value: String,
45 cursor: usize,
46 masked: bool,
47 edited: bool,
48 limit: usize,
49}
50
51impl Default for Input {
52 fn default() -> Self {
53 Input {
54 value: String::new(),
55 cursor: 0,
56 masked: false,
57 edited: false,
58 limit: usize::MAX,
59 }
60 }
61}
62
63impl fmt::Debug for Input {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 let mut out = f.debug_struct("Input");
66 if self.masked {
67 out.field("value", &format_args!("<masked>"));
68 } else {
69 out.field("value", &self.value);
70 }
71 out.field("cursor", &self.cursor())
72 .field("masked", &self.masked)
73 .finish()
74 }
75}
76
77impl Input {
78 pub fn new() -> Self {
79 Input::default()
80 }
81
82 pub fn masked(mut self, masked: bool) -> Self {
83 self.masked = masked;
84 self
85 }
86
87 pub fn limit(mut self, chars: usize) -> Self {
88 self.limit = chars;
89 let kept = self.value.char_indices().nth(chars).map(|(at, _)| at);
90 if let Some(at) = kept {
91 self.value.truncate(at);
92 self.cursor = self.cursor.min(at);
93 self.snap();
94 }
95 self
96 }
97
98 pub fn with(mut self, value: &str) -> Self {
99 self.set(value);
100 self
101 }
102
103 pub fn edited(&self) -> bool {
104 self.edited
105 }
106
107 pub fn value(&self) -> &str {
108 &self.value
109 }
110
111 pub fn is_masked(&self) -> bool {
112 self.masked
113 }
114
115 pub fn is_empty(&self) -> bool {
116 self.value.is_empty()
117 }
118
119 pub fn cursor(&self) -> usize {
120 self.value
121 .grapheme_indices(true)
122 .take_while(|(at, _)| *at < self.cursor)
123 .count()
124 }
125
126 pub fn set(&mut self, value: &str) {
127 self.value = clean(value);
128 if let Some((at, _)) = self.value.char_indices().nth(self.limit) {
129 self.value.truncate(at);
130 }
131 self.cursor = self.value.len();
132 self.edited = false;
133 }
134
135 pub fn clear(&mut self) {
136 self.value.clear();
137 self.cursor = 0;
138 self.edited = false;
139 }
140
141 pub fn key(&mut self, key: Key) -> Edit {
142 match key {
143 Key::Enter => Edit::Submit(self.value.clone()),
144 Key::Esc => Edit::Cancel,
145 Key::Left => self.go(self.prev()),
146 Key::Right => self.go(self.next()),
147 Key::Home | Key::Ctrl('a') => self.go(0),
148 Key::End | Key::Ctrl('e') => self.go(self.value.len()),
149 Key::Backspace => self.cut(self.prev(), self.cursor),
150 Key::Delete => self.cut(self.cursor, self.next()),
151 Key::Ctrl('u') => self.cut(0, self.cursor),
152 Key::Ctrl('w') => self.cut(self.word(), self.cursor),
153 Key::Char(c) if !c.is_control() => self.insert(c.encode_utf8(&mut [0; 4])),
154 _ => Edit::Pass,
155 }
156 }
157
158 pub fn paste(&mut self, text: &str) -> Edit {
159 self.insert(&clean(text))
160 }
161
162 fn go(&mut self, at: usize) -> Edit {
163 self.cursor = at;
164 Edit::Held
165 }
166
167 fn cut(&mut self, from: usize, to: usize) -> Edit {
168 if from >= to {
169 return Edit::Held;
170 }
171 self.value.replace_range(from..to, "");
172 self.cursor = from;
173 self.snap();
174 self.edited = true;
175 Edit::Changed
176 }
177
178 fn insert(&mut self, text: &str) -> Edit {
179 let room = self.limit.saturating_sub(self.value.chars().count());
180 let text = match text.char_indices().nth(room) {
181 Some((at, _)) => &text[..at],
182 None => text,
183 };
184 if text.is_empty() {
185 return Edit::Held;
186 }
187 self.value.insert_str(self.cursor, text);
188 self.cursor += text.len();
189 self.snap();
190 self.edited = true;
191 Edit::Changed
192 }
193
194 fn snap(&mut self) {
195 self.cursor = self
196 .value
197 .grapheme_indices(true)
198 .map(|(at, _)| at)
199 .chain([self.value.len()])
200 .find(|at| *at >= self.cursor)
201 .unwrap_or(self.value.len());
202 }
203
204 fn prev(&self) -> usize {
205 self.value
206 .grapheme_indices(true)
207 .rev()
208 .find(|(at, _)| *at < self.cursor)
209 .map_or(0, |(at, _)| at)
210 }
211
212 fn next(&self) -> usize {
213 self.value
214 .grapheme_indices(true)
215 .map(|(at, grapheme)| at + grapheme.len())
216 .find(|end| *end > self.cursor)
217 .unwrap_or(self.value.len())
218 }
219
220 fn word(&self) -> usize {
221 let mut at = self.cursor;
222 let mut word = false;
223 for (start, grapheme) in self
224 .value
225 .grapheme_indices(true)
226 .rev()
227 .skip_while(|(start, _)| *start >= self.cursor)
228 {
229 let space = grapheme.chars().all(char::is_whitespace);
230 if space && word {
231 break;
232 }
233 word |= !space;
234 at = start;
235 }
236 at
237 }
238
239 fn units<'a>(
240 &'a self,
241 mask: &'a str,
242 shape: Shape,
243 ) -> impl Iterator<Item = (&'a str, bool)> + Clone {
244 self.value
245 .graphemes(true)
246 .enumerate()
247 .map(move |(index, grapheme)| shape.unit(index, grapheme, mask))
248 }
249
250 fn units_back<'a>(
251 &'a self,
252 mask: &'a str,
253 shape: Shape,
254 ) -> impl Iterator<Item = (usize, &'a str)> {
255 self.value
256 .graphemes(true)
257 .rev()
258 .scan(shape.count, move |index, grapheme| {
259 *index -= 1;
260 Some((*index, shape.unit(*index, grapheme, mask).0))
261 })
262 }
263
264 fn shape(&self, reveal: usize) -> Shape {
265 let count = self.value.graphemes(true).count();
266 let open =
267 self.masked && !self.edited && reveal > 0 && count >= reveal.saturating_mul(SHARE);
268 Shape {
269 count,
270 masked: self.masked,
271 reveal: if open { reveal } else { 0 },
272 }
273 }
274}
275
276#[derive(Debug, Clone, Copy)]
277struct Shape {
278 count: usize,
279 masked: bool,
280 reveal: usize,
281}
282
283impl Shape {
284 fn unit<'a>(&self, index: usize, grapheme: &'a str, mask: &'a str) -> (&'a str, bool) {
285 let shown = !self.masked
286 || (self.reveal > 0 && (index < self.reveal || index >= self.count - self.reveal));
287 if shown {
288 (grapheme, false)
289 } else {
290 (mask, true)
291 }
292 }
293}
294
295fn clean(text: &str) -> String {
296 let mut out = String::with_capacity(text.len());
297 for line in text.split(['\n', '\r']).filter(|line| !line.is_empty()) {
298 if !out.is_empty() {
299 out.push(' ');
300 }
301 for c in line.chars() {
302 if c == '\t' {
303 out.push(' ');
304 } else if !c.is_control() {
305 out.push(c);
306 }
307 }
308 }
309 out
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313pub struct InputBar<'a> {
314 label: &'a str,
315 input: &'a Input,
316 placeholder: &'a str,
317 hint: Option<&'a str>,
318 mask: &'a str,
319 rule: bool,
320 caret: bool,
321 reveal: usize,
322 tone: Tone,
323 styles: Option<Styles>,
324}
325
326struct Field {
327 line: Rect,
328 x: u16,
329 start: usize,
330 stop: usize,
331 count: usize,
332 cursor: usize,
333 shape: Shape,
334}
335
336impl<'a> InputBar<'a> {
337 pub fn new(label: &'a str, input: &'a Input) -> Self {
338 InputBar {
339 label,
340 input,
341 placeholder: "",
342 hint: None,
343 mask: MASK,
344 rule: true,
345 caret: true,
346 reveal: 0,
347 tone: Tone::Ink,
348 styles: None,
349 }
350 }
351
352 pub fn placeholder(mut self, placeholder: &'a str) -> Self {
353 self.placeholder = placeholder;
354 self
355 }
356
357 pub fn hint(mut self, hint: &'a str) -> Self {
358 self.hint = Some(hint);
359 self
360 }
361
362 pub fn mask(mut self, mask: &'a str) -> Self {
363 self.mask = mask;
364 self
365 }
366
367 pub fn rule(mut self, rule: bool) -> Self {
368 self.rule = rule;
369 self
370 }
371
372 pub fn caret(mut self, caret: bool) -> Self {
373 self.caret = caret;
374 self
375 }
376
377 pub fn reveal_ends(mut self, chars: usize) -> Self {
378 self.reveal = chars;
379 self
380 }
381
382 pub fn tone(mut self, tone: Tone) -> Self {
383 self.tone = tone;
384 self
385 }
386
387 pub fn styles(mut self, styles: Styles) -> Self {
388 self.styles = Some(styles);
389 self
390 }
391
392 pub(crate) fn styled(mut self, fallback: Styles) -> Self {
393 self.styles = Some(self.styles.unwrap_or(fallback));
394 self
395 }
396
397 fn look(&self) -> Styles {
398 self.styles.unwrap_or_default()
399 }
400
401 pub fn height(&self) -> u16 {
402 1 + u16::from(self.rule) + u16::from(self.hint.is_some())
403 }
404
405 pub fn cursor(&self, area: Rect) -> Option<Position> {
406 let field = self.field(area)?;
407 let before = self.span(&field, field.start, field.cursor.max(field.start));
408 let x = field
409 .x
410 .saturating_add(u16::from(field.start > 0))
411 .saturating_add(before);
412 (x < field.line.right()).then_some(Position::new(x, field.line.y))
413 }
414
415 fn label_width(&self, width: u16) -> u16 {
416 text::width(self.label).min(width.saturating_sub(GAP) / 2)
417 }
418
419 fn span(&self, field: &Field, from: usize, to: usize) -> u16 {
420 self.input
421 .units(self.mask, field.shape)
422 .skip(from)
423 .take(to.saturating_sub(from))
424 .fold(0u16, |total, (unit, _)| {
425 total.saturating_add(text::width(unit))
426 })
427 }
428
429 fn field(&self, area: Rect) -> Option<Field> {
430 let y = area.y.checked_add(u16::from(self.rule))?;
431 if area.is_empty() || y >= area.bottom() {
432 return None;
433 }
434 let line = Rect::new(area.x, y, area.width, 1);
435 let label = self.label_width(line.width);
436 let x = if label > 0 {
437 line.x.saturating_add(label).saturating_add(GAP)
438 } else {
439 line.x
440 };
441 let room = line.right().saturating_sub(x);
442 let shape = self.input.shape(self.reveal);
443 let count = shape.count;
444 let cursor = self.input.cursor();
445 let mut field = Field {
446 line,
447 x,
448 start: 0,
449 stop: count,
450 count,
451 cursor,
452 shape,
453 };
454 let mut total = 0u16;
455 let mut caret = 1;
456 for (index, (unit, _)) in self.input.units(self.mask, shape).enumerate() {
457 let wide = text::width(unit);
458 total = total.saturating_add(wide);
459 if index == cursor {
460 caret = wide;
461 }
462 }
463 if total.saturating_add(u16::from(cursor == count)) <= room {
464 return Some(field);
465 }
466 let tail = u16::from(cursor + 1 < count);
467 let need = caret.saturating_add(tail);
468 let mut behind = 0u16;
469 let mut start = cursor;
470 for (index, unit) in self
471 .input
472 .units_back(self.mask, shape)
473 .skip(count - cursor.min(count))
474 {
475 behind = behind.saturating_add(text::width(unit));
476 if behind.saturating_add(need) > room {
477 break;
478 }
479 let lead = u16::from(index > 0);
480 if behind.saturating_add(need).saturating_add(lead) <= room {
481 start = index;
482 }
483 }
484 let lead = u16::from(start > 0);
485 let mut used = self.span(&field, start, (cursor + 1).min(count));
486 let mut stop = (cursor + 1).min(count);
487 for (unit, _) in self.input.units(self.mask, shape).skip(stop) {
488 let next = used.saturating_add(text::width(unit));
489 let more = u16::from(stop + 1 < count);
490 if lead.saturating_add(next).saturating_add(more) > room {
491 break;
492 }
493 used = next;
494 stop += 1;
495 }
496 field.start = start;
497 field.stop = stop;
498 Some(field)
499 }
500
501 fn draw_field(&self, buf: &mut Buffer, field: &Field) -> Option<Position> {
502 let mut pen = Pen::new(buf, field.line, field.x, field.line.y)?;
503 let styles = self.look();
504 let ink = styles.ink;
505 let muted = styles.muted;
506 let caret = |style: Style| {
507 if self.caret {
508 style.add_modifier(Modifier::REVERSED)
509 } else {
510 style
511 }
512 };
513 if field.count == 0 {
514 let mut graphemes = self.placeholder.graphemes(true);
515 match graphemes.next() {
516 Some(first) => {
517 pen.put(first, caret(muted));
518 pen.rest(graphemes.as_str(), muted);
519 }
520 None => pen.put(CARET, caret(ink)),
521 }
522 return Some(Position::new(pen.x, field.line.y));
523 }
524 if field.start > 0 {
525 pen.put(ELLIPSIS, muted);
526 }
527 for (index, (unit, hidden)) in self
528 .input
529 .units(self.mask, field.shape)
530 .enumerate()
531 .skip(field.start)
532 .take(field.stop - field.start)
533 {
534 let style = if hidden { muted } else { ink };
535 if index == field.cursor {
536 pen.put(unit, caret(style));
537 } else {
538 pen.put(unit, style);
539 }
540 }
541 if field.stop < field.count {
542 pen.put(ELLIPSIS, muted);
543 } else if field.cursor == field.count {
544 pen.put(CARET, caret(ink));
545 }
546 Some(Position::new(pen.x, field.line.y))
547 }
548}
549
550impl Widget for InputBar<'_> {
551 fn render(self, area: Rect, buf: &mut Buffer) {
552 (&self).render(area, buf);
553 }
554}
555
556impl Widget for &InputBar<'_> {
557 fn render(self, area: Rect, buf: &mut Buffer) {
558 self.paint(area, buf);
559 }
560}
561
562impl InputBar<'_> {
563 pub(crate) fn paint(&self, area: Rect, buf: &mut Buffer) -> Option<Position> {
564 if area.is_empty() {
565 return None;
566 }
567 let styles = self.look();
568 if self.rule {
569 text::rule(buf, area, styles.rule);
570 }
571 let field = self.field(area)?;
572 if let Some(mut pen) = Pen::new(buf, field.line, field.line.x, field.line.y) {
573 let style = self.tone.style(&styles).add_modifier(Modifier::BOLD);
574 pen.clip(self.label, style, self.label_width(field.line.width));
575 }
576 let end = self.draw_field(buf, &field);
577 let y = field.line.y.saturating_add(1);
578 if let Some(hint) = self.hint
579 && let Some(mut pen) = Pen::new(buf, area, area.x, y)
580 {
581 pen.indented(hint, styles.muted);
582 return Some(Position::new(pen.x, y));
583 }
584 end
585 }
586}