Files
managarr/src/models/stateful_table.rs

300 lines
7.1 KiB
Rust

use crate::models::stateful_list::StatefulList;
use crate::models::{strip_non_search_characters, HorizontallyScrollableText, Scrollable};
use ratatui::widgets::TableState;
use std::cmp::Ordering;
use std::fmt::Debug;
#[cfg(test)]
#[path = "stateful_table_tests.rs"]
mod stateful_table_tests;
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct SortOption<T>
where
T: Clone + PartialEq + Eq + Debug,
{
pub name: &'static str,
pub cmp_fn: Option<fn(&T, &T) -> Ordering>,
}
#[derive(Default)]
pub struct StatefulTable<T>
where
T: Clone + PartialEq + Eq + Debug,
{
pub state: TableState,
pub items: Vec<T>,
pub filter: Option<HorizontallyScrollableText>,
pub search: Option<HorizontallyScrollableText>,
pub filtered_items: Option<Vec<T>>,
pub filtered_state: Option<TableState>,
pub sort_asc: bool,
pub sort: Option<StatefulList<SortOption<T>>>,
}
impl<T> Scrollable for StatefulTable<T>
where
T: Clone + PartialEq + Eq + Debug,
{
fn scroll_down(&mut self) {
if let Some(filtered_items) = self.filtered_items.as_ref() {
if filtered_items.is_empty() {
return;
}
match self.filtered_state.as_ref().unwrap().selected() {
Some(i) => {
if i >= filtered_items.len() - 1 {
self.filtered_state.as_mut().unwrap().select_first();
} else {
self.filtered_state.as_mut().unwrap().select_next();
}
}
None => self.filtered_state.as_mut().unwrap().select_first(),
};
return;
}
if self.items.is_empty() {
return;
}
match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
self.state.select_first();
} else {
self.state.select_next();
}
}
None => self.state.select_first(),
};
}
fn scroll_up(&mut self) {
if let Some(filtered_items) = self.filtered_items.as_ref() {
if filtered_items.is_empty() {
return;
}
match self.filtered_state.as_ref().unwrap().selected() {
Some(i) => {
if i == 0 {
self
.filtered_state
.as_mut()
.unwrap()
.select(Some(filtered_items.len() - 1));
} else {
self.filtered_state.as_mut().unwrap().select_previous();
}
}
None => self.filtered_state.as_mut().unwrap().select_first(),
};
return;
}
if self.items.is_empty() {
return;
}
match self.state.selected() {
Some(i) => {
if i == 0 {
self.state.select(Some(self.items.len() - 1));
} else {
self.state.select_previous();
}
}
None => self.state.select_first(),
};
}
fn scroll_to_top(&mut self) {
if let Some(filtered_items) = self.filtered_items.as_ref() {
if filtered_items.is_empty() {
return;
}
self.filtered_state.as_mut().unwrap().select_first();
return;
}
if self.items.is_empty() {
return;
}
self.state.select_first();
}
fn scroll_to_bottom(&mut self) {
if let Some(filtered_items) = self.filtered_items.as_ref() {
if filtered_items.is_empty() {
return;
}
self
.filtered_state
.as_mut()
.unwrap()
.select(Some(filtered_items.len() - 1));
return;
}
if self.items.is_empty() {
return;
}
self.state.select(Some(self.items.len() - 1));
}
}
impl<T> StatefulTable<T>
where
T: Clone + PartialEq + Eq + Debug + Default,
{
pub fn set_items(&mut self, items: Vec<T>) {
let items_len = items.len();
self.items = items;
if !self.items.is_empty() {
let selected_row = self.state.selected().map_or(0, |i| {
if i > 0 && i < items_len {
i
} else if i >= items_len {
items_len - 1
} else {
0
}
});
self.state.select(Some(selected_row));
}
}
pub fn set_filtered_items(&mut self, filtered_items: Vec<T>) {
self.filtered_items = Some(filtered_items);
let mut filtered_state: TableState = Default::default();
filtered_state.select(Some(0));
self.filtered_state = Some(filtered_state);
}
pub fn select_index(&mut self, index: Option<usize>) {
if let Some(filtered_state) = &mut self.filtered_state {
filtered_state.select(index);
} else {
self.state.select(index);
}
}
pub fn current_selection(&self) -> &T {
if let Some(filtered_items) = &self.filtered_items {
&filtered_items[self
.filtered_state
.as_ref()
.unwrap()
.selected()
.unwrap_or(0)]
} else {
&self.items[self.state.selected().unwrap_or(0)]
}
}
pub fn sorting(&mut self, sort_options: Vec<SortOption<T>>) {
let mut sort_options_list = StatefulList::default();
sort_options_list.set_items(sort_options);
self.sort = Some(sort_options_list);
}
pub fn apply_sorting(&mut self) {
self.apply_sorting_toggle(true);
}
pub fn apply_sorting_toggle(&mut self, toggle_dir: bool) {
if let Some(sort_options) = &mut self.sort {
if toggle_dir {
self.sort_asc = !self.sort_asc;
}
let selected_sort_option = sort_options.current_selection();
let mut items = self.filtered_items.as_ref().unwrap_or(&self.items).clone();
if let Some(cmp_fn) = selected_sort_option.cmp_fn {
if !self.sort_asc {
items.sort_by(|a, b| cmp_fn(a, b).reverse());
} else {
items.sort_by(cmp_fn);
}
if self.filtered_items.is_some() {
self.set_filtered_items(items.clone());
} else {
self.set_items(items);
}
}
}
}
pub fn apply_filter(&mut self, filter_field: fn(&T) -> &str) -> bool {
let filter_matches = match self.filter {
Some(ref filter) if !filter.text.is_empty() => {
let scrubbed_filter = strip_non_search_characters(&filter.text.clone());
self
.items
.iter()
.filter(|item| strip_non_search_characters(filter_field(item)).contains(&scrubbed_filter))
.cloned()
.collect()
}
_ => Vec::new(),
};
self.filter = None;
if filter_matches.is_empty() {
return false;
}
self.set_filtered_items(filter_matches);
true
}
pub fn reset_filter(&mut self) {
self.filter = None;
self.filtered_items = None;
self.filtered_state = None;
}
pub fn apply_search(&mut self, search_field: fn(&T) -> &str) -> bool {
let search_index = if let Some(search) = self.search.as_ref() {
let search_string = search.text.clone().to_lowercase();
self
.filtered_items
.as_ref()
.unwrap_or(&self.items)
.iter()
.position(|item| strip_non_search_characters(search_field(item)).contains(&search_string))
} else {
None
};
self.search = None;
if search_index.is_none() {
return false;
}
self.select_index(search_index);
true
}
pub fn reset_search(&mut self) {
self.search = None;
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
}