summaryrefslogtreecommitdiffstats
path: root/src/app
diff options
context:
space:
mode:
authorClementTsang <cjhtsang@uwaterloo.ca>2021-08-26 16:49:20 -0400
committerClementTsang <cjhtsang@uwaterloo.ca>2021-08-28 04:16:12 -0400
commit0afc371eaa9a608861ff79f5a2164e578ac0014f (patch)
treeff38b34439369283ff59f2d1e05248452e3083a1 /src/app
parentdd7e183ec8a152d5fda84c49a7f79141171e1448 (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.rs177
-rw-r--r--src/app/widgets.rs20
-rw-r--r--src/app/widgets/base/scrollable.rs8
-rw-r--r--src/app/widgets/base/text_table.rs405
-rw-r--r--src/app/widgets/battery.rs17
-rw-r--r--src/app/widgets/cpu.rs1
-rw-r--r--src/app/widgets/disk.rs35
-rw-r--r--src/app/widgets/mem.rs1
-rw-r--r--src/app/widgets/net.rs50
-rw-r--r--src/app/widgets/process.rs27
-rw-r--r--src/app/widgets/temp.rs31
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