tui_realm_stdlib/components/
input.rs

1//! ## Input
2//!
3//! `Input` represents a read-write input field. This component supports different input types, input length
4//! and handles input events related to cursor position, backspace, canc, ...
5
6use super::props::{INPUT_INVALID_STYLE, INPUT_PLACEHOLDER, INPUT_PLACEHOLDER_STYLE};
7use crate::utils::calc_utf8_cursor_position;
8use tuirealm::command::{Cmd, CmdResult, Direction, Position};
9use tuirealm::props::{
10    Alignment, AttrValue, Attribute, Borders, Color, InputType, Props, Style, TextModifiers,
11};
12use tuirealm::ratatui::{layout::Rect, widgets::Paragraph};
13use tuirealm::{Frame, MockComponent, State, StateValue};
14
15// -- states
16
17#[derive(Default)]
18pub struct InputStates {
19    pub input: Vec<char>, // Current input
20    pub cursor: usize,    // Input position
21}
22
23impl InputStates {
24    /// ### append
25    ///
26    /// Append, if possible according to input type, the character to the input vec
27    pub fn append(&mut self, ch: char, itype: &InputType, max_len: Option<usize>) {
28        // Check if max length has been reached
29        if self.input.len() < max_len.unwrap_or(usize::MAX) {
30            // Check whether can push
31            if itype.char_valid(self.input.iter().collect::<String>().as_str(), ch) {
32                self.input.insert(self.cursor, ch);
33                self.incr_cursor();
34            }
35        }
36    }
37
38    /// ### backspace
39    ///
40    /// Delete element at cursor -1; then decrement cursor by 1
41    pub fn backspace(&mut self) {
42        if self.cursor > 0 && !self.input.is_empty() {
43            self.input.remove(self.cursor - 1);
44            // Decrement cursor
45            self.cursor -= 1;
46        }
47    }
48
49    /// ### delete
50    ///
51    /// Delete element at cursor
52    pub fn delete(&mut self) {
53        if self.cursor < self.input.len() {
54            self.input.remove(self.cursor);
55        }
56    }
57
58    /// ### incr_cursor
59    ///
60    /// Increment cursor value by one if possible
61    pub fn incr_cursor(&mut self) {
62        if self.cursor < self.input.len() {
63            self.cursor += 1;
64        }
65    }
66
67    /// ### cursoro_at_begin
68    ///
69    /// Place cursor at the begin of the input
70    pub fn cursor_at_begin(&mut self) {
71        self.cursor = 0;
72    }
73
74    /// ### cursor_at_end
75    ///
76    /// Place cursor at the end of the input
77    pub fn cursor_at_end(&mut self) {
78        self.cursor = self.input.len();
79    }
80
81    /// ### decr_cursor
82    ///
83    /// Decrement cursor value by one if possible
84    pub fn decr_cursor(&mut self) {
85        if self.cursor > 0 {
86            self.cursor -= 1;
87        }
88    }
89
90    /// ### render_value
91    ///
92    /// Get value as string to render
93    pub fn render_value(&self, itype: InputType) -> String {
94        self.render_value_chars(itype).iter().collect::<String>()
95    }
96
97    /// ### render_value_chars
98    ///
99    /// Render value as a vec of chars
100    pub fn render_value_chars(&self, itype: InputType) -> Vec<char> {
101        match itype {
102            InputType::Password(ch) | InputType::CustomPassword(ch, _, _) => {
103                (0..self.input.len()).map(|_| ch).collect()
104            }
105            _ => self.input.clone(),
106        }
107    }
108
109    /// ### get_value
110    ///
111    /// Get value as string
112    pub fn get_value(&self) -> String {
113        self.input.iter().collect()
114    }
115}
116
117// -- Component
118
119/// ## Input
120///
121/// Input list component
122#[derive(Default)]
123pub struct Input {
124    props: Props,
125    pub states: InputStates,
126}
127
128impl Input {
129    pub fn foreground(mut self, fg: Color) -> Self {
130        self.attr(Attribute::Foreground, AttrValue::Color(fg));
131        self
132    }
133
134    pub fn background(mut self, bg: Color) -> Self {
135        self.attr(Attribute::Background, AttrValue::Color(bg));
136        self
137    }
138
139    pub fn inactive(mut self, s: Style) -> Self {
140        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
141        self
142    }
143
144    pub fn borders(mut self, b: Borders) -> Self {
145        self.attr(Attribute::Borders, AttrValue::Borders(b));
146        self
147    }
148
149    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
150        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
151        self
152    }
153
154    pub fn input_type(mut self, itype: InputType) -> Self {
155        self.attr(Attribute::InputType, AttrValue::InputType(itype));
156        self
157    }
158
159    pub fn input_len(mut self, ilen: usize) -> Self {
160        self.attr(Attribute::InputLength, AttrValue::Length(ilen));
161        self
162    }
163
164    pub fn value<S: Into<String>>(mut self, s: S) -> Self {
165        self.attr(Attribute::Value, AttrValue::String(s.into()));
166        self
167    }
168
169    pub fn invalid_style(mut self, s: Style) -> Self {
170        self.attr(Attribute::Custom(INPUT_INVALID_STYLE), AttrValue::Style(s));
171        self
172    }
173
174    pub fn placeholder<S: Into<String>>(mut self, placeholder: S, style: Style) -> Self {
175        self.attr(
176            Attribute::Custom(INPUT_PLACEHOLDER),
177            AttrValue::String(placeholder.into()),
178        );
179        self.attr(
180            Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
181            AttrValue::Style(style),
182        );
183        self
184    }
185
186    fn get_input_len(&self) -> Option<usize> {
187        self.props
188            .get(Attribute::InputLength)
189            .map(|x| x.unwrap_length())
190    }
191
192    fn get_input_type(&self) -> InputType {
193        self.props
194            .get_or(Attribute::InputType, AttrValue::InputType(InputType::Text))
195            .unwrap_input_type()
196    }
197
198    /// ### is_valid
199    ///
200    /// Checks whether current input is valid
201    fn is_valid(&self) -> bool {
202        let value = self.states.get_value();
203        self.get_input_type().validate(value.as_str())
204    }
205}
206
207impl MockComponent for Input {
208    fn view(&mut self, render: &mut Frame, area: Rect) {
209        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
210            let mut foreground = self
211                .props
212                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
213                .unwrap_color();
214            let mut background = self
215                .props
216                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
217                .unwrap_color();
218            let modifiers = self
219                .props
220                .get_or(
221                    Attribute::TextProps,
222                    AttrValue::TextModifiers(TextModifiers::empty()),
223                )
224                .unwrap_text_modifiers();
225            let title = self
226                .props
227                .get_or(
228                    Attribute::Title,
229                    AttrValue::Title((String::default(), Alignment::Center)),
230                )
231                .unwrap_title();
232            let borders = self
233                .props
234                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
235                .unwrap_borders();
236            let focus = self
237                .props
238                .get_or(Attribute::Focus, AttrValue::Flag(false))
239                .unwrap_flag();
240            let inactive_style = self
241                .props
242                .get(Attribute::FocusStyle)
243                .map(|x| x.unwrap_style());
244            let itype = self.get_input_type();
245            let mut block = crate::utils::get_block(borders, Some(title), focus, inactive_style);
246            // Apply invalid style
247            if focus && !self.is_valid() {
248                if let Some(style) = self
249                    .props
250                    .get(Attribute::Custom(INPUT_INVALID_STYLE))
251                    .map(|x| x.unwrap_style())
252                {
253                    let borders = self
254                        .props
255                        .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
256                        .unwrap_borders()
257                        .color(style.fg.unwrap_or(Color::Reset));
258                    let title = self
259                        .props
260                        .get_or(
261                            Attribute::Title,
262                            AttrValue::Title((String::default(), Alignment::Center)),
263                        )
264                        .unwrap_title();
265                    block = crate::utils::get_block(borders, Some(title), focus, None);
266                    foreground = style.fg.unwrap_or(Color::Reset);
267                    background = style.bg.unwrap_or(Color::Reset);
268                }
269            }
270            let text_to_display = self.states.render_value(self.get_input_type());
271            let show_placeholder = text_to_display.is_empty();
272            // Choose whether to show placeholder; if placeholder is unset, show nothing
273            let text_to_display = match show_placeholder {
274                true => self
275                    .props
276                    .get_or(
277                        Attribute::Custom(INPUT_PLACEHOLDER),
278                        AttrValue::String(String::new()),
279                    )
280                    .unwrap_string(),
281                false => text_to_display,
282            };
283            // Choose paragraph style based on whether is valid or not and if has focus and if should show placeholder
284            let paragraph_style = match focus {
285                true => Style::default()
286                    .fg(foreground)
287                    .bg(background)
288                    .add_modifier(modifiers),
289                false => inactive_style.unwrap_or_default(),
290            };
291            let paragraph_style = match show_placeholder {
292                true => self
293                    .props
294                    .get_or(
295                        Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
296                        AttrValue::Style(paragraph_style),
297                    )
298                    .unwrap_style(),
299                false => paragraph_style,
300            };
301            // Create widget
302            let block_inner_area = block.inner(area);
303            let p: Paragraph = Paragraph::new(text_to_display)
304                .style(paragraph_style)
305                .block(block);
306            render.render_widget(p, area);
307            // Set cursor, if focus
308            if focus {
309                let x: u16 = block_inner_area.x
310                    + calc_utf8_cursor_position(
311                        &self.states.render_value_chars(itype)[0..self.states.cursor],
312                    );
313                render
314                    .set_cursor_position(tuirealm::ratatui::prelude::Position { x, y: area.y + 1 });
315            }
316        }
317    }
318
319    fn query(&self, attr: Attribute) -> Option<AttrValue> {
320        self.props.get(attr)
321    }
322
323    fn attr(&mut self, attr: Attribute, value: AttrValue) {
324        let sanitize_input = matches!(
325            attr,
326            Attribute::InputLength | Attribute::InputType | Attribute::Value
327        );
328        // Check if new input
329        let new_input = match attr {
330            Attribute::Value => Some(value.clone().unwrap_string()),
331            _ => None,
332        };
333        self.props.set(attr, value);
334        if sanitize_input {
335            let input = match new_input {
336                None => self.states.input.clone(),
337                Some(v) => v.chars().collect(),
338            };
339            self.states.input = Vec::new();
340            self.states.cursor = 0;
341            let itype = self.get_input_type();
342            let max_len = self.get_input_len();
343            for ch in input.into_iter() {
344                self.states.append(ch, &itype, max_len);
345            }
346        }
347    }
348
349    fn state(&self) -> State {
350        // Validate input
351        if self.is_valid() {
352            State::One(StateValue::String(self.states.get_value()))
353        } else {
354            State::None
355        }
356    }
357
358    fn perform(&mut self, cmd: Cmd) -> CmdResult {
359        match cmd {
360            Cmd::Delete => {
361                // Backspace and None
362                let prev_input = self.states.input.clone();
363                self.states.backspace();
364                if prev_input != self.states.input {
365                    CmdResult::Changed(self.state())
366                } else {
367                    CmdResult::None
368                }
369            }
370            Cmd::Cancel => {
371                // Delete and None
372                let prev_input = self.states.input.clone();
373                self.states.delete();
374                if prev_input != self.states.input {
375                    CmdResult::Changed(self.state())
376                } else {
377                    CmdResult::None
378                }
379            }
380            Cmd::Submit => CmdResult::Submit(self.state()),
381            Cmd::Move(Direction::Left) => {
382                self.states.decr_cursor();
383                CmdResult::None
384            }
385            Cmd::Move(Direction::Right) => {
386                self.states.incr_cursor();
387                CmdResult::None
388            }
389            Cmd::GoTo(Position::Begin) => {
390                self.states.cursor_at_begin();
391                CmdResult::None
392            }
393            Cmd::GoTo(Position::End) => {
394                self.states.cursor_at_end();
395                CmdResult::None
396            }
397            Cmd::Type(ch) => {
398                // Push char to input
399                let prev_input = self.states.input.clone();
400                self.states
401                    .append(ch, &self.get_input_type(), self.get_input_len());
402                // Message on change
403                if prev_input != self.states.input {
404                    CmdResult::Changed(self.state())
405                } else {
406                    CmdResult::None
407                }
408            }
409            _ => CmdResult::None,
410        }
411    }
412}
413
414#[cfg(test)]
415mod tests {
416
417    use super::*;
418
419    use pretty_assertions::assert_eq;
420
421    #[test]
422    fn test_components_input_states() {
423        let mut states: InputStates = InputStates::default();
424        states.append('a', &InputType::Text, Some(3));
425        assert_eq!(states.input, vec!['a']);
426        states.append('b', &InputType::Text, Some(3));
427        assert_eq!(states.input, vec!['a', 'b']);
428        states.append('c', &InputType::Text, Some(3));
429        assert_eq!(states.input, vec!['a', 'b', 'c']);
430        // Reached length
431        states.append('d', &InputType::Text, Some(3));
432        assert_eq!(states.input, vec!['a', 'b', 'c']);
433        // Push char to numbers
434        states.append('d', &InputType::Number, None);
435        assert_eq!(states.input, vec!['a', 'b', 'c']);
436        // move cursor
437        // decr cursor
438        states.decr_cursor();
439        assert_eq!(states.cursor, 2);
440        states.cursor = 1;
441        states.decr_cursor();
442        assert_eq!(states.cursor, 0);
443        states.decr_cursor();
444        assert_eq!(states.cursor, 0);
445        // Incr
446        states.incr_cursor();
447        assert_eq!(states.cursor, 1);
448        states.incr_cursor();
449        assert_eq!(states.cursor, 2);
450        states.incr_cursor();
451        assert_eq!(states.cursor, 3);
452        // Render value
453        assert_eq!(states.render_value(InputType::Text).as_str(), "abc");
454        assert_eq!(
455            states.render_value(InputType::Password('*')).as_str(),
456            "***"
457        );
458    }
459
460    #[test]
461    fn test_components_input_text() {
462        // Instantiate Input with value
463        let mut component: Input = Input::default()
464            .background(Color::Yellow)
465            .borders(Borders::default())
466            .foreground(Color::Cyan)
467            .inactive(Style::default())
468            .input_len(5)
469            .input_type(InputType::Text)
470            .title("pippo", Alignment::Center)
471            .value("home");
472        // Verify initial state
473        assert_eq!(component.states.cursor, 4);
474        assert_eq!(component.states.input.len(), 4);
475        // Get value
476        assert_eq!(
477            component.state(),
478            State::One(StateValue::String(String::from("home")))
479        );
480        // Character
481        assert_eq!(
482            component.perform(Cmd::Type('/')),
483            CmdResult::Changed(State::One(StateValue::String(String::from("home/"))))
484        );
485        assert_eq!(
486            component.state(),
487            State::One(StateValue::String(String::from("home/")))
488        );
489        assert_eq!(component.states.cursor, 5);
490        // Verify max length (shouldn't push any character)
491        assert_eq!(component.perform(Cmd::Type('a')), CmdResult::None);
492        assert_eq!(
493            component.state(),
494            State::One(StateValue::String(String::from("home/")))
495        );
496        assert_eq!(component.states.cursor, 5);
497        // Submit
498        assert_eq!(
499            component.perform(Cmd::Submit),
500            CmdResult::Submit(State::One(StateValue::String(String::from("home/"))))
501        );
502        // Backspace
503        assert_eq!(
504            component.perform(Cmd::Delete),
505            CmdResult::Changed(State::One(StateValue::String(String::from("home"))))
506        );
507        assert_eq!(
508            component.state(),
509            State::One(StateValue::String(String::from("home")))
510        );
511        assert_eq!(component.states.cursor, 4);
512        // Check backspace at 0
513        component.states.input = vec!['h'];
514        component.states.cursor = 1;
515        assert_eq!(
516            component.perform(Cmd::Delete),
517            CmdResult::Changed(State::One(StateValue::String(String::from(""))))
518        );
519        assert_eq!(
520            component.state(),
521            State::One(StateValue::String(String::from("")))
522        );
523        assert_eq!(component.states.cursor, 0);
524        // Another one...
525        assert_eq!(component.perform(Cmd::Delete), CmdResult::None);
526        assert_eq!(
527            component.state(),
528            State::One(StateValue::String(String::from("")))
529        );
530        assert_eq!(component.states.cursor, 0);
531        // See del behaviour here
532        assert_eq!(component.perform(Cmd::Cancel), CmdResult::None);
533        assert_eq!(
534            component.state(),
535            State::One(StateValue::String(String::from("")))
536        );
537        assert_eq!(component.states.cursor, 0);
538        // Check del behaviour
539        component.states.input = vec!['h', 'e'];
540        component.states.cursor = 1;
541        assert_eq!(
542            component.perform(Cmd::Cancel),
543            CmdResult::Changed(State::One(StateValue::String(String::from("h"))))
544        );
545        assert_eq!(
546            component.state(),
547            State::One(StateValue::String(String::from("h")))
548        );
549        assert_eq!(component.states.cursor, 1);
550        // Another one (should do nothing)
551        assert_eq!(component.perform(Cmd::Cancel), CmdResult::None);
552        assert_eq!(
553            component.state(),
554            State::One(StateValue::String(String::from("h")))
555        );
556        assert_eq!(component.states.cursor, 1);
557        // Move cursor right
558        component.states.input = vec!['h', 'e', 'l', 'l', 'o'];
559        // Update length to 16
560        component.attr(Attribute::InputLength, AttrValue::Length(16));
561        component.states.cursor = 1;
562        assert_eq!(
563            component.perform(Cmd::Move(Direction::Right)), // between 'e' and 'l'
564            CmdResult::None
565        );
566        assert_eq!(component.states.cursor, 2);
567        // Put a character here
568        assert_eq!(
569            component.perform(Cmd::Type('a')),
570            CmdResult::Changed(State::One(StateValue::String(String::from("heallo"))))
571        );
572        assert_eq!(
573            component.state(),
574            State::One(StateValue::String(String::from("heallo")))
575        );
576        assert_eq!(component.states.cursor, 3);
577        // Move left
578        assert_eq!(
579            component.perform(Cmd::Move(Direction::Left)),
580            CmdResult::None
581        );
582        assert_eq!(component.states.cursor, 2);
583        // Go at the end
584        component.states.cursor = 6;
585        // Move right
586        assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
587        assert_eq!(component.states.cursor, 6);
588        // Move left
589        assert_eq!(
590            component.perform(Cmd::Move(Direction::Left)),
591            CmdResult::None
592        );
593        assert_eq!(component.states.cursor, 5);
594        // Go at the beginning
595        component.states.cursor = 0;
596        assert_eq!(
597            component.perform(Cmd::Move(Direction::Left)),
598            CmdResult::None
599        );
600        //assert_eq!(component.render().unwrap().cursor, 0); // Should stay
601        assert_eq!(component.states.cursor, 0);
602        // End - begin
603        assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
604        assert_eq!(component.states.cursor, 6);
605        assert_eq!(
606            component.perform(Cmd::GoTo(Position::Begin)),
607            CmdResult::None
608        );
609        assert_eq!(component.states.cursor, 0);
610        // Update value
611        component.attr(Attribute::Value, AttrValue::String("new-value".to_string()));
612        assert_eq!(
613            component.state(),
614            State::One(StateValue::String(String::from("new-value")))
615        );
616        // Invalidate input type
617        component.attr(
618            Attribute::InputType,
619            AttrValue::InputType(InputType::Number),
620        );
621        assert_eq!(component.state(), State::None);
622    }
623}