diff options
Diffstat (limited to 'src/components/text_table/draw.rs')
-rw-r--r-- | src/components/text_table/draw.rs | 505 |
1 files changed, 0 insertions, 505 deletions
diff --git a/src/components/text_table/draw.rs b/src/components/text_table/draw.rs deleted file mode 100644 index e62734b6..00000000 --- a/src/components/text_table/draw.rs +++ /dev/null @@ -1,505 +0,0 @@ -use std::{ - borrow::Cow, - cmp::{max, min}, -}; - -use concat_string::concat_string; -use tui::{ - backend::Backend, - layout::{Constraint, Direction, Layout, Rect}, - style::Style, - text::{Span, Spans, Text}, - widgets::{Block, Borders, Row, Table}, - Frame, -}; -use unicode_segmentation::UnicodeSegmentation; - -use crate::{ - app::{self, layout_manager::BottomWidget}, - components::text_table::SortOrder, - constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT}, - data_conversion::{TableData, TableRow}, -}; - -use super::{ - CellContent, SortState, TableComponentColumn, TableComponentHeader, TableComponentState, - WidthBounds, -}; - -pub struct TextTableTitle<'a> { - pub title: Cow<'a, str>, - pub is_expanded: bool, -} - -pub struct TextTable<'a> { - pub table_gap: u16, - pub is_force_redraw: bool, // TODO: Is this force redraw thing needed? Or is there a better way? - pub recalculate_column_widths: bool, - - /// The header style. - pub header_style: Style, - - /// The border style. - pub border_style: Style, - - /// The highlighted text style. - pub highlighted_text_style: Style, - - /// The graph title and whether it is expanded (if there is one). - pub title: Option<TextTableTitle<'a>>, - - /// Whether this widget is selected. - pub is_on_widget: bool, - - /// Whether to draw all borders. - pub draw_border: bool, - - /// Whether to show the scroll position. - pub show_table_scroll_position: bool, - - /// The title style. - pub title_style: Style, - - /// The text style. - pub text_style: Style, - - /// Whether to determine widths from left to right. - pub left_to_right: bool, -} - -impl<'a> TextTable<'a> { - /// Generates a title for the [`TextTable`] widget, given the available space. - fn generate_title(&self, draw_loc: Rect, pos: usize, total: usize) -> Option<Spans<'_>> { - self.title - .as_ref() - .map(|TextTableTitle { title, is_expanded }| { - let title = if self.show_table_scroll_position { - let title_string = concat_string!( - title, - "(", - pos.to_string(), - " of ", - total.to_string(), - ") " - ); - - if title_string.len() + 2 <= draw_loc.width.into() { - title_string - } else { - title.to_string() - } - } else { - title.to_string() - }; - - if *is_expanded { - let title_base = concat_string!(title, "── Esc to go back "); - let esc = concat_string!( - "─", - "─".repeat(usize::from(draw_loc.width).saturating_sub( - UnicodeSegmentation::graphemes(title_base.as_str(), true).count() + 2 - )), - "─ Esc to go back " - ); - Spans::from(vec![ - Span::styled(title, self.title_style), - Span::styled(esc, self.border_style), - ]) - } else { - Spans::from(Span::styled(title, self.title_style)) - } - }) - } - - pub fn draw_text_table<B: Backend, H: TableComponentHeader>( - &self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState<H>, - table_data: &TableData, btm_widget: Option<&mut BottomWidget>, - ) { - // TODO: This is a *really* ugly hack to get basic mode to hide the border when not selected, without shifting everything. - let is_not_basic = self.is_on_widget || self.draw_border; - let margined_draw_loc = Layout::default() - .constraints([Constraint::Percentage(100)]) - .horizontal_margin(if is_not_basic { 0 } else { 1 }) - .direction(Direction::Horizontal) - .split(draw_loc)[0]; - - let block = if self.draw_border { - let block = Block::default() - .borders(Borders::ALL) - .border_style(self.border_style); - - if let Some(title) = self.generate_title( - draw_loc, - state.current_scroll_position.saturating_add(1), - table_data.data.len(), - ) { - block.title(title) - } else { - block - } - } else if self.is_on_widget { - Block::default() - .borders(SIDE_BORDERS) - .border_style(self.border_style) - } else { - Block::default().borders(Borders::NONE) - }; - - let inner_rect = block.inner(margined_draw_loc); - let (inner_width, inner_height) = { (inner_rect.width, inner_rect.height) }; - - if inner_width == 0 || inner_height == 0 { - f.render_widget(block, margined_draw_loc); - } else { - let show_header = inner_height > 1; - let header_height = if show_header { 1 } else { 0 }; - let table_gap = if !show_header || draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { - 0 - } else { - self.table_gap - }; - - let sliced_vec = { - let num_rows = usize::from(inner_height.saturating_sub(table_gap + header_height)); - let start = get_start_position( - num_rows, - &state.scroll_direction, - &mut state.scroll_bar, - state.current_scroll_position, - self.is_force_redraw, - ); - let end = min(table_data.data.len(), start + num_rows); - state - .table_state - .select(Some(state.current_scroll_position.saturating_sub(start))); - &table_data.data[start..end] - }; - - // Calculate widths - if self.recalculate_column_widths { - state - .columns - .iter_mut() - .zip(&table_data.col_widths) - .for_each(|(column, data_width)| match &mut column.width_bounds { - WidthBounds::Soft { - min_width: _, - desired, - max_percentage: _, - } => { - *desired = max( - *desired, - max(column.header.header_text().len(), *data_width) as u16, - ); - } - WidthBounds::CellWidth => {} - WidthBounds::Hard(_width) => {} - }); - - state.calculate_column_widths(inner_width, self.left_to_right); - - if let SortState::Sortable(st) = &mut state.sort_state { - let row_widths = state - .columns - .iter() - .filter_map(|c| { - if c.calculated_width == 0 { - None - } else { - Some(c.calculated_width) - } - }) - .collect::<Vec<_>>(); - - st.update_visual_index(inner_rect, &row_widths); - } - - // Update draw loc in widget map - if let Some(btm_widget) = btm_widget { - btm_widget.top_left_corner = Some((draw_loc.x, draw_loc.y)); - btm_widget.bottom_right_corner = - Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height)); - } - } - - let columns = &state.columns; - 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), - TableRow::Styled(row, style) => (row, Some(*style)), - }; - - Row::new(row.iter().zip(columns).filter_map(|(cell, c)| { - if c.calculated_width == 0 { - None - } else { - Some(truncate_text(cell, c.calculated_width.into(), style)) - } - })) - }); - - if !table_data.data.is_empty() { - let widget = { - let mut table = Table::new(table_rows) - .block(block) - .highlight_style(self.highlighted_text_style) - .style(self.text_style); - - if show_header { - table = table.header(header); - } - - table - }; - - f.render_stateful_widget( - widget.widths( - &(columns - .iter() - .filter_map(|c| { - if c.calculated_width == 0 { - None - } else { - Some(Constraint::Length(c.calculated_width)) - } - }) - .collect::<Vec<_>>()), - ), - margined_draw_loc, - &mut state.table_state, - ); - } else { - f.render_widget(block, margined_draw_loc); - } - } - } -} - -/// 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(s) => { - let order = &s.order; - let index = s.current_index; - - let arrow = match order { - SortOrder::Ascending => UP_ARROW, - 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 (main_text, alt_text) = match content { - CellContent::Simple(s) => (s, None), - CellContent::HasAlt { - alt: short, - main: long, - } => (long, Some(short)), - }; - - 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 { - 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) - } - }; - - if let Some(row_style) = row_style { - text.patch_style(row_style); - } - - text -} - -/// Gets the starting position of a table. -pub fn get_start_position( - num_rows: usize, scroll_direction: &app::ScrollDirection, scroll_position_bar: &mut usize, - currently_selected_position: usize, is_force_redraw: bool, -) -> usize { - if is_force_redraw { - *scroll_position_bar = 0; - } - - match scroll_direction { - app::ScrollDirection::Down => { - if currently_selected_position < *scroll_position_bar + num_rows { - // If, using previous_scrolled_position, we can see the element - // (so within that and + num_rows) just reuse the current previously scrolled position - *scroll_position_bar - } else if currently_selected_position >= num_rows { - // Else if the current position past the last element visible in the list, omit - // until we can see that element - *scroll_position_bar = currently_selected_position - num_rows + 1; - *scroll_position_bar - } else { - // Else, if it is not past the last element visible, do not omit anything - 0 - } - } - app::ScrollDirection::Up => { - if currently_selected_position <= *scroll_position_bar { - // If it's past the first element, then show from that element downwards - *scroll_position_bar = currently_selected_position; - } else if currently_selected_position >= *scroll_position_bar + num_rows { - *scroll_position_bar = currently_selected_position - num_rows + 1; - } - // Else, don't change what our start position is from whatever it is set to! - *scroll_position_bar - } - } -} - -#[cfg(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, rows: usize, direction: ScrollDirection, selected: usize, force: bool, - expected_posn: usize, expected_bar: usize, - ) { - let mut bar = bar; - assert_eq!( - get_start_position(rows, &direction, &mut bar, selected, force), - expected_posn, - "returned start position should match" - ); - assert_eq!(bar, expected_bar, "bar positions should match"); - } - - // 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, 4, false, 0, 0); - - // Scrolling down into boundary - test_get(0, 10, Down, 10, false, 1, 1); - test_get(0, 10, Down, 11, false, 2, 2); - - // Scrolling down from the with non-zero bar - test_get(5, 10, Down, 14, false, 5, 5); - - // Force redraw scrolling down (e.g. resize) - test_get(5, 15, Down, 14, true, 0, 0); - - // Test jumping down - test_get(1, 10, Down, 19, true, 10, 10); - - // Scrolling up from bottom - test_get(10, 10, Up, 19, false, 10, 10); - - // Simple scrolling up - test_get(10, 10, Up, 18, 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, 14, true, 5, 5); - - // Test jumping up - test_get(10, 10, Up, 0, false, 0, 0); - } -} |