use std::io;
use crossterm::event::{KeyCode, KeyEvent};
use crate::utils::{
key_listener::{self, Typeable},
renderer::{DrawTime, Printable, Renderer},
theme,
};
use super::select::{Direction, SelectInput, SelectOption};
type Formatter<'a, T> = dyn Fn(&MultiSelect<T>, DrawTime) -> String + 'a;
pub struct MultiSelect<'a, T> {
pub message: &'a str,
pub options: Vec<SelectOption<'a, T>>,
pub min: Option<usize>,
pub max: Option<usize>,
pub input: SelectInput,
selected_count: usize,
formatter: Box<Formatter<'a, T>>,
}
impl<'a, T: 'a> MultiSelect<'a, T> {
pub fn new<I>(message: &'a str, iter: I) -> Self
where
I: IntoIterator<Item = T>,
T: ToString,
{
let options = iter.into_iter().map(|o| SelectOption::new(o)).collect();
Self::new_complex(message, options)
}
pub fn new_complex(message: &'a str, options: Vec<SelectOption<'a, T>>) -> Self {
let options_len = options.len();
MultiSelect {
message,
options,
min: None,
max: None,
selected_count: 0,
input: SelectInput::new(options_len),
formatter: Box::new(theme::fmt_multi_select),
}
}
pub fn selected(&mut self, indices: &[usize]) -> &mut Self {
for i in indices {
if let Some(option) = self.options.get_mut(*i) {
option.active = true;
self.selected_count += 1;
}
}
self
}
pub fn in_loop(&mut self, is_loop: bool) -> &mut Self {
self.input.set_loop_mode(is_loop);
self
}
pub fn items_per_page(&mut self, items_per_page: usize) -> &mut Self {
self.input.set_items_per_page(items_per_page);
self
}
pub fn min(&mut self, min: usize) -> &mut Self {
self.min = Some(min);
self
}
pub fn max(&mut self, max: usize) -> &mut Self {
self.max = Some(max);
self
}
pub fn format<F>(&mut self, formatter: F) -> &mut Self
where
F: Fn(&MultiSelect<T>, DrawTime) -> String + 'a,
{
self.formatter = Box::new(formatter);
self
}
pub fn prompt(&mut self) -> io::Result<Vec<T>> {
key_listener::listen(self, true)?;
let (selected, _): (Vec<_>, Vec<_>) = self.options.drain(..).partition(|x| x.active);
let selected = selected.into_iter().map(|x| x.value).collect();
Ok(selected)
}
}
impl<T> MultiSelect<'_, T> {
fn toggle_focused(&mut self) {
let selected = self.input.focused;
let focused = &self.options[selected];
if focused.disabled {
return;
}
let under_limit = match self.max {
None => true,
Some(max) => self.selected_count < max,
};
let focused = &mut self.options[selected];
if focused.active {
focused.active = false;
self.selected_count -= 1;
} else if under_limit {
focused.active = true;
self.selected_count += 1;
}
}
fn validate_to_submit(&self) -> bool {
match self.min {
None => true,
Some(min) => self.selected_count >= min,
}
}
}
impl<T> Typeable for MultiSelect<'_, T> {
fn handle_key(&mut self, key: KeyEvent) -> bool {
let mut submit = false;
match key.code {
KeyCode::Enter | KeyCode::Backspace => submit = self.validate_to_submit(),
KeyCode::Char(' ') => self.toggle_focused(),
KeyCode::Up | KeyCode::Char('k' | 'K') => self.input.move_cursor(Direction::Up),
KeyCode::Down | KeyCode::Char('j' | 'J') => self.input.move_cursor(Direction::Down),
KeyCode::Left | KeyCode::Char('h' | 'H') => self.input.move_cursor(Direction::Left),
KeyCode::Right | KeyCode::Char('l' | 'L') => self.input.move_cursor(Direction::Right),
_ => (),
}
submit
}
}
impl<T> Printable for MultiSelect<'_, T> {
fn draw(&self, renderer: &mut Renderer) -> io::Result<()> {
let text = (self.formatter)(self, renderer.draw_time);
renderer.print(text)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_selected_values() {
let mut prompt = MultiSelect::new("", ["a", "b", "c"]);
prompt.selected(&[0, 2]);
assert!(prompt.options[0].active);
assert!(prompt.options[2].active);
}
#[test]
fn set_min() {
let mut prompt = MultiSelect::<&str>::new("", vec![]);
prompt.min(2);
assert_eq!(prompt.min, Some(2));
}
#[test]
fn set_max() {
let mut prompt = MultiSelect::<&str>::new("", vec![]);
prompt.max(2);
assert_eq!(prompt.max, Some(2));
}
#[test]
fn set_in_loop() {
let mut prompt = MultiSelect::new("", ["a", "b", "c"]);
prompt.in_loop(false);
assert!(!prompt.input.loop_mode);
prompt.in_loop(true);
assert!(prompt.input.loop_mode);
}
#[test]
fn set_custom_formatter() {
let mut prompt: MultiSelect<u8> = MultiSelect::new("", vec![]);
let draw_time = DrawTime::First;
const EXPECTED_VALUE: &str = "foo";
prompt.format(|_, _| String::from(EXPECTED_VALUE));
assert_eq!((prompt.formatter)(&prompt, draw_time), EXPECTED_VALUE);
}
#[test]
fn submit_keys() {
let events = [KeyCode::Enter, KeyCode::Backspace];
for event in events {
let mut prompt = MultiSelect::new("", ["a", "b", "c"]);
let simulated_key = KeyEvent::from(event);
let submit = prompt.handle_key(simulated_key);
assert!(submit);
}
}
#[test]
fn not_submit_without_min() {
let mut prompt = MultiSelect::new("", ["a", "b", "c"]);
prompt.min(1);
let mut submit = prompt.handle_key(KeyEvent::from(KeyCode::Enter));
assert!(!submit);
prompt.handle_key(KeyEvent::from(KeyCode::Char(' ')));
submit = prompt.handle_key(KeyEvent::from(KeyCode::Enter));
assert!(submit);
}
#[test]
fn move_cursor() {
let mut prompt = MultiSelect::new("", ["a", "b", "c"]);
let prev_keys = [KeyCode::Up, KeyCode::Char('k'), KeyCode::Char('K')];
let next_keys = [KeyCode::Down, KeyCode::Char('j'), KeyCode::Char('j')];
prompt.in_loop(false);
for key in next_keys {
prompt.input.focused = 0;
prompt.handle_key(KeyEvent::from(key));
assert_eq!(prompt.input.focused, 1);
}
prompt.in_loop(true);
for key in next_keys {
prompt.input.focused = 2;
prompt.handle_key(KeyEvent::from(key));
assert_eq!(prompt.input.focused, 0);
}
prompt.in_loop(false);
for key in prev_keys {
prompt.input.focused = 2;
prompt.handle_key(KeyEvent::from(key));
assert_eq!(prompt.input.focused, 1);
}
prompt.in_loop(true);
for key in prev_keys {
prompt.input.focused = 0;
prompt.handle_key(KeyEvent::from(key));
assert_eq!(prompt.input.focused, 2);
}
}
#[test]
fn update_focused_selected() {
let mut prompt = MultiSelect::new("", ["a", "b", "c"]);
prompt.max(1);
assert!(!prompt.options[1].active);
assert!(!prompt.options[2].active);
prompt.input.focused = 1;
prompt.handle_key(KeyEvent::from(KeyCode::Char(' ')));
prompt.input.focused = 2;
prompt.handle_key(KeyEvent::from(KeyCode::Char(' ')));
assert!(prompt.options[1].active);
assert!(!prompt.options[2].active);
}
}