summaryrefslogtreecommitdiffstats
path: root/src/canvas/components/text_table.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/canvas/components/text_table.rs')
-rw-r--r--src/canvas/components/text_table.rs206
1 files changed, 178 insertions, 28 deletions
diff --git a/src/canvas/components/text_table.rs b/src/canvas/components/text_table.rs
index cf9788ad..f75a5eb3 100644
--- a/src/canvas/components/text_table.rs
+++ b/src/canvas/components/text_table.rs
@@ -1,4 +1,7 @@
-use std::{borrow::Cow, cmp::min};
+use std::{
+ borrow::Cow,
+ cmp::{max, min},
+};
use concat_string::concat_string;
use tui::{
@@ -12,9 +15,12 @@ use tui::{
use unicode_segmentation::UnicodeSegmentation;
use crate::{
- app::{self, TableComponentState},
+ app::{
+ self, CellContent, SortState, TableComponentColumn, TableComponentHeader,
+ TableComponentState,
+ },
constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT},
- data_conversion::{CellContent, TableData, TableRow},
+ data_conversion::{TableData, TableRow},
};
pub struct TextTableTitle<'a> {
@@ -101,8 +107,8 @@ impl<'a> TextTable<'a> {
}
})
}
- pub fn draw_text_table<B: Backend>(
- &self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState,
+ pub fn draw_text_table<B: Backend, H: TableComponentHeader>(
+ &self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState<H>,
table_data: &TableData,
) {
// TODO: This is a *really* ugly hack to get basic mode to hide the border when not selected, without shifting everything.
@@ -179,7 +185,7 @@ impl<'a> TextTable<'a> {
desired,
max_percentage: _,
} => {
- *desired = std::cmp::max(column.name.len(), *data_width) as u16;
+ *desired = max(column.header.header_text().len(), *data_width) as u16;
}
app::WidthBounds::Hard(_width) => {}
});
@@ -188,15 +194,9 @@ impl<'a> TextTable<'a> {
}
let columns = &state.columns;
- let header = Row::new(columns.iter().filter_map(|c| {
- if c.calculated_width == 0 {
- None
- } else {
- Some(truncate_text(&c.name, c.calculated_width.into(), None))
- }
- }))
- .style(self.header_style)
- .bottom_margin(table_gap);
+ let header = build_header(columns, &state.sort_state)
+ .style(self.header_style)
+ .bottom_margin(table_gap);
let table_rows = sliced_vec.iter().map(|row| {
let (row, style) = match row {
TableRow::Raw(row) => (row, None),
@@ -245,9 +245,60 @@ impl<'a> TextTable<'a> {
}
}
+/// Constructs the table header.
+fn build_header<'a, H: TableComponentHeader>(
+ columns: &'a [TableComponentColumn<H>], sort_state: &SortState,
+) -> Row<'a> {
+ use itertools::Either;
+
+ const UP_ARROW: &str = "▲";
+ const DOWN_ARROW: &str = "▼";
+
+ let iter = match sort_state {
+ SortState::Unsortable => Either::Left(columns.iter().filter_map(|c| {
+ if c.calculated_width == 0 {
+ None
+ } else {
+ Some(truncate_text(
+ c.header.header_text(),
+ c.calculated_width.into(),
+ None,
+ ))
+ }
+ })),
+ SortState::Sortable { index, order } => {
+ let arrow = match order {
+ app::SortOrder::Ascending => UP_ARROW,
+ app::SortOrder::Descending => DOWN_ARROW,
+ };
+
+ Either::Right(columns.iter().enumerate().filter_map(move |(itx, c)| {
+ if c.calculated_width == 0 {
+ None
+ } else if itx == *index {
+ Some(truncate_suffixed_text(
+ c.header.header_text(),
+ arrow,
+ c.calculated_width.into(),
+ None,
+ ))
+ } else {
+ Some(truncate_text(
+ c.header.header_text(),
+ c.calculated_width.into(),
+ None,
+ ))
+ }
+ }))
+ }
+ };
+
+ Row::new(iter)
+}
+
/// Truncates text if it is too long, and adds an ellipsis at the end if needed.
-fn truncate_text(content: &CellContent, width: usize, row_style: Option<Style>) -> Text<'_> {
- let (text, opt) = match content {
+fn truncate_text<'a>(content: &'a CellContent, width: usize, row_style: Option<Style>) -> Text<'a> {
+ let (main_text, alt_text) = match content {
CellContent::Simple(s) => (s, None),
CellContent::HasAlt {
alt: short,
@@ -255,18 +306,57 @@ fn truncate_text(content: &CellContent, width: usize, row_style: Option<Style>)
} => (long, Some(short)),
};
- let graphemes = UnicodeSegmentation::graphemes(text.as_ref(), true).collect::<Vec<&str>>();
- let mut text = if graphemes.len() > width && width > 0 {
- if let Some(s) = opt {
- // If an alternative exists, use that.
- Text::raw(s.as_ref())
+ let mut text = {
+ let graphemes: Vec<&str> =
+ UnicodeSegmentation::graphemes(main_text.as_ref(), true).collect();
+ if graphemes.len() > width && width > 0 {
+ if let Some(s) = alt_text {
+ // If an alternative exists, use that.
+ Text::raw(s.as_ref())
+ } else {
+ // Truncate with ellipsis
+ let first_n = graphemes[..(width - 1)].concat();
+ Text::raw(concat_string!(first_n, "…"))
+ }
} else {
- // Truncate with ellipsis
- let first_n = graphemes[..(width - 1)].concat();
- Text::raw(concat_string!(first_n, "…"))
+ Text::raw(main_text.as_ref())
+ }
+ };
+
+ if let Some(row_style) = row_style {
+ text.patch_style(row_style);
+ }
+
+ text
+}
+
+fn truncate_suffixed_text<'a>(
+ content: &'a CellContent, suffix: &str, width: usize, row_style: Option<Style>,
+) -> Text<'a> {
+ let (main_text, alt_text) = match content {
+ CellContent::Simple(s) => (s, None),
+ CellContent::HasAlt {
+ alt: short,
+ main: long,
+ } => (long, Some(short)),
+ };
+
+ let mut text = {
+ let suffixed = concat_string!(main_text, suffix);
+ let graphemes: Vec<&str> =
+ UnicodeSegmentation::graphemes(suffixed.as_str(), true).collect();
+ if graphemes.len() > width && width > 1 {
+ if let Some(alt) = alt_text {
+ // If an alternative exists, use that + arrow.
+ Text::raw(concat_string!(alt, suffix))
+ } else {
+ // Truncate with ellipsis + arrow.
+ let first_n = graphemes[..(width - 2)].concat();
+ Text::raw(concat_string!(first_n, "…", suffix))
+ }
+ } else {
+ Text::raw(suffixed)
}
- } else {
- Text::raw(text.as_ref())
};
if let Some(row_style) = row_style {
@@ -315,4 +405,64 @@ pub fn get_start_position(
}
#[cfg(test)]
-mod test {}
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_get_start_position() {
+ use crate::app::ScrollDirection::{self, Down, Up};
+
+ #[track_caller]
+
+ fn test_get(
+ bar: usize, num: usize, direction: ScrollDirection, selected: usize, force: bool,
+ expected_posn: usize, expected_bar: usize,
+ ) {
+ let mut bar = bar;
+ assert_eq!(
+ get_start_position(num, &direction, &mut bar, selected, force),
+ expected_posn
+ );
+ assert_eq!(bar, expected_bar);
+ }
+
+ // Scrolling down from start
+ test_get(0, 10, Down, 0, false, 0, 0);
+
+ // Simple scrolling down
+ test_get(0, 10, Down, 1, false, 0, 0);
+
+ // Scrolling down from the middle high up
+ test_get(0, 10, Down, 5, false, 0, 0);
+
+ // Scrolling down into boundary
+ test_get(0, 10, Down, 11, false, 1, 1);
+
+ // Scrolling down from the with non-zero bar
+ test_get(5, 10, Down, 15, false, 5, 5);
+
+ // Force redraw scrolling down (e.g. resize)
+ test_get(5, 15, Down, 15, true, 0, 0);
+
+ // Test jumping down
+ test_get(1, 10, Down, 20, true, 10, 10);
+
+ // Scrolling up from bottom
+ test_get(10, 10, Up, 20, false, 10, 10);
+
+ // Simple scrolling up
+ test_get(10, 10, Up, 19, false, 10, 10);
+
+ // Scrolling up from the middle
+ test_get(10, 10, Up, 10, false, 10, 10);
+
+ // Scrolling up into boundary
+ test_get(10, 10, Up, 9, false, 9, 9);
+
+ // Force redraw scrolling up (e.g. resize)
+ test_get(5, 10, Up, 15, true, 5, 5);
+
+ // Test jumping up
+ test_get(10, 10, Up, 0, false, 0, 0);
+ }
+}