glyph_ui/view/
input_line.rs

1//! Single line text input
2
3use std::convert::TryFrom;
4
5use euclid::Size2D;
6use keyboard_types::Key;
7use unicode_segmentation::UnicodeSegmentation;
8
9use crate::{event::Event, unit::Cell, Printer, View as ViewTrait};
10
11// The view can be as short as 2 cells but 10 provides a reasonably editable
12// space without being too large
13const SOFT_MIN_LINE_WIDTH: u16 = 10;
14
15/// Shorthand for [`View::new()`]
16///
17/// [`View::new()`]: View::new
18pub fn new<F, M>(state: &mut State, on_submit: F) -> View<'_, F>
19where
20    F: Fn(String) -> M,
21{
22    View::new(state, on_submit)
23}
24
25/// The view itself
26pub struct View<'a, F> {
27    state: &'a mut State,
28    on_submit: F,
29    clear_on_submit: bool,
30    placeholder: Option<char>,
31    echo: Echo,
32}
33
34impl<'a, F, M> View<'a, F>
35where
36    F: Fn(String) -> M,
37{
38    pub fn new(state: &'a mut State, on_submit: F) -> Self {
39        Self {
40            state,
41            on_submit,
42            clear_on_submit: true,
43            placeholder: Some('_'),
44            echo: Default::default(),
45        }
46    }
47
48    /// Set a placeholder character
49    ///
50    /// The default is `Some('_')`.
51    pub fn placeholder(mut self, placeholder: Option<char>) -> Self {
52        self.placeholder = placeholder;
53
54        self
55    }
56
57    /// Change echo mode
58    ///
59    /// This can be set to [`Echo::Off`](Echo::Off) or
60    /// [`Echo::Faux`](Echo::Faux) for entering secrets such as passwords. The
61    /// default is [`Echo::On`](Echo::On), which is suitable for regular text.
62    pub fn echo(mut self, echo: Echo) -> Self {
63        self.echo = echo;
64
65        self
66    }
67
68    /// Change buffer behavior on submission
69    ///
70    /// By default, pressing `Enter` will clear the user input. If the text
71    /// should remain in the input line after the user submits it, call this
72    /// function with `false`.
73    pub fn clear_on_submit(mut self, enable: bool) -> Self {
74        self.clear_on_submit = enable;
75
76        self
77    }
78}
79
80/// Controls how the input line view echoes text
81pub enum Echo {
82    /// Normal text input
83    ///
84    /// This displays the text entered by the user as-is.
85    On,
86
87    /// Secret text input; less secure but more intuitive
88    ///
89    /// This mode echoes one character repeatedly (`*` is the most common
90    /// choice), so it's possible to see the length of the secret. However,
91    /// this is less likely to cause the user to think that their keyboard has
92    /// suddenly stopped working.
93    Faux(char),
94
95    /// Secret text input; more secure but less intuitive
96    ///
97    /// This mode echoes no input at all, a side effect of which is that it's
98    /// impossible to tell the length of the password by looking at the screen.
99    Off,
100}
101
102impl Default for Echo {
103    fn default() -> Self {
104        Self::On
105    }
106}
107
108/// Persistent state for this view
109#[derive(Default)]
110pub struct State {
111    input: String,
112}
113
114impl State {
115    /// Returns the current text the user has entered
116    pub fn content(&self) -> &str {
117        &self.input
118    }
119}
120
121impl<T, M, F> ViewTrait<T, M> for View<'_, F>
122where
123    F: Fn(String) -> M,
124    M: 'static,
125{
126    fn draw(&self, printer: &Printer, focused: bool) {
127        if let Some(c) = self.placeholder {
128            let line = std::iter::repeat(c)
129                .take(printer.size().width.into())
130                .fold(String::new(), |mut s, c| {
131                    s.push(c);
132
133                    s
134                });
135
136            printer.print(&line, (0, 0)).unwrap();
137        }
138
139        // Show only the ending of the input if it's too long to fit inside the
140        // printable area. The +1 gives us a cell for the cursor at the end.
141        let width = printer.size().width;
142        let start = self
143            .state
144            .input
145            .len()
146            .saturating_add(1)
147            .saturating_sub(width.into());
148
149        // On and Faux will simply overwrite the placeholders if they were
150        // printed and the input has contents
151        match self.echo {
152            // Show the actual text
153            Echo::On => {
154                printer.print(&self.state.input[start..], (0, 0)).unwrap();
155            }
156
157            // Generate/show a line of characters equal to the length of the
158            // actual text
159            Echo::Faux(c) => {
160                let line: String =
161                    std::iter::repeat(c).take(self.state.input.len()).collect();
162
163                printer.print(&line[start..], (0, 0)).unwrap();
164            }
165
166            // Obvious
167            Echo::Off => (),
168        }
169
170        // Only mess with the cursor location if we're the focused view
171        if focused {
172            match self.echo {
173                // Calculate the cursor position based on input length and show
174                // it
175                Echo::On | Echo::Faux(_) => {
176                    let cursor_x =
177                        u16::try_from(self.state.input.graphemes(true).count())
178                            .unwrap()
179                            .min(width.saturating_sub(1));
180
181                    printer.show_cursor_at((cursor_x, 0)).unwrap();
182                }
183
184                // The cursor will only ever be at the very beginning
185                Echo::Off => {
186                    printer.show_cursor_at((0, 0)).unwrap();
187                }
188            }
189        }
190    }
191
192    fn width(&self) -> Size2D<u16, Cell> {
193        (SOFT_MIN_LINE_WIDTH, 1).into()
194    }
195
196    fn height(&self) -> Size2D<u16, Cell> {
197        (SOFT_MIN_LINE_WIDTH, 1).into()
198    }
199
200    fn layout(&self, constraint: Size2D<u16, Cell>) -> Size2D<u16, Cell> {
201        (constraint.width, 1).into()
202    }
203
204    fn event(
205        &mut self,
206        event: &Event<T>,
207        focused: bool,
208    ) -> Box<dyn Iterator<Item = M>> {
209        if !focused {
210            return Box::new(std::iter::empty());
211        }
212
213        let message = if let Event::Key(k) = event {
214            match &k.key {
215                Key::Character(c) => {
216                    self.state.input.push_str(c);
217
218                    None
219                }
220                Key::Enter => {
221                    let line = self.state.input.clone();
222                    if self.clear_on_submit {
223                        self.state.input.clear();
224                    }
225                    Some((self.on_submit)(line))
226                }
227                Key::Backspace => {
228                    self.state.input.pop();
229
230                    None
231                }
232                // TODO handle arrow keys and delete
233                _ => None,
234            }
235        } else {
236            None
237        };
238
239        if let Some(message) = message {
240            Box::new(std::iter::once(message))
241        } else {
242            Box::new(std::iter::empty())
243        }
244    }
245
246    fn interactive(&self) -> bool {
247        true
248    }
249}