summaryrefslogtreecommitdiffstats
path: root/src/components/data_table/draw.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/data_table/draw.rs')
-rw-r--r--src/components/data_table/draw.rs289
1 files changed, 289 insertions, 0 deletions
diff --git a/src/components/data_table/draw.rs b/src/components/data_table/draw.rs
new file mode 100644
index 00000000..25f48deb
--- /dev/null
+++ b/src/components/data_table/draw.rs
@@ -0,0 +1,289 @@
+use std::{
+ cmp::{max, min},
+ iter::once,
+};
+
+use concat_string::concat_string;
+use tui::{
+ backend::Backend,
+ layout::{Constraint, Direction, Layout, Rect},
+ text::{Span, Spans, Text},
+ widgets::{Block, Borders, Row, Table},
+ Frame,
+};
+use unicode_segmentation::UnicodeSegmentation;
+
+use crate::{
+ app::layout_manager::BottomWidget,
+ canvas::Painter,
+ constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT},
+};
+
+use super::{
+ CalculateColumnWidths, ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataToCell,
+ SortType,
+};
+
+pub enum SelectionState {
+ NotSelected,
+ Selected,
+ Expanded,
+}
+
+impl SelectionState {
+ pub fn new(is_expanded: bool, is_on_widget: bool) -> Self {
+ if is_expanded {
+ SelectionState::Expanded
+ } else if is_on_widget {
+ SelectionState::Selected
+ } else {
+ SelectionState::NotSelected
+ }
+ }
+}
+
+/// A [`DrawInfo`] is information required on each draw call.
+pub struct DrawInfo {
+ pub loc: Rect,
+ pub force_redraw: bool,
+ pub recalculate_column_widths: bool,
+ pub selection_state: SelectionState,
+}
+
+impl DrawInfo {
+ pub fn is_on_widget(&self) -> bool {
+ matches!(self.selection_state, SelectionState::Selected)
+ || matches!(self.selection_state, SelectionState::Expanded)
+ }
+
+ pub fn is_expanded(&self) -> bool {
+ matches!(self.selection_state, SelectionState::Expanded)
+ }
+}
+
+impl<DataType, H, S, C> DataTable<DataType, H, S, C>
+where
+ DataType: DataToCell<H>,
+ H: ColumnHeader,
+ S: SortType,
+ C: DataTableColumn<H>,
+{
+ fn block<'a>(&self, draw_info: &'a DrawInfo, data_len: usize) -> Block<'a> {
+ let border_style = match draw_info.selection_state {
+ SelectionState::NotSelected => self.styling.border_style,
+ SelectionState::Selected | SelectionState::Expanded => {
+ self.styling.highlighted_border_style
+ }
+ };
+
+ if !self.props.is_basic {
+ let block = Block::default()
+ .borders(Borders::ALL)
+ .border_style(border_style);
+
+ if let Some(title) = self.generate_title(draw_info, data_len) {
+ block.title(title)
+ } else {
+ block
+ }
+ } else if draw_info.is_on_widget() {
+ // Implies it is basic mode but selected.
+ Block::default()
+ .borders(SIDE_BORDERS)
+ .border_style(border_style)
+ } else {
+ Block::default().borders(Borders::NONE)
+ }
+ }
+
+ /// Generates a title, given the available space.
+ pub fn generate_title<'a>(
+ &self, draw_info: &'a DrawInfo, total_items: usize,
+ ) -> Option<Spans<'a>> {
+ self.props.title.as_ref().map(|title| {
+ let current_index = self.state.current_index.saturating_add(1);
+ let draw_loc = draw_info.loc;
+ let title_style = self.styling.title_style;
+ let border_style = if draw_info.is_on_widget() {
+ self.styling.highlighted_border_style
+ } else {
+ self.styling.border_style
+ };
+
+ let title = if self.props.show_table_scroll_position {
+ let pos = current_index.to_string();
+ let tot = total_items.to_string();
+ let title_string = concat_string!(title, "(", pos, " of ", tot, ") ");
+
+ if title_string.len() + 2 <= draw_loc.width.into() {
+ title_string
+ } else {
+ title.to_string()
+ }
+ } else {
+ title.to_string()
+ };
+
+ if draw_info.is_expanded() {
+ let title_base = concat_string!(title, "── Esc to go back ");
+ let lines = "─".repeat(usize::from(draw_loc.width).saturating_sub(
+ UnicodeSegmentation::graphemes(title_base.as_str(), true).count() + 2,
+ ));
+ let esc = concat_string!("─", lines, "─ Esc to go back ");
+ Spans::from(vec![
+ Span::styled(title, title_style),
+ Span::styled(esc, border_style),
+ ])
+ } else {
+ Spans::from(Span::styled(title, title_style))
+ }
+ })
+ }
+
+ pub fn draw<B: Backend>(
+ &mut self, f: &mut Frame<'_, B>, draw_info: &DrawInfo, data: Vec<DataType>,
+ widget: Option<&mut BottomWidget>, painter: &Painter,
+ ) {
+ self.set_data(data);
+
+ let draw_horizontal = !self.props.is_basic || draw_info.is_on_widget();
+ let draw_loc = draw_info.loc;
+ let margined_draw_loc = Layout::default()
+ .constraints([Constraint::Percentage(100)])
+ .horizontal_margin(if draw_horizontal { 0 } else { 1 })
+ .direction(Direction::Horizontal)
+ .split(draw_loc)[0];
+
+ let block = self.block(draw_info, self.data.len());
+
+ let (inner_width, inner_height) = {
+ let inner_rect = block.inner(margined_draw_loc);
+ self.state.inner_rect = inner_rect;
+ (inner_rect.width, inner_rect.height)
+ };
+
+ if inner_width == 0 || inner_height == 0 {
+ f.render_widget(block, margined_draw_loc);
+ } else {
+ // Calculate widths
+ if draw_info.recalculate_column_widths {
+ let col_widths = DataType::column_widths(&self.data, &self.columns);
+
+ self.columns
+ .iter_mut()
+ .zip(&col_widths)
+ .for_each(|(column, &width)| {
+ let header_len = column.header_len() as u16;
+ if let ColumnWidthBounds::Soft {
+ desired,
+ max_percentage: _,
+ } = &mut column.bounds_mut()
+ {
+ *desired = max(header_len, width);
+ }
+ });
+
+ self.state.calculated_widths = self
+ .columns
+ .calculate_column_widths(inner_width, self.props.left_to_right);
+
+ // Update draw loc in widget map
+ if let Some(widget) = widget {
+ widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
+ widget.bottom_right_corner =
+ Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
+ }
+ }
+
+ 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.props.table_gap
+ };
+
+ let columns = &self.columns;
+ if !self.data.is_empty() || !self.first_draw {
+ self.first_draw = false; // TODO: Doing it this way is fine, but it could be done better (e.g. showing custom no results/entries message)
+
+ let rows = {
+ let num_rows =
+ usize::from(inner_height.saturating_sub(table_gap + header_height));
+ self.state
+ .get_start_position(num_rows, draw_info.force_redraw);
+ let start = self.state.display_start_index;
+ let end = min(self.data.len(), start + num_rows);
+ self.state
+ .table_state
+ .select(Some(self.state.current_index.saturating_sub(start)));
+
+ self.data[start..end].iter().map(|data_row| {
+ let row = Row::new(
+ columns
+ .iter()
+ .zip(&self.state.calculated_widths)
+ .filter_map(|(column, &width)| {
+ data_row.to_cell(column.inner(), width)
+ }),
+ );
+
+ data_row.style_row(row, painter)
+ })
+ };
+
+ let headers = self
+ .sort_type
+ .build_header(columns, &self.state.calculated_widths)
+ .style(self.styling.header_style)
+ .bottom_margin(table_gap);
+
+ let widget = {
+ let highlight_style = if draw_info.is_on_widget()
+ || self.props.show_current_entry_when_unfocused
+ {
+ self.styling.highlighted_text_style
+ } else {
+ self.styling.text_style
+ };
+ let mut table = Table::new(rows)
+ .block(block)
+ .highlight_style(highlight_style)
+ .style(self.styling.text_style);
+
+ if show_header {
+ table = table.header(headers);
+ }
+
+ table
+ };
+
+ let table_state = &mut self.state.table_state;
+ f.render_stateful_widget(
+ widget.widths(
+ &(self
+ .state
+ .calculated_widths
+ .iter()
+ .filter_map(|&width| {
+ if width == 0 {
+ None
+ } else {
+ Some(Constraint::Length(width))
+ }
+ })
+ .collect::<Vec<_>>()),
+ ),
+ margined_draw_loc,
+ table_state,
+ );
+ } else {
+ let table = Table::new(once(Row::new(Text::raw("No data"))))
+ .block(block)
+ .style(self.styling.text_style)
+ .widths(&[Constraint::Percentage(100)]);
+ f.render_widget(table, margined_draw_loc);
+ }
+ }
+ }
+}