summaryrefslogtreecommitdiffstats
path: root/src/canvas/components/data_table.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/canvas/components/data_table.rs')
-rw-r--r--src/canvas/components/data_table.rs247
1 files changed, 247 insertions, 0 deletions
diff --git a/src/canvas/components/data_table.rs b/src/canvas/components/data_table.rs
new file mode 100644
index 00000000..5fb4a591
--- /dev/null
+++ b/src/canvas/components/data_table.rs
@@ -0,0 +1,247 @@
+pub mod column;
+pub mod data_type;
+pub mod draw;
+pub mod props;
+pub mod sortable;
+pub mod state;
+pub mod styling;
+
+use std::{convert::TryInto, marker::PhantomData};
+
+pub use column::*;
+pub use data_type::*;
+pub use draw::*;
+pub use props::DataTableProps;
+pub use sortable::*;
+pub use state::{DataTableState, ScrollDirection};
+pub use styling::*;
+
+use crate::utils::general::ClampExt;
+
+/// 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<DataType, Header, S = Unsortable, C = Column<Header>> {
+ pub columns: Vec<C>,
+ pub state: DataTableState,
+ pub props: DataTableProps,
+ pub styling: DataTableStyling,
+ data: Vec<DataType>,
+ sort_type: S,
+ first_draw: bool,
+ first_index: Option<usize>,
+ _pd: PhantomData<(DataType, S, Header)>,
+}
+
+impl<DataType: DataToCell<H>, H: ColumnHeader> DataTable<DataType, H, Unsortable, Column<H>> {
+ pub fn new<C: Into<Vec<Column<H>>>>(
+ columns: C, props: DataTableProps, styling: DataTableStyling,
+ ) -> Self {
+ Self {
+ columns: columns.into(),
+ state: DataTableState::default(),
+ props,
+ styling,
+ data: vec![],
+ sort_type: Unsortable,
+ first_draw: true,
+ first_index: None,
+ _pd: PhantomData,
+ }
+ }
+}
+
+impl<DataType: DataToCell<H>, H: ColumnHeader, S: SortType, C: DataTableColumn<H>>
+ DataTable<DataType, H, S, C>
+{
+ /// Sets the default value selected on first initialization, if possible.
+ pub fn first_draw_index(mut self, first_index: usize) -> Self {
+ self.first_index = Some(first_index);
+ self
+ }
+
+ /// Sets the scroll position to the first value.
+ pub fn to_first(&mut self) {
+ self.state.current_index = 0;
+ self.state.scroll_direction = ScrollDirection::Up;
+ }
+
+ /// Sets the scroll position to the last value.
+ pub fn to_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<DataType>) {
+ 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<usize> {
+ 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<i64, _> = self.state.current_index.try_into();
+ if let Ok(csp) = csp {
+ let proposed: Result<usize, _> = (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_upper(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<usize> {
+ 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(
+ &self, _column: &&'static str, _calculated_width: u16,
+ ) -> Option<tui::text::Text<'_>> {
+ None
+ }
+
+ fn column_widths<C: DataTableColumn<&'static str>>(
+ _data: &[Self], _columns: &[C],
+ ) -> Vec<u16>
+ 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::<Vec<_>>());
+
+ table.to_last();
+ assert_eq!(table.current_index(), 4);
+ assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
+
+ table.to_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::<Vec<_>>());
+ assert_eq!(table.current_index(), 2);
+ assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
+ assert_eq!(table.current_item(), Some(&TestType { index: 2 }));
+ }
+}