From 2a65bc95fe7a20e81725e6f559aee015fba852cc Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Fri, 29 Apr 2022 15:47:05 -0400 Subject: [PATCH 01/16] refactor: consolidate disk and temp table drawing, refactor state Disk and temp tables now share the same drawing logic, as well as consolidating the "text table" states into one single state, as opposed to two separate states (one for scroll and one for width calculations). BTW I know this is kinda an ugly design - creating a giant struct to call a function - hopefully that's temporary, I want to do a bigger refactor to consolidate more stuff together and therefore avoid this problem, but baby steps, right? --- src/app.rs | 39 +++-- src/app/states.rs | 238 +++++++++++++++++++++---- src/canvas.rs | 6 +- src/canvas/components/text_table.rs | 256 +++++++++++++++++++++++++++ src/canvas/components/time_chart.rs | 24 +-- src/canvas/components/time_graph.rs | 1 - src/canvas/drawing_utils.rs | 8 +- src/canvas/widgets/disk_table.rs | 260 +++------------------------- src/canvas/widgets/temp_table.rs | 251 +++------------------------ src/data_conversion.rs | 40 ++++- src/options.rs | 4 +- 11 files changed, 591 insertions(+), 536 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5f9ac413..8e49dd3a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2216,8 +2216,8 @@ impl App { .temp_state .get_mut_widget_state(self.current_widget.widget_id) { - temp_widget_state.scroll_state.current_scroll_position = 0; - temp_widget_state.scroll_state.scroll_direction = ScrollDirection::Up; + temp_widget_state.table_state.current_scroll_position = 0; + temp_widget_state.table_state.scroll_direction = ScrollDirection::Up; } } BottomWidgetType::Disk => { @@ -2225,8 +2225,8 @@ impl App { .disk_state .get_mut_widget_state(self.current_widget.widget_id) { - disk_widget_state.scroll_state.current_scroll_position = 0; - disk_widget_state.scroll_state.scroll_direction = ScrollDirection::Up; + disk_widget_state.table_state.current_scroll_position = 0; + disk_widget_state.table_state.scroll_direction = ScrollDirection::Up; } } BottomWidgetType::CpuLegend => { @@ -2286,10 +2286,10 @@ impl App { .temp_state .get_mut_widget_state(self.current_widget.widget_id) { - if !self.canvas_data.temp_sensor_data.is_empty() { - temp_widget_state.scroll_state.current_scroll_position = - self.canvas_data.temp_sensor_data.len() - 1; - temp_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; + if !self.canvas_data.temp_sensor_data.data.is_empty() { + temp_widget_state.table_state.current_scroll_position = + self.canvas_data.temp_sensor_data.data.len() - 1; + temp_widget_state.table_state.scroll_direction = ScrollDirection::Down; } } } @@ -2298,10 +2298,10 @@ impl App { .disk_state .get_mut_widget_state(self.current_widget.widget_id) { - if !self.canvas_data.disk_data.is_empty() { - disk_widget_state.scroll_state.current_scroll_position = - self.canvas_data.disk_data.len() - 1; - disk_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; + if !self.canvas_data.disk_data.data.is_empty() { + disk_widget_state.table_state.current_scroll_position = + self.canvas_data.disk_data.data.len() - 1; + disk_widget_state.table_state.scroll_direction = ScrollDirection::Down; } } } @@ -2419,9 +2419,10 @@ impl App { .widget_states .get_mut(&self.current_widget.widget_id) { - temp_widget_state - .scroll_state - .update_position(num_to_change_by, self.canvas_data.temp_sensor_data.len()); + temp_widget_state.table_state.update_position( + num_to_change_by, + self.canvas_data.temp_sensor_data.data.len(), + ); } } @@ -2432,8 +2433,8 @@ impl App { .get_mut(&self.current_widget.widget_id) { disk_widget_state - .scroll_state - .update_position(num_to_change_by, self.canvas_data.disk_data.len()); + .table_state + .update_position(num_to_change_by, self.canvas_data.disk_data.data.len()); } } @@ -2981,7 +2982,7 @@ impl App { .get_widget_state(self.current_widget.widget_id) { if let Some(visual_index) = - temp_widget_state.scroll_state.table_state.selected() + temp_widget_state.table_state.table_state.selected() { self.change_temp_position( offset_clicked_entry as i64 - visual_index as i64, @@ -2995,7 +2996,7 @@ impl App { .get_widget_state(self.current_widget.widget_id) { if let Some(visual_index) = - disk_widget_state.scroll_state.table_state.selected() + disk_widget_state.table_state.table_state.selected() { self.change_disk_position( offset_clicked_entry as i64 - visual_index as i64, diff --git a/src/app/states.rs b/src/app/states.rs index 8cbb8ae1..5d3ace9c 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, convert::TryInto, time::Instant}; +use std::{borrow::Cow, collections::HashMap, convert::TryInto, time::Instant}; use unicode_segmentation::GraphemeCursor; @@ -31,16 +31,173 @@ pub enum CursorDirection { Right, } -/// AppScrollWidgetState deals with fields for a scrollable app's current state. +/// Meant for canvas operations involving table column widths. #[derive(Default)] -pub struct AppScrollWidgetState { +pub struct CanvasTableWidthState { + pub desired_column_widths: Vec, + pub calculated_column_widths: Vec, +} + +/// A bound on the width of a column. +#[derive(Clone, Copy, Debug)] +pub enum WidthBounds { + /// A width of this type is either as long as `min`, but can otherwise shrink and grow up to a point. + Soft { + /// The minimum amount before giving up and hiding. + min_width: u16, + + /// The desired, calculated width. Take this if possible as the base starting width. + desired: u16, + + /// The max width, as a percentage of the total width available. If [`None`], + /// then it can grow as desired. + max_percentage: Option, + }, + + /// A width of this type is either as long as specified, or does not appear at all. + Hard(u16), +} + +impl WidthBounds { + pub const fn soft_from_str(name: &'static str, max_percentage: Option) -> WidthBounds { + WidthBounds::Soft { + min_width: name.len() as u16, + desired: name.len() as u16, + max_percentage, + } + } +} + +pub struct TableComponentColumn { + /// The name of the column. Displayed if possible as the header. + pub name: Cow<'static, str>, + + /// An optional alternative column name. Displayed if `name` doesn't fit. + pub alt: Option>, + + /// A restriction on this column's width, if desired. + pub width_bounds: WidthBounds, +} + +impl TableComponentColumn { + pub fn new(name: I, alt: Option, width_bounds: WidthBounds) -> Self + where + I: Into>, + { + Self { + name: name.into(), + alt: alt.map(Into::into), + width_bounds, + } + } +} + +/// [`TableComponentState`] deals with fields for a scrollable's current state. +#[derive(Default)] +pub struct TableComponentState { pub current_scroll_position: usize, pub scroll_bar: usize, pub scroll_direction: ScrollDirection, pub table_state: TableState, + pub columns: Vec, + pub calculated_widths: Vec, } -impl AppScrollWidgetState { +impl TableComponentState { + pub fn new(columns: Vec) -> Self { + Self { + current_scroll_position: 0, + scroll_bar: 0, + scroll_direction: ScrollDirection::Down, + table_state: Default::default(), + columns, + calculated_widths: Vec::default(), + } + } + + /// Calculates widths for the columns for this table. + /// + /// * `total_width` is the, well, total width available. **NOTE:** This function automatically + /// takes away 2 from the width as part of the left/right + /// bounds. + /// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if + /// false. + /// + /// **NOTE:** Trailing 0's may break tui-rs, remember to filter them out later! + pub fn calculate_column_widths(&mut self, total_width: u16, left_to_right: bool) { + use itertools::Either; + use std::cmp::{max, min}; + + if total_width > 2 { + let initial_width = total_width - 2; + let mut total_width_left = initial_width; + + let column_widths = &mut self.calculated_widths; + *column_widths = vec![0; self.columns.len()]; + + let columns = if left_to_right { + Either::Left(self.columns.iter().enumerate()) + } else { + Either::Right(self.columns.iter().enumerate().rev()) + }; + + for (itx, column) in columns { + match &column.width_bounds { + WidthBounds::Soft { + min_width, + desired, + max_percentage, + } => { + let soft_limit = max( + if let Some(max_percentage) = max_percentage { + // Rust doesn't have an `into()` or `try_into()` for floats to integers??? + ((*max_percentage * f32::from(initial_width)).ceil()) as u16 + } else { + *desired + }, + *min_width, + ); + let space_taken = min(min(soft_limit, *desired), total_width_left); + + if *min_width > space_taken { + break; + } else { + total_width_left = total_width_left.saturating_sub(space_taken + 1); + column_widths[itx] = space_taken; + } + } + WidthBounds::Hard(width) => { + let space_taken = min(*width, total_width_left); + + if *width > space_taken { + break; + } else { + total_width_left = total_width_left.saturating_sub(space_taken + 1); + column_widths[itx] = space_taken; + } + } + } + } + + while let Some(0) = column_widths.last() { + column_widths.pop(); + } + + if !column_widths.is_empty() { + // Redistribute remaining. + let amount_per_slot = total_width_left / column_widths.len() as u16; + total_width_left %= column_widths.len() as u16; + for (index, width) in column_widths.iter_mut().enumerate() { + if index < total_width_left.into() { + *width += amount_per_slot + 1; + } else { + *width += amount_per_slot; + } + } + } + } + } + /// Updates the position if possible, and if there is a valid change, returns the new position. pub fn update_position(&mut self, change: i64, num_entries: usize) -> Option { if change == 0 { @@ -159,13 +316,6 @@ impl AppSearchState { } } -/// Meant for canvas operations involving table column widths. -#[derive(Default)] -pub struct CanvasTableWidthState { - pub desired_column_widths: Vec, - pub calculated_column_widths: Vec, -} - /// ProcessSearchState only deals with process' search's current settings and state. pub struct ProcessSearchState { pub search_state: AppSearchState, @@ -426,7 +576,8 @@ impl ProcColumn { /// NOTE: ALWAYS call this when opening the sorted window. pub fn set_to_sorted_index_from_type(&mut self, proc_sorting_type: &ProcessSorting) { - // TODO [Custom Columns]: If we add custom columns, this may be needed! Since column indices will change, this runs the risk of OOB. So, when you change columns, CALL THIS AND ADAPT! + // TODO [Custom Columns]: If we add custom columns, this may be needed! + // Since column indices will change, this runs the risk of OOB. So, if you change columns, CALL THIS AND ADAPT! let mut true_index = 0; for column in &self.ordered_columns { if *column == *proc_sorting_type { @@ -489,7 +640,7 @@ impl ProcColumn { pub struct ProcWidgetState { pub process_search_state: ProcessSearchState, pub is_grouped: bool, - pub scroll_state: AppScrollWidgetState, + pub scroll_state: TableComponentState, pub process_sorting_type: processes::ProcessSorting, pub is_process_sort_descending: bool, pub is_using_command: bool, @@ -546,7 +697,7 @@ impl ProcWidgetState { ProcWidgetState { process_search_state, is_grouped, - scroll_state: AppScrollWidgetState::default(), + scroll_state: TableComponentState::default(), process_sorting_type, is_process_sort_descending, is_using_command, @@ -774,7 +925,7 @@ pub struct CpuWidgetState { pub current_display_time: u64, pub is_legend_hidden: bool, pub autohide_timer: Option, - pub scroll_state: AppScrollWidgetState, + pub scroll_state: TableComponentState, pub is_multi_graph_mode: bool, pub table_width_state: CanvasTableWidthState, } @@ -785,7 +936,7 @@ impl CpuWidgetState { current_display_time, is_legend_hidden: false, autohide_timer, - scroll_state: AppScrollWidgetState::default(), + scroll_state: TableComponentState::default(), is_multi_graph_mode: false, table_width_state: CanvasTableWidthState::default(), } @@ -850,15 +1001,25 @@ impl MemState { } pub struct TempWidgetState { - pub scroll_state: AppScrollWidgetState, - pub table_width_state: CanvasTableWidthState, + pub table_state: TableComponentState, } -impl TempWidgetState { - pub fn init() -> Self { +impl Default for TempWidgetState { + fn default() -> Self { + const TEMP_HEADERS: [&str; 2] = ["Sensor", "Temp"]; + const WIDTHS: [WidthBounds; TEMP_HEADERS.len()] = [ + WidthBounds::soft_from_str(TEMP_HEADERS[0], Some(0.8)), + WidthBounds::soft_from_str(TEMP_HEADERS[1], None), + ]; + TempWidgetState { - scroll_state: AppScrollWidgetState::default(), - table_width_state: CanvasTableWidthState::default(), + table_state: TableComponentState::new( + TEMP_HEADERS + .iter() + .zip(WIDTHS) + .map(|(header, width)| TableComponentColumn::new(*header, None, width)) + .collect(), + ), } } } @@ -882,15 +1043,30 @@ impl TempState { } pub struct DiskWidgetState { - pub scroll_state: AppScrollWidgetState, - pub table_width_state: CanvasTableWidthState, + pub table_state: TableComponentState, } -impl DiskWidgetState { - pub fn init() -> Self { +impl Default for DiskWidgetState { + fn default() -> Self { + const DISK_HEADERS: [&str; 7] = ["Disk", "Mount", "Used", "Free", "Total", "R/s", "W/s"]; + const WIDTHS: [WidthBounds; DISK_HEADERS.len()] = [ + WidthBounds::soft_from_str(DISK_HEADERS[0], Some(0.2)), + WidthBounds::soft_from_str(DISK_HEADERS[1], Some(0.2)), + WidthBounds::Hard(4), + WidthBounds::Hard(6), + WidthBounds::Hard(6), + WidthBounds::Hard(7), + WidthBounds::Hard(7), + ]; + DiskWidgetState { - scroll_state: AppScrollWidgetState::default(), - table_width_state: CanvasTableWidthState::default(), + table_state: TableComponentState::new( + DISK_HEADERS + .iter() + .zip(WIDTHS) + .map(|(header, width)| TableComponentColumn::new(*header, None, width)) + .collect(), + ), } } } @@ -978,18 +1154,20 @@ mod test { #[test] fn test_scroll_update_position() { fn check_scroll_update( - scroll: &mut AppScrollWidgetState, change: i64, max: usize, ret: Option, + scroll: &mut TableComponentState, change: i64, max: usize, ret: Option, new_position: usize, ) { assert_eq!(scroll.update_position(change, max), ret); assert_eq!(scroll.current_scroll_position, new_position); } - let mut scroll = AppScrollWidgetState { + let mut scroll = TableComponentState { current_scroll_position: 5, scroll_bar: 0, scroll_direction: ScrollDirection::Down, table_state: Default::default(), + columns: vec![], + calculated_widths: vec![], }; let s = &mut scroll; diff --git a/src/canvas.rs b/src/canvas.rs index 2070327a..21cd5197 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -18,7 +18,7 @@ use crate::{ App, }, constants::*, - data_conversion::{ConvertedBatteryData, ConvertedCpuData, ConvertedProcessData}, + data_conversion::{ConvertedBatteryData, ConvertedCpuData, ConvertedProcessData, TableData}, options::Config, utils::error, utils::error::BottomError, @@ -41,8 +41,8 @@ pub struct DisplayableData { pub total_tx_display: String, pub network_data_rx: Vec, pub network_data_tx: Vec, - pub disk_data: Vec>, - pub temp_sensor_data: Vec>, + pub disk_data: TableData, + pub temp_sensor_data: TableData, pub single_process_data: HashMap, // Contains single process data, key is PID pub finalized_process_data_map: HashMap>, // What's actually displayed, key is the widget ID. pub stringified_process_data_map: HashMap)>, bool)>>, // Represents the row and whether it is disabled, key is the widget ID diff --git a/src/canvas/components/text_table.rs b/src/canvas/components/text_table.rs index 8b137891..16e049ed 100644 --- a/src/canvas/components/text_table.rs +++ b/src/canvas/components/text_table.rs @@ -1 +1,257 @@ +use std::borrow::Cow; +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, TableComponentState}, + constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT}, + data_conversion::TableData, +}; + +pub struct TextTable<'a> { + pub table_gap: u16, + pub table_height_offset: u16, + pub is_force_redraw: bool, + pub recalculate_column_widths: bool, + + /// The header style. + pub header_style: Style, + + /// The border style. + pub border_style: Style, + + /// The highlighted entry style. + pub highlighted_style: Style, + + /// The graph title. + pub title: Cow<'a, str>, + + /// Whether this graph is expanded. + pub is_expanded: bool, + + /// 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) -> Spans<'_> { + let title = if self.show_table_scroll_position { + let title_string = concat_string!( + self.title, + "(", + pos.to_string(), + " of ", + total.to_string(), + ") " + ); + + if title_string.len() + 2 <= draw_loc.width.into() { + title_string + } else { + self.title.to_string() + } + } else { + self.title.to_string() + }; + + if self.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( + &self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState, + table_data: &TableData, + ) -> Rect { + let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { + 0 + } else { + self.table_gap + }; + let start_position = get_start_position( + usize::from( + (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset), + ), + &state.scroll_direction, + &mut state.scroll_bar, + state.current_scroll_position, + self.is_force_redraw, + ); + state.table_state.select(Some( + state.current_scroll_position.saturating_sub(start_position), + )); + let sliced_vec = &table_data.data[start_position..]; + + // Calculate widths + if self.recalculate_column_widths { + state + .columns + .iter_mut() + .zip(&table_data.row_widths) + .for_each(|(column, data_width)| match &mut column.width_bounds { + app::WidthBounds::Soft { + min_width: _, + desired, + max_percentage: _, + } => { + *desired = std::cmp::max(column.name.len(), *data_width) as u16; + } + app::WidthBounds::Hard(_width) => {} + }); + + state.calculate_column_widths(draw_loc.width, self.left_to_right); + } + + let columns = &state.columns; + let widths = &state.calculated_widths; + // TODO: Maybe truncate this too? + let header = Row::new(columns.iter().map(|c| Text::raw(c.name.as_ref()))) + .style(self.header_style) + .bottom_margin(table_gap); + let disk_rows = sliced_vec.iter().map(|row| { + Row::new( + row.iter() + .zip(widths) + .map(|(cell, width)| truncate_text(cell, (*width).into())), + ) + }); + + let title = self.generate_title( + draw_loc, + state.current_scroll_position.saturating_add(1), + table_data.data.len(), + ); + + let disk_block = if self.draw_border { + Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(self.border_style) + } else if self.is_on_widget { + Block::default() + .borders(SIDE_BORDERS) + .border_style(self.border_style) + } else { + Block::default().borders(Borders::NONE) + }; + + let margined_draw_loc = Layout::default() + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(if self.is_on_widget || self.draw_border { + 0 + } else { + 1 + }) + .direction(Direction::Horizontal) + .split(draw_loc)[0]; + + // Draw! + f.render_stateful_widget( + Table::new(disk_rows) + .block(disk_block) + .header(header) + .highlight_style(self.highlighted_style) + .style(self.text_style) + .widths( + &(widths + .iter() + .map(|w| Constraint::Length(*w)) + .collect::>()), + ), + margined_draw_loc, + &mut state.table_state, + ); + + margined_draw_loc + } +} + +/// Truncates text if it is too long, and adds an ellipsis at the end if needed. +fn truncate_text(text: &str, width: usize) -> Text<'_> { + let graphemes = UnicodeSegmentation::graphemes(text, true).collect::>(); + if graphemes.len() > width && width > 0 { + // Truncate with ellipsis + let first_n = graphemes[..(width - 1)].concat(); + return Text::raw(concat_string!(first_n, "…")); + } else { + Text::raw(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; + *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; + } + // Else, don't change what our start position is from whatever it is set to! + *scroll_position_bar + } + } +} + +#[cfg(test)] +mod test {} diff --git a/src/canvas/components/time_chart.rs b/src/canvas/components/time_chart.rs index 5bb0ff15..23b05219 100644 --- a/src/canvas/components/time_chart.rs +++ b/src/canvas/components/time_chart.rs @@ -621,20 +621,20 @@ mod test { assert_eq!(interpolate_point(&data[0], &data[1], -3.0), 8.0); } + #[test] + fn time_chart_empty_dataset() { + let data = []; + let dataset = Dataset::default().data(&data); + + assert_eq!(get_start(&dataset, -100.0), (0, None)); + assert_eq!(get_start(&dataset, -3.0), (0, None)); + + assert_eq!(get_end(&dataset, 0.0), (0, None)); + assert_eq!(get_end(&dataset, 100.0), (0, None)); + } + #[test] fn time_chart_test_data_trimming() { - // Quick test on a completely empty dataset... - { - let data = []; - let dataset = Dataset::default().data(&data); - - assert_eq!(get_start(&dataset, -100.0), (0, None)); - assert_eq!(get_start(&dataset, -3.0), (0, None)); - - assert_eq!(get_end(&dataset, 0.0), (0, None)); - assert_eq!(get_end(&dataset, 100.0), (0, None)); - } - let data = [ (-3.0, 8.0), (-2.5, 15.0), diff --git a/src/canvas/components/time_graph.rs b/src/canvas/components/time_graph.rs index ab5cec2e..723483be 100644 --- a/src/canvas/components/time_graph.rs +++ b/src/canvas/components/time_graph.rs @@ -25,7 +25,6 @@ pub struct GraphData<'a> { pub name: Option>, } -#[derive(Default)] pub struct TimeGraph<'a> { /// Whether to use a dot marker over the default braille markers. pub use_dot: bool, diff --git a/src/canvas/drawing_utils.rs b/src/canvas/drawing_utils.rs index f7b81281..839017a9 100644 --- a/src/canvas/drawing_utils.rs +++ b/src/canvas/drawing_utils.rs @@ -1,6 +1,6 @@ use tui::layout::Rect; -use crate::app; +use crate::app::{self}; use std::{ cmp::{max, min}, time::Instant, @@ -328,6 +328,12 @@ mod test { assert!(over_timer.is_none()); } + #[test] + fn test_width_calculation() { + // TODO: Implement width calculation test; can reuse old ones as basis + todo!() + } + #[test] fn test_zero_width() { assert_eq!( diff --git a/src/canvas/widgets/disk_table.rs b/src/canvas/widgets/disk_table.rs index 252ec70b..8c7adc7d 100644 --- a/src/canvas/widgets/disk_table.rs +++ b/src/canvas/widgets/disk_table.rs @@ -1,31 +1,9 @@ -use once_cell::sync::Lazy; -use tui::{ - backend::Backend, - layout::{Constraint, Direction, Layout, Rect}, - terminal::Frame, - text::Span, - text::{Spans, Text}, - widgets::{Block, Borders, Row, Table}, -}; +use tui::{backend::Backend, layout::Rect, terminal::Frame}; use crate::{ app, - canvas::{ - drawing_utils::{get_column_widths, get_start_position}, - Painter, - }, - constants::*, + canvas::{components::TextTable, Painter}, }; -use unicode_segmentation::UnicodeSegmentation; - -const DISK_HEADERS: [&str; 7] = ["Disk", "Mount", "Used", "Free", "Total", "R/s", "W/s"]; - -static DISK_HEADERS_LENS: Lazy> = Lazy::new(|| { - DISK_HEADERS - .iter() - .map(|entry| entry.len() as u16) - .collect::>() -}); impl Painter { pub fn draw_disk_table( @@ -34,120 +12,9 @@ impl Painter { ) { let recalculate_column_widths = app_state.should_get_widget_bounds(); if let Some(disk_widget_state) = app_state.disk_state.widget_states.get_mut(&widget_id) { - let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { - 0 - } else { - app_state.app_config_fields.table_gap - }; - let start_position = get_start_position( - usize::from( - (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset), - ), - &disk_widget_state.scroll_state.scroll_direction, - &mut disk_widget_state.scroll_state.scroll_bar, - disk_widget_state.scroll_state.current_scroll_position, - app_state.is_force_redraw, - ); let is_on_widget = app_state.current_widget.widget_id == widget_id; - let disk_table_state = &mut disk_widget_state.scroll_state.table_state; - disk_table_state.select(Some( - disk_widget_state - .scroll_state - .current_scroll_position - .saturating_sub(start_position), - )); - let sliced_vec = &app_state.canvas_data.disk_data[start_position..]; - // Calculate widths - let hard_widths = [None, None, Some(4), Some(6), Some(6), Some(7), Some(7)]; - if recalculate_column_widths { - disk_widget_state.table_width_state.desired_column_widths = { - let mut column_widths = DISK_HEADERS_LENS.clone(); - for row in sliced_vec { - for (col, entry) in row.iter().enumerate() { - if entry.len() as u16 > column_widths[col] { - column_widths[col] = entry.len() as u16; - } - } - } - column_widths - }; - disk_widget_state.table_width_state.desired_column_widths = disk_widget_state - .table_width_state - .desired_column_widths - .iter() - .zip(&hard_widths) - .map(|(current, hard)| { - if let Some(hard) = hard { - if *hard > *current { - *hard - } else { - *current - } - } else { - *current - } - }) - .collect::>(); - - disk_widget_state.table_width_state.calculated_column_widths = get_column_widths( - draw_loc.width, - &hard_widths, - &(DISK_HEADERS_LENS - .iter() - .map(|w| Some(*w)) - .collect::>()), - &[Some(0.2), Some(0.2), None, None, None, None, None], - &(disk_widget_state - .table_width_state - .desired_column_widths - .iter() - .map(|w| Some(*w)) - .collect::>()), - true, - ); - } - - let dcw = &disk_widget_state.table_width_state.desired_column_widths; - let ccw = &disk_widget_state.table_width_state.calculated_column_widths; - let disk_rows = - sliced_vec.iter().map(|disk_row| { - let truncated_data = disk_row.iter().zip(&hard_widths).enumerate().map( - |(itx, (entry, width))| { - if width.is_none() { - if let (Some(desired_col_width), Some(calculated_col_width)) = - (dcw.get(itx), ccw.get(itx)) - { - if *desired_col_width > *calculated_col_width - && *calculated_col_width > 0 - { - let calculated_col_width: usize = - (*calculated_col_width).into(); - - let graphemes = - UnicodeSegmentation::graphemes(entry.as_str(), true) - .collect::>(); - - if graphemes.len() > calculated_col_width - && calculated_col_width > 1 - { - // Truncate with ellipsis - let first_n = - graphemes[..(calculated_col_width - 1)].concat(); - return Text::raw(format!("{}…", first_n)); - } - } - } - } - - Text::raw(entry) - }, - ); - - Row::new(truncated_data) - }); - - let (border_style, highlight_style) = if is_on_widget { + let (border_style, highlighted_style) = if is_on_widget { ( self.colours.highlighted_border_style, self.colours.currently_selected_text_style, @@ -155,105 +22,28 @@ impl Painter { } else { (self.colours.border_style, self.colours.text_style) }; - - let title_base = if app_state.app_config_fields.show_table_scroll_position { - let title_string = format!( - " Disk ({} of {}) ", - disk_widget_state - .scroll_state - .current_scroll_position - .saturating_add(1), - app_state.canvas_data.disk_data.len() - ); - - if title_string.len() <= draw_loc.width.into() { - title_string - } else { - " Disk ".to_string() - } - } else { - " Disk ".to_string() - }; - - let title = if app_state.is_expanded { - const ESCAPE_ENDING: &str = "── Esc to go back "; - - let (chosen_title_base, expanded_title_base) = { - let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING); - - if temp_title_base.len() > draw_loc.width.into() { - ( - " Disk ".to_string(), - format!("{}{}", " Disk ", ESCAPE_ENDING), - ) - } else { - (title_base, temp_title_base) - } - }; - - Spans::from(vec![ - Span::styled(chosen_title_base, self.colours.widget_title_style), - Span::styled( - format!( - "─{}─ Esc to go back ", - "─".repeat( - usize::from(draw_loc.width).saturating_sub( - UnicodeSegmentation::graphemes( - expanded_title_base.as_str(), - true - ) - .count() - + 2 - ) - ) - ), - border_style, - ), - ]) - } else { - Spans::from(Span::styled(title_base, self.colours.widget_title_style)) - }; - - let disk_block = if draw_border { - Block::default() - .title(title) - .borders(Borders::ALL) - .border_style(border_style) - } else if is_on_widget { - Block::default() - .borders(SIDE_BORDERS) - .border_style(self.colours.highlighted_border_style) - } else { - Block::default().borders(Borders::NONE) - }; - - let margined_draw_loc = Layout::default() - .constraints([Constraint::Percentage(100)]) - .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 }) - .direction(Direction::Horizontal) - .split(draw_loc)[0]; - - // Draw! - f.render_stateful_widget( - Table::new(disk_rows) - .block(disk_block) - .header( - Row::new(DISK_HEADERS.to_vec()) - .style(self.colours.table_header_style) - .bottom_margin(table_gap), - ) - .highlight_style(highlight_style) - .style(self.colours.text_style) - .widths( - &(disk_widget_state - .table_width_state - .calculated_column_widths - .iter() - .map(|calculated_width| Constraint::Length(*calculated_width)) - .collect::>()), - ), - margined_draw_loc, - disk_table_state, + let margined_draw_loc = TextTable { + table_gap: app_state.app_config_fields.table_gap, + table_height_offset: self.table_height_offset, + is_force_redraw: app_state.is_force_redraw, + recalculate_column_widths, + header_style: self.colours.table_header_style, + border_style, + highlighted_style, + title: " Disks ".into(), + is_expanded: app_state.is_expanded, + is_on_widget, + draw_border, + show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position, + title_style: self.colours.widget_title_style, + text_style: self.colours.text_style, + left_to_right: true, + } + .draw_text_table( + f, + draw_loc, + &mut disk_widget_state.table_state, + &app_state.canvas_data.disk_data, ); if app_state.should_get_widget_bounds() { diff --git a/src/canvas/widgets/temp_table.rs b/src/canvas/widgets/temp_table.rs index 974afd14..2e95a13a 100644 --- a/src/canvas/widgets/temp_table.rs +++ b/src/canvas/widgets/temp_table.rs @@ -1,31 +1,9 @@ -use once_cell::sync::Lazy; -use tui::{ - backend::Backend, - layout::{Constraint, Direction, Layout, Rect}, - terminal::Frame, - text::Span, - text::{Spans, Text}, - widgets::{Block, Borders, Row, Table}, -}; +use tui::{backend::Backend, layout::Rect, terminal::Frame}; use crate::{ app, - canvas::{ - drawing_utils::{get_column_widths, get_start_position}, - Painter, - }, - constants::*, + canvas::{components::TextTable, Painter}, }; -use unicode_segmentation::UnicodeSegmentation; - -const TEMP_HEADERS: [&str; 2] = ["Sensor", "Temp"]; - -static TEMP_HEADERS_LENS: Lazy> = Lazy::new(|| { - TEMP_HEADERS - .iter() - .map(|entry| entry.len() as u16) - .collect::>() -}); impl Painter { pub fn draw_temp_table( @@ -34,109 +12,9 @@ impl Painter { ) { let recalculate_column_widths = app_state.should_get_widget_bounds(); if let Some(temp_widget_state) = app_state.temp_state.widget_states.get_mut(&widget_id) { - let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { - 0 - } else { - app_state.app_config_fields.table_gap - }; - let start_position = get_start_position( - usize::from( - (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset), - ), - &temp_widget_state.scroll_state.scroll_direction, - &mut temp_widget_state.scroll_state.scroll_bar, - temp_widget_state.scroll_state.current_scroll_position, - app_state.is_force_redraw, - ); - let is_on_widget = widget_id == app_state.current_widget.widget_id; - let temp_table_state = &mut temp_widget_state.scroll_state.table_state; - temp_table_state.select(Some( - temp_widget_state - .scroll_state - .current_scroll_position - .saturating_sub(start_position), - )); - let sliced_vec = &app_state.canvas_data.temp_sensor_data[start_position..]; + let is_on_widget = app_state.current_widget.widget_id == widget_id; - // Calculate widths - let hard_widths = [None, None]; - if recalculate_column_widths { - temp_widget_state.table_width_state.desired_column_widths = { - let mut column_widths = TEMP_HEADERS_LENS.clone(); - for row in sliced_vec { - for (col, entry) in row.iter().enumerate() { - if entry.len() as u16 > column_widths[col] { - column_widths[col] = entry.len() as u16; - } - } - } - - column_widths - }; - temp_widget_state.table_width_state.calculated_column_widths = get_column_widths( - draw_loc.width, - &hard_widths, - &(TEMP_HEADERS_LENS - .iter() - .map(|width| Some(*width)) - .collect::>()), - &[Some(0.80), Some(-1.0)], - &temp_widget_state - .table_width_state - .desired_column_widths - .iter() - .map(|width| Some(*width)) - .collect::>(), - false, - ); - } - - let dcw = &temp_widget_state.table_width_state.desired_column_widths; - let ccw = &temp_widget_state.table_width_state.calculated_column_widths; - let temperature_rows = - sliced_vec.iter().map(|temp_row| { - let truncated_data = temp_row.iter().zip(&hard_widths).enumerate().map( - |(itx, (entry, width))| { - if width.is_none() { - if let (Some(desired_col_width), Some(calculated_col_width)) = - (dcw.get(itx), ccw.get(itx)) - { - if *desired_col_width > *calculated_col_width - && *calculated_col_width > 0 - { - let calculated_col_width: usize = - (*calculated_col_width).into(); - - let graphemes = - UnicodeSegmentation::graphemes(entry.as_str(), true) - .collect::>(); - - if graphemes.len() > calculated_col_width - && calculated_col_width > 1 - { - // Truncate with ellipsis - let first_n = - graphemes[..(calculated_col_width - 1)].concat(); - Text::raw(format!("{}…", first_n)) - } else { - Text::raw(entry) - } - } else { - Text::raw(entry) - } - } else { - Text::raw(entry) - } - } else { - Text::raw(entry) - } - }, - ); - - Row::new(truncated_data) - }); - - let (border_style, highlight_style) = if is_on_widget { + let (border_style, highlighted_style) = if is_on_widget { ( self.colours.highlighted_border_style, self.colours.currently_selected_text_style, @@ -144,105 +22,28 @@ impl Painter { } else { (self.colours.border_style, self.colours.text_style) }; - - let title_base = if app_state.app_config_fields.show_table_scroll_position { - let title_string = format!( - " Temperatures ({} of {}) ", - temp_widget_state - .scroll_state - .current_scroll_position - .saturating_add(1), - app_state.canvas_data.temp_sensor_data.len() - ); - - if title_string.len() <= draw_loc.width.into() { - title_string - } else { - " Temperatures ".to_string() - } - } else { - " Temperatures ".to_string() - }; - - let title = if app_state.is_expanded { - const ESCAPE_ENDING: &str = "── Esc to go back "; - - let (chosen_title_base, expanded_title_base) = { - let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING); - - if temp_title_base.len() > draw_loc.width.into() { - ( - " Temperatures ".to_string(), - format!("{}{}", " Temperatures ", ESCAPE_ENDING), - ) - } else { - (title_base, temp_title_base) - } - }; - - Spans::from(vec![ - Span::styled(chosen_title_base, self.colours.widget_title_style), - Span::styled( - format!( - "─{}─ Esc to go back ", - "─".repeat( - usize::from(draw_loc.width).saturating_sub( - UnicodeSegmentation::graphemes( - expanded_title_base.as_str(), - true - ) - .count() - + 2 - ) - ) - ), - border_style, - ), - ]) - } else { - Spans::from(Span::styled(title_base, self.colours.widget_title_style)) - }; - - let temp_block = if draw_border { - Block::default() - .title(title) - .borders(Borders::ALL) - .border_style(border_style) - } else if is_on_widget { - Block::default() - .borders(SIDE_BORDERS) - .border_style(self.colours.highlighted_border_style) - } else { - Block::default().borders(Borders::NONE) - }; - - let margined_draw_loc = Layout::default() - .constraints([Constraint::Percentage(100)]) - .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 }) - .direction(Direction::Horizontal) - .split(draw_loc)[0]; - - // Draw - f.render_stateful_widget( - Table::new(temperature_rows) - .header( - Row::new(TEMP_HEADERS.to_vec()) - .style(self.colours.table_header_style) - .bottom_margin(table_gap), - ) - .block(temp_block) - .highlight_style(highlight_style) - .style(self.colours.text_style) - .widths( - &(temp_widget_state - .table_width_state - .calculated_column_widths - .iter() - .map(|calculated_width| Constraint::Length(*calculated_width)) - .collect::>()), - ), - margined_draw_loc, - temp_table_state, + let margined_draw_loc = TextTable { + table_gap: app_state.app_config_fields.table_gap, + table_height_offset: self.table_height_offset, + is_force_redraw: app_state.is_force_redraw, + recalculate_column_widths, + header_style: self.colours.table_header_style, + border_style, + highlighted_style, + title: " Temperatures ".into(), + is_expanded: app_state.is_expanded, + is_on_widget, + draw_border, + show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position, + title_style: self.colours.widget_title_style, + text_style: self.colours.text_style, + left_to_right: false, + } + .draw_text_table( + f, + draw_loc, + &mut temp_widget_state.table_state, + &app_state.canvas_data.temp_sensor_data, ); if app_state.should_get_widget_bounds() { diff --git a/src/data_conversion.rs b/src/data_conversion.rs index 7f9d184c..93470fc5 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -23,6 +23,12 @@ pub struct ConvertedBatteryData { pub health: String, } +#[derive(Default)] +pub struct TableData { + pub data: Vec>, + pub row_widths: Vec, +} + #[derive(Default, Debug)] pub struct ConvertedNetworkData { pub rx: Vec, @@ -83,15 +89,16 @@ pub struct ConvertedCpuData { pub legend_value: String, } -pub fn convert_temp_row(app: &App) -> Vec> { +pub fn convert_temp_row(app: &App) -> TableData { let current_data = &app.data_collection; let temp_type = &app.app_config_fields.temperature_type; + let mut row_widths = vec![0; 2]; let mut sensor_vector: Vec> = current_data .temp_harvest .iter() .map(|temp_harvest| { - vec![ + let row = vec![ temp_harvest.name.clone(), (temp_harvest.temperature.ceil() as u64).to_string() + match temp_type { @@ -99,7 +106,13 @@ pub fn convert_temp_row(app: &App) -> Vec> { data_harvester::temperature::TemperatureType::Kelvin => "K", data_harvester::temperature::TemperatureType::Fahrenheit => "°F", }, - ] + ]; + + row_widths.iter_mut().zip(&row).for_each(|(curr, r)| { + *curr = std::cmp::max(*curr, r.len()); + }); + + row }) .collect(); @@ -107,11 +120,15 @@ pub fn convert_temp_row(app: &App) -> Vec> { sensor_vector.push(vec!["No Sensors Found".to_string(), "".to_string()]); } - sensor_vector + TableData { + data: sensor_vector, + row_widths, + } } -pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> Vec> { +pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> TableData { let mut disk_vector: Vec> = Vec::new(); + let mut row_widths = vec![0; 8]; current_data .disk_harvest @@ -142,7 +159,7 @@ pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> Vec Vec { - disk_state_map.insert(widget.widget_id, DiskWidgetState::init()); + disk_state_map.insert(widget.widget_id, DiskWidgetState::default()); } Temp => { - temp_state_map.insert(widget.widget_id, TempWidgetState::init()); + temp_state_map.insert(widget.widget_id, TempWidgetState::default()); } Battery => { battery_state_map From 64ed45083ea763690c618ce9fabd22d5224d7625 Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Sat, 30 Apr 2022 20:19:51 -0400 Subject: [PATCH 02/16] refactor: remove unneeded freeze param --- src/app.rs | 8 +- src/app/data_farmer.rs | 6 +- src/bin/main.rs | 6 +- src/canvas/components/text_table.rs | 24 ++++-- src/data_conversion.rs | 129 +++++++++++++++------------- src/lib.rs | 11 +-- 6 files changed, 100 insertions(+), 84 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8e49dd3a..8ea22fd4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -147,8 +147,8 @@ pub struct App { pub current_widget: BottomWidget, pub used_widgets: UsedWidgets, pub filters: DataFilters, - pub config: Config, - pub config_path: Option, + pub config: Config, // TODO: Is this even used...? + pub config_path: Option, // TODO: Is this even used...? } #[cfg(target_os = "windows")] @@ -1528,7 +1528,9 @@ impl App { 'f' => { self.is_frozen = !self.is_frozen; if self.is_frozen { - self.data_collection.set_frozen_time(); + self.data_collection.freeze(); + } else { + self.data_collection.thaw(); } } 'C' => { diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs index 1860c91b..64e08602 100644 --- a/src/app/data_farmer.rs +++ b/src/app/data_farmer.rs @@ -108,10 +108,14 @@ impl DataCollection { } } - pub fn set_frozen_time(&mut self) { + pub fn freeze(&mut self) { self.frozen_instant = Some(self.current_instant); } + pub fn thaw(&mut self) { + self.frozen_instant = None; + } + pub fn clean_data(&mut self, max_time_millis: u64) { let current_time = Instant::now(); diff --git a/src/bin/main.rs b/src/bin/main.rs index 6a1f0aae..627e1e2a 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -158,7 +158,6 @@ fn main() -> Result<()> { if app.used_widgets.use_net { let network_data = convert_network_data_points( &app.data_collection, - false, app.app_config_fields.use_basic_mode || app.app_config_fields.use_old_network_legend, &app.app_config_fields.network_scale_type, @@ -190,9 +189,9 @@ fn main() -> Result<()> { // Memory if app.used_widgets.use_mem { app.canvas_data.mem_data = - convert_mem_data_points(&app.data_collection, false); + convert_mem_data_points(&app.data_collection); app.canvas_data.swap_data = - convert_swap_data_points(&app.data_collection, false); + convert_swap_data_points(&app.data_collection); let (memory_labels, swap_labels) = convert_mem_labels(&app.data_collection); @@ -206,7 +205,6 @@ fn main() -> Result<()> { convert_cpu_data_points( &app.data_collection, &mut app.canvas_data.cpu_data, - false, ); app.canvas_data.load_avg_data = app.data_collection.load_avg_harvest; } diff --git a/src/canvas/components/text_table.rs b/src/canvas/components/text_table.rs index 16e049ed..c1119c43 100644 --- a/src/canvas/components/text_table.rs +++ b/src/canvas/components/text_table.rs @@ -14,7 +14,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::{ app::{self, TableComponentState}, constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT}, - data_conversion::TableData, + data_conversion::{CellContent, TableData}, }; pub struct TextTable<'a> { @@ -204,14 +204,24 @@ impl<'a> TextTable<'a> { } /// Truncates text if it is too long, and adds an ellipsis at the end if needed. -fn truncate_text(text: &str, width: usize) -> Text<'_> { - let graphemes = UnicodeSegmentation::graphemes(text, true).collect::>(); +fn truncate_text(content: &CellContent, width: usize) -> Text<'_> { + let (text, opt) = match content { + CellContent::Simple(s) => (s, None), + CellContent::HasShort { short, long } => (long, Some(short)), + }; + + let graphemes = UnicodeSegmentation::graphemes(text.as_ref(), true).collect::>(); if graphemes.len() > width && width > 0 { - // Truncate with ellipsis - let first_n = graphemes[..(width - 1)].concat(); - return Text::raw(concat_string!(first_n, "…")); + if let Some(s) = opt { + // 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(text) + Text::raw(text.as_ref()) } } diff --git a/src/data_conversion.rs b/src/data_conversion.rs index 93470fc5..4b2dc7df 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -1,18 +1,18 @@ //! This mainly concerns converting collected data into things that the canvas //! can actually handle. +use crate::canvas::Point; use crate::{app::AxisScaling, units::data_units::DataUnit, Pid}; use crate::{ app::{data_farmer, data_harvester, App, ProcWidgetState}, utils::{self, gen_util::*}, }; +use concat_string::concat_string; use data_harvester::processes::ProcessSorting; use fxhash::FxBuildHasher; use indexmap::IndexSet; +use std::borrow::Cow; use std::collections::{HashMap, VecDeque}; -/// Point is of time, data -type Point = (f64, f64); - #[derive(Default, Debug)] pub struct ConvertedBatteryData { pub battery_name: String, @@ -23,9 +23,26 @@ pub struct ConvertedBatteryData { pub health: String, } +pub enum CellContent { + Simple(Cow<'static, str>), + HasShort { + short: Cow<'static, str>, + long: Cow<'static, str>, + }, +} + +impl CellContent { + pub fn len(&self) -> usize { + match self { + CellContent::Simple(s) => s.len(), + CellContent::HasShort { short: _, long } => long.len(), + } + } +} + #[derive(Default)] pub struct TableData { - pub data: Vec>, + pub data: Vec>, pub row_widths: Vec, } @@ -94,18 +111,23 @@ pub fn convert_temp_row(app: &App) -> TableData { let temp_type = &app.app_config_fields.temperature_type; let mut row_widths = vec![0; 2]; - let mut sensor_vector: Vec> = current_data + let mut sensor_vector: Vec> = current_data .temp_harvest .iter() .map(|temp_harvest| { let row = vec![ - temp_harvest.name.clone(), - (temp_harvest.temperature.ceil() as u64).to_string() - + match temp_type { - data_harvester::temperature::TemperatureType::Celsius => "°C", - data_harvester::temperature::TemperatureType::Kelvin => "K", - data_harvester::temperature::TemperatureType::Fahrenheit => "°F", - }, + CellContent::Simple(temp_harvest.name.clone().into()), + CellContent::Simple( + concat_string!( + (temp_harvest.temperature.ceil() as u64).to_string(), + match temp_type { + data_harvester::temperature::TemperatureType::Celsius => "°C", + data_harvester::temperature::TemperatureType::Kelvin => "K", + data_harvester::temperature::TemperatureType::Fahrenheit => "°F", + } + ) + .into(), + ), ]; row_widths.iter_mut().zip(&row).for_each(|(curr, r)| { @@ -117,7 +139,10 @@ pub fn convert_temp_row(app: &App) -> TableData { .collect(); if sensor_vector.is_empty() { - sensor_vector.push(vec!["No Sensors Found".to_string(), "".to_string()]); + sensor_vector.push(vec![ + CellContent::Simple("No Sensors Found".into()), + CellContent::Simple("".into()), + ]); } TableData { @@ -127,7 +152,7 @@ pub fn convert_temp_row(app: &App) -> TableData { } pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> TableData { - let mut disk_vector: Vec> = Vec::new(); + let mut disk_vector: Vec> = Vec::new(); let mut row_widths = vec![0; 8]; current_data @@ -137,9 +162,9 @@ pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> TableData .for_each(|(disk, (io_read, io_write))| { let free_space_fmt = if let Some(free_space) = disk.free_space { let converted_free_space = get_decimal_bytes(free_space); - format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1) + format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1).into() } else { - "N/A".to_string() + "N/A".into() }; let total_space_fmt = if let Some(total_space) = disk.total_space { let converted_total_space = get_decimal_bytes(total_space); @@ -147,26 +172,27 @@ pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> TableData "{:.*}{}", 0, converted_total_space.0, converted_total_space.1 ) + .into() } else { - "N/A".to_string() + "N/A".into() }; let usage_fmt = if let (Some(used_space), Some(total_space)) = (disk.used_space, disk.total_space) { - format!("{:.0}%", used_space as f64 / total_space as f64 * 100_f64) + format!("{:.0}%", used_space as f64 / total_space as f64 * 100_f64).into() } else { - "N/A".to_string() + "N/A".into() }; let row = vec![ - disk.name.to_string(), - disk.mount_point.to_string(), - usage_fmt, - free_space_fmt, - total_space_fmt, - io_read.to_string(), - io_write.to_string(), + CellContent::Simple(disk.name.clone().into()), + CellContent::Simple(disk.mount_point.clone().into()), + CellContent::Simple(usage_fmt), + CellContent::Simple(free_space_fmt), + CellContent::Simple(total_space_fmt), + CellContent::Simple(io_read.clone().into()), + CellContent::Simple(io_write.clone().into()), ]; row_widths.iter_mut().zip(&row).for_each(|(curr, r)| { *curr = std::cmp::max(*curr, r.len()); @@ -175,7 +201,10 @@ pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> TableData }); if disk_vector.is_empty() { - disk_vector.push(vec!["No Disks Found".to_string(), "".to_string()]); + disk_vector.push(vec![ + CellContent::Simple("No Disks Found".into()), + CellContent::Simple("".into()), + ]); } TableData { @@ -186,14 +215,9 @@ pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> TableData pub fn convert_cpu_data_points( current_data: &data_farmer::DataCollection, existing_cpu_data: &mut Vec, - is_frozen: bool, ) { - let current_time = if is_frozen { - if let Some(frozen_instant) = current_data.frozen_instant { - frozen_instant - } else { - current_data.current_instant - } + let current_time = if let Some(frozen_instant) = current_data.frozen_instant { + frozen_instant } else { current_data.current_instant }; @@ -264,16 +288,10 @@ pub fn convert_cpu_data_points( } } -pub fn convert_mem_data_points( - current_data: &data_farmer::DataCollection, is_frozen: bool, -) -> Vec { +pub fn convert_mem_data_points(current_data: &data_farmer::DataCollection) -> Vec { let mut result: Vec = Vec::new(); - let current_time = if is_frozen { - if let Some(frozen_instant) = current_data.frozen_instant { - frozen_instant - } else { - current_data.current_instant - } + let current_time = if let Some(frozen_instant) = current_data.frozen_instant { + frozen_instant } else { current_data.current_instant }; @@ -292,16 +310,10 @@ pub fn convert_mem_data_points( result } -pub fn convert_swap_data_points( - current_data: &data_farmer::DataCollection, is_frozen: bool, -) -> Vec { +pub fn convert_swap_data_points(current_data: &data_farmer::DataCollection) -> Vec { let mut result: Vec = Vec::new(); - let current_time = if is_frozen { - if let Some(frozen_instant) = current_data.frozen_instant { - frozen_instant - } else { - current_data.current_instant - } + let current_time = if let Some(frozen_instant) = current_data.frozen_instant { + frozen_instant } else { current_data.current_instant }; @@ -391,18 +403,14 @@ pub fn convert_mem_labels( } pub fn get_rx_tx_data_points( - current_data: &data_farmer::DataCollection, is_frozen: bool, network_scale_type: &AxisScaling, + current_data: &data_farmer::DataCollection, network_scale_type: &AxisScaling, network_unit_type: &DataUnit, network_use_binary_prefix: bool, ) -> (Vec, Vec) { let mut rx: Vec = Vec::new(); let mut tx: Vec = Vec::new(); - let current_time = if is_frozen { - if let Some(frozen_instant) = current_data.frozen_instant { - frozen_instant - } else { - current_data.current_instant - } + let current_time = if let Some(frozen_instant) = current_data.frozen_instant { + frozen_instant } else { current_data.current_instant }; @@ -446,13 +454,12 @@ pub fn get_rx_tx_data_points( } pub fn convert_network_data_points( - current_data: &data_farmer::DataCollection, is_frozen: bool, need_four_points: bool, + current_data: &data_farmer::DataCollection, need_four_points: bool, network_scale_type: &AxisScaling, network_unit_type: &DataUnit, network_use_binary_prefix: bool, ) -> ConvertedNetworkData { let (rx, tx) = get_rx_tx_data_points( current_data, - is_frozen, network_scale_type, network_unit_type, network_use_binary_prefix, diff --git a/src/lib.rs b/src/lib.rs index 4ae7ed07..4f39f5f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -314,26 +314,21 @@ pub fn handle_force_redraws(app: &mut App) { } if app.cpu_state.force_update.is_some() { - convert_cpu_data_points( - &app.data_collection, - &mut app.canvas_data.cpu_data, - app.is_frozen, - ); + convert_cpu_data_points(&app.data_collection, &mut app.canvas_data.cpu_data); app.canvas_data.load_avg_data = app.data_collection.load_avg_harvest; app.cpu_state.force_update = None; } // FIXME: [OPT] Prefer reassignment over new vectors? if app.mem_state.force_update.is_some() { - app.canvas_data.mem_data = convert_mem_data_points(&app.data_collection, app.is_frozen); - app.canvas_data.swap_data = convert_swap_data_points(&app.data_collection, app.is_frozen); + app.canvas_data.mem_data = convert_mem_data_points(&app.data_collection); + app.canvas_data.swap_data = convert_swap_data_points(&app.data_collection); app.mem_state.force_update = None; } if app.net_state.force_update.is_some() { let (rx, tx) = get_rx_tx_data_points( &app.data_collection, - app.is_frozen, &app.app_config_fields.network_scale_type, &app.app_config_fields.network_unit_type, app.app_config_fields.network_use_binary_prefix, From c296b8bf5aacf200995db6312c0e92233b65b766 Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Sun, 1 May 2022 15:45:52 -0400 Subject: [PATCH 03/16] refactor: bind the start and end ranges for tables --- src/canvas/components/text_table.rs | 39 ++++++++++++++++------------- src/canvas/widgets/disk_table.rs | 4 +-- src/canvas/widgets/temp_table.rs | 4 +-- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/canvas/components/text_table.rs b/src/canvas/components/text_table.rs index c1119c43..767b8e6c 100644 --- a/src/canvas/components/text_table.rs +++ b/src/canvas/components/text_table.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::{borrow::Cow, cmp::min}; use concat_string::concat_string; use tui::{ @@ -29,8 +29,8 @@ pub struct TextTable<'a> { /// The border style. pub border_style: Style, - /// The highlighted entry style. - pub highlighted_style: Style, + /// The highlighted text style. + pub highlighted_text_style: Style, /// The graph title. pub title: Cow<'a, str>, @@ -105,19 +105,24 @@ impl<'a> TextTable<'a> { } else { self.table_gap }; - let start_position = get_start_position( - usize::from( - (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset), - ), - &state.scroll_direction, - &mut state.scroll_bar, - state.current_scroll_position, - self.is_force_redraw, - ); - state.table_state.select(Some( - state.current_scroll_position.saturating_sub(start_position), - )); - let sliced_vec = &table_data.data[start_position..]; + + let sliced_vec = { + let num_rows = usize::from( + (draw_loc.height + 1 - table_gap).saturating_sub(self.table_height_offset), + ); + 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 + 1); + state + .table_state + .select(Some(state.current_scroll_position.saturating_sub(start))); + &table_data.data[start..end] + }; // Calculate widths if self.recalculate_column_widths { @@ -187,7 +192,7 @@ impl<'a> TextTable<'a> { Table::new(disk_rows) .block(disk_block) .header(header) - .highlight_style(self.highlighted_style) + .highlight_style(self.highlighted_text_style) .style(self.text_style) .widths( &(widths diff --git a/src/canvas/widgets/disk_table.rs b/src/canvas/widgets/disk_table.rs index 8c7adc7d..e897a770 100644 --- a/src/canvas/widgets/disk_table.rs +++ b/src/canvas/widgets/disk_table.rs @@ -14,7 +14,7 @@ impl Painter { if let Some(disk_widget_state) = app_state.disk_state.widget_states.get_mut(&widget_id) { let is_on_widget = app_state.current_widget.widget_id == widget_id; - let (border_style, highlighted_style) = if is_on_widget { + let (border_style, highlighted_text_style) = if is_on_widget { ( self.colours.highlighted_border_style, self.colours.currently_selected_text_style, @@ -29,7 +29,7 @@ impl Painter { recalculate_column_widths, header_style: self.colours.table_header_style, border_style, - highlighted_style, + highlighted_text_style, title: " Disks ".into(), is_expanded: app_state.is_expanded, is_on_widget, diff --git a/src/canvas/widgets/temp_table.rs b/src/canvas/widgets/temp_table.rs index 2e95a13a..890411e2 100644 --- a/src/canvas/widgets/temp_table.rs +++ b/src/canvas/widgets/temp_table.rs @@ -14,7 +14,7 @@ impl Painter { if let Some(temp_widget_state) = app_state.temp_state.widget_states.get_mut(&widget_id) { let is_on_widget = app_state.current_widget.widget_id == widget_id; - let (border_style, highlighted_style) = if is_on_widget { + let (border_style, highlighted_text_style) = if is_on_widget { ( self.colours.highlighted_border_style, self.colours.currently_selected_text_style, @@ -29,7 +29,7 @@ impl Painter { recalculate_column_widths, header_style: self.colours.table_header_style, border_style, - highlighted_style, + highlighted_text_style, title: " Temperatures ".into(), is_expanded: app_state.is_expanded, is_on_widget, From 2e51590bf5e819f4881b5f1b50ec54631ea0fd70 Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Mon, 2 May 2022 18:27:09 -0400 Subject: [PATCH 04/16] refactor: don't draw header if too short --- src/app/states.rs | 109 ++++++++--------- src/canvas/components/text_table.rs | 182 +++++++++++++++------------- src/canvas/widgets/cpu_graph.rs | 102 +++++++++------- src/canvas/widgets/disk_table.rs | 1 - src/canvas/widgets/temp_table.rs | 1 - 5 files changed, 207 insertions(+), 188 deletions(-) diff --git a/src/app/states.rs b/src/app/states.rs index 5d3ace9c..ebb28996 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -117,9 +117,7 @@ impl TableComponentState { /// Calculates widths for the columns for this table. /// - /// * `total_width` is the, well, total width available. **NOTE:** This function automatically - /// takes away 2 from the width as part of the left/right - /// bounds. + /// * `total_width` is the, well, total width available. /// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if /// false. /// @@ -128,71 +126,68 @@ impl TableComponentState { use itertools::Either; use std::cmp::{max, min}; - if total_width > 2 { - let initial_width = total_width - 2; - let mut total_width_left = initial_width; + let mut total_width_left = total_width; - let column_widths = &mut self.calculated_widths; - *column_widths = vec![0; self.columns.len()]; + let column_widths = &mut self.calculated_widths; + *column_widths = vec![0; self.columns.len()]; - let columns = if left_to_right { - Either::Left(self.columns.iter().enumerate()) - } else { - Either::Right(self.columns.iter().enumerate().rev()) - }; + let columns = if left_to_right { + Either::Left(self.columns.iter().enumerate()) + } else { + Either::Right(self.columns.iter().enumerate().rev()) + }; - for (itx, column) in columns { - match &column.width_bounds { - WidthBounds::Soft { - min_width, - desired, - max_percentage, - } => { - let soft_limit = max( - if let Some(max_percentage) = max_percentage { - // Rust doesn't have an `into()` or `try_into()` for floats to integers??? - ((*max_percentage * f32::from(initial_width)).ceil()) as u16 - } else { - *desired - }, - *min_width, - ); - let space_taken = min(min(soft_limit, *desired), total_width_left); - - if *min_width > space_taken { - break; + for (itx, column) in columns { + match &column.width_bounds { + WidthBounds::Soft { + min_width, + desired, + max_percentage, + } => { + let soft_limit = max( + if let Some(max_percentage) = max_percentage { + // Rust doesn't have an `into()` or `try_into()` for floats to integers??? + ((*max_percentage * f32::from(total_width)).ceil()) as u16 } else { - total_width_left = total_width_left.saturating_sub(space_taken + 1); - column_widths[itx] = space_taken; - } + *desired + }, + *min_width, + ); + let space_taken = min(min(soft_limit, *desired), total_width_left); + + if *min_width > space_taken { + break; + } else { + total_width_left = total_width_left.saturating_sub(space_taken + 1); + column_widths[itx] = space_taken; } - WidthBounds::Hard(width) => { - let space_taken = min(*width, total_width_left); + } + WidthBounds::Hard(width) => { + let space_taken = min(*width, total_width_left); - if *width > space_taken { - break; - } else { - total_width_left = total_width_left.saturating_sub(space_taken + 1); - column_widths[itx] = space_taken; - } + if *width > space_taken { + break; + } else { + total_width_left = total_width_left.saturating_sub(space_taken + 1); + column_widths[itx] = space_taken; } } } + } - while let Some(0) = column_widths.last() { - column_widths.pop(); - } + while let Some(0) = column_widths.last() { + column_widths.pop(); + } - if !column_widths.is_empty() { - // Redistribute remaining. - let amount_per_slot = total_width_left / column_widths.len() as u16; - total_width_left %= column_widths.len() as u16; - for (index, width) in column_widths.iter_mut().enumerate() { - if index < total_width_left.into() { - *width += amount_per_slot + 1; - } else { - *width += amount_per_slot; - } + if !column_widths.is_empty() { + // Redistribute remaining. + let amount_per_slot = total_width_left / column_widths.len() as u16; + total_width_left %= column_widths.len() as u16; + for (index, width) in column_widths.iter_mut().enumerate() { + if index < total_width_left.into() { + *width += amount_per_slot + 1; + } else { + *width += amount_per_slot; } } } diff --git a/src/canvas/components/text_table.rs b/src/canvas/components/text_table.rs index 767b8e6c..86cc5d1b 100644 --- a/src/canvas/components/text_table.rs +++ b/src/canvas/components/text_table.rs @@ -19,7 +19,6 @@ use crate::{ pub struct TextTable<'a> { pub table_gap: u16, - pub table_height_offset: u16, pub is_force_redraw: bool, pub recalculate_column_widths: bool, @@ -100,71 +99,23 @@ impl<'a> TextTable<'a> { &self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState, table_data: &TableData, ) -> Rect { - let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { - 0 - } else { - self.table_gap - }; - - let sliced_vec = { - let num_rows = usize::from( - (draw_loc.height + 1 - table_gap).saturating_sub(self.table_height_offset), - ); - 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 + 1); - 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.row_widths) - .for_each(|(column, data_width)| match &mut column.width_bounds { - app::WidthBounds::Soft { - min_width: _, - desired, - max_percentage: _, - } => { - *desired = std::cmp::max(column.name.len(), *data_width) as u16; - } - app::WidthBounds::Hard(_width) => {} - }); - - state.calculate_column_widths(draw_loc.width, self.left_to_right); - } - - let columns = &state.columns; - let widths = &state.calculated_widths; - // TODO: Maybe truncate this too? - let header = Row::new(columns.iter().map(|c| Text::raw(c.name.as_ref()))) - .style(self.header_style) - .bottom_margin(table_gap); - let disk_rows = sliced_vec.iter().map(|row| { - Row::new( - row.iter() - .zip(widths) - .map(|(cell, width)| truncate_text(cell, (*width).into())), - ) - }); - - let title = self.generate_title( - draw_loc, - state.current_scroll_position.saturating_add(1), - table_data.data.len(), - ); + let margined_draw_loc = Layout::default() + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(if self.is_on_widget || self.draw_border { + 0 + } else { + 1 + }) + .direction(Direction::Horizontal) + .split(draw_loc)[0]; let disk_block = if self.draw_border { + let title = self.generate_title( + draw_loc, + state.current_scroll_position.saturating_add(1), + table_data.data.len(), + ); + Block::default() .title(title) .borders(Borders::ALL) @@ -177,34 +128,99 @@ impl<'a> TextTable<'a> { Block::default().borders(Borders::NONE) }; - let margined_draw_loc = Layout::default() - .constraints([Constraint::Percentage(100)]) - .horizontal_margin(if self.is_on_widget || self.draw_border { + let (inner_width, inner_height) = { + let inner = disk_block.inner(margined_draw_loc); + (inner.width, inner.height) + }; + + if inner_width == 0 || inner_height == 0 { + f.render_widget(disk_block, margined_draw_loc); + 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 { - 1 - }) - .direction(Direction::Horizontal) - .split(draw_loc)[0]; + self.table_gap + }; - // Draw! - f.render_stateful_widget( - Table::new(disk_rows) - .block(disk_block) - .header(header) - .highlight_style(self.highlighted_text_style) - .style(self.text_style) - .widths( + 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 + 1); + 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.row_widths) + .for_each(|(column, data_width)| match &mut column.width_bounds { + app::WidthBounds::Soft { + min_width: _, + desired, + max_percentage: _, + } => { + *desired = std::cmp::max(column.name.len(), *data_width) as u16; + } + app::WidthBounds::Hard(_width) => {} + }); + + state.calculate_column_widths(inner_width, self.left_to_right); + } + + let columns = &state.columns; + let widths = &state.calculated_widths; + // TODO: Maybe truncate this too? + let header = Row::new(columns.iter().map(|c| Text::raw(c.name.as_ref()))) + .style(self.header_style) + .bottom_margin(table_gap); + let disk_rows = sliced_vec.iter().map(|row| { + Row::new( + row.iter() + .zip(widths) + .map(|(cell, width)| truncate_text(cell, (*width).into())), + ) + }); + + let widget = { + let mut table = Table::new(disk_rows) + .block(disk_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( &(widths .iter() .map(|w| Constraint::Length(*w)) .collect::>()), ), - margined_draw_loc, - &mut state.table_state, - ); + margined_draw_loc, + &mut state.table_state, + ); - margined_draw_loc + margined_draw_loc + } } } diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/widgets/cpu_graph.rs index fcb7daa2..36ba33e8 100644 --- a/src/canvas/widgets/cpu_graph.rs +++ b/src/canvas/widgets/cpu_graph.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use crate::{ - app::{layout_manager::WidgetDirection, App}, + app::{layout_manager::WidgetDirection, App, CpuWidgetState}, canvas::{ components::{GraphData, TimeGraph}, drawing_utils::{get_column_widths, get_start_position, should_hide_x_label}, @@ -115,6 +115,56 @@ impl Painter { } } + fn generate_points<'a>( + &self, cpu_widget_state: &CpuWidgetState, cpu_data: &'a [ConvertedCpuData], + show_avg_cpu: bool, + ) -> Vec> { + let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 }; + + let current_scroll_position = cpu_widget_state.scroll_state.current_scroll_position; + if current_scroll_position == ALL_POSITION { + // This case ensures the other cases cannot have the position be equal to 0. + cpu_data + .iter() + .enumerate() + .rev() + .map(|(itx, cpu)| { + let style = if show_avg_cpu && itx == AVG_POSITION { + self.colours.avg_colour_style + } else if itx == ALL_POSITION { + self.colours.all_colour_style + } else { + let offset_position = itx - 1; // Because of the all position + self.colours.cpu_colour_styles[(offset_position - show_avg_offset) + % self.colours.cpu_colour_styles.len()] + }; + + GraphData { + points: &cpu.cpu_data[..], + style, + name: None, + } + }) + .collect::>() + } else if let Some(cpu) = cpu_data.get(current_scroll_position) { + let style = if show_avg_cpu && current_scroll_position == AVG_POSITION { + self.colours.avg_colour_style + } else { + let offset_position = current_scroll_position - 1; // Because of the all position + self.colours.cpu_colour_styles + [(offset_position - show_avg_offset) % self.colours.cpu_colour_styles.len()] + }; + + vec![GraphData { + points: &cpu.cpu_data[..], + style, + name: None, + }] + } else { + vec![] + } + } + fn draw_cpu_graph( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { @@ -131,52 +181,12 @@ impl Painter { &mut cpu_widget_state.autohide_timer, draw_loc, ); - let show_avg_cpu = app_state.app_config_fields.show_average_cpu; - let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 }; - let points = { - let current_scroll_position = cpu_widget_state.scroll_state.current_scroll_position; - if current_scroll_position == ALL_POSITION { - // This case ensures the other cases cannot have the position be equal to 0. - cpu_data - .iter() - .enumerate() - .rev() - .map(|(itx, cpu)| { - let style = if show_avg_cpu && itx == AVG_POSITION { - self.colours.avg_colour_style - } else if itx == ALL_POSITION { - self.colours.all_colour_style - } else { - let offset_position = itx - 1; // Because of the all position - self.colours.cpu_colour_styles[(offset_position - show_avg_offset) - % self.colours.cpu_colour_styles.len()] - }; - GraphData { - points: &cpu.cpu_data[..], - style, - name: None, - } - }) - .collect::>() - } else if let Some(cpu) = cpu_data.get(current_scroll_position) { - let style = if show_avg_cpu && current_scroll_position == AVG_POSITION { - self.colours.avg_colour_style - } else { - let offset_position = current_scroll_position - 1; // Because of the all position - self.colours.cpu_colour_styles[(offset_position - show_avg_offset) - % self.colours.cpu_colour_styles.len()] - }; - - vec![GraphData { - points: &cpu.cpu_data[..], - style, - name: None, - }] - } else { - vec![] - } - }; + let points = self.generate_points( + &cpu_widget_state, + cpu_data, + app_state.app_config_fields.show_average_cpu, + ); // TODO: Maybe hide load avg if too long? Or maybe the CPU part. let title = if cfg!(target_family = "unix") { diff --git a/src/canvas/widgets/disk_table.rs b/src/canvas/widgets/disk_table.rs index e897a770..fd667290 100644 --- a/src/canvas/widgets/disk_table.rs +++ b/src/canvas/widgets/disk_table.rs @@ -24,7 +24,6 @@ impl Painter { }; let margined_draw_loc = TextTable { table_gap: app_state.app_config_fields.table_gap, - table_height_offset: self.table_height_offset, is_force_redraw: app_state.is_force_redraw, recalculate_column_widths, header_style: self.colours.table_header_style, diff --git a/src/canvas/widgets/temp_table.rs b/src/canvas/widgets/temp_table.rs index 890411e2..bdd30134 100644 --- a/src/canvas/widgets/temp_table.rs +++ b/src/canvas/widgets/temp_table.rs @@ -24,7 +24,6 @@ impl Painter { }; let margined_draw_loc = TextTable { table_gap: app_state.app_config_fields.table_gap, - table_height_offset: self.table_height_offset, is_force_redraw: app_state.is_force_redraw, recalculate_column_widths, header_style: self.colours.table_header_style, From df1a4183273f3940aeddd109c25414a056b1e010 Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Tue, 3 May 2022 04:46:43 -0400 Subject: [PATCH 05/16] refactor: per-row styling, remove seemingly redundant table code --- src/app/states.rs | 16 ++++++---- src/canvas/components/text_table.rs | 49 ++++++++++++++++++----------- src/canvas/widgets/cpu_graph.rs | 4 +-- src/canvas/widgets/disk_table.rs | 10 +++--- src/canvas/widgets/temp_table.rs | 11 +++---- src/data_conversion.rs | 37 ++++++++++++++-------- 6 files changed, 75 insertions(+), 52 deletions(-) diff --git a/src/app/states.rs b/src/app/states.rs index ebb28996..4659249c 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -7,6 +7,7 @@ use tui::widgets::TableState; use crate::{ app::{layout_manager::BottomWidgetType, query::*}, constants, + data_conversion::CellContent, data_harvester::processes::{self, ProcessSorting}, }; use ProcessSorting::*; @@ -70,10 +71,7 @@ impl WidthBounds { pub struct TableComponentColumn { /// The name of the column. Displayed if possible as the header. - pub name: Cow<'static, str>, - - /// An optional alternative column name. Displayed if `name` doesn't fit. - pub alt: Option>, + pub name: CellContent, /// A restriction on this column's width, if desired. pub width_bounds: WidthBounds, @@ -85,8 +83,14 @@ impl TableComponentColumn { I: Into>, { Self { - name: name.into(), - alt: alt.map(Into::into), + name: if let Some(alt) = alt { + CellContent::HasAlt { + alt: alt.into(), + main: name.into(), + } + } else { + CellContent::Simple(name.into()) + }, width_bounds, } } diff --git a/src/canvas/components/text_table.rs b/src/canvas/components/text_table.rs index 86cc5d1b..15dc15cf 100644 --- a/src/canvas/components/text_table.rs +++ b/src/canvas/components/text_table.rs @@ -14,7 +14,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::{ app::{self, TableComponentState}, constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT}, - data_conversion::{CellContent, TableData}, + data_conversion::{CellContent, TableData, TableRow}, }; pub struct TextTable<'a> { @@ -98,14 +98,12 @@ impl<'a> TextTable<'a> { pub fn draw_text_table( &self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState, table_data: &TableData, - ) -> Rect { + ) { + // 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 self.is_on_widget || self.draw_border { - 0 - } else { - 1 - }) + .horizontal_margin(if is_not_basic { 0 } else { 1 }) .direction(Direction::Horizontal) .split(draw_loc)[0]; @@ -135,7 +133,6 @@ impl<'a> TextTable<'a> { if inner_width == 0 || inner_height == 0 { f.render_widget(disk_block, margined_draw_loc); - margined_draw_loc } else { let show_header = inner_height > 1; let header_height = if show_header { 1 } else { 0 }; @@ -183,15 +180,24 @@ impl<'a> TextTable<'a> { let columns = &state.columns; let widths = &state.calculated_widths; - // TODO: Maybe truncate this too? - let header = Row::new(columns.iter().map(|c| Text::raw(c.name.as_ref()))) - .style(self.header_style) - .bottom_margin(table_gap); + let header = Row::new( + columns + .iter() + .zip(widths) + .map(|(c, width)| truncate_text(&c.name, (*width).into(), None)), + ) + .style(self.header_style) + .bottom_margin(table_gap); let disk_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(widths) - .map(|(cell, width)| truncate_text(cell, (*width).into())), + .map(|(cell, width)| truncate_text(cell, (*width).into(), style)), ) }); @@ -218,21 +224,22 @@ impl<'a> TextTable<'a> { margined_draw_loc, &mut state.table_state, ); - - margined_draw_loc } } } /// Truncates text if it is too long, and adds an ellipsis at the end if needed. -fn truncate_text(content: &CellContent, width: usize) -> Text<'_> { +fn truncate_text(content: &CellContent, width: usize, row_style: Option