use std::{convert::TryInto, marker::PhantomData}; pub mod column; pub use column::*; pub mod styling; pub use styling::*; pub mod props; pub use props::DataTableProps; pub mod state; pub use state::{DataTableState, ScrollDirection}; pub mod draw; pub use draw::*; pub mod data_type; pub use data_type::*; pub mod sortable; pub use sortable::*; /// A [`DataTable`] is a component that displays data in a tabular form. /// /// Note that [`DataTable`] takes a generic type `S`, bounded by [`SortType`]. This controls whether this table /// expects sorted data or not, with two expected types: /// /// - [`Unsortable`]: The default if otherwise not specified. This table does not expect sorted data. /// - [`Sortable`]: This table expects sorted data, and there are helper functions to /// facilitate things like sorting based on a selected column, shortcut column selection support, mouse column /// selection support, etc. pub struct DataTable> { pub columns: Vec, pub state: DataTableState, pub props: DataTableProps, pub styling: DataTableStyling, data: Vec, sort_type: S, first_draw: bool, _pd: PhantomData<(DataType, S, Header)>, } impl, H: ColumnHeader> DataTable> { pub fn new>>>( columns: C, props: DataTableProps, styling: DataTableStyling, ) -> Self { Self { columns: columns.into(), state: DataTableState::default(), props, styling, data: vec![], sort_type: Unsortable, first_draw: true, _pd: PhantomData, } } } impl, H: ColumnHeader, S: SortType, C: DataTableColumn> DataTable { /// Sets the scroll position to the first value. pub fn set_first(&mut self) { self.state.current_index = 0; self.state.scroll_direction = ScrollDirection::Up; } /// Sets the scroll position to the last value. pub fn set_last(&mut self) { self.state.current_index = self.data.len().saturating_sub(1); self.state.scroll_direction = ScrollDirection::Down; } /// Updates the scroll position to be valid for the number of entries. pub fn set_data(&mut self, data: Vec) { self.data = data; let max_pos = self.data.len().saturating_sub(1); if self.state.current_index > max_pos { self.state.current_index = max_pos; self.state.display_start_index = 0; self.state.scroll_direction = ScrollDirection::Down; } } /// Increments the scroll position if possible by a positive/negative offset. If there is a /// valid change, this function will also return the new position wrapped in an [`Option`]. pub fn increment_position(&mut self, change: i64) -> Option { let max_index = self.data.len(); let current_index = self.state.current_index; if change == 0 || (change > 0 && current_index == max_index) || (change < 0 && current_index == 0) { return None; } let csp: Result = self.state.current_index.try_into(); if let Ok(csp) = csp { let proposed: Result = (csp + change).try_into(); if let Ok(proposed) = proposed { if proposed < self.data.len() { self.state.current_index = proposed; self.state.scroll_direction = if change < 0 { ScrollDirection::Up } else { ScrollDirection::Down }; return Some(self.state.current_index); } } } None } /// Updates the scroll position to a selected index. #[allow(clippy::comparison_chain)] pub fn set_position(&mut self, new_index: usize) { let new_index = new_index.clamp(0, self.data.len().saturating_sub(1)); if self.state.current_index < new_index { self.state.scroll_direction = ScrollDirection::Down; } else if self.state.current_index > new_index { self.state.scroll_direction = ScrollDirection::Up; } self.state.current_index = new_index; } /// Returns the current scroll index. pub fn current_index(&self) -> usize { self.state.current_index } /// Optionally returns the currently selected item, if there is one. pub fn current_item(&self) -> Option<&DataType> { self.data.get(self.state.current_index) } /// Returns tui-rs' internal selection. pub fn tui_selected(&self) -> Option { self.state.table_state.selected() } } #[cfg(test)] mod test { use super::*; #[derive(Clone, PartialEq, Eq, Debug)] struct TestType { index: usize, } impl DataToCell<&'static str> for TestType { fn to_cell<'a>( &'a self, _column: &&'static str, _calculated_width: u16, ) -> Option> { None } fn column_widths>( _data: &[Self], _columns: &[C], ) -> Vec where Self: Sized, { vec![] } } #[test] fn test_data_table_operations() { let columns = [Column::hard("a", 10), Column::hard("b", 10)]; let props = DataTableProps { title: Some("test".into()), table_gap: 1, left_to_right: false, is_basic: false, show_table_scroll_position: true, show_current_entry_when_unfocused: false, }; let styling = DataTableStyling::default(); let mut table = DataTable::new(columns, props, styling); table.set_data((0..=4).map(|index| TestType { index }).collect::>()); table.set_last(); assert_eq!(table.current_index(), 4); assert_eq!(table.state.scroll_direction, ScrollDirection::Down); table.set_first(); assert_eq!(table.current_index(), 0); assert_eq!(table.state.scroll_direction, ScrollDirection::Up); table.set_position(4); assert_eq!(table.current_index(), 4); assert_eq!(table.state.scroll_direction, ScrollDirection::Down); table.set_position(100); assert_eq!(table.current_index(), 4); assert_eq!(table.state.scroll_direction, ScrollDirection::Down); assert_eq!(table.current_item(), Some(&TestType { index: 4 })); table.increment_position(-1); assert_eq!(table.current_index(), 3); assert_eq!(table.state.scroll_direction, ScrollDirection::Up); assert_eq!(table.current_item(), Some(&TestType { index: 3 })); table.increment_position(-3); assert_eq!(table.current_index(), 0); assert_eq!(table.state.scroll_direction, ScrollDirection::Up); assert_eq!(table.current_item(), Some(&TestType { index: 0 })); table.increment_position(-3); assert_eq!(table.current_index(), 0); assert_eq!(table.state.scroll_direction, ScrollDirection::Up); assert_eq!(table.current_item(), Some(&TestType { index: 0 })); table.increment_position(1); assert_eq!(table.current_index(), 1); assert_eq!(table.state.scroll_direction, ScrollDirection::Down); assert_eq!(table.current_item(), Some(&TestType { index: 1 })); table.increment_position(3); assert_eq!(table.current_index(), 4); assert_eq!(table.state.scroll_direction, ScrollDirection::Down); assert_eq!(table.current_item(), Some(&TestType { index: 4 })); table.increment_position(10); assert_eq!(table.current_index(), 4); assert_eq!(table.state.scroll_direction, ScrollDirection::Down); assert_eq!(table.current_item(), Some(&TestType { index: 4 })); table.set_data((0..=2).map(|index| TestType { index }).collect::>()); assert_eq!(table.current_index(), 2); assert_eq!(table.state.scroll_direction, ScrollDirection::Down); assert_eq!(table.current_item(), Some(&TestType { index: 2 })); } }