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 where T: Clone + PartialEq + Eq + Debug, { pub name: &'static str, pub cmp_fn: Option Ordering>, } impl PartialEq for SortOption where T: Clone + PartialEq + Eq + Debug, { fn eq(&self, other: &Self) -> bool { self.name == other.name } } impl Eq for SortOption where T: Clone + PartialEq + Eq + Debug {} #[derive(Default)] #[cfg_attr(test, derive(Debug))] pub struct StatefulTable where T: Clone + PartialEq + Eq + Debug, { pub state: TableState, pub items: Vec, pub filter: Option, pub search: Option, pub filtered_items: Option>, pub filtered_state: Option, pub sort_asc: bool, pub sort: Option>>, } impl Scrollable for StatefulTable 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 Paginated for StatefulTable 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 StatefulTable 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 StatefulTable where T: Clone + PartialEq + Eq + Debug + Default, { pub fn set_items(&mut self, items: Vec) { 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) { 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) { 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>) { 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() } }