entrust_dialog/input/
widget.rs1use 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 assert_eq!(Style::default(), spans[0].style);
198 assert_eq!("So it is, and so it will be", spans[0].content.as_ref());
199 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 assert_eq!(Style::default(), spans[2].style);
207 assert_eq!("", spans[2].content.as_ref());
208 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}