Skip to main content

rustic_rs/commands/tui/widgets/
text_input.rs

1use super::{Draw, Event, Frame, KeyCode, KeyEvent, ProcessEvent, Rect, SizedWidget, Style};
2
3use crossterm::event::KeyModifiers;
4use ratatui_textarea::{CursorMove, TextArea};
5
6pub struct TextInput {
7    textarea: TextArea<'static>,
8    lines: u16,
9    changeable: bool,
10}
11
12pub enum TextInputResult {
13    Cancel,
14    Input(String),
15    None,
16}
17
18impl TextInput {
19    pub fn new(text: Option<&str>, initial: &str, lines: u16, changeable: bool) -> Self {
20        let mut textarea = TextArea::default();
21        textarea.set_style(Style::default());
22        if let Some(text) = text {
23            textarea.set_placeholder_text(text);
24        }
25        _ = textarea.insert_str(initial);
26        if !changeable {
27            textarea.move_cursor(CursorMove::Top);
28            textarea.move_cursor(CursorMove::Head);
29        }
30        Self {
31            textarea,
32            lines,
33            changeable,
34        }
35    }
36}
37
38impl SizedWidget for TextInput {
39    fn height(&self) -> Option<u16> {
40        Some(self.lines)
41    }
42}
43
44impl Draw for TextInput {
45    fn draw(&mut self, area: Rect, f: &mut Frame<'_>) {
46        f.render_widget(&self.textarea, area);
47    }
48}
49
50impl ProcessEvent for TextInput {
51    type Result = TextInputResult;
52    fn input(&mut self, event: Event) -> TextInputResult {
53        if let Event::Key(key) = event {
54            let KeyEvent {
55                code, modifiers, ..
56            } = key;
57            if self.changeable {
58                match (code, modifiers) {
59                    (KeyCode::Esc, _) => return TextInputResult::Cancel,
60                    (KeyCode::Enter, _) if self.lines == 1 => {
61                        return TextInputResult::Input(self.textarea.lines().join("\n"));
62                    }
63                    (KeyCode::Char('s'), KeyModifiers::CONTROL) => {
64                        return TextInputResult::Input(self.textarea.lines().join("\n"));
65                    }
66                    _ => {
67                        _ = self.textarea.input(event);
68                    }
69                }
70            } else {
71                match (code, modifiers) {
72                    (KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q' | 'x'), _) => {
73                        return TextInputResult::Cancel;
74                    }
75                    (KeyCode::Home, _) => {
76                        self.textarea.move_cursor(CursorMove::Top);
77                    }
78                    (KeyCode::End, _) => {
79                        self.textarea.move_cursor(CursorMove::Bottom);
80                    }
81                    (
82                        KeyCode::PageDown
83                        | KeyCode::PageUp
84                        | KeyCode::Up
85                        | KeyCode::Down
86                        | KeyCode::Left
87                        | KeyCode::Right,
88                        _,
89                    ) => {
90                        _ = self.textarea.input(key);
91                    }
92                    _ => {}
93                }
94            }
95        }
96        TextInputResult::None
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use crossterm::event::{KeyEvent, KeyModifiers};
103
104    use super::*;
105
106    fn key(code: KeyCode) -> Event {
107        Event::Key(KeyEvent::new(code, KeyModifiers::NONE))
108    }
109
110    #[test]
111    fn read_only_text_can_be_scrolled_horizontally() {
112        let mut input = TextInput::new(None, "a long line", 1, false);
113
114        assert_eq!(input.textarea.cursor(), (0, 0));
115
116        _ = input.input(key(KeyCode::Right));
117        _ = input.input(key(KeyCode::Right));
118        assert_eq!(input.textarea.cursor(), (0, 2));
119
120        _ = input.input(key(KeyCode::Left));
121        assert_eq!(input.textarea.cursor(), (0, 1));
122    }
123}