requestty_ui/
char_input.rs

1use crate::{
2    backend::Backend,
3    events::{KeyCode, KeyEvent},
4    layout::Layout,
5};
6
7/// A widget that inputs a single character.
8///
9/// A `filter_map` function can optionally be provided to limit and change the characters allowed,
10/// similar to [`Iterator::filter_map`].
11///
12/// If multiple characters are received, they will overwrite the previous character. If a
13/// multi-character string is required, use [`StringInput`].
14///
15/// [`StringInput`]: crate::widgets::StringInput
16#[derive(Debug, Clone)]
17pub struct CharInput<F = super::widgets::FilterMapChar> {
18    value: Option<char>,
19    filter_map: F,
20}
21
22impl CharInput {
23    /// Creates a new [`CharInput`] which accepts all characters.
24    pub fn new() -> Self {
25        Self::with_filter_map(super::widgets::no_filter)
26    }
27}
28
29impl<F> CharInput<F>
30where
31    F: Fn(char) -> Option<char>,
32{
33    /// Creates a new [`CharInput`] which only accepts characters as per the `filter_map` function.
34    pub fn with_filter_map(filter_map: F) -> Self {
35        Self {
36            value: None,
37            filter_map,
38        }
39    }
40
41    /// The last inputted char (if any).
42    pub fn value(&self) -> Option<char> {
43        self.value
44    }
45
46    /// Sets the value to the given character.
47    pub fn set_value(&mut self, value: char) {
48        self.value = Some(value);
49    }
50
51    /// Clears the value.
52    pub fn clear_value(&mut self) {
53        self.value = None;
54    }
55}
56
57impl<F> super::Widget for CharInput<F>
58where
59    F: Fn(char) -> Option<char>,
60{
61    fn handle_key(&mut self, key: KeyEvent) -> bool {
62        match key.code {
63            KeyCode::Char(c) => {
64                if let Some(c) = (self.filter_map)(c) {
65                    self.value = Some(c);
66
67                    return true;
68                }
69
70                false
71            }
72
73            KeyCode::Backspace | KeyCode::Delete if self.value.is_some() => {
74                self.value = None;
75                true
76            }
77
78            _ => false,
79        }
80    }
81
82    fn render<B: Backend>(&mut self, layout: &mut Layout, backend: &mut B) -> std::io::Result<()> {
83        if let Some(value) = self.value {
84            layout.line_offset += char_width(value);
85
86            write!(backend, "{}", value)?;
87        }
88        Ok(())
89    }
90
91    fn height(&mut self, layout: &mut Layout) -> u16 {
92        layout.line_offset += self.value.map(char_width).unwrap_or(0);
93        1
94    }
95
96    /// Returns the position right after the character if any.
97    fn cursor_pos(&mut self, layout: Layout) -> (u16, u16) {
98        layout.offset_cursor((
99            layout.line_offset + self.value.map(char_width).unwrap_or(0),
100            0,
101        ))
102    }
103}
104
105impl Default for CharInput {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111fn char_width(c: char) -> u16 {
112    let mut buf = [0u8; 4];
113    textwrap::core::display_width(c.encode_utf8(&mut buf)) as u16
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::{backend::TestBackend, events::KeyModifiers, Widget};
120
121    #[test]
122    fn test_cursor_pos() {
123        let layout = Layout::new(0, (100, 20).into());
124        let mut input = CharInput::default();
125
126        assert_eq!(input.cursor_pos(layout), (0, 0));
127        assert_eq!(input.cursor_pos(layout.with_line_offset(5)), (5, 0));
128
129        assert_eq!(input.cursor_pos(layout.with_offset(0, 3)), (0, 3));
130        assert_eq!(
131            input.cursor_pos(layout.with_offset(0, 3).with_line_offset(5)),
132            (5, 3)
133        );
134
135        input.set_value('c');
136
137        assert_eq!(input.cursor_pos(layout), (1, 0));
138        assert_eq!(input.cursor_pos(layout.with_line_offset(5)), (6, 0));
139
140        assert_eq!(input.cursor_pos(layout.with_offset(0, 3)), (1, 3));
141        assert_eq!(
142            input.cursor_pos(layout.with_offset(0, 3).with_line_offset(5)),
143            (6, 3)
144        );
145
146        input.set_value('🔥');
147
148        assert_eq!(input.cursor_pos(layout), (2, 0));
149        assert_eq!(input.cursor_pos(layout.with_line_offset(5)), (7, 0));
150
151        assert_eq!(input.cursor_pos(layout.with_offset(0, 3)), (2, 3));
152        assert_eq!(
153            input.cursor_pos(layout.with_offset(0, 3).with_line_offset(5)),
154            (7, 3)
155        );
156    }
157
158    #[test]
159    fn test_handle_key() {
160        let modifiers = KeyModifiers::empty();
161
162        let mut input = CharInput::default();
163        assert!(input.handle_key(KeyEvent::new(KeyCode::Char('c'), modifiers)));
164        assert_eq!(input.value(), Some('c'));
165        assert!(!input.handle_key(KeyEvent::new(KeyCode::Tab, modifiers)));
166        assert!(input.handle_key(KeyEvent::new(KeyCode::Char('d'), modifiers)));
167        assert_eq!(input.value(), Some('d'));
168        assert!(input.handle_key(KeyEvent::new(KeyCode::Backspace, modifiers)));
169        assert_eq!(input.value(), None);
170        assert!(input.handle_key(KeyEvent::new(KeyCode::Char('c'), modifiers)));
171        assert_eq!(input.value(), Some('c'));
172        assert!(input.handle_key(KeyEvent::new(KeyCode::Delete, modifiers)));
173        assert_eq!(input.value(), None);
174        assert!(!input.handle_key(KeyEvent::new(KeyCode::Delete, modifiers)));
175        assert!(!input.handle_key(KeyEvent::new(KeyCode::Backspace, modifiers)));
176
177        let mut input =
178            CharInput::with_filter_map(|c| if c.is_uppercase() { None } else { Some(c) });
179        assert!(!input.handle_key(KeyEvent::new(KeyCode::Char('C'), modifiers)));
180        assert_eq!(input.value(), None);
181        assert!(input.handle_key(KeyEvent::new(KeyCode::Char('c'), modifiers)));
182        assert_eq!(input.value(), Some('c'));
183        assert!(!input.handle_key(KeyEvent::new(KeyCode::Char('C'), modifiers)));
184        assert_eq!(input.value(), Some('c'));
185    }
186
187    #[test]
188    fn test_render() {
189        let size = (30, 10).into();
190        let mut layout = Layout::new(0, size);
191        let mut input = CharInput::default();
192
193        let mut backend = TestBackend::new(size);
194        input.render(&mut layout, &mut backend).unwrap();
195        assert_eq!(backend, TestBackend::new(size));
196
197        assert_eq!(layout, Layout::new(0, size));
198
199        input.set_value('c');
200
201        let mut backend = TestBackend::new(size);
202        input.render(&mut layout, &mut backend).unwrap();
203
204        crate::assert_backend_snapshot!(backend);
205
206        assert_eq!(layout, Layout::new(0, size).with_line_offset(1));
207    }
208}