use crate::input::InputDialog;
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::prelude::{Line, Span, Widget};
use ratatui::style::{Color, Style};
use ratatui::widgets::Paragraph;
const NEWLINE_PLACEHOLDER: char = '';
const CARRIAGE_RETURN_PLACEHOLDER: char = '␍';
impl<'p, 'c> Widget for &mut InputDialog<'p, 'c> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let prompt = self.prompt_with_confirmation();
let validation_message = self.validation_message();
let (header_area, input_area, validation_area) = {
let header_height = if prompt.header.spans.is_empty() { 0 } else { 1 };
let validation_height = validation_message
.as_ref()
.map(|m| m.lines().count())
.unwrap_or(0);
let rects = Layout::vertical(vec![
Constraint::Length(header_height),
Constraint::Length(1),
Constraint::Length(validation_height as u16),
])
.split(area);
(rects[0], rects[1], rects[2])
};
let header_prompt = prompt.header.patch_style(self.theme.header_style);
Paragraph::new(header_prompt).render(header_area, buf);
let input_spans = self.input_spans(prompt.inline);
Paragraph::new(Line::from(input_spans)).render(input_area, buf);
if let Some(message) = validation_message {
Paragraph::new(Line::styled(message.as_ref(), Style::from(Color::LightRed)))
.render(validation_area, buf);
}
}
}
impl<'p, 'c> InputDialog<'p, 'c> {
fn input_spans<'l>(&'l self, inline_prompt: Line<'l>) -> Vec<Span<'l>> {
let mut spans = self.inline_prompt_spans(inline_prompt);
let completion = self.get_end_completion().unwrap_or("");
let cursor_style = self.cursor.current_style(&self.theme);
if self.mask.active {
spans.push(String::from_iter(vec![self.mask.char; self.content.len()]).into())
} else if self.content.is_empty() {
if !self.placeholder.is_empty() {
spans.push(Span::styled(self.placeholder, self.theme.placeholder_style))
} else if !completion.is_empty() {
spans.push(Span::styled(
&completion[0..1],
cursor_style.patch(self.theme.completion_style),
));
if completion.len() > 1 {
spans.push(Span::styled(&completion[1..], self.theme.completion_style));
}
} else {
spans.push(Span::styled(" ", cursor_style));
}
} else {
let (before_cursor, from_cursor) = self.content.split_at(self.cursor.index());
let completion_first_char = completion.chars().next();
let (at_cursor, is_cursor_at_completion) = {
if from_cursor.is_empty() {
(completion_first_char.unwrap_or(' '), true)
} else {
let at = from_cursor.iter().next().unwrap();
let at = *replace_newline(at);
(at, false)
}
};
let after_cursor: String = from_cursor.iter().skip(1).map(replace_newline).collect();
spans.push(Span::raw(
before_cursor
.iter()
.map(replace_newline)
.collect::<String>(),
));
let at_cursor_style = if is_cursor_at_completion {
cursor_style.patch(self.theme.completion_style)
} else {
cursor_style
};
spans.push(Span::styled(at_cursor.to_string(), at_cursor_style));
spans.push(Span::raw(after_cursor));
if !completion.is_empty() {
let remaining_completion = if is_cursor_at_completion {
&completion[1..]
} else {
completion
};
spans.push(Span::styled(
remaining_completion,
self.theme.completion_style,
))
}
};
spans
}
fn inline_prompt_spans<'s>(&'s self, inline_prompt: Line<'s>) -> Vec<Span<'s>> {
let prompt_line_style = inline_prompt.style;
inline_prompt
.spans
.into_iter()
.map(|s| {
let span_style = s.style;
s.style(
self.theme
.prompt_style
.patch(prompt_line_style)
.patch(span_style),
)
})
.collect()
}
}
fn replace_newline(char: &char) -> &char {
match char {
'\n' => &NEWLINE_PLACEHOLDER,
'\r' => &CARRIAGE_RETURN_PLACEHOLDER,
c => c,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dialog::Dialog;
use crate::input::prompt::Prompt;
use crate::theme::Theme;
use ratatui::prelude::*;
#[test]
fn test_input_line_prompt() {
let dialog = InputDialog::default()
.with_content("content")
.with_prompt(Prompt::inline("inline".bold()));
let spans = dialog.input_spans(dialog.prompt_with_confirmation().inline);
assert_eq!(4, spans.len());
let inline_prompt_style = Theme::default().prompt_style.patch(Style::new().bold());
assert_eq!(inline_prompt_style, spans[0].style);
assert_eq!("inline", spans[0].content.as_ref());
assert_eq!(Style::default(), spans[1].style);
assert_eq!("content", spans[1].content.as_ref());
}
#[test]
fn test_input_line_placeholder() {
let dialog = InputDialog::default().with_placeholder("placeholder");
let spans = dialog.input_spans(dialog.prompt_with_confirmation().inline);
assert_eq!(1, spans.len());
assert_eq!(Theme::default().placeholder_style, spans[0].style);
assert_eq!("placeholder", spans[0].content);
}
#[test]
fn test_input_line_cursor() {
let mut dialog = InputDialog::default();
{
let spans = dialog.input_spans(dialog.prompt_with_confirmation().inline);
assert_eq!(1, spans.len());
assert_eq!(Theme::default().cursor_on_style, spans[0].style);
assert_eq!(" ", spans[0].content);
}
dialog.tick();
{
let spans = dialog.input_spans(dialog.prompt_with_confirmation().inline);
assert_eq!(1, spans.len());
assert_eq!(Theme::default().cursor_off_style, spans[0].style);
assert_eq!(" ", spans[0].content);
}
}
#[test]
fn test_input_line_completion() {
let theme = Theme::default();
let dialog = InputDialog::default()
.with_content("So it is, and so it will be")
.with_completions(vec![
"So it is, and so it will be, for so it has been, time out of mind".into(),
]);
let spans = dialog.input_spans(dialog.prompt_with_confirmation().inline);
assert_eq!(4, spans.len());
assert_eq!(Style::default(), spans[0].style);
assert_eq!("So it is, and so it will be", spans[0].content.as_ref());
assert_eq!(
theme.cursor_on_style.patch(theme.completion_style),
spans[1].style
);
assert_eq!(",", spans[1].content.as_ref());
assert_eq!(Style::default(), spans[2].style);
assert_eq!("", spans[2].content.as_ref());
assert_eq!(theme.completion_style, spans[3].style);
assert_eq!(
" for so it has been, time out of mind",
spans[3].content.as_ref()
);
}
}