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