use std::fmt;
use promkit_core::grapheme::StyledGraphemes;
#[derive(Clone, Default)]
pub struct Listbox {
items: Vec<StyledGraphemes>,
selected: Option<usize>,
}
impl<E: fmt::Display, I: IntoIterator<Item = E>> From<I> for Listbox {
fn from(items: I) -> Self {
let items = items
.into_iter()
.map(|e| StyledGraphemes::from(format!("{}", e)))
.collect::<Vec<_>>();
let selected = (!items.is_empty()).then_some(0);
Self { items, selected }
}
}
impl Listbox {
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn push_string(&mut self, item: String) {
self.items.push(StyledGraphemes::from(item));
if self.selected.is_none() {
self.selected = Some(0);
}
}
pub fn from_styled_graphemes(items: Vec<StyledGraphemes>) -> Self {
let selected = (!items.is_empty()).then_some(0);
Self { items, selected }
}
pub fn items(&self) -> &Vec<StyledGraphemes> {
&self.items
}
pub fn position(&self) -> usize {
self.selected.unwrap_or_default()
}
pub fn selected(&self) -> Option<usize> {
self.selected
}
pub fn get(&self) -> StyledGraphemes {
self.selected
.and_then(|position| self.items.get(position))
.unwrap_or(&StyledGraphemes::default())
.clone()
}
pub fn backward(&mut self) -> bool {
let Some(position) = self.selected.filter(|position| *position > 0) else {
return false;
};
self.selected = Some(position - 1);
true
}
pub fn forward(&mut self) -> bool {
let Some(position) = self
.selected
.filter(|position| position.saturating_add(1) < self.items.len())
else {
return false;
};
self.selected = Some(position + 1);
true
}
pub fn move_to(&mut self, position: usize) -> bool {
if position < self.items.len() {
self.selected = Some(position);
true
} else {
false
}
}
pub fn move_to_head(&mut self) {
self.selected = (!self.items.is_empty()).then_some(0);
}
pub fn move_to_tail(&mut self) {
self.selected = self.items.len().checked_sub(1);
}
pub fn is_tail(&self) -> bool {
self.selected.is_some() && self.selected == self.items.len().checked_sub(1)
}
}
#[cfg(test)]
mod tests {
use promkit_core::grapheme::StyledGraphemes;
use super::Listbox;
mod default {
use super::*;
#[test]
fn creates_an_empty_list() {
let listbox = Listbox::default();
assert!(listbox.is_empty());
assert_eq!(listbox.len(), 0);
assert_eq!(listbox.selected(), None);
assert!(!listbox.is_tail());
}
}
mod push_string {
use super::*;
#[test]
fn selects_the_first_item() {
let mut listbox = Listbox::default();
listbox.push_string("first".into());
assert_eq!(listbox.selected(), Some(0));
assert_eq!(listbox.get(), StyledGraphemes::from("first"));
}
}
mod backward {
use super::*;
#[test]
fn stops_at_the_head() {
let mut listbox = Listbox::from(["first", "second"]);
assert!(!listbox.backward());
assert_eq!(listbox.selected(), Some(0));
}
}
mod forward {
use super::*;
#[test]
fn stops_at_the_tail() {
let mut listbox = Listbox::from(["first", "second"]);
assert!(listbox.forward());
assert_eq!(listbox.selected(), Some(1));
assert!(!listbox.forward());
assert_eq!(listbox.selected(), Some(1));
}
}
mod move_to {
use super::*;
#[test]
fn rejects_an_out_of_bounds_index() {
let mut listbox = Listbox::from(["first", "second"]);
assert!(!listbox.move_to(2));
assert_eq!(listbox.selected(), Some(0));
}
}
mod move_to_tail {
use super::*;
#[test]
fn selects_the_last_item() {
let mut listbox = Listbox::from(["first", "second"]);
listbox.move_to_tail();
assert_eq!(listbox.selected(), Some(1));
assert!(listbox.is_tail());
}
}
}