use ratatui::style::Style;
use ratatui::widgets::ListState;
pub struct ListStyles {
pub normal: Style,
pub selected: Style,
pub highlight_symbol: &'static str,
}
impl Default for ListStyles {
fn default() -> Self {
Self {
normal: Style::default(),
selected: Style::default(),
highlight_symbol: "→ ",
}
}
}
pub struct StatefulList<T> {
items: Vec<T>,
state: ListState,
filter: String,
filtered_indices: Vec<usize>,
}
impl<T> StatefulList<T> {
pub fn new(items: Vec<T>) -> Self {
let filtered_indices: Vec<usize> = (0..items.len()).collect();
let mut state = ListState::default();
if !filtered_indices.is_empty() {
state.select(Some(0));
}
Self {
items,
state,
filter: String::new(),
filtered_indices,
}
}
pub fn select_next(&mut self) {
self.state.select_next();
}
pub fn select_previous(&mut self) {
self.state.select_previous();
}
pub fn select_first(&mut self) {
self.state.select_first();
}
pub fn select_last(&mut self) {
if self.filtered_indices.is_empty() {
self.state.select(None);
} else {
let last = self.filtered_indices.len() - 1;
self.state.select(Some(last));
}
}
pub fn scroll_down_by(&mut self, n: u16) {
let n = n as usize;
if self.filtered_indices.is_empty() {
return;
}
let max = self.filtered_indices.len().saturating_sub(1);
let cur = self.state.selected().unwrap_or(0);
let next = (cur + n).min(max);
self.state.select(Some(next));
}
pub fn scroll_up_by(&mut self, n: u16) {
let n = n as usize;
let cur = self.state.selected().unwrap_or(0);
let prev = cur.saturating_sub(n);
self.state.select(Some(prev));
}
pub fn selected(&self) -> Option<&T> {
let idx = *self
.state
.selected()
.and_then(|i| self.filtered_indices.get(i))?;
self.items.get(idx)
}
pub fn selected_mut(&mut self) -> Option<&mut T> {
let idx = *self
.state
.selected()
.and_then(|i| self.filtered_indices.get(i))?;
self.items.get_mut(idx)
}
pub fn selected_index(&self) -> Option<usize> {
let i = self.state.selected()?;
self.filtered_indices.get(i).copied()
}
pub fn len(&self) -> usize {
self.filtered_indices.len()
}
pub fn is_empty(&self) -> bool {
self.filtered_indices.is_empty()
}
pub fn state_mut(&mut self) -> &mut ListState {
&mut self.state
}
pub fn items(&self) -> impl Iterator<Item = &T> {
self.filtered_indices
.iter()
.filter_map(|&i| self.items.get(i))
}
pub fn filter_text(&self) -> &str {
&self.filter
}
pub fn clear_filter(&mut self) {
self.filter.clear();
self.filtered_indices = (0..self.items.len()).collect();
if !self.filtered_indices.is_empty() {
self.state.select(Some(0));
} else {
self.state.select(None);
}
}
}
impl<T: AsRef<str>> StatefulList<T> {
pub fn set_filter(&mut self, filter: &str) {
self.filter = filter.to_owned();
let lower = self.filter.to_lowercase();
self.filtered_indices = self
.items
.iter()
.enumerate()
.filter(|(_, item)| item.as_ref().to_lowercase().contains(&lower))
.map(|(i, _)| i)
.collect();
if !self.filtered_indices.is_empty() {
self.state.select(Some(0));
} else {
self.state.select(None);
}
}
pub fn filter_input(&mut self, c: char) {
self.filter.push(c);
let f = self.filter.clone();
self.set_filter(&f);
}
pub fn filter_backspace(&mut self) {
self.filter.pop();
let f = self.filter.clone();
self.set_filter(&f);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_selects_first() {
let list: StatefulList<&str> = StatefulList::new(vec!["a", "b", "c"]);
assert_eq!(list.selected(), Some(&"a"));
assert_eq!(list.selected_index(), Some(0));
}
#[test]
fn navigation() {
let mut list = StatefulList::new(vec!["a", "b", "c"]);
list.select_next();
assert_eq!(list.selected(), Some(&"b"));
list.select_last();
assert_eq!(list.selected(), Some(&"c"));
list.select_previous();
assert_eq!(list.selected(), Some(&"b"));
list.select_first();
assert_eq!(list.selected(), Some(&"a"));
}
#[test]
fn scroll_by() {
let mut list = StatefulList::new(vec!["a", "b", "c", "d", "e"]);
list.scroll_down_by(2);
assert_eq!(list.selected(), Some(&"c"));
list.scroll_down_by(100);
assert_eq!(list.selected(), Some(&"e"));
list.scroll_up_by(3);
assert_eq!(list.selected(), Some(&"b"));
}
#[test]
fn filter() {
let mut list = StatefulList::new(vec!["apple", "banana", "apricot", "cherry"]);
list.set_filter("ap");
assert_eq!(list.len(), 2);
assert_eq!(list.selected(), Some(&"apple"));
assert_eq!(list.selected_index(), Some(0));
list.select_next();
assert_eq!(list.selected(), Some(&"apricot"));
assert_eq!(list.selected_index(), Some(2));
}
#[test]
fn filter_input_and_backspace() {
let mut list = StatefulList::new(vec!["foo", "bar", "baz"]);
list.filter_input('b');
assert_eq!(list.len(), 2);
list.filter_input('a');
assert_eq!(list.len(), 2); list.filter_backspace();
assert_eq!(list.len(), 2); list.filter_backspace();
assert_eq!(list.len(), 3); }
#[test]
fn clear_filter() {
let mut list = StatefulList::new(vec!["foo", "bar"]);
list.set_filter("z");
assert_eq!(list.len(), 0);
assert!(list.is_empty());
list.clear_filter();
assert_eq!(list.len(), 2);
assert_eq!(list.selected(), Some(&"foo"));
}
#[test]
fn empty_list() {
let list: StatefulList<&str> = StatefulList::new(vec![]);
assert!(list.is_empty());
assert_eq!(list.selected(), None);
}
#[test]
fn default_styles() {
let styles = ListStyles::default();
assert_eq!(styles.highlight_symbol, "→ ");
}
#[test]
fn items_iterator_respects_filter() {
let mut list = StatefulList::new(vec!["cat", "dog", "cow"]);
list.set_filter("c");
let collected: Vec<&&str> = list.items().collect();
assert_eq!(collected, vec![&"cat", &"cow"]);
}
}