Skip to main content

entrust_dialog/input/
widget.rs

1use crate::input::InputDialog;
2use ratatui::buffer::Buffer;
3use ratatui::layout::{Constraint, Layout, Rect};
4use ratatui::prelude::{Line, Span, Widget};
5use ratatui::style::{Color, Style};
6use ratatui::widgets::Paragraph;
7
8const NEWLINE_PLACEHOLDER: char = '␤';
9const CARRIAGE_RETURN_PLACEHOLDER: char = '␍';
10
11impl<'p, 'c> Widget for &mut InputDialog<'p, 'c> {
12    fn render(self, area: Rect, buf: &mut Buffer)
13    where
14        Self: Sized,
15    {
16        let prompt = self.prompt_with_confirmation();
17
18        let validation_message = self.validation_message();
19
20        let (header_area, input_area, validation_area) = {
21            let header_height = if prompt.header.spans.is_empty() { 0 } else { 1 };
22            let validation_height = validation_message
23                .as_ref()
24                .map(|m| m.lines().count())
25                .unwrap_or(0);
26            let rects = Layout::vertical(vec![
27                Constraint::Length(header_height),
28                Constraint::Length(1),
29                Constraint::Length(validation_height as u16),
30            ])
31            .split(area);
32            (rects[0], rects[1], rects[2])
33        };
34
35        let header_prompt = prompt.header.patch_style(self.theme.header_style);
36        Paragraph::new(header_prompt).render(header_area, buf);
37
38        let input_spans = self.input_spans(prompt.inline);
39        Paragraph::new(Line::from(input_spans)).render(input_area, buf);
40
41        if let Some(message) = validation_message {
42            Paragraph::new(Line::styled(message.as_ref(), Style::from(Color::LightRed)))
43                .render(validation_area, buf);
44        }
45    }
46}
47
48impl<'p, 'c> InputDialog<'p, 'c> {
49    fn input_spans<'l>(&'l self, inline_prompt: Line<'l>) -> Vec<Span<'l>> {
50        let mut spans = self.inline_prompt_spans(inline_prompt);
51        let completion = self.get_end_completion().unwrap_or("");
52        let cursor_style = self.cursor.current_style(&self.theme);
53        if self.mask.active {
54            spans.push(String::from_iter(vec![self.mask.char; self.content.len()]).into())
55        } else if self.content.is_empty() {
56            if !self.placeholder.is_empty() {
57                spans.push(Span::styled(self.placeholder, self.theme.placeholder_style))
58            } else if !completion.is_empty() {
59                spans.push(Span::styled(
60                    &completion[0..1],
61                    cursor_style.patch(self.theme.completion_style),
62                ));
63                if completion.len() > 1 {
64                    spans.push(Span::styled(&completion[1..], self.theme.completion_style));
65                }
66            } else {
67                spans.push(Span::styled(" ", cursor_style));
68            }
69        } else {
70            let (before_cursor, from_cursor) = self.content.split_at(self.cursor.index());
71            let completion_first_char = completion.chars().next();
72            let (at_cursor, is_cursor_at_completion) = {
73                if from_cursor.is_empty() {
74                    (completion_first_char.unwrap_or(' '), true)
75                } else {
76                    let at = from_cursor.iter().next().unwrap();
77                    let at = *replace_newline(at);
78                    (at, false)
79                }
80            };
81            let after_cursor: String = from_cursor.iter().skip(1).map(replace_newline).collect();
82            spans.push(Span::raw(
83                before_cursor
84                    .iter()
85                    .map(replace_newline)
86                    .collect::<String>(),
87            ));
88            let at_cursor_style = if is_cursor_at_completion {
89                cursor_style.patch(self.theme.completion_style)
90            } else {
91                cursor_style
92            };
93            spans.push(Span::styled(at_cursor.to_string(), at_cursor_style));
94            spans.push(Span::raw(after_cursor));
95            if !completion.is_empty() {
96                let remaining_completion = if is_cursor_at_completion {
97                    &completion[1..]
98                } else {
99                    completion
100                };
101                spans.push(Span::styled(
102                    remaining_completion,
103                    self.theme.completion_style,
104                ))
105            }
106        };
107        spans
108    }
109
110    fn inline_prompt_spans<'s>(&'s self, inline_prompt: Line<'s>) -> Vec<Span<'s>> {
111        let prompt_line_style = inline_prompt.style;
112        inline_prompt
113            .spans
114            .into_iter()
115            .map(|s| {
116                let span_style = s.style;
117                s.style(
118                    self.theme
119                        .prompt_style
120                        .patch(prompt_line_style)
121                        .patch(span_style),
122                )
123            })
124            .collect()
125    }
126}
127
128fn replace_newline(char: &char) -> &char {
129    match char {
130        '\n' => &NEWLINE_PLACEHOLDER,
131        '\r' => &CARRIAGE_RETURN_PLACEHOLDER,
132        c => c,
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::dialog::Dialog;
140    use crate::input::prompt::Prompt;
141    use crate::theme::Theme;
142    use ratatui::prelude::*;
143
144    #[test]
145    fn test_input_line_prompt() {
146        let dialog = InputDialog::default()
147            .with_content("content")
148            .with_prompt(Prompt::inline("inline".bold()));
149        let spans = dialog.input_spans(dialog.prompt_with_confirmation().inline);
150        assert_eq!(4, spans.len());
151        let inline_prompt_style = Theme::default().prompt_style.patch(Style::new().bold());
152        assert_eq!(inline_prompt_style, spans[0].style);
153        assert_eq!("inline", spans[0].content.as_ref());
154
155        assert_eq!(Style::default(), spans[1].style);
156        assert_eq!("content", spans[1].content.as_ref());
157    }
158
159    #[test]
160    fn test_input_line_placeholder() {
161        let dialog = InputDialog::default().with_placeholder("placeholder");
162        let spans = dialog.input_spans(dialog.prompt_with_confirmation().inline);
163        assert_eq!(1, spans.len());
164        assert_eq!(Theme::default().placeholder_style, spans[0].style);
165        assert_eq!("placeholder", spans[0].content);
166    }
167
168    #[test]
169    fn test_input_line_cursor() {
170        let mut dialog = InputDialog::default();
171        {
172            let spans = dialog.input_spans(dialog.prompt_with_confirmation().inline);
173            assert_eq!(1, spans.len());
174            assert_eq!(Theme::default().cursor_on_style, spans[0].style);
175            assert_eq!(" ", spans[0].content);
176        }
177        dialog.tick();
178        {
179            let spans = dialog.input_spans(dialog.prompt_with_confirmation().inline);
180            assert_eq!(1, spans.len());
181            assert_eq!(Theme::default().cursor_off_style, spans[0].style);
182            assert_eq!(" ", spans[0].content);
183        }
184    }
185
186    #[test]
187    fn test_input_line_completion() {
188        let theme = Theme::default();
189        let dialog = InputDialog::default()
190            .with_content("So it is, and so it will be")
191            .with_completions(vec![
192                "So it is, and so it will be, for so it has been, time out of mind".into(),
193            ]);
194        let spans = dialog.input_spans(dialog.prompt_with_confirmation().inline);
195        assert_eq!(4, spans.len());
196        // before cursor
197        assert_eq!(Style::default(), spans[0].style);
198        assert_eq!("So it is, and so it will be", spans[0].content.as_ref());
199        // cursor
200        assert_eq!(
201            theme.cursor_on_style.patch(theme.completion_style),
202            spans[1].style
203        );
204        assert_eq!(",", spans[1].content.as_ref());
205        // after cursor
206        assert_eq!(Style::default(), spans[2].style);
207        assert_eq!("", spans[2].content.as_ref());
208        // remaining completion
209        assert_eq!(theme.completion_style, spans[3].style);
210        assert_eq!(
211            " for so it has been, time out of mind",
212            spans[3].content.as_ref()
213        );
214    }
215}