diff options
author | ClementTsang <cjhtsang@uwaterloo.ca> | 2021-08-26 16:49:20 -0400 |
---|---|---|
committer | ClementTsang <cjhtsang@uwaterloo.ca> | 2021-08-28 04:16:12 -0400 |
commit | 0afc371eaa9a608861ff79f5a2164e578ac0014f (patch) | |
tree | ff38b34439369283ff59f2d1e05248452e3083a1 /src/app | |
parent | dd7e183ec8a152d5fda84c49a7f79141171e1448 (diff) |
refactor: start moving over drawing system
In particular, moving over table-style widgets
Diffstat (limited to 'src/app')
-rw-r--r-- | src/app/layout_manager.rs | 177 | ||||
-rw-r--r-- | src/app/widgets.rs | 20 | ||||
-rw-r--r-- | src/app/widgets/base/scrollable.rs | 8 | ||||
-rw-r--r-- | src/app/widgets/base/text_table.rs | 405 | ||||
-rw-r--r-- | src/app/widgets/battery.rs | 17 | ||||
-rw-r--r-- | src/app/widgets/cpu.rs | 1 | ||||
-rw-r--r-- | src/app/widgets/disk.rs | 35 | ||||
-rw-r--r-- | src/app/widgets/mem.rs | 1 | ||||
-rw-r--r-- | src/app/widgets/net.rs | 50 | ||||
-rw-r--r-- | src/app/widgets/process.rs | 27 | ||||
-rw-r--r-- | src/app/widgets/temp.rs | 31 |
11 files changed, 618 insertions, 154 deletions
diff --git a/src/app/layout_manager.rs b/src/app/layout_manager.rs index 065b89fb..a2a70a42 100644 --- a/src/app/layout_manager.rs +++ b/src/app/layout_manager.rs @@ -1,5 +1,7 @@ use crate::{ - app::{DiskTable, MemGraph, NetGraph, OldNetGraph, ProcessManager, TempTable}, + app::{ + text_table::Column, DiskTable, MemGraph, NetGraph, OldNetGraph, ProcessManager, TempTable, + }, error::{BottomError, Result}, options::layout_options::{Row, RowChildren}, }; @@ -12,7 +14,7 @@ use typed_builder::*; use crate::app::widgets::Widget; use crate::constants::DEFAULT_WIDGET_ID; -use super::{event::SelectionAction, CpuGraph, TextTable, TimeGraph, TmpBottomWidget}; +use super::{event::SelectionAction, CpuGraph, TextTable, TimeGraph, TmpBottomWidget, UsedWidgets}; /// Represents a more usable representation of the layout, derived from the /// config. @@ -985,49 +987,17 @@ Supported widget names: // --- New stuff --- /// Represents a row in the layout tree. -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Default)] pub struct RowLayout { last_selected_index: usize, - pub constraint: Constraint, -} - -impl Default for RowLayout { - fn default() -> Self { - Self { - last_selected_index: 0, - constraint: Constraint::Min(0), - } - } + pub constraints: Vec<Constraint>, } /// Represents a column in the layout tree. -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Default)] pub struct ColLayout { last_selected_index: usize, - pub constraint: Constraint, -} - -impl Default for ColLayout { - fn default() -> Self { - Self { - last_selected_index: 0, - constraint: Constraint::Min(0), - } - } -} - -/// Represents a widget in the layout tree. -#[derive(PartialEq, Eq)] -pub struct WidgetLayout { - pub constraint: Constraint, -} - -impl Default for WidgetLayout { - fn default() -> Self { - Self { - constraint: Constraint::Min(0), - } - } + pub constraints: Vec<Constraint>, } /// A [`LayoutNode`] represents a single node in the overall widget hierarchy. Each node is one of: @@ -1038,7 +1008,21 @@ impl Default for WidgetLayout { pub enum LayoutNode { Row(RowLayout), Col(ColLayout), - Widget(WidgetLayout), + Widget, +} + +impl LayoutNode { + pub fn set_constraints(&mut self, constraints: Vec<Constraint>) { + match self { + LayoutNode::Row(row) => { + row.constraints = constraints; + } + LayoutNode::Col(col) => { + col.constraints = constraints; + } + LayoutNode::Widget => {} + } + } } /// Relative movement direction from the currently selected widget. @@ -1054,7 +1038,8 @@ pub struct LayoutCreationOutput { pub layout_tree: Arena<LayoutNode>, pub root: NodeId, pub widget_lookup_map: FxHashMap<NodeId, TmpBottomWidget>, - pub selected: Option<NodeId>, + pub selected: NodeId, + pub used_widgets: UsedWidgets, } /// Creates a new [`Arena<LayoutNode>`] from the given config and returns it, along with the [`NodeId`] representing @@ -1066,14 +1051,17 @@ pub fn create_layout_tree( app_config_fields: &super::AppConfigFields, ) -> Result<LayoutCreationOutput> { fn add_widget_to_map( - widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, widget_type: &str, + widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, widget_type: BottomWidgetType, widget_id: NodeId, process_defaults: &crate::options::ProcessDefaults, app_config_fields: &super::AppConfigFields, ) -> Result<()> { - match widget_type.parse::<BottomWidgetType>()? { + match widget_type { BottomWidgetType::Cpu => { let graph = TimeGraph::from_config(app_config_fields); - let legend = TextTable::new(vec![("CPU", None, false), ("Use%", None, false)]); + let legend = TextTable::new(vec![ + Column::new_flex("CPU", None, false, 0.5), + Column::new_flex("Use%", None, false, 0.5), + ]); let legend_position = super::CpuGraphLegendPosition::Right; widget_lookup_map.insert( @@ -1100,20 +1088,10 @@ pub fn create_layout_tree( ); } BottomWidgetType::Temp => { - let table = TextTable::new(vec![("Sensor", None, false), ("Temp", None, false)]); - widget_lookup_map.insert(widget_id, TempTable::new(table).into()); + widget_lookup_map.insert(widget_id, TempTable::default().into()); } BottomWidgetType::Disk => { - let table = TextTable::new(vec![ - ("Disk", None, false), - ("Mount", None, false), - ("Used", None, false), - ("Free", None, false), - ("Total", None, false), - ("R/s", None, false), - ("W/s", None, false), - ]); - widget_lookup_map.insert(widget_id, DiskTable::new(table).into()); + widget_lookup_map.insert(widget_id, DiskTable::default().into()); } BottomWidgetType::Battery => {} _ => {} @@ -1125,19 +1103,20 @@ pub fn create_layout_tree( let mut layout_tree = Arena::new(); let root_id = layout_tree.new_node(LayoutNode::Col(ColLayout::default())); let mut widget_lookup_map = FxHashMap::default(); - let mut selected = None; + let mut first_selected = None; + let mut first_widget_seen = None; // Backup + let mut used_widgets = UsedWidgets::default(); let row_sum: u32 = rows.iter().map(|row| row.ratio.unwrap_or(1)).sum(); + let mut root_constraints = Vec::with_capacity(rows.len()); for row in rows { - let ratio = row.ratio.unwrap_or(1); - let layout_node = LayoutNode::Row(RowLayout { - constraint: Constraint::Ratio(ratio, row_sum), - ..Default::default() - }); + root_constraints.push(Constraint::Ratio(row.ratio.unwrap_or(1), row_sum)); + let layout_node = LayoutNode::Row(RowLayout::default()); let row_id = layout_tree.new_node(layout_node); root_id.append(row_id, &mut layout_tree); if let Some(cols) = &row.child { + let mut row_constraints = Vec::with_capacity(cols.len()); let col_sum: u32 = cols .iter() .map(|col| match col { @@ -1149,18 +1128,24 @@ pub fn create_layout_tree( for col in cols { match col { RowChildren::Widget(widget) => { - let widget_node = LayoutNode::Widget(WidgetLayout { - constraint: Constraint::Ratio(widget.ratio.unwrap_or(1), col_sum), - }); - let widget_id = layout_tree.new_node(widget_node); + row_constraints.push(Constraint::Ratio(widget.ratio.unwrap_or(1), col_sum)); + let widget_id = layout_tree.new_node(LayoutNode::Widget); row_id.append(widget_id, &mut layout_tree); if let Some(true) = widget.default { - selected = Some(widget_id); + first_selected = Some(widget_id); } + + if first_widget_seen.is_none() { + first_widget_seen = Some(widget_id); + } + + let widget_type = widget.widget_type.parse::<BottomWidgetType>()?; + used_widgets.add(&widget_type); + add_widget_to_map( &mut widget_lookup_map, - &widget.widget_type, + widget_type, widget_id, &process_defaults, app_config_fields, @@ -1170,45 +1155,73 @@ pub fn create_layout_tree( ratio, child: children, } => { - let col_node = LayoutNode::Col(ColLayout { - constraint: Constraint::Ratio(ratio.unwrap_or(1), col_sum), - ..Default::default() - }); + row_constraints.push(Constraint::Ratio(ratio.unwrap_or(1), col_sum)); + let col_node = LayoutNode::Col(ColLayout::default()); let col_id = layout_tree.new_node(col_node); row_id.append(col_id, &mut layout_tree); let child_sum: u32 = children.iter().map(|child| child.ratio.unwrap_or(1)).sum(); + let mut col_constraints = Vec::with_capacity(children.len()); for child in children { - let widget_node = LayoutNode::Widget(WidgetLayout { - constraint: Constraint::Ratio(child.ratio.unwrap_or(1), child_sum), - }); - let widget_id = layout_tree.new_node(widget_node); + col_constraints + .push(Constraint::Ratio(child.ratio.unwrap_or(1), child_sum)); + let widget_id = layout_tree.new_node(LayoutNode::Widget); col_id.append(widget_id, &mut layout_tree); if let Some(true) = child.default { - selected = Some(widget_id); + first_selected = Some(widget_id); } + + if first_widget_seen.is_none() { + first_widget_seen = Some(widget_id); + } + + let widget_type = child.widget_type.parse::<BottomWidgetType>()?; + used_widgets.add(&widget_type); + add_widget_to_map( &mut widget_lookup_map, - &child.widget_type, + widget_type, widget_id, &process_defaults, app_config_fields, )?; } + layout_tree[col_id] + .get_mut() + .set_constraints(col_constraints); } } } + layout_tree[row_id] + .get_mut() + .set_constraints(row_constraints); } } + layout_tree[root_id] + .get_mut() + .set_constraints(root_constraints); + + let selected: NodeId; + + if let Some(first_selected) = first_selected { + selected = first_selected; + } else if let Some(first_widget_seen) = first_widget_seen { + selected = first_widget_seen; + } else { + return Err(BottomError::ConfigError( + "A layout cannot contain zero widgets!".to_string(), + )); + } Ok(LayoutCreationOutput { layout_tree, root: root_id, widget_lookup_map, selected, + used_widgets, }) } @@ -1252,7 +1265,7 @@ pub fn move_widget_selection( .and_then(|(parent_id, parent_node)| match parent_node.get() { LayoutNode::Row(_) => Some((parent_id, current_id)), LayoutNode::Col(_) => find_first_row(layout_tree, parent_id), - LayoutNode::Widget(_) => None, + LayoutNode::Widget => None, }) } @@ -1273,7 +1286,7 @@ pub fn move_widget_selection( .and_then(|(parent_id, parent_node)| match parent_node.get() { LayoutNode::Row(_) => find_first_col(layout_tree, parent_id), LayoutNode::Col(_) => Some((parent_id, current_id)), - LayoutNode::Widget(_) => None, + LayoutNode::Widget => None, }) } @@ -1283,11 +1296,11 @@ pub fn move_widget_selection( match current_node.get() { LayoutNode::Row(RowLayout { last_selected_index, - constraint: _, + constraints: _, }) | LayoutNode::Col(ColLayout { last_selected_index, - constraint: _, + constraints: _, }) => { if let Some(next_child) = current_id.children(layout_tree).nth(*last_selected_index) @@ -1297,7 +1310,7 @@ pub fn move_widget_selection( current_id } } - LayoutNode::Widget(_) => { + LayoutNode::Widget => { // Halt! current_id } diff --git a/src/app/widgets.rs b/src/app/widgets.rs index d5ba3586..a99fe7b8 100644 --- a/src/app/widgets.rs +++ b/src/app/widgets.rs @@ -2,13 +2,19 @@ use std::time::Instant; use crossterm::event::{KeyEvent, MouseEvent}; use enum_dispatch::enum_dispatch; -use tui::{layout::Rect, widgets::TableState}; +use tui::{ + backend::Backend, + layout::Rect, + widgets::{Block, TableState}, + Frame, +}; use crate::{ app::{ event::{EventResult, SelectionAction}, layout_manager::BottomWidgetType, }, + canvas::{DisplayableData, Painter}, constants, }; @@ -64,6 +70,7 @@ pub trait Component { /// A trait for actual fully-fledged widgets to be displayed in bottom. #[enum_dispatch] +#[allow(unused_variables)] pub trait Widget { /// Updates a [`Widget`] given some data. Defaults to doing nothing. fn update(&mut self) {} @@ -92,10 +99,21 @@ pub trait Widget { SelectionAction::NotHandled } + /// Returns a [`Widget`]'s "pretty" display name. fn get_pretty_name(&self) -> &'static str; + + /// Draws a [`Widget`]. Defaults to doing nothing. + fn draw<B: Backend>( + &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, block: Block<'_>, + data: &DisplayableData, + ) { + // TODO: Remove the default implementation in the future! + // TODO: Do another pass on ALL of the draw code - currently it's just glue, it should eventually be done properly! + } } /// The "main" widgets that are used by bottom to display information! +#[allow(clippy::large_enum_variant)] #[enum_dispatch(Component, Widget)] pub enum TmpBottomWidget { MemGraph, diff --git a/src/app/widgets/base/scrollable.rs b/src/app/widgets/base/scrollable.rs index 4606b02b..c180192a 100644 --- a/src/app/widgets/base/scrollable.rs +++ b/src/app/widgets/base/scrollable.rs @@ -131,13 +131,17 @@ impl Scrollable { self.num_items = num_items; if num_items <= self.current_index { - self.current_index = num_items - 1; + self.current_index = num_items.saturating_sub(1); } if num_items <= self.previous_index { - self.previous_index = num_items - 1; + self.previous_index = num_items.saturating_sub(1); } } + + pub fn num_items(&self) -> usize { + self.num_items + } } impl Component for Scrollable { diff --git a/src/app/widgets/base/text_table.rs b/src/app/widgets/base/text_table.rs index d5ff3d2c..fef2e31f 100644 --- a/src/app/widgets/base/text_table.rs +++ b/src/app/widgets/base/text_table.rs @@ -1,32 +1,132 @@ -use crossterm::event::{KeyEvent, MouseEvent}; -use tui::layout::Rect; +use std::{ + borrow::Cow, + cmp::{max, min}, +}; -use crate::app::{event::EventResult, Component, Scrollable}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent}; +use tui::{ + layout::{Constraint, Rect}, + text::Text, + widgets::{Table, TableState}, +}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::{ + app::{event::EventResult, Component, Scrollable}, + canvas::Painter, + constants::TABLE_GAP_HEIGHT_LIMIT, +}; + +/// Represents the desired widths a column tries to have. +#[derive(Clone, Debug)] +pub enum DesiredColumnWidth { + Hard(u16), + Flex { desired: u16, max_percentage: f64 }, +} /// A [`Column`] represents some column in a [`TextTable`]. +#[derive(Debug)] pub struct Column { pub name: &'static str, - pub shortcut: Option<KeyEvent>, + pub shortcut: Option<(KeyEvent, String)>, pub default_descending: bool, // TODO: I would remove these in the future, storing them here feels weird... - pub desired_column_width: u16, - pub calculated_column_width: u16, + pub desired_width: DesiredColumnWidth, pub x_bounds: (u16, u16), } impl Column { - /// Creates a new [`Column`], given a name and optional shortcut. - pub fn new(name: &'static str, shortcut: Option<KeyEvent>, default_descending: bool) -> Self { + /// Creates a new [`Column`]. + pub fn new( + name: &'static str, shortcut: Option<KeyEvent>, default_descending: bool, + desired_width: DesiredColumnWidth, + ) -> Self { Self { name, - desired_column_width: 0, - calculated_column_width: 0, x_bounds: (0, 0), - shortcut, + 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, + /// it will instead use the name's length. + pub fn new_hard( + name: &'static str, shortcut: Option<KeyEvent>, default_descending: bool, + hard_length: Option<u16>, + ) -> Self { + Column::new( + name, + shortcut, + default_descending, + DesiredColumnWidth::Hard(hard_length.unwrap_or(name.len() as u16)), + ) + } + + /// 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( + name, + shortcut, + default_descending, + DesiredColumnWidth::Flex { + desired: name.len() as u16, + max_percentage, + }, + ) + } +} + +#[derive(Clone)] +enum CachedColumnWidths { + Uncached, + Cached { + cached_area: Rect, + cached_data: Vec<u16>, + }, } /// A sortable, scrollable table with columns. @@ -37,6 +137,9 @@ pub struct TextTable { /// The columns themselves. columns: Vec<Column>, + /// Cached column width data. + cached_column_widths: CachedColumnWidths, + /// Whether to show a gap between the column headers and the columns. show_gap: bool, @@ -48,30 +151,30 @@ pub struct TextTable { /// Whether we're sorting by ascending order. sort_ascending: bool, + + /// Whether we draw columns from left-to-right. + left_to_right: bool, } impl TextTable { - pub fn new(columns: Vec<(&'static str, Option<KeyEvent>, bool)>) -> Self { + pub fn new(columns: Vec<Column>) -> Self { Self { scrollable: Scrollable::new(0), - columns: columns - .into_iter() - .map(|(name, shortcut, default_descending)| Column { - name, - desired_column_width: 0, - calculated_column_width: 0, - x_bounds: (0, 0), - shortcut, - default_descending, - }) - .collect(), + 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 { + self.left_to_right = ltr; + self + } + pub fn try_show_gap(mut self, show_gap: bool) -> Self { self.show_gap = show_gap; self @@ -82,28 +185,48 @@ impl TextTable { self } - pub fn update_bounds(&mut self, new_bounds: Rect) { - self.bounds = new_bounds; + pub fn column_names(&self) -> Vec<&'static str> { + self.columns.iter().map(|column| column.name).collect() } - pub fn update_calculated_column_bounds(&mut self, calculated_bounds: &[u16]) { - self.columns - .iter_mut() - .zip(calculated_bounds.iter()) - .for_each(|(column, bound)| column.calculated_column_width = *bound); - } + pub fn sorted_column_names(&self) -> Vec<String> { + const UP_ARROW: char = '▲'; + const DOWN_ARROW: char = '▼'; - pub fn desired_column_bounds(&self) -> Vec<u16> { self.columns .iter() - .map(|column| column.desired_column_width) + .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 { + "" + } + ) + } + }) .collect() } - pub fn column_names(&self) -> Vec<&'static str> { - self.columns.iter().map(|column| column.name).collect() - } - pub fn update_num_items(&mut self, num_items: usize) { self.scrollable.update_num_items(num_items); } @@ -114,18 +237,216 @@ impl TextTable { } } - pub fn update_columns(&mut self, columns: Vec<Column>) { - self.columns = columns; - if self.columns.len() <= self.sort_index { - self.sort_index = self.columns.len() - 1; + pub fn get_desired_column_widths( + columns: &[Column], data: &[Vec<String>], + ) -> Vec<DesiredColumnWidth> { + columns + .iter() + .enumerate() + .map(|(column_index, c)| match c.desired_width { + DesiredColumnWidth::Hard(width) => { + let max_len = data + .iter() + .filter_map(|c| c.get(column_index)) + .max_by(|x, y| x.len().cmp(&y.len())) + .map(|s| s.len()) + .unwrap_or(0) as u16; + + DesiredColumnWidth::Hard(max(max_len, width)) + } + DesiredColumnWidth::Flex { + desired: _, + max_percentage: _, + } => c.desired_width.clone(), + }) + .collect::<Vec<_>>() + } + + fn get_cache(&mut self, area: Rect, data: &[Vec<String>]) -> Vec<u16> { + fn calculate_column_widths( + left_to_right: bool, mut desired_widths: Vec<DesiredColumnWidth>, total_width: u16, + ) -> Vec<u16> { + debug!("OG desired widths: {:?}", desired_widths); + let mut total_width_left = total_width; + if !left_to_right { + desired_widths.reverse(); + } + debug!("Desired widths: {:?}", desired_widths); + + let mut column_widths: Vec<u16> = Vec::with_capacity(desired_widths.len()); + for width in desired_widths { + match width { + DesiredColumnWidth::Hard(width) => { + if width > total_width_left { + break; + } else { + column_widths.push(width); + total_width_left = total_width_left.saturating_sub(width + 1); + } + } + DesiredColumnWidth::Flex { + desired, + max_percentage, + } => { + if desired > total_width_left { + break; + } else { + let calculated_width = min( + max(desired, (max_percentage * total_width as f64).ceil() as u16), + total_width_left, + ); + + column_widths.push(calculated_width); + total_width_left = + total_width_left.saturating_sub(calculated_width + 1); + } + } + } + } + debug!("Initial column widths: {:?}", column_widths); + + if !column_widths.is_empty() { + let amount_per_slot = total_width_left / column_widths.len() as u16; + total_width_left %= column_widths.len() as u16; + for (itx, width) in column_widths.iter_mut().enumerate() { + if (itx as u16) < total_width_left { + *width += amount_per_slot + 1; + } else { + *width += amount_per_slot; + } + } + + if !left_to_right { + column_widths.reverse(); + } + } + + debug!("Column widths: {:?}", column_widths); + + column_widths + } + + // If empty, do NOT save the cache! We have to get it again when it updates. + if data.is_empty() { + vec![0; self.columns.len()] + } else { + match &mut self.cached_column_widths { + CachedColumnWidths::Uncached => { + // Always recalculate. + let desired_widths = TextTable::get_desired_column_widths(&self.columns, data); + let calculated_widths = + calculate_column_widths(self.left_to_right, desired_widths, area.width); + self.cached_column_widths = CachedColumnWidths::Cached { + cached_area: area, + cached_data: calculated_widths.clone(), + }; + + calculated_widths + } + CachedColumnWidths::Cached { + cached_area, + cached_data, + } => { + if *cached_area != area { + // Recalculate! + let desired_widths = + TextTable::get_desired_column_widths(&self.columns, data); + let calculated_widths = + calculate_column_widths(self.left_to_right, desired_widths, area.width); + *cached_area = area; + *cached_data = calculated_widths.clone(); + + calculated_widths + } else { + cached_data.clone() + } + } + } } } + + /// Creates a [`Table`] given the [`TextTable`] and the given data, along with its + /// widths (because for some reason a [`Table`] only borrows the constraints...?) + /// and [`TableState`] (so we know which row is selected). + /// + /// Note if the number of columns don't match in the [`TextTable`] and data, + /// it will only create as many columns as it can grab data from both sources from. + pub fn create_draw_table( + &mut self, painter: &Painter, data: &[Vec<String>], area: Rect, + ) -> (Table<'_>, Vec<Constraint>, TableState) { + // TODO: Change data: &[Vec<String>] to &[Vec<Cow<'static, str>>] + use tui::widgets::Row; + + let table_gap = if !self.show_gap || area.height < TABLE_GAP_HEIGHT_LIMIT { + 0 + } else { + 1 + }; + + self.set_bounds(area); + let scrollable_height = area.height.saturating_sub(1 + table_gap); + self.scrollable.set_bounds(Rect::new( + area.x, + area.y + 1 + table_gap, + area.width, + scrollable_height, + )); + self.update_num_items(data.len()); + + // Calculate widths first, since we need them later. + let calculated_widths = self.get_cache(area, data); + let widths = calculated_widths + .iter() + .map(|column| Constraint::Length(*column)) + .collect::<Vec<_>>(); + + // Then calculate rows. We truncate the amount of data read based on height, + // as well as truncating some entries based on available width. + let data_slice = { + let start = self.scrollable.index(); + let end = std::cmp::min( + self.scrollable.num_items(), + start + scrollable_height as usize, + ); + &data[start..end] + }; + let rows = data_slice.iter().map(|row| { + Row::new(row.iter().zip(&calculated_widths).map(|(cell, width)| { + let width = *width as usize; + let graphemes = + UnicodeSegmentation::graphemes(cell.as_str(), true).collect::<Vec<&str>>(); + let grapheme_width = graphemes.len(); + if width < grapheme_width && width > 1 { + Text::raw(format!("{}…", graphemes[..(width - 1)].concat())) + } else { + Text::raw(cell.to_owned()) + } + })) + }); + + // Now build up our headers... + let header = Row::new(self.sorted_column_names()) + .style(painter.colours.table_header_style) + .bottom_margin(table_gap); + + // And return tui-rs's [`TableState`]. + let mut tui |