use std::{convert::TryFrom, io};
use crate::{
backend::Backend,
events,
layout::Layout,
style::{Color, Stylize},
Widget,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Delimiter {
Parentheses,
Braces,
SquareBracket,
AngleBracket,
Other(char, char),
None,
}
impl From<Delimiter> for Option<(char, char)> {
fn from(delim: Delimiter) -> Self {
match delim {
Delimiter::Parentheses => Some(('(', ')')),
Delimiter::Braces => Some(('{', '}')),
Delimiter::SquareBracket => Some(('[', ']')),
Delimiter::AngleBracket => Some(('<', '>')),
Delimiter::Other(start, end) => Some((start, end)),
Delimiter::None => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Prompt<M, H = &'static str> {
message: M,
hint: Option<H>,
delim: Delimiter,
message_len: u16,
hint_len: u16,
}
impl<M: AsRef<str>, H: AsRef<str>> Prompt<M, H> {
pub fn new(message: M) -> Self {
Self {
message_len: u16::try_from(textwrap::core::display_width(message.as_ref()))
.expect("message must fit within a u16"),
message,
hint: None,
delim: Delimiter::Parentheses,
hint_len: 0,
}
}
pub fn with_hint(mut self, hint: H) -> Self {
self.hint_len = u16::try_from(textwrap::core::display_width(hint.as_ref()))
.expect("hint must fit within a u16");
self.hint = Some(hint);
self
}
pub fn with_optional_hint(self, hint: Option<H>) -> Self {
match hint {
Some(hint) => self.with_hint(hint),
None => self,
}
}
pub fn with_delim(mut self, delim: Delimiter) -> Self {
self.delim = delim;
self
}
pub fn message(&self) -> &M {
&self.message
}
pub fn hint(&self) -> Option<&H> {
self.hint.as_ref()
}
pub fn delim(&self) -> Delimiter {
self.delim
}
pub fn into_message(self) -> M {
self.message
}
pub fn into_hint(self) -> Option<H> {
self.hint
}
pub fn into_message_and_hint(self) -> (M, Option<H>) {
(self.message, self.hint)
}
pub fn message_len(&self) -> u16 {
self.message_len
}
pub fn hint_len(&self) -> u16 {
if self.hint.is_some() {
match self.delim {
Delimiter::None => self.hint_len,
_ => self.hint_len + 2,
}
} else {
0
}
}
pub fn width(&self) -> u16 {
if self.hint.is_some() {
2 + self.message_len + 1 + self.hint_len() + 1
} else {
2 + self.message_len + 3
}
}
fn cursor_pos_impl(&self, layout: Layout) -> (u16, u16) {
let mut width = self.width();
let relative_pos = if width > layout.line_width() {
width -= layout.line_width();
(width % layout.width, 1 + width / layout.width)
} else {
(layout.line_offset + width, 0)
};
layout.offset_cursor(relative_pos)
}
}
impl<M: AsRef<str>> Prompt<M, &'static str> {
pub fn write_finished_message<B: Backend>(
message: &M,
skipped: bool,
backend: &mut B,
) -> io::Result<()> {
let symbol_set = crate::symbols::current();
if skipped {
backend.write_styled(&symbol_set.cross.yellow())?;
} else {
backend.write_styled(&symbol_set.completed.light_green())?;
}
backend.write_all(b" ")?;
backend.write_styled(&message.as_ref().bold())?;
backend.write_all(b" ")?;
backend.write_styled(&symbol_set.middle_dot.dark_grey())?;
backend.write_all(b" ")
}
}
impl<M: AsRef<str>, H: AsRef<str>> Widget for Prompt<M, H> {
fn render<B: Backend>(&mut self, layout: &mut Layout, b: &mut B) -> io::Result<()> {
b.write_styled(&"? ".light_green())?;
b.write_styled(&self.message.as_ref().bold())?;
b.write_all(b" ")?;
b.set_fg(Color::DarkGrey)?;
match (&self.hint, self.delim.into()) {
(Some(hint), Some((start, end))) => write!(b, "{}{}{}", start, hint.as_ref(), end)?,
(Some(hint), None) => write!(b, "{}", hint.as_ref())?,
(None, _) => {
write!(b, "{}", crate::symbols::current().arrow)?;
}
}
b.set_fg(Color::Reset)?;
b.write_all(b" ")?;
*layout = layout.with_cursor_pos(self.cursor_pos_impl(*layout));
Ok(())
}
fn height(&mut self, layout: &mut Layout) -> u16 {
let offset_y = layout.offset_y;
let cursor_pos = self.cursor_pos_impl(*layout);
*layout = layout.with_cursor_pos(cursor_pos);
cursor_pos.1 + 1 - offset_y
}
fn cursor_pos(&mut self, layout: Layout) -> (u16, u16) {
self.cursor_pos_impl(layout)
}
fn handle_key(&mut self, _: events::KeyEvent) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use crate::{backend::TestBackend, test_consts::*};
use super::*;
type Prompt = super::Prompt<&'static str, &'static str>;
#[test]
fn test_width() {
assert_eq!(Prompt::new("Hello").width(), 10);
assert_eq!(Prompt::new("Hello").with_hint("world").width(), 16);
assert_eq!(
Prompt::new("Hello")
.with_hint("world")
.with_delim(Delimiter::None)
.width(),
14
);
assert_eq!(Prompt::new(LOREM).with_hint(UNICODE).width(), 946);
}
#[test]
fn test_render() {
fn test(
message: &'static str,
hint: Option<&'static str>,
delim: Delimiter,
expected_layout: Layout,
) {
let size = (100, 20).into();
let mut layout = Layout::new(5, size);
let mut prompt = Prompt::new(message)
.with_optional_hint(hint)
.with_delim(delim);
let mut backend = TestBackend::new_with_layout(size, layout);
prompt.render(&mut layout, &mut backend).unwrap();
crate::assert_backend_snapshot!(backend);
assert_eq!(
layout,
expected_layout,
"\ncursor pos = {:?}, width = {:?}",
prompt.cursor_pos(Layout::new(5, size)),
prompt.width(),
);
}
let layout = Layout::new(5, (100, 20).into());
test("Hello", None, Delimiter::None, layout.with_line_offset(15));
test(
"Hello",
Some("world"),
Delimiter::Parentheses,
layout.with_line_offset(21),
);
test(
"Hello",
Some("world"),
Delimiter::Braces,
layout.with_line_offset(21),
);
test(
"Hello",
Some("world"),
Delimiter::SquareBracket,
layout.with_line_offset(21),
);
test(
"Hello",
Some("world"),
Delimiter::AngleBracket,
layout.with_line_offset(21),
);
test(
"Hello",
Some("world"),
Delimiter::Other('-', '|'),
layout.with_line_offset(21),
);
test(
LOREM,
Some(UNICODE),
Delimiter::None,
layout.with_line_offset(49).with_offset(0, 9),
);
}
#[test]
fn test_height() {
let mut layout = Layout::new(5, (100, 20).into());
assert_eq!(Prompt::new("Hello").height(&mut layout.clone()), 1);
assert_eq!(
Prompt::new("Hello")
.with_hint("world")
.height(&mut layout.clone()),
1
);
assert_eq!(
Prompt::new(LOREM).with_hint(UNICODE).height(&mut layout),
10
);
}
#[test]
fn test_cursor_pos() {
let layout = Layout::new(5, (100, 20).into());
assert_eq!(Prompt::new("Hello").cursor_pos_impl(layout), (15, 0));
assert_eq!(
Prompt::new("Hello")
.with_hint("world")
.cursor_pos_impl(layout),
(21, 0)
);
assert_eq!(
Prompt::new("Hello")
.with_hint("world")
.with_delim(Delimiter::None)
.cursor_pos_impl(layout),
(19, 0)
);
assert_eq!(
Prompt::new(LOREM)
.with_hint(UNICODE)
.cursor_pos_impl(layout),
(51, 9)
);
assert_eq!(
Prompt::new(LOREM)
.with_hint(UNICODE)
.cursor_pos_impl(layout.with_offset(0, 3)),
(51, 12)
);
}
}