summaryrefslogtreecommitdiffstats
path: root/src/app/widgets/base
diff options
context:
space:
mode:
authorClementTsang <cjhtsang@uwaterloo.ca>2021-08-28 01:17:46 -0400
committerClementTsang <cjhtsang@uwaterloo.ca>2021-08-28 04:16:32 -0400
commitb72e76aa71ef6fc4790ffd5b47ea1fb7c07bd464 (patch)
tree8c20c25e3ecd980b758bf7a446c938ee3c701eb2 /src/app/widgets/base
parent6b69e373def1eeecaed44ddb373814473eeb3dc2 (diff)
refactor: separate out sorted and non-sorted text tables
Diffstat (limited to 'src/app/widgets/base')
-rw-r--r--src/app/widgets/base/sort_text_table.rs275
-rw-r--r--src/app/widgets/base/text_table.rs271
2 files changed, 356 insertions, 190 deletions
diff --git a/src/app/widgets/base/sort_text_table.rs b/src/app/widgets/base/sort_text_table.rs
new file mode 100644
index 00000000..0c282d49
--- /dev/null
+++ b/src/app/widgets/base/sort_text_table.rs
@@ -0,0 +1,275 @@
+use std::borrow::Cow;
+
+use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
+use tui::{
+ layout::Rect,
+ widgets::{Table, TableState},
+};
+
+use crate::app::{event::EventResult, Component, TextTable};
+
+use super::text_table::{DesiredColumnWidth, SimpleColumn, TableColumn};
+
+fn get_shortcut_name(e: &KeyEvent) -> String {
+ let modifier = if e.modifiers.is_empty() {
+ ""
+ } else if let KeyModifiers::ALT = e.modifiers {
+ "Alt+"
+ } else if let KeyModifiers::SHIFT = e.modifiers {
+ "Shift+"
+ } else if let KeyModifiers::CONTROL = e.modifiers {
+ "Ctrl+"
+ } else {
+ // For now, that's all we support, though combos/more could be added.
+ ""
+ };
+
+ let key: Cow<'static, str> = match e.code {
+ KeyCode::Backspace => "Backspace".into(),
+ KeyCode::Enter => "Enter".into(),
+ KeyCode::Left => "Left".into(),
+ KeyCode::Right => "Right".into(),
+ KeyCode::Up => "Up".into(),
+ KeyCode::Down => "Down".into(),
+ KeyCode::Home => "Home".into(),
+ KeyCode::End => "End".into(),
+ KeyCode::PageUp => "PgUp".into(),
+ KeyCode::PageDown => "PgDown".into(),
+ KeyCode::Tab => "Tab".into(),
+ KeyCode::BackTab => "BackTab".into(),
+ KeyCode::Delete => "Del".into(),
+ KeyCode::Insert => "Insert".into(),
+ KeyCode::F(num) => format!("F{}", num).into(),
+ KeyCode::Char(c) => format!("{}", c).into(),
+ KeyCode::Null => "Null".into(),
+ KeyCode::Esc => "Esc".into(),
+ };
+
+ format!("({}{})", modifier, key).into()
+}
+
+#[derive(Debug)]
+enum SortStatus {
+ NotSorting,
+ SortAscending,
+ SortDescending,
+}
+
+/// A [`SortableColumn`] represents some column in a [`SortableTextTable`].
+#[derive(Debug)]
+pub struct SortableColumn {
+ pub shortcut: Option<(KeyEvent, String)>,
+ pub default_descending: bool,
+ pub internal: SimpleColumn,
+ sorting: SortStatus,
+}
+
+impl SortableColumn {
+ /// Creates a new [`SortableColumn`].
+ fn new(
+ shortcut_name: Cow<'static, str>, shortcut: Option<KeyEvent>, default_descending: bool,
+ desired_width: DesiredColumnWidth,
+ ) -> Self {
+ let shortcut = shortcut.map(|e| (e, get_shortcut_name(&e)));
+ Self {
+ shortcut,
+ default_descending,
+ internal: SimpleColumn::new(shortcut_name, desired_width),
+ sorting: SortStatus::NotSorting,
+ }
+ }
+
+ /// Creates a new [`SortableColumn`] with a hard desired width. If none is specified,
+ /// it will instead use the name's length + 1.
+ pub fn new_hard(
+ name: Cow<'static, str>, shortcut: Option<KeyEvent>, default_descending: bool,
+ hard_length: Option<u16>,
+ ) -> Self {
+ let shortcut_name = if let Some(shortcut) = shortcut {
+ get_shortcut_name(&shortcut).into()
+ } else {
+ name
+ };
+ let shortcut_name_len = shortcut_name.len();
+
+ SortableColumn::new(
+ shortcut_name,
+ shortcut,
+ default_descending,
+ DesiredColumnWidth::Hard(hard_length.unwrap_or(shortcut_name_len as u16 + 1)),
+ )
+ }
+
+ /// Creates a new [`SortableColumn`] with a flexible desired width.
+ pub fn new_flex(
+ name: Cow<'static, str>, shortcut: Option<KeyEvent>, default_descending: bool,
+ max_percentage: f64,
+ ) -> Self {
+ let shortcut_name = if let Some(shortcut) = shortcut {
+ get_shortcut_name(&shortcut).into()
+ } else {
+ name
+ };
+ let shortcut_name_len = shortcut_name.len();
+
+ SortableColumn::new(
+ shortcut_name,
+ shortcut,
+ default_descending,
+ DesiredColumnWidth::Flex {
+ desired: shortcut_name_len as u16,
+ max_percentage,
+ },
+ )
+ }
+}
+
+impl TableColumn for SortableColumn {
+ fn display_name(&self) -> Cow<'static, str> {
+ const UP_ARROW: &'static str = "▲";
+ const DOWN_ARROW: &'static str = "▼";
+ format!(
+ "{}{}",
+ self.internal.display_name(),
+ match &self.sorting {
+ SortStatus::NotSorting => "",
+ SortStatus::SortAscending => UP_ARROW,
+ SortStatus::SortDescending => DOWN_ARROW,
+ }
+ )
+ .into()
+ }
+
+ fn get_desired_width(&self) -> &DesiredColumnWidth {
+ self.internal.get_desired_width()
+ }
+
+ fn get_x_bounds(&self) -> Option<(u16, u16)> {
+ self.internal.get_x_bounds()
+ }
+
+ fn set_x_bounds(&mut self, x_bounds: Option<(u16, u16)>) {
+ self.internal.set_x_bounds(x_bounds)
+ }
+}
+
+/// A sortable, scrollable table with columns.
+pub struct SortableTextTable {
+ /// Which index we're sorting by.
+ sort_index: usize,
+
+ /// The underlying [`TextTable`].
+ pub table: TextTable<SortableColumn>,
+}
+
+impl SortableTextTable {
+ pub fn new(columns: Vec<SortableColumn>) -> Self {
+ let mut st = Self {
+ sort_index: 0,
+ table: TextTable::new(columns),
+ };
+ st.set_sort_index(0);
+ st
+ }
+
+ pub fn default_ltr(mut self, ltr: bool) -> Self {
+ self.table = self.table.default_ltr(ltr);
+ self
+ }
+
+ pub fn default_sort_index(mut self, index: usize) -> Self {
+ self.set_sort_index(index);
+ self
+ }
+
+ fn set_sort_index(&mut self, new_index: usize) {
+ if new_index == self.sort_index {
+ if let Some(column) = self.table.columns.get_mut(self.sort_index) {
+ match column.sorting {
+ SortStatus::NotSorting => {
+ if column.default_descending {
+ column.sorting = SortStatus::SortDescending;
+ } else {
+ column.sorting = SortStatus::SortAscending;
+ }
+ }
+ SortStatus::SortAscending => {
+ column.sorting = SortStatus::SortDescending;
+ }
+ SortStatus::SortDescending => {
+ column.sorting = SortStatus::SortAscending;
+ }
+ }
+ }
+ } else {
+ if let Some(column) = self.table.columns.get_mut(self.sort_index) {
+ column.sorting = SortStatus::NotSorting;
+ }
+
+ if let Some(column) = self.table.columns.get_mut(new_index) {
+ if column.default_descending {
+ column.sorting = SortStatus::SortDescending;
+ } else {
+ column.sorting = SortStatus::SortAscending;
+ }
+ }
+
+ self.sort_index = new_index;
+ }
+ }
+
+ /// Creates a [`Table`] representing the sort list.
+ pub fn create_sort_list(&mut self) -> (Table<'_>, TableState) {
+ todo!()
+ }
+}
+
+impl Component for SortableTextTable {
+ fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
+ for (index, column) in self.table.columns.iter().enumerate() {
+ if let Some((shortcut, _)) = column.shortcut {
+ if shortcut == event {
+ self.set_sort_index(index);
+ return EventResult::Redraw;
+ }
+ }
+ }
+
+ self.table.scrollable.handle_key_event(event)
+ }
+
+ fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
+ if let MouseEventKind::Down(MouseButton::Left) = event.kind {
+ if !self.does_intersect_mouse(&event) {
+ return EventResult::NoRedraw;
+ }
+
+ // Note these are representing RELATIVE coordinates! They *need* the above intersection check for validity!
+ let x = event.column - self.table.bounds.left();
+ let y = event.row - self.table.bounds.top();
+
+ if y == 0 {
+ for (index, column) in self.table.columns.iter().enumerate() {
+ if let Some((start, end)) = column.internal.get_x_bounds() {
+ if x >= start && x <= end {
+ self.set_sort_index(index);
+ return EventResult::Redraw;
+ }
+ }
+ }
+ }
+
+ self.table.scrollable.handle_mouse_event(event)
+ } else {
+ self.table.scrollable.handle_mouse_event(event)
+ }
+ }
+
+ fn bounds(&self) -> Rect {
+ self.table.bounds
+ }
+
+ fn set_bounds(&mut self, new_bounds: Rect) {
+ self.table.bounds = new_bounds;
+ }
+}
diff --git a/src/app/widgets/base/text_table.rs b/src/app/widgets/base/text_table.rs
index 59395578..0edda286 100644
--- a/src/app/widgets/base/text_table.rs
+++ b/src/app/widgets/base/text_table.rs
@@ -1,9 +1,9 @@
use std::{
borrow::Cow,
- cmp::{max, min, Ordering},
+ cmp::{max, min},
};
-use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
+use crossterm::event::{KeyEvent, MouseEvent};
use tui::{
layout::{Constraint, Rect},
text::Text,
@@ -24,110 +24,79 @@ pub enum DesiredColumnWidth {
Flex { desired: u16, max_percentage: f64 },
}
-/// A [`ColumnType`] is a
-pub trait ColumnType {
- type DataType;
+/// A trait that must be implemented for anything using a [`TextTable`].
+#[allow(unused_variables)]
+pub trait TableColumn {
+ fn display_name(&self) -> Cow<'static, str>;
- fn sort_function(a: Self::DataType, b: Self::DataType) -> Ordering;
+ fn get_desired_width(&self) -> &DesiredColumnWidth;
+
+ fn get_x_bounds(&self) -> Option<(u16, u16)>;
+
+ fn set_x_bounds(&mut self, x_bounds: Option<(u16, u16)>);
}
-/// A [`Column`] represents some column in a [`TextTable`].
+/// A [`SimpleColumn`] represents some column in a [`TextTable`].
#[derive(Debug)]
-pub struct Column {
- pub name: &'static str,
- pub shortcut: Option<(KeyEvent, String)>,
- pub default_descending: bool,
+pub struct SimpleColumn {
+ name: Cow<'static, str>,
// TODO: I would remove these in the future, storing them here feels weird...
- pub desired_width: DesiredColumnWidth,
- pub x_bounds: Option<(u16, u16)>,
+ desired_width: DesiredColumnWidth,
+ x_bounds: Option<(u16, u16)>,
}
-impl Column {
- /// Creates a new [`Column`].
- pub fn new(
- name: &'static str, shortcut: Option<KeyEvent>, default_descending: bool,
- desired_width: DesiredColumnWidth,
- ) -> Self {
+impl SimpleColumn {
+ /// Creates a new [`SimpleColumn`].
+ pub fn new(name: Cow<'static, str>, desired_width: DesiredColumnWidth) -> Self {
Self {
name,
x_bounds: None,
- shortcut: shortcut.map(|e| {
- let modifier = if e.modifiers.is_empty() {
- ""
- } else if let KeyModifiers::ALT = e.modifiers {
- "Alt+"
- } else if let KeyModifiers::SHIFT = e.modifiers {
- "Shift+"
- } else if let KeyModifiers::CONTROL = e.modifiers {
- "Ctrl+"
- } else {
- // For now, that's all we support, though combos/more could be added.
- ""
- };
-
- let key: Cow<'static, str> = match e.code {
- KeyCode::Backspace => "Backspace".into(),
- KeyCode::Enter => "Enter".into(),
- KeyCode::Left => "Left".into(),
- KeyCode::Right => "Right".into(),
- KeyCode::Up => "Up".into(),
- KeyCode::Down => "Down".into(),
- KeyCode::Home => "Home".into(),
- KeyCode::End => "End".into(),
- KeyCode::PageUp => "PgUp".into(),
- KeyCode::PageDown => "PgDown".into(),
- KeyCode::Tab => "Tab".into(),
- KeyCode::BackTab => "BackTab".into(),
- KeyCode::Delete => "Del".into(),
- KeyCode::Insert => "Insert".into(),
- KeyCode::F(num) => format!("F{}", num).into(),
- KeyCode::Char(c) => format!("{}", c).into(),
- KeyCode::Null => "Null".into(),
- KeyCode::Esc => "Esc".into(),
- };
-
- let shortcut_name = format!("({}{})", modifier, key);
-
- (e, shortcut_name)
- }),
- default_descending,
desired_width,
}
}
- /// Creates a new [`Column`] with a hard desired width. If none is specified,
+ /// Creates a new [`SimpleColumn`] with a hard desired width. If none is specified,
/// it will instead use the name's length + 1.
- pub fn new_hard(
- name: &'static str, shortcut: Option<KeyEvent>, default_descending: bool,
- hard_length: Option<u16>,
- ) -> Self {
- // TODO: It should really be based on the shortcut name...
- Column::new(
+ pub fn new_hard(name: Cow<'static, str>, hard_length: Option<u16>) -> Self {
+ let name_len = name.len();
+ SimpleColumn::new(
name,
- shortcut,
- default_descending,
- DesiredColumnWidth::Hard(hard_length.unwrap_or(name.len() as u16 + 1)),
+ DesiredColumnWidth::Hard(hard_length.unwrap_or(name_len as u16 + 1)),
)
}
- /// Creates a new [`Column`] with a flexible desired width.
- pub fn new_flex(
- name: &'static str, shortcut: Option<KeyEvent>, default_descending: bool,
- max_percentage: f64,
- ) -> Self {
- Column::new(
+ /// Creates a new [`SimpleColumn`] with a flexible desired width.
+ pub fn new_flex(name: Cow<'static, str>, max_percentage: f64) -> Self {
+ let name_len = name.len();
+ SimpleColumn::new(
name,
- shortcut,
- default_descending,
DesiredColumnWidth::Flex {
- desired: name.len() as u16,
+ desired: name_len as u16,
max_percentage,
},
)
}
}
+impl TableColumn for SimpleColumn {
+ fn display_name(&self) -> Cow<'static, str> {
+ self.name.clone()
+ }
+
+ fn get_desired_width(&self) -> &DesiredColumnWidth {
+ &self.desired_width
+ }
+
+ fn get_x_bounds(&self) -> Option<(u16, u16)> {
+ self.x_bounds
+ }
+
+ fn set_x_bounds(&mut self, x_bounds: Option<(u16, u16)>) {
+ self.x_bounds = x_bounds;
+ }
+}
+
#[derive(Clone)]
enum CachedColumnWidths {
Uncached,
@@ -138,47 +107,45 @@ enum CachedColumnWidths {
}
/// A sortable, scrollable table with columns.
-pub struct TextTable {
+pub struct TextTable<C = SimpleColumn>
+where
+ C: TableColumn,
+{
/// Controls the scrollable state.
- scrollable: Scrollable,
+ pub scrollable: Scrollable,
/// The columns themselves.
- columns: Vec<Column>,
+ pub columns: Vec<C>,
/// Cached column width data.
cached_column_widths: CachedColumnWidths,
/// Whether to show a gap between the column headers and the columns.
- show_gap: bool,
+ pub show_gap: bool,
/// The bounding box of the [`TextTable`].
- bounds: Rect, // TODO: Consider moving bounds to something else???
-
- /// Which index we're sorting by.
- sort_index: usize,
-
- /// Whether we're sorting by ascending order.
- sort_ascending: bool,
+ pub bounds: Rect, // TODO: Consider moving bounds to something else???
/// Whether we draw columns from left-to-right.
- left_to_right: bool,
+ pub left_to_right: bool,
}
-impl TextTable {
- pub fn new(columns: Vec<Column>) -> Self {
+impl<C> TextTable<C>
+where
+ C: TableColumn,
+{
+ pub fn new(columns: Vec<C>) -> Self {
Self {
scrollable: Scrollable::new(0),
columns,
cached_column_widths: CachedColumnWidths::Uncached,
show_gap: true,
bounds: Rect::default(),
- sort_index: 0,
- sort_ascending: true,
left_to_right: true,
}
}
- pub fn left_to_right(mut self, ltr: bool) -> Self {
+ pub fn default_ltr(mut self, ltr: bool) -> Self {
self.left_to_right = ltr;
self
}
@@ -188,50 +155,10 @@ impl TextTable {
self
}
- pub fn sort_index(mut self, sort_index: usize) -> Self {
- self.sort_index = sort_index;
- self
- }
-
- pub fn column_names(&self) -> Vec<&'static str> {
- self.columns.iter().map(|column| column.name).collect()
- }
-
- pub fn sorted_column_names(&self) -> Vec<String> {
- const UP_ARROW: char = '▲';
- const DOWN_ARROW: char = '▼';
-
+ pub fn displayed_column_names(&self) -> Vec<Cow<'static, str>> {
self.columns
.iter()
- .enumerate()
- .map(|(index, column)| {
- if index == self.sort_index {
- format!(
- "{}{}{}",
- column.name,
- if let Some(shortcut) = &column.shortcut {
- shortcut.1.as_str()
- } else {
- ""
- },
- if self.sort_ascending {
- UP_ARROW
- } else {
- DOWN_ARROW
- }
- )
- } else {
- format!(
- "{}{}",
- column.name,
- if let Some(shortcut) = &column.shortcut {
- shortcut.1.as_str()
- } else {
- ""
- }
- )
- }
- })
+ .map(|column| column.display_name())
.collect()
}
@@ -239,19 +166,19 @@ impl TextTable {
self.scrollable.update_num_items(num_items);
}
- pub fn update_a_column(&mut self, index: usize, column: Column) {
+ pub fn update_single_column(&mut self, index: usize, column: C) {
if let Some(c) = self.columns.get_mut(index) {
*c = column;
}
}
pub fn get_desired_column_widths(
- columns: &[Column], data: &[Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>],
+ columns: &[C], data: &[Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>],
) -> Vec<DesiredColumnWidth> {
columns
.iter()
.enumerate()
- .map(|(column_index, c)| match c.desired_width {
+ .map(|(column_index, c)| match c.get_desired_width() {
DesiredColumnWidth::Hard(width) => {
let max_len = data
.iter()
@@ -274,12 +201,12 @@ impl TextTable {
.map(|(s, _)| s.len())
.unwrap_or(0) as u16;
- DesiredColumnWidth::Hard(max(max_len, width))
+ DesiredColumnWidth::Hard(max(max_len, *width))
}
DesiredColumnWidth::Flex {
desired: _,
max_percentage: _,
- } => c.desired_width.clone(),
+ } => c.get_desired_width().clone(),
})
.collect::<Vec<_>>()
}
@@ -390,7 +317,7 @@ impl TextTable {
let mut column_start = 0;
for (column, width) in self.columns.iter_mut().zip(&column_widths) {
let column_end = column_start + *width;
- column.x_bounds = Some((column_start, column_end));
+ column.set_x_bounds(Some((column_start, column_end)));
column_start = column_end + 1;
}
}
@@ -468,7 +395,7 @@ impl TextTable {
});
// Now build up our headers...
- let header = Row::new(self.sorted_column_names())
+ let header = Row::new(self.displayed_column_names())
.style(painter.colours.table_header_style)
.bottom_margin(table_gap);
@@ -483,59 +410,23 @@ impl TextTable {
tui_state,
)
}
+
+ /// Creates a [`Table`] representing the sort list.
+ pub fn create_sort_list(&mut self) -> (Table<'_>, TableState) {
+ todo!()
+ }
}
-impl Component for TextTable {
+impl<C> Component for TextTable<C>
+where
+ C: TableColumn,
+{
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
- for (index, column) in self.columns.iter().enumerate() {
- if let Some((shortcut, _)) = column.shortcut {
- if shortcut == event {
- if self.sort_index == index {
- // Just flip the sort if we're already sorting by this.
- self.sort_ascending = !self.sort_ascending;
- } else {
- self.sort_index = index;
- self.sort_ascending = !column.default_descending;
- }
- return EventResult::Redraw;
- }
- }
- }
-
self.scrollable.handle_key_event(event)
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
- if let MouseEventKind::Down(MouseButton::Left) = event.kind {
- if !self.does_intersect_mouse(&event) {
- return EventResult::NoRedraw;
- }
-
- // Note these are representing RELATIVE coordinates! They *need* the above intersection check for validity!
- let x = event.column - self.bounds.left();
- let y = event.row - self.bounds.top();
-
- if y == 0 {
- for (index, column) in self.columns.iter().enumerate() {
- if let Some((start, end)) = column.x_bounds {
- if x >= start && x <= end {
- if self.sort_index == index {
- // Just flip the sort if we're already sorting by this.
- self.sort_ascending = !self.sort_ascending;
- } else {
- self.sort_index = index;
- self.sort_ascending = !column.default_descending;
- }
- return EventResult::Redraw;
- }
- }
- }
- }
-
- self.scrollable.handle_mouse_event(event)
- } else {
- self.scrollable.handle_mouse_event(event)
- }
+ self.scrollable.handle_mouse_event(event)
}
fn bounds(&self) -> Rect {