303 lines
7.1 KiB
Rust
303 lines
7.1 KiB
Rust
use crate::models::stateful_list::StatefulList;
|
|
use crate::models::{
|
|
HorizontallyScrollableText, Paginated, Scrollable, strip_non_search_characters,
|
|
};
|
|
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, Debug, Default)]
|
|
pub struct SortOption<T>
|
|
where
|
|
T: Clone + PartialEq + Eq + Debug,
|
|
{
|
|
pub name: &'static str,
|
|
pub cmp_fn: Option<fn(&T, &T) -> Ordering>,
|
|
}
|
|
|
|
impl<T> PartialEq for SortOption<T>
|
|
where
|
|
T: Clone + PartialEq + Eq + Debug,
|
|
{
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.name == other.name
|
|
}
|
|
}
|
|
|
|
impl<T> Eq for SortOption<T> where T: Clone + PartialEq + Eq + Debug {}
|
|
|
|
#[derive(Default)]
|
|
#[cfg_attr(test, derive(Debug))]
|
|
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) {
|
|
let items_len = self.active_items().len();
|
|
if items_len == 0 {
|
|
return;
|
|
}
|
|
|
|
let state = self.active_state_mut();
|
|
match state.selected() {
|
|
Some(i) if i >= items_len - 1 => state.select_first(),
|
|
Some(_) => state.select_next(),
|
|
None => state.select_first(),
|
|
}
|
|
}
|
|
|
|
fn scroll_up(&mut self) {
|
|
let items_len = self.active_items().len();
|
|
if items_len == 0 {
|
|
return;
|
|
}
|
|
|
|
let state = self.active_state_mut();
|
|
match state.selected() {
|
|
Some(0) => state.select(Some(items_len - 1)),
|
|
Some(_) => state.select_previous(),
|
|
None => state.select_first(),
|
|
}
|
|
}
|
|
|
|
fn scroll_to_top(&mut self) {
|
|
if self.active_items().is_empty() {
|
|
return;
|
|
}
|
|
self.active_state_mut().select_first();
|
|
}
|
|
|
|
fn scroll_to_bottom(&mut self) {
|
|
let items_len = self.active_items().len();
|
|
if items_len == 0 {
|
|
return;
|
|
}
|
|
self.active_state_mut().select(Some(items_len - 1));
|
|
}
|
|
}
|
|
|
|
impl<T> Paginated for StatefulTable<T>
|
|
where
|
|
T: Clone + PartialEq + Eq + Debug,
|
|
{
|
|
fn page_down(&mut self) {
|
|
let items_len = self.active_items().len();
|
|
if items_len == 0 {
|
|
return;
|
|
}
|
|
|
|
let state = self.active_state_mut();
|
|
match state.selected() {
|
|
Some(i) => state.select(Some(i.saturating_add(20) % (items_len - 1))),
|
|
None => state.select_first(),
|
|
}
|
|
}
|
|
|
|
fn page_up(&mut self) {
|
|
let items_len = self.active_items().len();
|
|
if items_len == 0 {
|
|
return;
|
|
}
|
|
|
|
let state = self.active_state_mut();
|
|
match state.selected() {
|
|
Some(i) => {
|
|
let len = items_len - 1;
|
|
state.select(Some((i + len - (20 % len)) % len));
|
|
}
|
|
None => state.select_last(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T> StatefulTable<T>
|
|
where
|
|
T: Clone + PartialEq + Eq + Debug,
|
|
{
|
|
fn active_items(&self) -> &[T] {
|
|
self
|
|
.filtered_items
|
|
.as_ref()
|
|
.map_or(&self.items[..], |items| &items[..])
|
|
}
|
|
|
|
fn active_state_mut(&mut self) -> &mut TableState {
|
|
if let Some(ref mut filtered_state) = self.filtered_state {
|
|
filtered_state
|
|
} else {
|
|
&mut self.state
|
|
}
|
|
}
|
|
|
|
fn active_state(&self) -> &TableState {
|
|
self.filtered_state.as_ref().unwrap_or(&self.state)
|
|
}
|
|
}
|
|
|
|
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>) {
|
|
let items_len = filtered_items.len();
|
|
self.filtered_items = Some(filtered_items);
|
|
|
|
let preserved_selection = self
|
|
.filtered_state
|
|
.as_ref()
|
|
.and_then(|state| state.selected())
|
|
.map_or(0, |i| {
|
|
if i > 0 && i < items_len {
|
|
i
|
|
} else if i >= items_len && items_len > 0 {
|
|
items_len - 1
|
|
} else {
|
|
0
|
|
}
|
|
});
|
|
|
|
let mut filtered_state: TableState = Default::default();
|
|
filtered_state.select(Some(preserved_selection));
|
|
self.filtered_state = Some(filtered_state);
|
|
}
|
|
|
|
pub fn select_index(&mut self, index: Option<usize>) {
|
|
self.active_state_mut().select(index);
|
|
}
|
|
|
|
pub fn current_selection(&self) -> &T {
|
|
let items = self.active_items();
|
|
let index = self.active_state().selected().unwrap_or(0);
|
|
&items[index]
|
|
}
|
|
|
|
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);
|
|
} else {
|
|
self.set_items(items);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn apply_filter(&mut self, filter_field: fn(&T) -> &str) -> bool {
|
|
let filter_matches = match self.filter.take() {
|
|
Some(filter) if !filter.text.is_empty() => {
|
|
let scrubbed_filter = strip_non_search_characters(&filter.text);
|
|
|
|
self
|
|
.items
|
|
.iter()
|
|
.filter(|item| strip_non_search_characters(filter_field(item)).contains(&scrubbed_filter))
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
_ => Vec::new(),
|
|
};
|
|
|
|
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 = match self.search.take() {
|
|
Some(search) => {
|
|
let search_string = search.text.to_lowercase();
|
|
|
|
self
|
|
.filtered_items
|
|
.as_ref()
|
|
.unwrap_or(&self.items)
|
|
.iter()
|
|
.position(|item| strip_non_search_characters(search_field(item)).contains(&search_string))
|
|
}
|
|
_ => 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()
|
|
}
|
|
}
|