Skip to main content

pito_footer/
input.rs

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}