From dd7e183ec8a152d5fda84c49a7f79141171e1448 Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Tue, 24 Aug 2021 22:34:25 -0400 Subject: [PATCH] refactor: rip out trait system for drawing widgets This rips out this weird trait system I previously used for drawing widgets, where I implemented a trait onto the Painter struct that did the drawing. I have no idea what I was thinking back then. --- src/app.rs | 4 + src/bin/main.rs | 6 + src/canvas.rs | 93 +- src/canvas/widgets.rs | 22 +- src/canvas/widgets/basic_table_arrows.rs | 264 ++-- src/canvas/widgets/battery_display.rs | 342 +++--- src/canvas/widgets/cpu_basic.rs | 319 +++-- src/canvas/widgets/cpu_graph.rs | 850 +++++++------ src/canvas/widgets/disk_table.rs | 436 ++++--- src/canvas/widgets/mem_basic.rs | 207 ++-- src/canvas/widgets/mem_graph.rs | 377 +++--- src/canvas/widgets/network_basic.rs | 111 +- src/canvas/widgets/network_graph.rs | 1406 +++++++++++---------- src/canvas/widgets/process_table.rs | 1435 +++++++++++----------- src/canvas/widgets/temp_table.rs | 412 +++---- src/lib.rs | 7 +- 16 files changed, 3070 insertions(+), 3221 deletions(-) diff --git a/src/app.rs b/src/app.rs index d0237d94..fe4e44da 100644 --- a/src/app.rs +++ b/src/app.rs @@ -270,6 +270,10 @@ impl AppState { EventResult::NoRedraw } } + BottomEvent::Resize { + width: _, + height: _, + } => EventResult::Redraw, BottomEvent::Clean => { self.data_collection .clean_data(constants::STALE_MAX_MILLISECONDS); diff --git a/src/bin/main.rs b/src/bin/main.rs index 075ea051..37431389 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -233,6 +233,12 @@ fn main() -> Result<()> { try_drawing(&mut terminal, &mut app, &mut painter)?; } } + BottomEvent::Resize { + width: _, + height: _, + } => { + try_drawing(&mut terminal, &mut app, &mut painter)?; + } BottomEvent::Clean => { app.data_collection .clean_data(constants::STALE_MAX_MILLISECONDS); diff --git a/src/canvas.rs b/src/canvas.rs index 55bbe968..5d9fe89f 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -95,8 +95,6 @@ impl FromStr for ColourScheme { /// Handles the canvas' state. TODO: [OPT] implement this. pub struct Painter { pub colours: CanvasColours, - height: u16, - width: u16, styled_help_text: Vec>, is_mac_os: bool, // FIXME: This feels out of place... row_constraints: Vec, @@ -182,8 +180,6 @@ impl Painter { let mut painter = Painter { colours: CanvasColours::default(), - height: 0, - width: 0, styled_help_text: Vec::default(), is_mac_os: cfg!(target_os = "macos"), row_constraints, @@ -313,36 +309,6 @@ impl Painter { let terminal_height = terminal_size.height; let terminal_width = terminal_size.width; - if (self.height == 0 && self.width == 0) - || (self.height != terminal_height || self.width != terminal_width) - { - app_state.is_force_redraw = true; - self.height = terminal_height; - self.width = terminal_width; - } - - if app_state.should_get_widget_bounds() { - // If we're force drawing, reset ALL mouse boundaries. - for widget in app_state.widget_map.values_mut() { - widget.top_left_corner = None; - widget.bottom_right_corner = None; - } - - // Reset dd_dialog... - app_state.delete_dialog_state.button_positions = vec![]; - - // Reset battery dialog... - for battery_widget in app_state.battery_state.widget_states.values_mut() { - battery_widget.tab_click_locs = None; - } - - // Reset column headers for sorting in process widget... - for proc_widget in app_state.proc_state.widget_states.values_mut() { - proc_widget.columns.column_header_y_loc = None; - proc_widget.columns.column_header_x_locs = None; - } - } - if app_state.help_dialog_state.is_showing_help { let gen_help_len = GENERAL_HELP_TEXT.len() as u16 + 3; let border_len = terminal_height.saturating_sub(gen_help_len) / 2; @@ -461,39 +427,45 @@ impl Painter { .constraints([Constraint::Percentage(100)]) .split(terminal_size); match &app_state.current_widget.widget_type { - Cpu => self.draw_cpu( + Cpu => draw_cpu( + self, &mut f, app_state, rect[0], app_state.current_widget.widget_id, ), - CpuLegend => self.draw_cpu( + CpuLegend => draw_cpu( + self, &mut f, app_state, rect[0], app_state.current_widget.widget_id - 1, ), - Mem | BasicMem => self.draw_memory_graph( + Mem | BasicMem => draw_memory_graph( + self, &mut f, app_state, rect[0], app_state.current_widget.widget_id, ), - Disk => self.draw_disk_table( + Disk => draw_disk_table( + self, &mut f, app_state, rect[0], true, app_state.current_widget.widget_id, ), - Temp => self.draw_temp_table( + Temp => draw_temp_table( + self, &mut f, app_state, rect[0], true, app_state.current_widget.widget_id, ), - Net => self.draw_network_graph( + Net => draw_network_graph( + self, &mut f, app_state, rect[0], @@ -508,9 +480,10 @@ impl Painter { _ => 0, }; - self.draw_process_features(&mut f, app_state, rect[0], true, widget_id); + draw_process_features(self, &mut f, app_state, rect[0], true, widget_id); } - Battery => self.draw_battery_display( + Battery => draw_battery_display( + self, &mut f, app_state, rect[0], @@ -555,16 +528,17 @@ impl Painter { .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .split(vertical_chunks[1]); - self.draw_basic_cpu(&mut f, app_state, vertical_chunks[0], 1); - self.draw_basic_memory(&mut f, app_state, middle_chunks[0], 2); - self.draw_basic_network(&mut f, app_state, middle_chunks[1], 3); + draw_basic_cpu(self, &mut f, app_state, vertical_chunks[0], 1); + draw_basic_memory(self, &mut f, app_state, middle_chunks[0], 2); + draw_basic_network(self, &mut f, app_state, middle_chunks[1], 3); let mut later_widget_id: Option = None; if let Some(basic_table_widget_state) = &app_state.basic_table_widget_state { let widget_id = basic_table_widget_state.currently_displayed_widget_id; later_widget_id = Some(widget_id); match basic_table_widget_state.currently_displayed_widget_type { - Disk => self.draw_disk_table( + Disk => draw_disk_table( + self, &mut f, app_state, vertical_chunks[3], @@ -578,7 +552,8 @@ impl Painter { ProcSort => 2, _ => 0, }; - self.draw_process_features( + draw_process_features( + self, &mut f, app_state, vertical_chunks[3], @@ -586,14 +561,16 @@ impl Painter { wid, ); } - Temp => self.draw_temp_table( + Temp => draw_temp_table( + self, &mut f, app_state, vertical_chunks[3], false, widget_id, ), - Battery => self.draw_battery_display( + Battery => draw_battery_display( + self, &mut f, app_state, vertical_chunks[3], @@ -605,7 +582,7 @@ impl Painter { } if let Some(widget_id) = later_widget_id { - self.draw_basic_table_arrows(&mut f, app_state, vertical_chunks[2], widget_id); + draw_basic_table_arrows(self, &mut f, app_state, vertical_chunks[2], widget_id); } } else { // Draws using the passed in (or default) layout. @@ -713,23 +690,25 @@ impl Painter { for (widget, widget_draw_loc) in widgets.children.iter().zip(widget_draw_locs) { match &widget.widget_type { Empty => {} - Cpu => self.draw_cpu(f, app_state, *widget_draw_loc, widget.widget_id), - Mem => self.draw_memory_graph(f, app_state, *widget_draw_loc, widget.widget_id), - Net => self.draw_network(f, app_state, *widget_draw_loc, widget.widget_id), + Cpu => draw_cpu(self, f, app_state, *widget_draw_loc, widget.widget_id), + Mem => draw_memory_graph(self, f, app_state, *widget_draw_loc, widget.widget_id), + Net => draw_network(self, f, app_state, *widget_draw_loc, widget.widget_id), Temp => { - self.draw_temp_table(f, app_state, *widget_draw_loc, true, widget.widget_id) + draw_temp_table(self, f, app_state, *widget_draw_loc, true, widget.widget_id) } Disk => { - self.draw_disk_table(f, app_state, *widget_draw_loc, true, widget.widget_id) + draw_disk_table(self, f, app_state, *widget_draw_loc, true, widget.widget_id) } - Proc => self.draw_process_features( + Proc => draw_process_features( + self, f, app_state, *widget_draw_loc, true, widget.widget_id, ), - Battery => self.draw_battery_display( + Battery => draw_battery_display( + self, f, app_state, *widget_draw_loc, diff --git a/src/canvas/widgets.rs b/src/canvas/widgets.rs index a76b4591..c2f5f91e 100644 --- a/src/canvas/widgets.rs +++ b/src/canvas/widgets.rs @@ -10,14 +10,14 @@ pub mod network_graph; pub mod process_table; pub mod temp_table; -pub use basic_table_arrows::BasicTableArrows; -pub use battery_display::BatteryDisplayWidget; -pub use cpu_basic::CpuBasicWidget; -pub use cpu_graph::CpuGraphWidget; -pub use disk_table::DiskTableWidget; -pub use mem_basic::MemBasicWidget; -pub use mem_graph::MemGraphWidget; -pub use network_basic::NetworkBasicWidget; -pub use network_graph::NetworkGraphWidget; -pub use process_table::ProcessTableWidget; -pub use temp_table::TempTableWidget; +pub use basic_table_arrows::*; +pub use battery_display::*; +pub use cpu_basic::*; +pub use cpu_graph::*; +pub use disk_table::*; +pub use mem_basic::*; +pub use mem_graph::*; +pub use network_basic::*; +pub use network_graph::*; +pub use process_table::*; +pub use temp_table::*; diff --git a/src/canvas/widgets/basic_table_arrows.rs b/src/canvas/widgets/basic_table_arrows.rs index 82c23245..8e1f476e 100644 --- a/src/canvas/widgets/basic_table_arrows.rs +++ b/src/canvas/widgets/basic_table_arrows.rs @@ -12,154 +12,142 @@ use tui::{ widgets::{Block, Paragraph}, }; -pub trait BasicTableArrows { - fn draw_basic_table_arrows( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ); -} +pub fn draw_basic_table_arrows( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + widget_id: u64, +) { + if let Some(current_table) = app_state.widget_map.get(&widget_id) { + let current_table = if let BottomWidgetType::ProcSort = current_table.widget_type { + current_table + .right_neighbour + .map(|id| app_state.widget_map.get(&id).unwrap()) + .unwrap() + } else { + current_table + }; -impl BasicTableArrows for Painter { - fn draw_basic_table_arrows( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ) { - if let Some(current_table) = app_state.widget_map.get(&widget_id) { - let current_table = if let BottomWidgetType::ProcSort = current_table.widget_type { + let (left_table, right_table) = ( + { + current_table + .left_neighbour + .map(|left_widget_id| { + app_state + .widget_map + .get(&left_widget_id) + .map(|left_widget| { + if left_widget.widget_type == BottomWidgetType::ProcSort { + left_widget + .left_neighbour + .map(|left_left_widget_id| { + app_state.widget_map.get(&left_left_widget_id).map( + |left_left_widget| &left_left_widget.widget_type, + ) + }) + .unwrap_or(Some(&BottomWidgetType::Temp)) + .unwrap_or(&BottomWidgetType::Temp) + } else { + &left_widget.widget_type + } + }) + .unwrap_or(&BottomWidgetType::Temp) + }) + .unwrap_or(&BottomWidgetType::Temp) + }, + { current_table .right_neighbour - .map(|id| app_state.widget_map.get(&id).unwrap()) - .unwrap() - } else { - current_table - }; + .map(|right_widget_id| { + app_state + .widget_map + .get(&right_widget_id) + .map(|right_widget| { + if right_widget.widget_type == BottomWidgetType::ProcSort { + right_widget + .right_neighbour + .map(|right_right_widget_id| { + app_state.widget_map.get(&right_right_widget_id).map( + |right_right_widget| { + &right_right_widget.widget_type + }, + ) + }) + .unwrap_or(Some(&BottomWidgetType::Disk)) + .unwrap_or(&BottomWidgetType::Disk) + } else { + &right_widget.widget_type + } + }) + .unwrap_or(&BottomWidgetType::Disk) + }) + .unwrap_or(&BottomWidgetType::Disk) + }, + ); - let (left_table, right_table) = ( - { - current_table - .left_neighbour - .map(|left_widget_id| { - app_state - .widget_map - .get(&left_widget_id) - .map(|left_widget| { - if left_widget.widget_type == BottomWidgetType::ProcSort { - left_widget - .left_neighbour - .map(|left_left_widget_id| { - app_state.widget_map.get(&left_left_widget_id).map( - |left_left_widget| { - &left_left_widget.widget_type - }, - ) - }) - .unwrap_or(Some(&BottomWidgetType::Temp)) - .unwrap_or(&BottomWidgetType::Temp) - } else { - &left_widget.widget_type - } - }) - .unwrap_or(&BottomWidgetType::Temp) - }) - .unwrap_or(&BottomWidgetType::Temp) - }, - { - current_table - .right_neighbour - .map(|right_widget_id| { - app_state - .widget_map - .get(&right_widget_id) - .map(|right_widget| { - if right_widget.widget_type == BottomWidgetType::ProcSort { - right_widget - .right_neighbour - .map(|right_right_widget_id| { - app_state - .widget_map - .get(&right_right_widget_id) - .map(|right_right_widget| { - &right_right_widget.widget_type - }) - }) - .unwrap_or(Some(&BottomWidgetType::Disk)) - .unwrap_or(&BottomWidgetType::Disk) - } else { - &right_widget.widget_type - } - }) - .unwrap_or(&BottomWidgetType::Disk) - }) - .unwrap_or(&BottomWidgetType::Disk) - }, - ); + let left_name = left_table.get_pretty_name(); + let right_name = right_table.get_pretty_name(); - let left_name = left_table.get_pretty_name(); - let right_name = right_table.get_pretty_name(); + let num_spaces = + usize::from(draw_loc.width).saturating_sub(6 + left_name.len() + right_name.len()); - let num_spaces = - usize::from(draw_loc.width).saturating_sub(6 + left_name.len() + right_name.len()); + let left_arrow_text = vec![ + Spans::default(), + Spans::from(Span::styled( + format!("◄ {}", left_name), + painter.colours.text_style, + )), + ]; - let left_arrow_text = vec![ - Spans::default(), - Spans::from(Span::styled( - format!("◄ {}", left_name), - self.colours.text_style, - )), - ]; + let right_arrow_text = vec![ + Spans::default(), + Spans::from(Span::styled( + format!("{} ►", right_name), + painter.colours.text_style, + )), + ]; - let right_arrow_text = vec![ - Spans::default(), - Spans::from(Span::styled( - format!("{} ►", right_name), - self.colours.text_style, - )), - ]; + let margined_draw_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Length(2 + left_name.len() as u16), + Constraint::Length(num_spaces as u16), + Constraint::Length(2 + right_name.len() as u16), + ]) + .horizontal_margin(1) + .split(draw_loc); - let margined_draw_loc = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Length(2 + left_name.len() as u16), - Constraint::Length(num_spaces as u16), - Constraint::Length(2 + right_name.len() as u16), - ]) - .horizontal_margin(1) - .split(draw_loc); + f.render_widget( + Paragraph::new(left_arrow_text).block(Block::default()), + margined_draw_loc[0], + ); + f.render_widget( + Paragraph::new(right_arrow_text) + .block(Block::default()) + .alignment(Alignment::Right), + margined_draw_loc[2], + ); - f.render_widget( - Paragraph::new(left_arrow_text).block(Block::default()), - margined_draw_loc[0], - ); - f.render_widget( - Paragraph::new(right_arrow_text) - .block(Block::default()) - .alignment(Alignment::Right), - margined_draw_loc[2], - ); - - if app_state.should_get_widget_bounds() { - // Some explanations for future readers: - // - The "height" as of writing of this entire widget is 2. If it's 1, it occasionally doesn't draw. - // - As such, the buttons are only on the lower part of this 2-high widget. - // - So, we want to only check at one location, the `draw_loc.y + 1`, and that's it. - // - But why is it "+2" then? Well, it's because I have a REALLY ugly hack - // for mouse button checking, since most button checks are of the form `(draw_loc.y + draw_loc.height)`, - // and the same for the x and width. Unfortunately, if you check using >= and <=, the outer bound is - // actually too large - so, we assume all of them are one too big and check via < (see - // https://github.com/ClementTsang/bottom/pull/459 for details). - // - So in other words, to make it simple, we keep this to a standard and overshoot by one here. - if let Some(basic_table) = &mut app_state.basic_table_widget_state { - basic_table.left_tlc = - Some((margined_draw_loc[0].x, margined_draw_loc[0].y + 1)); - basic_table.left_brc = Some(( - margined_draw_loc[0].x + margined_draw_loc[0].width, - margined_draw_loc[0].y + 2, - )); - basic_table.right_tlc = - Some((margined_draw_loc[2].x, margined_draw_loc[2].y + 1)); - basic_table.right_brc = Some(( - margined_draw_loc[2].x + margined_draw_loc[2].width, - margined_draw_loc[2].y + 2, - )); - } + if app_state.should_get_widget_bounds() { + // Some explanations for future readers: + // - The "height" as of writing of this entire widget is 2. If it's 1, it occasionally doesn't draw. + // - As such, the buttons are only on the lower part of this 2-high widget. + // - So, we want to only check at one location, the `draw_loc.y + 1`, and that's it. + // - But why is it "+2" then? Well, it's because I have a REALLY ugly hack + // for mouse button checking, since most button checks are of the form `(draw_loc.y + draw_loc.height)`, + // and the same for the x and width. Unfortunately, if you check using >= and <=, the outer bound is + // actually too large - so, we assume all of them are one too big and check via < (see + // https://github.com/ClementTsang/bottom/pull/459 for details). + // - So in other words, to make it simple, we keep this to a standard and overshoot by one here. + if let Some(basic_table) = &mut app_state.basic_table_widget_state { + basic_table.left_tlc = Some((margined_draw_loc[0].x, margined_draw_loc[0].y + 1)); + basic_table.left_brc = Some(( + margined_draw_loc[0].x + margined_draw_loc[0].width, + margined_draw_loc[0].y + 2, + )); + basic_table.right_tlc = Some((margined_draw_loc[2].x, margined_draw_loc[2].y + 1)); + basic_table.right_brc = Some(( + margined_draw_loc[2].x + margined_draw_loc[2].width, + margined_draw_loc[2].y + 2, + )); } } } diff --git a/src/canvas/widgets/battery_display.rs b/src/canvas/widgets/battery_display.rs index 34d9783c..2b9fe5f5 100644 --- a/src/canvas/widgets/battery_display.rs +++ b/src/canvas/widgets/battery_display.rs @@ -13,198 +13,184 @@ use tui::{ }; use unicode_segmentation::UnicodeSegmentation; -pub trait BatteryDisplayWidget { - fn draw_battery_display( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, - widget_id: u64, - ); -} +pub fn draw_battery_display( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + draw_border: bool, widget_id: u64, +) { + let should_get_widget_bounds = app_state.should_get_widget_bounds(); + if let Some(battery_widget_state) = app_state.battery_state.widget_states.get_mut(&widget_id) { + let is_on_widget = widget_id == app_state.current_widget.widget_id; + let border_style = if is_on_widget { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }; + let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { + 0 + } else { + app_state.app_config_fields.table_gap + }; -impl BatteryDisplayWidget for Painter { - fn draw_battery_display( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, - widget_id: u64, - ) { - let should_get_widget_bounds = app_state.should_get_widget_bounds(); - if let Some(battery_widget_state) = - app_state.battery_state.widget_states.get_mut(&widget_id) - { - let is_on_widget = widget_id == app_state.current_widget.widget_id; - let border_style = if is_on_widget { - self.colours.highlighted_border_style - } else { - self.colours.border_style - }; - let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { - 0 - } else { - app_state.app_config_fields.table_gap - }; - - let title = if app_state.is_expanded { - const TITLE_BASE: &str = " Battery ── Esc to go back "; - Spans::from(vec![ - Span::styled(" Battery ".to_string(), self.colours.widget_title_style), - Span::styled( - format!( - "─{}─ Esc to go back ", - "─".repeat(usize::from(draw_loc.width).saturating_sub( - UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2 - )) - ), - border_style, + let title = if app_state.is_expanded { + const TITLE_BASE: &str = " Battery ── Esc to go back "; + Spans::from(vec![ + Span::styled(" Battery ".to_string(), painter.colours.widget_title_style), + Span::styled( + format!( + "─{}─ Esc to go back ", + "─".repeat(usize::from(draw_loc.width).saturating_sub( + UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2 + )) ), - ]) - } else { - Spans::from(Span::styled( - " Battery ".to_string(), - self.colours.widget_title_style, - )) - }; + border_style, + ), + ]) + } else { + Spans::from(Span::styled( + " Battery ".to_string(), + painter.colours.widget_title_style, + )) + }; - let battery_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 battery_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(painter.colours.highlighted_border_style) + } else { + Block::default().borders(Borders::NONE) + }; - let battery_names = app_state - .canvas_data - .battery_data - .iter() - .map(|battery| &battery.battery_name) - .collect::>(); + let battery_names = app_state + .canvas_data + .battery_data + .iter() + .map(|battery| &battery.battery_name) + .collect::>(); - let tab_draw_loc = Layout::default() - .constraints([ - Constraint::Length(1), - Constraint::Length(2), - Constraint::Min(0), - ]) - .direction(Direction::Vertical) - .split(draw_loc)[1]; + let tab_draw_loc = Layout::default() + .constraints([ + Constraint::Length(1), + Constraint::Length(2), + Constraint::Min(0), + ]) + .direction(Direction::Vertical) + .split(draw_loc)[1]; - f.render_widget( - Tabs::new( - battery_names - .iter() - .map(|name| Spans::from((*name).clone())) - .collect::>(), - ) - .block(Block::default()) - .divider(tui::symbols::line::VERTICAL) - .style(self.colours.text_style) - .highlight_style(self.colours.currently_selected_text_style) - .select(battery_widget_state.currently_selected_battery_index), - tab_draw_loc, + f.render_widget( + Tabs::new( + battery_names + .iter() + .map(|name| Spans::from((*name).clone())) + .collect::>(), + ) + .block(Block::default()) + .divider(tui::symbols::line::VERTICAL) + .style(painter.colours.text_style) + .highlight_style(painter.colours.currently_selected_text_style) + .select(battery_widget_state.currently_selected_battery_index), + tab_draw_loc, + ); + + 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]; + + if let Some(battery_details) = app_state + .canvas_data + .battery_data + .get(battery_widget_state.currently_selected_battery_index) + { + // Assuming a 50/50 split in width + let bar_length = usize::from((draw_loc.width.saturating_sub(2) / 2).saturating_sub(8)); + let charge_percentage = battery_details.charge_percentage; + let num_bars = calculate_basic_use_bars(charge_percentage, bar_length); + let bars = format!( + "[{}{}{:3.0}%]", + "|".repeat(num_bars), + " ".repeat(bar_length - num_bars), + charge_percentage, ); - 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]; - - if let Some(battery_details) = app_state - .canvas_data - .battery_data - .get(battery_widget_state.currently_selected_battery_index) - { - // Assuming a 50/50 split in width - let bar_length = - usize::from((draw_loc.width.saturating_sub(2) / 2).saturating_sub(8)); - let charge_percentage = battery_details.charge_percentage; - let num_bars = calculate_basic_use_bars(charge_percentage, bar_length); - let bars = format!( - "[{}{}{:3.0}%]", - "|".repeat(num_bars), - " ".repeat(bar_length - num_bars), - charge_percentage, - ); - - let battery_rows = vec![ - Row::new(vec![ - Cell::from("Charge %").style(self.colours.text_style), - Cell::from(bars).style(if charge_percentage < 10.0 { - self.colours.low_battery_colour - } else if charge_percentage < 50.0 { - self.colours.medium_battery_colour - } else { - self.colours.high_battery_colour - }), - ]), - Row::new(vec!["Consumption", &battery_details.watt_consumption]) - .style(self.colours.text_style), - if let Some(duration_until_full) = &battery_details.duration_until_full { - Row::new(vec!["Time to full", duration_until_full]) - .style(self.colours.text_style) - } else if let Some(duration_until_empty) = &battery_details.duration_until_empty - { - Row::new(vec!["Time to empty", duration_until_empty]) - .style(self.colours.text_style) + let battery_rows = vec![ + Row::new(vec![ + Cell::from("Charge %").style(painter.colours.text_style), + Cell::from(bars).style(if charge_percentage < 10.0 { + painter.colours.low_battery_colour + } else if charge_percentage < 50.0 { + painter.colours.medium_battery_colour } else { - Row::new(vec!["Time to full/empty", "N/A"]).style(self.colours.text_style) - }, - Row::new(vec!["Health %", &battery_details.health]) - .style(self.colours.text_style), - ]; + painter.colours.high_battery_colour + }), + ]), + Row::new(vec!["Consumption", &battery_details.watt_consumption]) + .style(painter.colours.text_style), + if let Some(duration_until_full) = &battery_details.duration_until_full { + Row::new(vec!["Time to full", duration_until_full]) + .style(painter.colours.text_style) + } else if let Some(duration_until_empty) = &battery_details.duration_until_empty { + Row::new(vec!["Time to empty", duration_until_empty]) + .style(painter.colours.text_style) + } else { + Row::new(vec!["Time to full/empty", "N/A"]).style(painter.colours.text_style) + }, + Row::new(vec!["Health %", &battery_details.health]) + .style(painter.colours.text_style), + ]; - // Draw - f.render_widget( - Table::new(battery_rows) - .block(battery_block) - .header(Row::new(vec![""]).bottom_margin(table_gap)) - .widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]), - margined_draw_loc, - ); - } else { - let mut contents = vec![Spans::default(); table_gap as usize]; + // Draw + f.render_widget( + Table::new(battery_rows) + .block(battery_block) + .header(Row::new(vec![""]).bottom_margin(table_gap)) + .widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]), + margined_draw_loc, + ); + } else { + let mut contents = vec![Spans::default(); table_gap as usize]; - contents.push(Spans::from(Span::styled( - "No data found for this battery", - self.colours.text_style, - ))); + contents.push(Spans::from(Span::styled( + "No data found for this battery", + painter.colours.text_style, + ))); - f.render_widget( - Paragraph::new(contents).block(battery_block), - margined_draw_loc, - ); + f.render_widget( + Paragraph::new(contents).block(battery_block), + margined_draw_loc, + ); + } + + if should_get_widget_bounds { + // Tab wizardry + if !battery_names.is_empty() { + let mut current_x = tab_draw_loc.x; + let current_y = tab_draw_loc.y; + let mut tab_click_locs: Vec<((u16, u16), (u16, u16))> = vec![]; + for battery in battery_names { + // +1 because there's a space after the tab label. + let width = unicode_width::UnicodeWidthStr::width(battery.as_str()) as u16; + tab_click_locs.push(((current_x, current_y), (current_x + width, current_y))); + + // +4 because we want to go one space, then one space past to get to the '|', then 2 more + // to start at the blank space before the tab label. + current_x += width + 4; + } + battery_widget_state.tab_click_locs = Some(tab_click_locs); } - if should_get_widget_bounds { - // Tab wizardry - if !battery_names.is_empty() { - let mut current_x = tab_draw_loc.x; - let current_y = tab_draw_loc.y; - let mut tab_click_locs: Vec<((u16, u16), (u16, u16))> = vec![]; - for battery in battery_names { - // +1 because there's a space after the tab label. - let width = unicode_width::UnicodeWidthStr::width(battery.as_str()) as u16; - tab_click_locs - .push(((current_x, current_y), (current_x + width, current_y))); - - // +4 because we want to go one space, then one space past to get to the '|', then 2 more - // to start at the blank space before the tab label. - current_x += width + 4; - } - battery_widget_state.tab_click_locs = Some(tab_click_locs); - } - - // Update draw loc in widget map - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { - widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); - widget.bottom_right_corner = Some(( - margined_draw_loc.x + margined_draw_loc.width, - margined_draw_loc.y + margined_draw_loc.height, - )); - } + // Update draw loc in widget map + if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); + widget.bottom_right_corner = Some(( + margined_draw_loc.x + margined_draw_loc.width, + margined_draw_loc.y + margined_draw_loc.height, + )); } } } diff --git a/src/canvas/widgets/cpu_basic.rs b/src/canvas/widgets/cpu_basic.rs index 84f12f07..a9dbc4b5 100644 --- a/src/canvas/widgets/cpu_basic.rs +++ b/src/canvas/widgets/cpu_basic.rs @@ -15,192 +15,185 @@ use tui::{ widgets::{Block, Paragraph}, }; -pub trait CpuBasicWidget { - fn draw_basic_cpu( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ); -} +pub fn draw_basic_cpu( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + widget_id: u64, +) { + // Skip the first element, it's the "all" element + if app_state.canvas_data.cpu_data.len() > 1 { + let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data[1..]; -impl CpuBasicWidget for Painter { - fn draw_basic_cpu( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ) { - // Skip the first element, it's the "all" element - if app_state.canvas_data.cpu_data.len() > 1 { - let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data[1..]; + // This is a bit complicated, but basically, we want to draw SOME number + // of columns to draw all CPUs. Ideally, as well, we want to not have + // to ever scroll. + // **General logic** - count number of elements in cpu_data. Then see how + // many rows and columns we have in draw_loc (-2 on both sides for border?). + // I think what we can do is try to fit in as many in one column as possible. + // If not, then add a new column. + // Then, from this, split the row space across ALL columns. From there, generate + // the desired lengths. - // This is a bit complicated, but basically, we want to draw SOME number - // of columns to draw all CPUs. Ideally, as well, we want to not have - // to ever scroll. - // **General logic** - count number of elements in cpu_data. Then see how - // many rows and columns we have in draw_loc (-2 on both sides for border?). - // I think what we can do is try to fit in as many in one column as possible. - // If not, then add a new column. - // Then, from this, split the row space across ALL columns. From there, generate - // the desired lengths. + if app_state.current_widget.widget_id == widget_id { + f.render_widget( + Block::default() + .borders(*SIDE_BORDERS) + .border_style(painter.colours.highlighted_border_style), + draw_loc, + ); + } - if app_state.current_widget.widget_id == widget_id { - f.render_widget( - Block::default() - .borders(*SIDE_BORDERS) - .border_style(self.colours.highlighted_border_style), - draw_loc, - ); - } + let num_cpus = cpu_data.len(); + let show_avg_cpu = app_state.app_config_fields.show_average_cpu; - let num_cpus = cpu_data.len(); - let show_avg_cpu = app_state.app_config_fields.show_average_cpu; + if draw_loc.height > 0 { + let remaining_height = usize::from(draw_loc.height); + const REQUIRED_COLUMNS: usize = 4; - if draw_loc.height > 0 { - let remaining_height = usize::from(draw_loc.height); - const REQUIRED_COLUMNS: usize = 4; + let chunk_vec = + vec![Constraint::Percentage((100 / REQUIRED_COLUMNS) as u16); REQUIRED_COLUMNS]; + let chunks = Layout::default() + .constraints(chunk_vec) + .direction(Direction::Horizontal) + .split(draw_loc); - let chunk_vec = - vec![Constraint::Percentage((100 / REQUIRED_COLUMNS) as u16); REQUIRED_COLUMNS]; - let chunks = Layout::default() - .constraints(chunk_vec) - .direction(Direction::Horizontal) - .split(draw_loc); + const CPU_NAME_SPACE: usize = 3; + const BAR_BOUND_SPACE: usize = 2; + const PERCENTAGE_SPACE: usize = 4; + const MARGIN_SPACE: usize = 2; - const CPU_NAME_SPACE: usize = 3; - const BAR_BOUND_SPACE: usize = 2; - const PERCENTAGE_SPACE: usize = 4; - const MARGIN_SPACE: usize = 2; + const COMBINED_SPACING: usize = + CPU_NAME_SPACE + BAR_BOUND_SPACE + PERCENTAGE_SPACE + MARGIN_SPACE; + const REDUCED_SPACING: usize = CPU_NAME_SPACE + PERCENTAGE_SPACE + MARGIN_SPACE; + let chunk_width = chunks[0].width as usize; - const COMBINED_SPACING: usize = - CPU_NAME_SPACE + BAR_BOUND_SPACE + PERCENTAGE_SPACE + MARGIN_SPACE; - const REDUCED_SPACING: usize = CPU_NAME_SPACE + PERCENTAGE_SPACE + MARGIN_SPACE; - let chunk_width = chunks[0].width as usize; + // Inspired by htop. + // We do +4 as if it's too few bars in the bar length, it's kinda pointless. + let cpu_bars = if chunk_width >= COMBINED_SPACING + 4 { + let bar_length = chunk_width - COMBINED_SPACING; + (0..num_cpus) + .map(|cpu_index| { + let use_percentage = + if let Some(cpu_usage) = cpu_data[cpu_index].cpu_data.last() { + cpu_usage.1 + } else { + 0.0 + }; - // Inspired by htop. - // We do +4 as if it's too few bars in the bar length, it's kinda pointless. - let cpu_bars = if chunk_width >= COMBINED_SPACING + 4 { - let bar_length = chunk_width - COMBINED_SPACING; - (0..num_cpus) - .map(|cpu_index| { - let use_percentage = - if let Some(cpu_usage) = cpu_data[cpu_index].cpu_data.last() { - cpu_usage.1 + let num_bars = calculate_basic_use_bars(use_percentage, bar_length); + format!( + "{:3}[{}{}{:3.0}%]", + if app_state.app_config_fields.show_average_cpu { + if cpu_index == 0 { + "AVG".to_string() } else { - 0.0 - }; + (cpu_index - 1).to_string() + } + } else { + cpu_index.to_string() + }, + "|".repeat(num_bars), + " ".repeat(bar_length - num_bars), + use_percentage.round(), + ) + }) + .collect::>() + } else if chunk_width >= REDUCED_SPACING { + (0..num_cpus) + .map(|cpu_index| { + let use_percentage = + if let Some(cpu_usage) = cpu_data[cpu_index].cpu_data.last() { + cpu_usage.1 + } else { + 0.0 + }; - let num_bars = calculate_basic_use_bars(use_percentage, bar_length); - format!( - "{:3}[{}{}{:3.0}%]", - if app_state.app_config_fields.show_average_cpu { - if cpu_index == 0 { - "AVG".to_string() + format!( + "{:3} {:3.0}%", + if app_state.app_config_fields.show_average_cpu { + if cpu_index == 0 { + "AVG".to_string() + } else { + (cpu_index - 1).to_string() + } + } else { + cpu_index.to_string() + }, + use_percentage.round(), + ) + }) + .collect::>() + } else { + (0..num_cpus) + .map(|cpu_index| { + let use_percentage = + if let Some(cpu_usage) = cpu_data[cpu_index].cpu_data.last() { + cpu_usage.1 + } else { + 0.0 + }; + + format!("{:3.0}%", use_percentage.round(),) + }) + .collect::>() + }; + + let mut row_counter = num_cpus; + let mut start_index = 0; + for (itx, chunk) in chunks.iter().enumerate() { + // Explicitly check... don't want an accidental DBZ or underflow, this ensures + // to_divide is > 0 + if REQUIRED_COLUMNS > itx { + let to_divide = REQUIRED_COLUMNS - itx; + let how_many_cpus = min( + remaining_height, + (row_counter / to_divide) + + (if row_counter % to_divide == 0 { 0 } else { 1 }), + ); + row_counter -= how_many_cpus; + let end_index = min(start_index + how_many_cpus, num_cpus); + + let cpu_column = (start_index..end_index) + .map(|itx| { + Spans::from(Span { + content: (&cpu_bars[itx]).into(), + style: if show_avg_cpu { + if itx == 0 { + painter.colours.avg_colour_style } else { - (cpu_index - 1).to_string() + painter.colours.cpu_colour_styles + [(itx - 1) % painter.colours.cpu_colour_styles.len()] } } else { - cpu_index.to_string() + painter.colours.cpu_colour_styles + [itx % painter.colours.cpu_colour_styles.len()] }, - "|".repeat(num_bars), - " ".repeat(bar_length - num_bars), - use_percentage.round(), - ) - }) - .collect::>() - } else if chunk_width >= REDUCED_SPACING { - (0..num_cpus) - .map(|cpu_index| { - let use_percentage = - if let Some(cpu_usage) = cpu_data[cpu_index].cpu_data.last() { - cpu_usage.1 - } else { - 0.0 - }; - - format!( - "{:3} {:3.0}%", - if app_state.app_config_fields.show_average_cpu { - if cpu_index == 0 { - "AVG".to_string() - } else { - (cpu_index - 1).to_string() - } - } else { - cpu_index.to_string() - }, - use_percentage.round(), - ) - }) - .collect::>() - } else { - (0..num_cpus) - .map(|cpu_index| { - let use_percentage = - if let Some(cpu_usage) = cpu_data[cpu_index].cpu_data.last() { - cpu_usage.1 - } else { - 0.0 - }; - - format!("{:3.0}%", use_percentage.round(),) - }) - .collect::>() - }; - - let mut row_counter = num_cpus; - let mut start_index = 0; - for (itx, chunk) in chunks.iter().enumerate() { - // Explicitly check... don't want an accidental DBZ or underflow, this ensures - // to_divide is > 0 - if REQUIRED_COLUMNS > itx { - let to_divide = REQUIRED_COLUMNS - itx; - let how_many_cpus = min( - remaining_height, - (row_counter / to_divide) - + (if row_counter % to_divide == 0 { 0 } else { 1 }), - ); - row_counter -= how_many_cpus; - let end_index = min(start_index + how_many_cpus, num_cpus); - - let cpu_column = (start_index..end_index) - .map(|itx| { - Spans::from(Span { - content: (&cpu_bars[itx]).into(), - style: if show_avg_cpu { - if itx == 0 { - self.colours.avg_colour_style - } else { - self.colours.cpu_colour_styles - [(itx - 1) % self.colours.cpu_colour_styles.len()] - } - } else { - self.colours.cpu_colour_styles - [itx % self.colours.cpu_colour_styles.len()] - }, - }) }) - .collect::>(); + }) + .collect::>(); - start_index += how_many_cpus; + start_index += how_many_cpus; - let margined_loc = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(100)]) - .horizontal_margin(1) - .split(*chunk)[0]; + let margined_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(1) + .split(*chunk)[0]; - f.render_widget( - Paragraph::new(cpu_column).block(Block::default()), - margined_loc, - ); - } + f.render_widget( + Paragraph::new(cpu_column).block(Block::default()), + margined_loc, + ); } } } + } - if app_state.should_get_widget_bounds() { - // Update draw loc in widget map - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { - widget.top_left_corner = Some((draw_loc.x, draw_loc.y)); - widget.bottom_right_corner = - Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height)); - } + if app_state.should_get_widget_bounds() { + // Update draw loc in widget map + if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + widget.top_left_corner = Some((draw_loc.x, draw_loc.y)); + widget.bottom_right_corner = + Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height)); } } } diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/widgets/cpu_graph.rs index 8e07ec28..75bb9851 100644 --- a/src/canvas/widgets/cpu_graph.rs +++ b/src/canvas/widgets/cpu_graph.rs @@ -32,253 +32,245 @@ static CPU_LEGEND_HEADER_LENS: Lazy> = Lazy::new(|| { .collect::>() }); -pub trait CpuGraphWidget { - fn draw_cpu( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ); - fn draw_cpu_graph( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ); - fn draw_cpu_legend( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ); -} +pub fn draw_cpu( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + widget_id: u64, +) { + if draw_loc.width as f64 * 0.15 <= 6.0 { + // Skip drawing legend + if app_state.current_widget.widget_id == (widget_id + 1) { + if app_state.app_config_fields.left_legend { + app_state.move_widget_selection(&WidgetDirection::Right); + } else { + app_state.move_widget_selection(&WidgetDirection::Left); + } + } + draw_cpu_graph(painter, f, app_state, draw_loc, widget_id); + if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&widget_id) { + cpu_widget_state.is_legend_hidden = true; + } -impl CpuGraphWidget for Painter { - fn draw_cpu( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ) { - if draw_loc.width as f64 * 0.15 <= 6.0 { - // Skip drawing legend - if app_state.current_widget.widget_id == (widget_id + 1) { - if app_state.app_config_fields.left_legend { - app_state.move_widget_selection(&WidgetDirection::Right); - } else { - app_state.move_widget_selection(&WidgetDirection::Left); - } - } - self.draw_cpu_graph(f, app_state, draw_loc, widget_id); - if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&widget_id) { - cpu_widget_state.is_legend_hidden = true; - } - - // Update draw loc in widget map - if app_state.should_get_widget_bounds() { - if let Some(bottom_widget) = app_state.widget_map.get_mut(&widget_id) { - bottom_widget.top_left_corner = Some((draw_loc.x, draw_loc.y)); - bottom_widget.bottom_right_corner = - Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height)); - } + // Update draw loc in widget map + if app_state.should_get_widget_bounds() { + if let Some(bottom_widget) = app_state.widget_map.get_mut(&widget_id) { + bottom_widget.top_left_corner = Some((draw_loc.x, draw_loc.y)); + bottom_widget.bottom_right_corner = + Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height)); } + } + } else { + let (graph_index, legend_index, constraints) = if app_state.app_config_fields.left_legend { + ( + 1, + 0, + [Constraint::Percentage(15), Constraint::Percentage(85)], + ) } else { - let (graph_index, legend_index, constraints) = - if app_state.app_config_fields.left_legend { - ( - 1, - 0, - [Constraint::Percentage(15), Constraint::Percentage(85)], - ) - } else { - ( - 0, - 1, - [Constraint::Percentage(85), Constraint::Percentage(15)], - ) - }; + ( + 0, + 1, + [Constraint::Percentage(85), Constraint::Percentage(15)], + ) + }; - let partitioned_draw_loc = Layout::default() - .margin(0) - .direction(Direction::Horizontal) - .constraints(constraints) - .split(draw_loc); + let partitioned_draw_loc = Layout::default() + .margin(0) + .direction(Direction::Horizontal) + .constraints(constraints) + .split(draw_loc); - self.draw_cpu_graph(f, app_state, partitioned_draw_loc[graph_index], widget_id); - self.draw_cpu_legend( - f, - app_state, - partitioned_draw_loc[legend_index], - widget_id + 1, - ); + draw_cpu_graph( + painter, + f, + app_state, + partitioned_draw_loc[graph_index], + widget_id, + ); + draw_cpu_legend( + painter, + f, + app_state, + partitioned_draw_loc[legend_index], + widget_id + 1, + ); - if app_state.should_get_widget_bounds() { - // Update draw loc in widget map - if let Some(cpu_widget) = app_state.widget_map.get_mut(&widget_id) { - cpu_widget.top_left_corner = Some(( - partitioned_draw_loc[graph_index].x, - partitioned_draw_loc[graph_index].y, - )); - cpu_widget.bottom_right_corner = Some(( - partitioned_draw_loc[graph_index].x - + partitioned_draw_loc[graph_index].width, - partitioned_draw_loc[graph_index].y - + partitioned_draw_loc[graph_index].height, - )); - } + if app_state.should_get_widget_bounds() { + // Update draw loc in widget map + if let Some(cpu_widget) = app_state.widget_map.get_mut(&widget_id) { + cpu_widget.top_left_corner = Some(( + partitioned_draw_loc[graph_index].x, + partitioned_draw_loc[graph_index].y, + )); + cpu_widget.bottom_right_corner = Some(( + partitioned_draw_loc[graph_index].x + partitioned_draw_loc[graph_index].width, + partitioned_draw_loc[graph_index].y + partitioned_draw_loc[graph_index].height, + )); + } - if let Some(legend_widget) = app_state.widget_map.get_mut(&(widget_id + 1)) { - legend_widget.top_left_corner = Some(( - partitioned_draw_loc[legend_index].x, - partitioned_draw_loc[legend_index].y, - )); - legend_widget.bottom_right_corner = Some(( - partitioned_draw_loc[legend_index].x - + partitioned_draw_loc[legend_index].width, - partitioned_draw_loc[legend_index].y - + partitioned_draw_loc[legend_index].height, - )); - } + if let Some(legend_widget) = app_state.widget_map.get_mut(&(widget_id + 1)) { + legend_widget.top_left_corner = Some(( + partitioned_draw_loc[legend_index].x, + partitioned_draw_loc[legend_index].y, + )); + legend_widget.bottom_right_corner = Some(( + partitioned_draw_loc[legend_index].x + partitioned_draw_loc[legend_index].width, + partitioned_draw_loc[legend_index].y + + partitioned_draw_loc[legend_index].height, + )); } } } +} - fn draw_cpu_graph( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ) { - if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&widget_id) { - let cpu_data: &mut [ConvertedCpuData] = &mut app_state.canvas_data.cpu_data; +fn draw_cpu_graph( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + widget_id: u64, +) { + if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&widget_id) { + let cpu_data: &mut [ConvertedCpuData] = &mut app_state.canvas_data.cpu_data; - let display_time_labels = vec![ - Span::styled( - format!("{}s", cpu_widget_state.current_display_time / 1000), - self.colours.graph_style, - ), - Span::styled("0s".to_string(), self.colours.graph_style), - ]; + let display_time_labels = vec![ + Span::styled( + format!("{}s", cpu_widget_state.current_display_time / 1000), + painter.colours.graph_style, + ), + Span::styled("0s".to_string(), painter.colours.graph_style), + ]; - let y_axis_labels = vec![ - Span::styled(" 0%", self.colours.graph_style), - Span::styled("100%", self.colours.graph_style), - ]; + let y_axis_labels = vec![ + Span::styled(" 0%", painter.colours.graph_style), + Span::styled("100%", painter.colours.graph_style), + ]; - let time_start = -(cpu_widget_state.current_display_time as f64); + let time_start = -(cpu_widget_state.current_display_time as f64); - let x_axis = if app_state.app_config_fields.hide_time - || (app_state.app_config_fields.autohide_time - && cpu_widget_state.autohide_timer.is_none()) + let x_axis = if app_state.app_config_fields.hide_time + || (app_state.app_config_fields.autohide_time + && cpu_widget_state.autohide_timer.is_none()) + { + Axis::default().bounds([time_start, 0.0]) + } else if let Some(time) = cpu_widget_state.autohide_timer { + if std::time::Instant::now().duration_since(time).as_millis() + < AUTOHIDE_TIMEOUT_MILLISECONDS as u128 { - Axis::default().bounds([time_start, 0.0]) - } else if let Some(time) = cpu_widget_state.autohide_timer { - if std::time::Instant::now().duration_since(time).as_millis() - < AUTOHIDE_TIMEOUT_MILLISECONDS as u128 - { - Axis::default() - .bounds([time_start, 0.0]) - .style(self.colours.graph_style) - .labels(display_time_labels) - } else { - cpu_widget_state.autohide_timer = None; - Axis::default().bounds([time_start, 0.0]) - } - } else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT { - Axis::default().bounds([time_start, 0.0]) - } else { Axis::default() .bounds([time_start, 0.0]) - .style(self.colours.graph_style) + .style(painter.colours.graph_style) .labels(display_time_labels) - }; + } else { + cpu_widget_state.autohide_timer = None; + Axis::default().bounds([time_start, 0.0]) + } + } else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT { + Axis::default().bounds([time_start, 0.0]) + } else { + Axis::default() + .bounds([time_start, 0.0]) + .style(painter.colours.graph_style) + .labels(display_time_labels) + }; - let y_axis = Axis::default() - .style(self.colours.graph_style) - .bounds([0.0, 100.5]) - .labels(y_axis_labels); + let y_axis = Axis::default() + .style(painter.colours.graph_style) + .bounds([0.0, 100.5]) + .labels(y_axis_labels); - let use_dot = app_state.app_config_fields.use_dot; - let show_avg_cpu = app_state.app_config_fields.show_average_cpu; - let current_scroll_position = cpu_widget_state.scroll_state.current_scroll_position; + let use_dot = app_state.app_config_fields.use_dot; + let show_avg_cpu = app_state.app_config_fields.show_average_cpu; + let current_scroll_position = cpu_widget_state.scroll_state.current_scroll_position; - let interpolated_cpu_points = cpu_data - .iter_mut() - .enumerate() - .map(|(itx, cpu)| { - let to_show = if current_scroll_position == ALL_POSITION { - true - } else { - itx == current_scroll_position - }; + let interpolated_cpu_points = cpu_data + .iter_mut() + .enumerate() + .map(|(itx, cpu)| { + let to_show = if current_scroll_position == ALL_POSITION { + true + } else { + itx == current_scroll_position + }; - if to_show { - if let Some(end_pos) = cpu - .cpu_data - .iter() - .position(|(time, _data)| *time >= time_start) - { - if end_pos > 1 { - let start_pos = end_pos - 1; - let outside_point = cpu.cpu_data.get(start_pos); - let inside_point = cpu.cpu_data.get(end_pos); + if to_show { + if let Some(end_pos) = cpu + .cpu_data + .iter() + .position(|(time, _data)| *time >= time_start) + { + if end_pos > 1 { + let start_pos = end_pos - 1; + let outside_point = cpu.cpu_data.get(start_pos); + let inside_point = cpu.cpu_data.get(end_pos); - if let (Some(outside_point), Some(inside_point)) = - (outside_point, inside_point) - { - let old = *outside_point; + if let (Some(outside_point), Some(inside_point)) = + (outside_point, inside_point) + { + let old = *outside_point; - let new_point = ( - time_start, - interpolate_points(outside_point, inside_point, time_start), - ); + let new_point = ( + time_start, + interpolate_points(outside_point, inside_point, time_start), + ); - if let Some(to_replace) = cpu.cpu_data.get_mut(start_pos) { - *to_replace = new_point; - Some((start_pos, old)) - } else { - None // Failed to get mutable reference. - } + if let Some(to_replace) = cpu.cpu_data.get_mut(start_pos) { + *to_replace = new_point; + Some((start_pos, old)) } else { - None // Point somehow doesn't exist in our data + None // Failed to get mutable reference. } } else { - None // Point is already "leftmost", no need to interpolate. + None // Point somehow doesn't exist in our data } } else { - None // There is no point. + None // Point is already "leftmost", no need to interpolate. } } else { - None + None // There is no point. } - }) - .collect::>(); + } else { + None + } + }) + .collect::>(); - let dataset_vector: Vec> = if current_scroll_position == ALL_POSITION { - cpu_data - .iter() - .enumerate() - .rev() - .map(|(itx, cpu)| { - Dataset::default() - .marker(if use_dot { - Marker::Dot - } else { - Marker::Braille - }) - .style(if show_avg_cpu && itx == AVG_POSITION { - self.colours.avg_colour_style - } else if itx == ALL_POSITION { - self.colours.all_colour_style - } else { - self.colours.cpu_colour_styles[(itx - 1 // Because of the all position + let dataset_vector: Vec> = if current_scroll_position == ALL_POSITION { + cpu_data + .iter() + .enumerate() + .rev() + .map(|(itx, cpu)| { + Dataset::default() + .marker(if use_dot { + Marker::Dot + } else { + Marker::Braille + }) + .style(if show_avg_cpu && itx == AVG_POSITION { + painter.colours.avg_colour_style + } else if itx == ALL_POSITION { + painter.colours.all_colour_style + } else { + painter.colours.cpu_colour_styles[(itx - 1 // Because of the all position - (if show_avg_cpu { AVG_POSITION } else { 0 })) - % self.colours.cpu_colour_styles.len()] - }) - .data(&cpu.cpu_data[..]) - .graph_type(tui::widgets::GraphType::Line) - }) - .collect() - } else if let Some(cpu) = cpu_data.get(current_scroll_position) { - vec![Dataset::default() - .marker(if use_dot { - Marker::Dot - } else { - Marker::Braille - }) - .style(if show_avg_cpu && current_scroll_position == AVG_POSITION { - self.colours.avg_colour_style - } else { - self.colours.cpu_colour_styles[(cpu_widget_state + % painter.colours.cpu_colour_styles.len()] + }) + .data(&cpu.cpu_data[..]) + .graph_type(tui::widgets::GraphType::Line) + }) + .collect() + } else if let Some(cpu) = cpu_data.get(current_scroll_position) { + vec![Dataset::default() + .marker(if use_dot { + Marker::Dot + } else { + Marker::Braille + }) + .style(if show_avg_cpu && current_scroll_position == AVG_POSITION { + painter.colours.avg_colour_style + } else { + painter.colours.cpu_colour_styles[(cpu_widget_state .scroll_state .current_scroll_position - 1 // Because of the all position @@ -287,249 +279,249 @@ impl CpuGraphWidget for Painter { } else { 0 })) - % self.colours.cpu_colour_styles.len()] - }) - .data(&cpu.cpu_data[..]) - .graph_type(tui::widgets::GraphType::Line)] - } else { - vec![] - }; + % painter.colours.cpu_colour_styles.len()] + }) + .data(&cpu.cpu_data[..]) + .graph_type(tui::widgets::GraphType::Line)] + } else { + vec![] + }; - let is_on_widget = widget_id == app_state.current_widget.widget_id; - let border_style = if is_on_widget { - self.colours.highlighted_border_style - } else { - self.colours.border_style - }; + let is_on_widget = widget_id == app_state.current_widget.widget_id; + let border_style = if is_on_widget { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }; - let title = if cfg!(target_family = "unix") { - let load_avg = app_state.canvas_data.load_avg_data; - let load_avg_str = format!( - "─ {:.2} {:.2} {:.2} ", - load_avg[0], load_avg[1], load_avg[2] - ); - let load_avg_str_size = - UnicodeSegmentation::graphemes(load_avg_str.as_str(), true).count(); + let title = if cfg!(target_family = "unix") { + let load_avg = app_state.canvas_data.load_avg_data; + let load_avg_str = format!( + "─ {:.2} {:.2} {:.2} ", + load_avg[0], load_avg[1], load_avg[2] + ); + let load_avg_str_size = + UnicodeSegmentation::graphemes(load_avg_str.as_str(), true).count(); - if app_state.is_expanded { - const TITLE_BASE: &str = " CPU ── Esc to go back "; - - Spans::from(vec![ - Span::styled(" CPU ", self.colours.widget_title_style), - Span::styled(load_avg_str, self.colours.widget_title_style), - Span::styled( - format!( - "─{}─ Esc to go back ", - "─".repeat(usize::from(draw_loc.width).saturating_sub( - load_avg_str_size - + UnicodeSegmentation::graphemes(TITLE_BASE, true).count() - + 2 - )) - ), - border_style, - ), - ]) - } else { - Spans::from(vec![ - Span::styled(" CPU ", self.colours.widget_title_style), - Span::styled(load_avg_str, self.colours.widget_title_style), - ]) - } - } else if app_state.is_expanded { + if app_state.is_expanded { const TITLE_BASE: &str = " CPU ── Esc to go back "; Spans::from(vec![ - Span::styled(" CPU ", self.colours.widget_title_style), + Span::styled(" CPU ", painter.colours.widget_title_style), + Span::styled(load_avg_str, painter.colours.widget_title_style), Span::styled( format!( "─{}─ Esc to go back ", "─".repeat(usize::from(draw_loc.width).saturating_sub( - UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2 + load_avg_str_size + + UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + + 2 )) ), border_style, ), ]) } else { - Spans::from(vec![Span::styled(" CPU ", self.colours.widget_title_style)]) - }; - - f.render_widget( - Chart::new(dataset_vector) - .block( - Block::default() - .title(title) - .borders(Borders::ALL) - .border_style(border_style), - ) - .x_axis(x_axis) - .y_axis(y_axis), - draw_loc, - ); - - // Reset interpolated points - cpu_data - .iter_mut() - .zip(interpolated_cpu_points) - .for_each(|(cpu, interpolation)| { - if let Some((index, old_value)) = interpolation { - if let Some(to_replace) = cpu.cpu_data.get_mut(index) { - *to_replace = old_value; - } - } - }); - } - } - - fn draw_cpu_legend( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ) { - let recalculate_column_widths = app_state.should_get_widget_bounds(); - if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&(widget_id - 1)) - { - cpu_widget_state.is_legend_hidden = false; - let cpu_data: &mut [ConvertedCpuData] = &mut app_state.canvas_data.cpu_data; - let cpu_table_state = &mut cpu_widget_state.scroll_state.table_state; - let is_on_widget = widget_id == app_state.current_widget.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), - ), - &cpu_widget_state.scroll_state.scroll_direction, - &mut cpu_widget_state.scroll_state.previous_scroll_position, - cpu_widget_state.scroll_state.current_scroll_position, - app_state.is_force_redraw, - ); - cpu_table_state.select(Some( - cpu_widget_state - .scroll_state - .current_scroll_position - .saturating_sub(start_position), - )); - - let sliced_cpu_data = &cpu_data[start_position..]; - - let offset_scroll_index = cpu_widget_state - .scroll_state - .current_scroll_position - .saturating_sub(start_position); - let show_avg_cpu = app_state.app_config_fields.show_average_cpu; - - // Calculate widths - if recalculate_column_widths { - cpu_widget_state.table_width_state.desired_column_widths = vec![6, 4]; - cpu_widget_state.table_width_state.calculated_column_widths = get_column_widths( - draw_loc.width, - &[None, None], - &(CPU_LEGEND_HEADER_LENS - .iter() - .map(|width| Some(*width)) - .collect::>()), - &[Some(0.5), Some(0.5)], - &(cpu_widget_state - .table_width_state - .desired_column_widths - .iter() - .map(|width| Some(*width)) - .collect::>()), - false, - ); + Spans::from(vec![ + Span::styled(" CPU ", painter.colours.widget_title_style), + Span::styled(load_avg_str, painter.colours.widget_title_style), + ]) } + } else if app_state.is_expanded { + const TITLE_BASE: &str = " CPU ── Esc to go back "; - let dcw = &cpu_widget_state.table_width_state.desired_column_widths; - let ccw = &cpu_widget_state.table_width_state.calculated_column_widths; - let cpu_rows = sliced_cpu_data.iter().enumerate().map(|(itx, cpu)| { - let mut truncated_name = - if let (Some(desired_column_width), Some(calculated_column_width)) = - (dcw.get(0), ccw.get(0)) - { - if *desired_column_width > *calculated_column_width { - Text::raw(&cpu.short_cpu_name) - } else { - Text::raw(&cpu.cpu_name) - } - } else { - Text::raw(&cpu.cpu_name) - }; + Spans::from(vec![ + Span::styled(" CPU ", painter.colours.widget_title_style), + Span::styled( + format!( + "─{}─ Esc to go back ", + "─".repeat(usize::from(draw_loc.width).saturating_sub( + UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2 + )) + ), + border_style, + ), + ]) + } else { + Spans::from(vec![Span::styled( + " CPU ", + painter.colours.widget_title_style, + )]) + }; - let is_first_column_hidden = if let Some(calculated_column_width) = ccw.get(0) { - *calculated_column_width == 0 - } else { - false - }; + f.render_widget( + Chart::new(dataset_vector) + .block( + Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(border_style), + ) + .x_axis(x_axis) + .y_axis(y_axis), + draw_loc, + ); - let truncated_legend = if is_first_column_hidden && cpu.legend_value.is_empty() { - // For the case where we only have room for one column, display "All" in the normally blank area. - Text::raw("All") - } else { - Text::raw(&cpu.legend_value) - }; - - if !is_first_column_hidden - && itx == offset_scroll_index - && itx + start_position == ALL_POSITION - { - truncated_name.patch_style(self.colours.currently_selected_text_style); - Row::new(vec![truncated_name, truncated_legend]) - } else { - let cpu_string_row = vec![truncated_name, truncated_legend]; - - Row::new(cpu_string_row).style(if itx == offset_scroll_index { - self.colours.currently_selected_text_style - } else if itx + start_position == ALL_POSITION { - self.colours.all_colour_style - } else if show_avg_cpu { - if itx + start_position == AVG_POSITION { - self.colours.avg_colour_style - } else { - self.colours.cpu_colour_styles[(itx + start_position - - AVG_POSITION - - 1) - % self.colours.cpu_colour_styles.len()] - } - } else { - self.colours.cpu_colour_styles[(itx + start_position - ALL_POSITION - 1) - % self.colours.cpu_colour_styles.len()] - }) + // Reset interpolated points + cpu_data + .iter_mut() + .zip(interpolated_cpu_points) + .for_each(|(cpu, interpolation)| { + if let Some((index, old_value)) = interpolation { + if let Some(to_replace) = cpu.cpu_data.get_mut(index) { + *to_replace = old_value; + } } }); - - // Note we don't set highlight_style, as it should always be shown for this widget. - let border_and_title_style = if is_on_widget { - self.colours.highlighted_border_style - } else { - self.colours.border_style - }; - - // Draw - f.render_stateful_widget( - Table::new(cpu_rows) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(border_and_title_style), - ) - .header( - Row::new(CPU_LEGEND_HEADER.to_vec()) - .style(self.colours.table_header_style) - .bottom_margin(table_gap), - ) - .widths( - &(cpu_widget_state - .table_width_state - .calculated_column_widths - .iter() - .map(|calculated_width| Constraint::Length(*calculated_width as u16)) - .collect::>()), - ), - draw_loc, - cpu_table_state, - ); - } + } +} + +fn draw_cpu_legend( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + widget_id: u64, +) { + let recalculate_column_widths = app_state.should_get_widget_bounds(); + if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&(widget_id - 1)) { + cpu_widget_state.is_legend_hidden = false; + let cpu_data: &mut [ConvertedCpuData] = &mut app_state.canvas_data.cpu_data; + let cpu_table_state = &mut cpu_widget_state.scroll_state.table_state; + let is_on_widget = widget_id == app_state.current_widget.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(painter.table_height_offset), + ), + &cpu_widget_state.scroll_state.scroll_direction, + &mut cpu_widget_state.scroll_state.previous_scroll_position, + cpu_widget_state.scroll_state.current_scroll_position, + app_state.is_force_redraw, + ); + cpu_table_state.select(Some( + cpu_widget_state + .scroll_state + .current_scroll_position + .saturating_sub(start_position), + )); + + let sliced_cpu_data = &cpu_data[start_position..]; + + let offset_scroll_index = cpu_widget_state + .scroll_state + .current_scroll_position + .saturating_sub(start_position); + let show_avg_cpu = app_state.app_config_fields.show_average_cpu; + + // Calculate widths + if recalculate_column_widths { + cpu_widget_state.table_width_state.desired_column_widths = vec![6, 4]; + cpu_widget_state.table_width_state.calculated_column_widths = get_column_widths( + draw_loc.width, + &[None, None], + &(CPU_LEGEND_HEADER_LENS + .iter() + .map(|width| Some(*width)) + .collect::>()), + &[Some(0.5), Some(0.5)], + &(cpu_widget_state + .table_width_state + .desired_column_widths + .iter() + .map(|width| Some(*width)) + .collect::>()), + false, + ); + } + + let dcw = &cpu_widget_state.table_width_state.desired_column_widths; + let ccw = &cpu_widget_state.table_width_state.calculated_column_widths; + let cpu_rows = sliced_cpu_data.iter().enumerate().map(|(itx, cpu)| { + let mut truncated_name = + if let (Some(desired_column_width), Some(calculated_column_width)) = + (dcw.get(0), ccw.get(0)) + { + if *desired_column_width > *calculated_column_width { + Text::raw(&cpu.short_cpu_name) + } else { + Text::raw(&cpu.cpu_name) + } + } else { + Text::raw(&cpu.cpu_name) + }; + + let is_first_column_hidden = if let Some(calculated_column_width) = ccw.get(0) { + *calculated_column_width == 0 + } else { + false + }; + + let truncated_legend = if is_first_column_hidden && cpu.legend_value.is_empty() { + // For the case where we only have room for one column, display "All" in the normally blank area. + Text::raw("All") + } else { + Text::raw(&cpu.legend_value) + }; + + if !is_first_column_hidden + && itx == offset_scroll_index + && itx + start_position == ALL_POSITION + { + truncated_name.patch_style(painter.colours.currently_selected_text_style); + Row::new(vec![truncated_name, truncated_legend]) + } else { + let cpu_string_row = vec![truncated_name, truncated_legend]; + + Row::new(cpu_string_row).style(if itx == offset_scroll_index { + painter.colours.currently_selected_text_style + } else if itx + start_position == ALL_POSITION { + painter.colours.all_colour_style + } else if show_avg_cpu { + if itx + start_position == AVG_POSITION { + painter.colours.avg_colour_style + } else { + painter.colours.cpu_colour_styles[(itx + start_position - AVG_POSITION - 1) + % painter.colours.cpu_colour_styles.len()] + } + } else { + painter.colours.cpu_colour_styles[(itx + start_position - ALL_POSITION - 1) + % painter.colours.cpu_colour_styles.len()] + }) + } + }); + + // Note we don't set highlight_style, as it should always be shown for this widget. + let border_and_title_style = if is_on_widget { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }; + + // Draw + f.render_stateful_widget( + Table::new(cpu_rows) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(border_and_title_style), + ) + .header( + Row::new(CPU_LEGEND_HEADER.to_vec()) + .style(painter.colours.table_header_style) + .bottom_margin(table_gap), + ) + .widths( + &(cpu_widget_state + .table_width_state + .calculated_column_widths + .iter() + .map(|calculated_width| Constraint::Length(*calculated_width as u16)) + .collect::>()), + ), + draw_loc, + cpu_table_state, + ); } } diff --git a/src/canvas/widgets/disk_table.rs b/src/canvas/widgets/disk_table.rs index 6a79c5b6..3404cb99 100644 --- a/src/canvas/widgets/disk_table.rs +++ b/src/canvas/widgets/disk_table.rs @@ -27,249 +27,239 @@ static DISK_HEADERS_LENS: Lazy> = Lazy::new(|| { .collect::>() }); -pub trait DiskTableWidget { - fn draw_disk_table( - &self, f: &mut Frame<'_, B>, app_state: &mut app::AppState, draw_loc: Rect, - draw_border: bool, widget_id: u64, - ); -} +pub fn draw_disk_table( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut app::AppState, draw_loc: Rect, + draw_border: bool, widget_id: u64, +) { + 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(painter.table_height_offset), + ), + &disk_widget_state.scroll_state.scroll_direction, + &mut disk_widget_state.scroll_state.previous_scroll_position, + 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..]; -impl DiskTableWidget for Painter { - fn draw_disk_table( - &self, f: &mut Frame<'_, B>, app_state: &mut app::AppState, draw_loc: Rect, - draw_border: bool, widget_id: u64, - ) { - 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.previous_scroll_position, - 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; - } + // 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 - } + } + 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 } - }) - .collect::>(); + } 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, - ); - } + 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)) + 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 { - if *desired_col_width > *calculated_col_width - && *calculated_col_width > 0 - { - let graphemes = - UnicodeSegmentation::graphemes(entry.as_str(), true) - .collect::>(); + let graphemes = + UnicodeSegmentation::graphemes(entry.as_str(), true) + .collect::>(); - if graphemes.len() > *calculated_col_width as usize - && *calculated_col_width > 1 - { - // Truncate with ellipsis - let first_n = graphemes - [..(*calculated_col_width as usize - 1)] - .concat(); - return Text::raw(format!("{}…", first_n)); - } + if graphemes.len() > *calculated_col_width as usize + && *calculated_col_width > 1 + { + // Truncate with ellipsis + let first_n = graphemes + [..(*calculated_col_width as usize - 1)] + .concat(); + return Text::raw(format!("{}…", first_n)); } } } + } - Text::raw(entry) - }, - ); + Text::raw(entry) + }); - Row::new(truncated_data) - }); + Row::new(truncated_data) + }); - let (border_style, highlight_style) = if is_on_widget { - ( - self.colours.highlighted_border_style, - self.colours.currently_selected_text_style, - ) - } else { - (self.colours.border_style, self.colours.text_style) - }; + let (border_style, highlight_style) = if is_on_widget { + ( + painter.colours.highlighted_border_style, + painter.colours.currently_selected_text_style, + ) + } else { + (painter.colours.border_style, painter.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 as usize { - 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 as usize { - ( - " Disk ".to_string(), - format!("{}{}", " Disk ".to_string(), 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 as u16)) - .collect::>()), - ), - margined_draw_loc, - disk_table_state, + 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 app_state.should_get_widget_bounds() { - // Update draw loc in widget map - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { - widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); - widget.bottom_right_corner = Some(( - margined_draw_loc.x + margined_draw_loc.width, - margined_draw_loc.y + margined_draw_loc.height, - )); + if title_string.len() <= draw_loc.width as usize { + 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 as usize { + ( + " Disk ".to_string(), + format!("{}{}", " Disk ".to_string(), ESCAPE_ENDING), + ) + } else { + (title_base, temp_title_base) } + }; + + Spans::from(vec![ + Span::styled(chosen_title_base, painter.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, painter.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(painter.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(painter.colours.table_header_style) + .bottom_margin(table_gap), + ) + .highlight_style(highlight_style) + .style(painter.colours.text_style) + .widths( + &(disk_widget_state + .table_width_state + .calculated_column_widths + .iter() + .map(|calculated_width| Constraint::Length(*calculated_width as u16)) + .collect::>()), + ), + margined_draw_loc, + disk_table_state, + ); + + if app_state.should_get_widget_bounds() { + // Update draw loc in widget map + if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); + widget.bottom_right_corner = Some(( + margined_draw_loc.x + margined_draw_loc.width, + margined_draw_loc.y + margined_draw_loc.height, + )); } } } diff --git a/src/canvas/widgets/mem_basic.rs b/src/canvas/widgets/mem_basic.rs index afb3da53..e8a0d0f6 100644 --- a/src/canvas/widgets/mem_basic.rs +++ b/src/canvas/widgets/mem_basic.rs @@ -13,118 +13,111 @@ use tui::{ widgets::{Block, Paragraph}, }; -pub trait MemBasicWidget { - fn draw_basic_memory( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ); -} +pub fn draw_basic_memory( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + widget_id: u64, +) { + let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data; + let swap_data: &[(f64, f64)] = &app_state.canvas_data.swap_data; -impl MemBasicWidget for Painter { - fn draw_basic_memory( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ) { - let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data; - let swap_data: &[(f64, f64)] = &app_state.canvas_data.swap_data; - - let margined_loc = Layout::default() - .constraints([Constraint::Percentage(100)]) - .horizontal_margin(1) - .split(draw_loc); - - if app_state.current_widget.widget_id == widget_id { - f.render_widget( - Block::default() - .borders(*SIDE_BORDERS) - .border_style(self.colours.highlighted_border_style), - draw_loc, - ); - } - - let ram_use_percentage = if let Some(mem) = mem_data.last() { - mem.1 - } else { - 0.0 - }; - let swap_use_percentage = if let Some(swap) = swap_data.last() { - swap.1 - } else { - 0.0 - }; - - const EMPTY_MEMORY_FRAC_STRING: &str = "0.0B/0.0B"; - - let trimmed_memory_frac = - if let Some((_label_percent, label_frac)) = &app_state.canvas_data.mem_labels { - label_frac.trim() - } else { - EMPTY_MEMORY_FRAC_STRING - }; - - let trimmed_swap_frac = - if let Some((_label_percent, label_frac)) = &app_state.canvas_data.swap_labels { - label_frac.trim() - } else { - EMPTY_MEMORY_FRAC_STRING - }; - - // +7 due to 3 + 2 + 2 columns for the name & space + bar bounds + margin spacing - // Then + length of fraction - let ram_bar_length = - usize::from(draw_loc.width.saturating_sub(7)).saturating_sub(trimmed_memory_frac.len()); - let swap_bar_length = - usize::from(draw_loc.width.saturating_sub(7)).saturating_sub(trimmed_swap_frac.len()); - - let num_bars_ram = calculate_basic_use_bars(ram_use_percentage, ram_bar_length); - let num_bars_swap = calculate_basic_use_bars(swap_use_percentage, swap_bar_length); - // TODO: Use different styling for the frac. - let mem_label = if app_state.basic_mode_use_percent { - format!( - "RAM[{}{}{:3.0}%]\n", - "|".repeat(num_bars_ram), - " ".repeat(ram_bar_length - num_bars_ram + trimmed_memory_frac.len() - 4), - ram_use_percentage.round() - ) - } else { - format!( - "RAM[{}{}{}]\n", - "|".repeat(num_bars_ram), - " ".repeat(ram_bar_length - num_bars_ram), - trimmed_memory_frac - ) - }; - let swap_label = if app_state.basic_mode_use_percent { - format!( - "SWP[{}{}{:3.0}%]", - "|".repeat(num_bars_swap), - " ".repeat(swap_bar_length - num_bars_swap + trimmed_swap_frac.len() - 4), - swap_use_percentage.round() - ) - } else { - format!( - "SWP[{}{}{}]", - "|".repeat(num_bars_swap), - " ".repeat(swap_bar_length - num_bars_swap), - trimmed_swap_frac - ) - }; - - let mem_text = vec![ - Spans::from(Span::styled(mem_label, self.colours.ram_style)), - Spans::from(Span::styled(swap_label, self.colours.swap_style)), - ]; + let margined_loc = Layout::default() + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(1) + .split(draw_loc); + if app_state.current_widget.widget_id == widget_id { f.render_widget( - Paragraph::new(mem_text).block(Block::default()), - margined_loc[0], + Block::default() + .borders(*SIDE_BORDERS) + .border_style(painter.colours.highlighted_border_style), + draw_loc, ); + } - // Update draw loc in widget map - if app_state.should_get_widget_bounds() { - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { - widget.top_left_corner = Some((draw_loc.x, draw_loc.y)); - widget.bottom_right_corner = - Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height)); - } + let ram_use_percentage = if let Some(mem) = mem_data.last() { + mem.1 + } else { + 0.0 + }; + let swap_use_percentage = if let Some(swap) = swap_data.last() { + swap.1 + } else { + 0.0 + }; + + const EMPTY_MEMORY_FRAC_STRING: &str = "0.0B/0.0B"; + + let trimmed_memory_frac = + if let Some((_label_percent, label_frac)) = &app_state.canvas_data.mem_labels { + label_frac.trim() + } else { + EMPTY_MEMORY_FRAC_STRING + }; + + let trimmed_swap_frac = + if let Some((_label_percent, label_frac)) = &app_state.canvas_data.swap_labels { + label_frac.trim() + } else { + EMPTY_MEMORY_FRAC_STRING + }; + + // +7 due to 3 + 2 + 2 columns for the name & space + bar bounds + margin spacing + // Then + length of fraction + let ram_bar_length = + usize::from(draw_loc.width.saturating_sub(7)).saturating_sub(trimmed_memory_frac.len()); + let swap_bar_length = + usize::from(draw_loc.width.saturating_sub(7)).saturating_sub(trimmed_swap_frac.len()); + + let num_bars_ram = calculate_basic_use_bars(ram_use_percentage, ram_bar_length); + let num_bars_swap = calculate_basic_use_bars(swap_use_percentage, swap_bar_length); + // TODO: Use different styling for the frac. + let mem_label = if app_state.basic_mode_use_percent { + format!( + "RAM[{}{}{:3.0}%]\n", + "|".repeat(num_bars_ram), + " ".repeat(ram_bar_length - num_bars_ram + trimmed_memory_frac.len() - 4), + ram_use_percentage.round() + ) + } else { + format!( + "RAM[{}{}{}]\n", + "|".repeat(num_bars_ram), + " ".repeat(ram_bar_length - num_bars_ram), + trimmed_memory_frac + ) + }; + let swap_label = if app_state.basic_mode_use_percent { + format!( + "SWP[{}{}{:3.0}%]", + "|".repeat(num_bars_swap), + " ".repeat(swap_bar_length - num_bars_swap + trimmed_swap_frac.len() - 4), + swap_use_percentage.round() + ) + } else { + format!( + "SWP[{}{}{}]", + "|".repeat(num_bars_swap), + " ".repeat(swap_bar_length - num_bars_swap), + trimmed_swap_frac + ) + }; + + let mem_text = vec![ + Spans::from(Span::styled(mem_label, painter.colours.ram_style)), + Spans::from(Span::styled(swap_label, painter.colours.swap_style)), + ]; + + f.render_widget( + Paragraph::new(mem_text).block(Block::default()), + margined_loc[0], + ); + + // Update draw loc in widget map + if app_state.should_get_widget_bounds() { + if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + widget.top_left_corner = Some((draw_loc.x, draw_loc.y)); + widget.bottom_right_corner = + Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height)); } } } diff --git a/src/canvas/widgets/mem_graph.rs b/src/canvas/widgets/mem_graph.rs index 913e09d1..60ad989d 100644 --- a/src/canvas/widgets/mem_graph.rs +++ b/src/canvas/widgets/mem_graph.rs @@ -15,235 +15,226 @@ use tui::{ }; use unicode_segmentation::UnicodeSegmentation; -pub trait MemGraphWidget { - fn draw_memory_graph( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ); -} +pub fn draw_memory_graph( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + widget_id: u64, +) { + if let Some(mem_widget_state) = app_state.mem_state.widget_states.get_mut(&widget_id) { + let mem_data: &mut [(f64, f64)] = &mut app_state.canvas_data.mem_data; + let swap_data: &mut [(f64, f64)] = &mut app_state.canvas_data.swap_data; -impl MemGraphWidget for Painter { - fn draw_memory_graph( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ) { - if let Some(mem_widget_state) = app_state.mem_state.widget_states.get_mut(&widget_id) { - let mem_data: &mut [(f64, f64)] = &mut app_state.canvas_data.mem_data; - let swap_data: &mut [(f64, f64)] = &mut app_state.canvas_data.swap_data; + let time_start = -(mem_widget_state.current_display_time as f64); - let time_start = -(mem_widget_state.current_display_time as f64); + let display_time_labels = vec![ + Span::styled( + format!("{}s", mem_widget_state.current_display_time / 1000), + painter.colours.graph_style, + ), + Span::styled("0s".to_string(), painter.colours.graph_style), + ]; + let y_axis_label = vec![ + Span::styled(" 0%", painter.colours.graph_style), + Span::styled("100%", painter.colours.graph_style), + ]; - let display_time_labels = vec![ - Span::styled( - format!("{}s", mem_widget_state.current_display_time / 1000), - self.colours.graph_style, - ), - Span::styled("0s".to_string(), self.colours.graph_style), - ]; - let y_axis_label = vec![ - Span::styled(" 0%", self.colours.graph_style), - Span::styled("100%", self.colours.graph_style), - ]; - - let x_axis = if app_state.app_config_fields.hide_time - || (app_state.app_config_fields.autohide_time - && mem_widget_state.autohide_timer.is_none()) + let x_axis = if app_state.app_config_fields.hide_time + || (app_state.app_config_fields.autohide_time + && mem_widget_state.autohide_timer.is_none()) + { + Axis::default().bounds([time_start, 0.0]) + } else if let Some(time) = mem_widget_state.autohide_timer { + if std::time::Instant::now().duration_since(time).as_millis() + < AUTOHIDE_TIMEOUT_MILLISECONDS as u128 { - Axis::default().bounds([time_start, 0.0]) - } else if let Some(time) = mem_widget_state.autohide_timer { - if std::time::Instant::now().duration_since(time).as_millis() - < AUTOHIDE_TIMEOUT_MILLISECONDS as u128 - { - Axis::default() - .bounds([time_start, 0.0]) - .style(self.colours.graph_style) - .labels(display_time_labels) - } else { - mem_widget_state.autohide_timer = None; - Axis::default().bounds([time_start, 0.0]) - } - } else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT { - Axis::default().bounds([time_start, 0.0]) - } else { Axis::default() .bounds([time_start, 0.0]) - .style(self.colours.graph_style) + .style(painter.colours.graph_style) .labels(display_time_labels) - }; + } else { + mem_widget_state.autohide_timer = None; + Axis::default().bounds([time_start, 0.0]) + } + } else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT { + Axis::default().bounds([time_start, 0.0]) + } else { + Axis::default() + .bounds([time_start, 0.0]) + .style(painter.colours.graph_style) + .labels(display_time_labels) + }; - let y_axis = Axis::default() - .style(self.colours.graph_style) - .bounds([0.0, 100.5]) - .labels(y_axis_label); + let y_axis = Axis::default() + .style(painter.colours.graph_style) + .bounds([0.0, 100.5]) + .labels(y_axis_label); - // Interpolate values to avoid ugly gaps - let interpolated_mem_point = if let Some(end_pos) = mem_data - .iter() - .position(|(time, _data)| *time >= time_start) - { - if end_pos > 1 { - let start_pos = end_pos - 1; - let outside_point = mem_data.get(start_pos); - let inside_point = mem_data.get(end_pos); + // Interpolate values to avoid ugly gaps + let interpolated_mem_point = if let Some(end_pos) = mem_data + .iter() + .position(|(time, _data)| *time >= time_start) + { + if end_pos > 1 { + let start_pos = end_pos - 1; + let outside_point = mem_data.get(start_pos); + let inside_point = mem_data.get(end_pos); - if let (Some(outside_point), Some(inside_point)) = (outside_point, inside_point) - { - let old = *outside_point; + if let (Some(outside_point), Some(inside_point)) = (outside_point, inside_point) { + let old = *outside_point; - let new_point = ( - time_start, - interpolate_points(outside_point, inside_point, time_start), - ); + let new_point = ( + time_start, + interpolate_points(outside_point, inside_point, time_start), + ); - if let Some(to_replace) = mem_data.get_mut(start_pos) { - *to_replace = new_point; - Some((start_pos, old)) - } else { - None // Failed to get mutable reference. - } + if let Some(to_replace) = mem_data.get_mut(start_pos) { + *to_replace = new_point; + Some((start_pos, old)) } else { - None // Point somehow doesn't exist in our data + None // Failed to get mutable reference. } } else { - None // Point is already "leftmost", no need to interpolate. + None // Point somehow doesn't exist in our data } } else { - None // There is no point. - }; + None // Point is already "leftmost", no need to interpolate. + } + } else { + None // There is no point. + }; - let interpolated_swap_point = if let Some(end_pos) = swap_data - .iter() - .position(|(time, _data)| *time >= time_start) - { - if end_pos > 1 { - let start_pos = end_pos - 1; - let outside_point = swap_data.get(start_pos); - let inside_point = swap_data.get(end_pos); + let interpolated_swap_point = if let Some(end_pos) = swap_data + .iter() + .position(|(time, _data)| *time >= time_start) + { + if end_pos > 1 { + let start_pos = end_pos - 1; + let outside_point = swap_data.get(start_pos); + let inside_point = swap_data.get(end_pos); - if let (Some(outside_point), Some(inside_point)) = (outside_point, inside_point) - { - let old = *outside_point; + if let (Some(outside_point), Some(inside_point)) = (outside_point, inside_point) { + let old = *outside_point; - let new_point = ( - time_start, - interpolate_points(outside_point, inside_point, time_start), - ); + let new_point = ( + time_start, + interpolate_points(outside_point, inside_point, time_start), + ); - if let Some(to_replace) = swap_data.get_mut(start_pos) { - *to_replace = new_point; - Some((start_pos, old)) - } else { - None // Failed to get mutable reference. - } + if let Some(to_replace) = swap_data.get_mut(start_pos) { + *to_replace = new_point; + Some((start_pos, old)) } else { - None // Point somehow doesn't exist in our data + None // Failed to get mutable reference. } } else { - None // Point is already "leftmost", no need to interpolate. + None // Point somehow doesn't exist in our data } } else { - None // There is no point. - }; - - let mut mem_canvas_vec: Vec> = vec![]; - - if let Some((label_percent, label_frac)) = &app_state.canvas_data.mem_labels { - let mem_label = format!("RAM:{}{}", label_percent, label_frac); - mem_canvas_vec.push( - Dataset::default() - .name(mem_label) - .marker(if app_state.app_config_fields.use_dot { - Marker::Dot - } else { - Marker::Braille - }) - .style(self.colours.ram_style) - .data(mem_data) - .graph_type(tui::widgets::GraphType::Line), - ); + None // Point is already "leftmost", no need to interpolate. } + } else { + None // There is no point. + }; - if let Some((label_percent, label_frac)) = &app_state.canvas_data.swap_labels { - let swap_label = format!("SWP:{}{}", label_percent, label_frac); - mem_canvas_vec.push( - Dataset::default() - .name(swap_label) - .marker(if app_state.app_config_fields.use_dot { - Marker::Dot - } else { - Marker::Braille - }) - .style(self.colours.swap_style) - .data(swap_data) - .graph_type(tui::widgets::GraphType::Line), - ); - } + let mut mem_canvas_vec: Vec> = vec![]; - let is_on_widget = widget_id == app_state.current_widget.widget_id; - let border_style = if is_on_widget { - self.colours.highlighted_border_style - } else { - self.colours.border_style - }; - - let title = if app_state.is_expanded { - const TITLE_BASE: &str = " Memory ── Esc to go back "; - Spans::from(vec![ - Span::styled(" Memory ", self.colours.widget_title_style), - Span::styled( - format!( - "─{}─ Esc to go back ", - "─".repeat(usize::from(draw_loc.width).saturating_sub( - UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2 - )) - ), - border_style, - ), - ]) - } else { - Spans::from(Span::styled( - " Memory ".to_string(), - self.colours.widget_title_style, - )) - }; - - f.render_widget( - Chart::new(mem_canvas_vec) - .block( - Block::default() - .title(title) - .borders(Borders::ALL) - .border_style(if app_state.current_widget.widget_id == widget_id { - self.colours.highlighted_border_style - } else { - self.colours.border_style - }), - ) - .x_axis(x_axis) - .y_axis(y_axis) - .hidden_legend_constraints((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))), - draw_loc, + if let Some((label_percent, label_frac)) = &app_state.canvas_data.mem_labels { + let mem_label = format!("RAM:{}{}", label_percent, label_frac); + mem_canvas_vec.push( + Dataset::default() + .name(mem_label) + .marker(if app_state.app_config_fields.use_dot { + Marker::Dot + } else { + Marker::Braille + }) + .style(painter.colours.ram_style) + .data(mem_data) + .graph_type(tui::widgets::GraphType::Line), ); + } - // Now if you're done, reset any interpolated points! - if let Some((index, old_value)) = interpolated_mem_point { - if let Some(to_replace) = mem_data.get_mut(index) { - *to_replace = old_value; - } - } + if let Some((label_percent, label_frac)) = &app_state.canvas_data.swap_labels { + let swap_label = format!("SWP:{}{}", label_percent, label_frac); + mem_canvas_vec.push( + Dataset::default() + .name(swap_label) + .marker(if app_state.app_config_fields.use_dot { + Marker::Dot + } else { + Marker::Braille + }) + .style(painter.colours.swap_style) + .data(swap_data) + .graph_type(tui::widgets::GraphType::Line), + ); + } - if let Some((index, old_value)) = interpolated_swap_point { - if let Some(to_replace) = swap_data.get_mut(index) { - *to_replace = old_value; - } + let is_on_widget = widget_id == app_state.current_widget.widget_id; + let border_style = if is_on_widget { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }; + + let title = if app_state.is_expanded { + const TITLE_BASE: &str = " Memory ── Esc to go back "; + Spans::from(vec![ + Span::styled(" Memory ", painter.colours.widget_title_style), + Span::styled( + format!( + "─{}─ Esc to go back ", + "─".repeat(usize::from(draw_loc.width).saturating_sub( + UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2 + )) + ), + border_style, + ), + ]) + } else { + Spans::from(Span::styled( + " Memory ".to_string(), + painter.colours.widget_title_style, + )) + }; + + f.render_widget( + Chart::new(mem_canvas_vec) + .block( + Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(if app_state.current_widget.widget_id == widget_id { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }), + ) + .x_axis(x_axis) + .y_axis(y_axis) + .hidden_legend_constraints((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))), + draw_loc, + ); + + // Now if you're done, reset any interpolated points! + if let Some((index, old_value)) = interpolated_mem_point { + if let Some(to_replace) = mem_data.get_mut(index) { + *to_replace = old_value; } } - if app_state.should_get_widget_bounds() { - // Update draw loc in widget map - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { - widget.top_left_corner = Some((draw_loc.x, draw_loc.y)); - widget.bottom_right_corner = - Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height)); + if let Some((index, old_value)) = interpolated_swap_point { + if let Some(to_replace) = swap_data.get_mut(index) { + *to_replace = old_value; } } } + + if app_state.should_get_widget_bounds() { + // Update draw loc in widget map + if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + widget.top_left_corner = Some((draw_loc.x, draw_loc.y)); + widget.bottom_right_corner = + Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height)); + } + } } diff --git a/src/canvas/widgets/network_basic.rs b/src/canvas/widgets/network_basic.rs index 42ebd73b..e5dcf579 100644 --- a/src/canvas/widgets/network_basic.rs +++ b/src/canvas/widgets/network_basic.rs @@ -8,71 +8,64 @@ use tui::{ widgets::{Block, Paragraph}, }; -pub trait NetworkBasicWidget { - fn draw_basic_network( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ); -} +pub fn draw_basic_network( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + widget_id: u64, +) { + let divided_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(draw_loc); -impl NetworkBasicWidget for Painter { - fn draw_basic_network( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ) { - let divided_loc = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(draw_loc); + let net_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(1) + .split(divided_loc[0]); - let net_loc = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(100)]) - .horizontal_margin(1) - .split(divided_loc[0]); - - let total_loc = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(100)]) - .horizontal_margin(1) - .split(divided_loc[1]); - - if app_state.current_widget.widget_id == widget_id { - f.render_widget( - Block::default() - .borders(*SIDE_BORDERS) - .border_style(self.colours.highlighted_border_style), - draw_loc, - ); - } - - let rx_label = format!("RX: {}", &app_state.canvas_data.rx_display); - let tx_label = format!("TX: {}", &app_state.canvas_data.tx_display); - let total_rx_label = format!("Total RX: {}", &app_state.canvas_data.total_rx_display); - let total_tx_label = format!("Total TX: {}", &app_state.canvas_data.total_tx_display); - - let net_text = vec![ - Spans::from(Span::styled(rx_label, self.colours.rx_style)), - Spans::from(Span::styled(tx_label, self.colours.tx_style)), - ]; - - let total_net_text = vec![ - Spans::from(Span::styled(total_rx_label, self.colours.total_rx_style)), - Spans::from(Span::styled(total_tx_label, self.colours.total_tx_style)), - ]; - - f.render_widget(Paragraph::new(net_text).block(Block::default()), net_loc[0]); + let total_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(1) + .split(divided_loc[1]); + if app_state.current_widget.widget_id == widget_id { f.render_widget( - Paragraph::new(total_net_text).block(Block::default()), - total_loc[0], + Block::default() + .borders(*SIDE_BORDERS) + .border_style(painter.colours.highlighted_border_style), + draw_loc, ); + } - // Update draw loc in widget map - if app_state.should_get_widget_bounds() { - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { - widget.top_left_corner = Some((draw_loc.x, draw_loc.y)); - widget.bottom_right_corner = - Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height)); - } + let rx_label = format!("RX: {}", &app_state.canvas_data.rx_display); + let tx_label = format!("TX: {}", &app_state.canvas_data.tx_display); + let total_rx_label = format!("Total RX: {}", &app_state.canvas_data.total_rx_display); + let total_tx_label = format!("Total TX: {}", &app_state.canvas_data.total_tx_display); + + let net_text = vec![ + Spans::from(Span::styled(rx_label, painter.colours.rx_style)), + Spans::from(Span::styled(tx_label, painter.colours.tx_style)), + ]; + + let total_net_text = vec![ + Spans::from(Span::styled(total_rx_label, painter.colours.total_rx_style)), + Spans::from(Span::styled(total_tx_label, painter.colours.total_tx_style)), + ]; + + f.render_widget(Paragraph::new(net_text).block(Block::default()), net_loc[0]); + + f.render_widget( + Paragraph::new(total_net_text).block(Block::default()), + total_loc[0], + ); + + // Update draw loc in widget map + if app_state.should_get_widget_bounds() { + if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + widget.top_left_corner = Some((draw_loc.x, draw_loc.y)); + widget.bottom_right_corner = + Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height)); } } } diff --git a/src/canvas/widgets/network_graph.rs b/src/canvas/widgets/network_graph.rs index 08a26807..85901f9c 100644 --- a/src/canvas/widgets/network_graph.rs +++ b/src/canvas/widgets/network_graph.rs @@ -32,739 +32,727 @@ static NETWORK_HEADERS_LENS: Lazy> = Lazy::new(|| { .collect::>() }); -pub trait NetworkGraphWidget { - fn draw_network( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ); +pub fn draw_network( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + widget_id: u64, +) { + if app_state.app_config_fields.use_old_network_legend { + let network_chunk = Layout::default() + .direction(Direction::Vertical) + .margin(0) + .constraints([ + Constraint::Length(max(draw_loc.height as i64 - 5, 0) as u16), + Constraint::Length(5), + ]) + .split(draw_loc); - fn draw_network_graph( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - hide_legend: bool, - ); + draw_network_graph(painter, f, app_state, network_chunk[0], widget_id, true); + draw_network_labels(painter, f, app_state, network_chunk[1], widget_id); + } else { + draw_network_graph(painter, f, app_state, draw_loc, widget_id, false); + } - fn draw_network_labels( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ); + if app_state.should_get_widget_bounds() { + // Update draw loc in widget map + // Note that in both cases, we always go to the same widget id so it's fine to do it like + // this lol. + if let Some(network_widget) = app_state.widget_map.get_mut(&widget_id) { + network_widget.top_left_corner = Some((draw_loc.x, draw_loc.y)); + network_widget.bottom_right_corner = + Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height)); + } + } } -impl NetworkGraphWidget for Painter { - fn draw_network( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ) { - if app_state.app_config_fields.use_old_network_legend { - let network_chunk = Layout::default() - .direction(Direction::Vertical) - .margin(0) - .constraints([ - Constraint::Length(max(draw_loc.height as i64 - 5, 0) as u16), - Constraint::Length(5), - ]) - .split(draw_loc); - - self.draw_network_graph(f, app_state, network_chunk[0], widget_id, true); - self.draw_network_labels(f, app_state, network_chunk[1], widget_id); - } else { - self.draw_network_graph(f, app_state, draw_loc, widget_id, false); - } - - if app_state.should_get_widget_bounds() { - // Update draw loc in widget map - // Note that in both cases, we always go to the same widget id so it's fine to do it like - // this lol. - if let Some(network_widget) = app_state.widget_map.get_mut(&widget_id) { - network_widget.top_left_corner = Some((draw_loc.x, draw_loc.y)); - network_widget.bottom_right_corner = - Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height)); - } - } - } - - fn draw_network_graph( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - hide_legend: bool, - ) { - /// Point is of time, data - type Point = (f64, f64); - - /// Returns the max data point and time given a time. - fn get_max_entry( - rx: &[Point], tx: &[Point], time_start: f64, network_scale_type: &AxisScaling, - network_use_binary_prefix: bool, - ) -> (f64, f64) { - /// Determines a "fake" max value in circumstances where we couldn't find one from the data. - fn calculate_missing_max( - network_scale_type: &AxisScaling, network_use_binary_prefix: bool, - ) -> f64 { - match network_scale_type { - AxisScaling::Log => { - if network_use_binary_prefix { - LOG_KIBI_LIMIT - } else { - LOG_KILO_LIMIT - } - } - AxisScaling::Linear => { - if network_use_binary_prefix { - KIBI_LIMIT_F64 - } else { - KILO_LIMIT_F64 - } - } - } - } - - // First, let's shorten our ranges to actually look. We can abuse the fact that our rx and tx arrays - // are sorted, so we can short-circuit our search to filter out only the relevant data points... - let filtered_rx = if let (Some(rx_start), Some(rx_end)) = ( - rx.iter().position(|(time, _data)| *time >= time_start), - rx.iter().rposition(|(time, _data)| *time <= 0.0), - ) { - Some(&rx[rx_start..=rx_end]) - } else { - None - }; - - let filtered_tx = if let (Some(tx_start), Some(tx_end)) = ( - tx.iter().position(|(time, _data)| *time >= time_start), - tx.iter().rposition(|(time, _data)| *time <= 0.0), - ) { - Some(&tx[tx_start..=tx_end]) - } else { - None - }; - - // Then, find the maximal rx/tx so we know how to scale, and return it. - match (filtered_rx, filtered_tx) { - (None, None) => ( - time_start, - calculate_missing_max(network_scale_type, network_use_binary_prefix), - ), - (None, Some(filtered_tx)) => { - match filtered_tx - .iter() - .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) - { - Some((best_time, max_val)) => { - if *max_val == 0.0 { - ( - time_start, - calculate_missing_max( - network_scale_type, - network_use_binary_prefix, - ), - ) - } else { - (*best_time, *max_val) - } - } - None => ( - time_start, - calculate_missing_max(network_scale_type, network_use_binary_prefix), - ), - } - } - (Some(filtered_rx), None) => { - match filtered_rx - .iter() - .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) - { - Some((best_time, max_val)) => { - if *max_val == 0.0 { - ( - time_start, - calculate_missing_max( - network_scale_type, - network_use_binary_prefix, - ), - ) - } else { - (*best_time, *max_val) - } - } - None => ( - time_start, - calculate_missing_max(network_scale_type, network_use_binary_prefix), - ), - } - } - (Some(filtered_rx), Some(filtered_tx)) => { - match filtered_rx - .iter() - .chain(filtered_tx) - .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) - { - Some((best_time, max_val)) => { - if *max_val == 0.0 { - ( - *best_time, - calculate_missing_max( - network_scale_type, - network_use_binary_prefix, - ), - ) - } else { - (*best_time, *max_val) - } - } - None => ( - time_start, - calculate_missing_max(network_scale_type, network_use_binary_prefix), - ), - } - } - } - } - - /// Returns the required max data point and labels. - fn adjust_network_data_point( - max_entry: f64, network_scale_type: &AxisScaling, network_unit_type: &DataUnit, - network_use_binary_prefix: bool, - ) -> (f64, Vec) { - // So, we're going with an approach like this for linear data: - // - Main goal is to maximize the amount of information displayed given a specific height. - // We don't want to drown out some data if the ranges are too far though! Nor do we want to filter - // out too much data... - // - Change the y-axis unit (kilo/kibi, mega/mebi...) dynamically based on max load. - // - // The idea is we take the top value, build our scale such that each "point" is a scaled version of that. - // So for example, let's say I use 390 Mb/s. If I drew 4 segments, it would be 97.5, 195, 292.5, 390, and - // probably something like 438.75? - // - // So, how do we do this in tui-rs? Well, if we are using intervals that tie in perfectly to the max - // value we want... then it's actually not that hard. Since tui-rs accepts a vector as labels and will - // properly space them all out... we just work with that and space it out properly. - // - // Dynamic chart idea based off of FreeNAS's chart design. - // - // === - // - // For log data, we just use the old method of log intervals (kilo/mega/giga/etc.). Keep it nice and simple. - - // Now just check the largest unit we correspond to... then proceed to build some entries from there! - - let unit_char = match network_unit_type { - DataUnit::Byte => "B", - DataUnit::Bit => "b", - }; +pub fn draw_network_graph( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + widget_id: u64, hide_legend: bool, +) { + /// Point is of time, data + type Point = (f64, f64); + /// Returns the max data point and time given a time. + fn get_max_entry( + rx: &[Point], tx: &[Point], time_start: f64, network_scale_type: &AxisScaling, + network_use_binary_prefix: bool, + ) -> (f64, f64) { + /// Determines a "fake" max value in circumstances where we couldn't find one from the data. + fn calculate_missing_max( + network_scale_type: &AxisScaling, network_use_binary_prefix: bool, + ) -> f64 { match network_scale_type { - AxisScaling::Linear => { - let (k_limit, m_limit, g_limit, t_limit) = if network_use_binary_prefix { - ( - KIBI_LIMIT_F64, - MEBI_LIMIT_F64, - GIBI_LIMIT_F64, - TEBI_LIMIT_F64, - ) - } else { - ( - KILO_LIMIT_F64, - MEGA_LIMIT_F64, - GIGA_LIMIT_F64, - TERA_LIMIT_F64, - ) - }; - - let bumped_max_entry = max_entry * 1.5; // We use the bumped up version to calculate our unit type. - let (max_value_scaled, unit_prefix, unit_type): (f64, &str, &str) = - if bumped_max_entry < k_limit { - (max_entry, "", unit_char) - } else if bumped_max_entry < m_limit { - ( - max_entry / k_limit, - if network_use_binary_prefix { "Ki" } else { "K" }, - unit_char, - ) - } else if bumped_max_entry < g_limit { - ( - max_entry / m_limit, - if network_use_binary_prefix { "Mi" } else { "M" }, - unit_char, - ) - } else if bumped_max_entry < t_limit { - ( - max_entry / g_limit, - if network_use_binary_prefix { "Gi" } else { "G" }, - unit_char, - ) - } else { - ( - max_entry / t_limit, - if network_use_binary_prefix { "Ti" } else { "T" }, - unit_char, - ) - }; - - // Finally, build an acceptable range starting from there, using the given height! - // Note we try to put more of a weight on the bottom section vs. the top, since the top has less data. - - let base_unit = max_value_scaled; - let labels: Vec = vec![ - format!("0{}{}", unit_prefix, unit_type), - format!("{:.1}", base_unit * 0.5), - format!("{:.1}", base_unit), - format!("{:.1}", base_unit * 1.5), - ] - .into_iter() - .map(|s| format!("{:>5}", s)) // Pull 5 as the longest legend value is generally going to be 5 digits (if they somehow hit over 5 terabits per second) - .collect(); - - (bumped_max_entry, labels) - } AxisScaling::Log => { - let (m_limit, g_limit, t_limit) = if network_use_binary_prefix { - (LOG_MEBI_LIMIT, LOG_GIBI_LIMIT, LOG_TEBI_LIMIT) + if network_use_binary_prefix { + LOG_KIBI_LIMIT } else { - (LOG_MEGA_LIMIT, LOG_GIGA_LIMIT, LOG_TERA_LIMIT) - }; - - fn get_zero(network_use_binary_prefix: bool, unit_char: &str) -> String { - format!( - "{}0{}", - if network_use_binary_prefix { " " } else { " " }, - unit_char - ) + LOG_KILO_LIMIT } - - fn get_k(network_use_binary_prefix: bool, unit_char: &str) -> String { - format!( - "1{}{}", - if network_use_binary_prefix { "Ki" } else { "K" }, - unit_char - ) - } - - fn get_m(network_use_binary_prefix: bool, unit_char: &str) -> String { - format!( - "1{}{}", - if network_use_binary_prefix { "Mi" } else { "M" }, - unit_char - ) - } - - fn get_g(network_use_binary_prefix: bool, unit_char: &str) -> String { - format!( - "1{}{}", - if network_use_binary_prefix { "Gi" } else { "G" }, - unit_char - ) - } - - fn get_t(network_use_binary_prefix: bool, unit_char: &str) -> String { - format!( - "1{}{}", - if network_use_binary_prefix { "Ti" } else { "T" }, - unit_char - ) - } - - fn get_p(network_use_binary_prefix: bool, unit_char: &str) -> String { - format!( - "1{}{}", - if network_use_binary_prefix { "Pi" } else { "P" }, - unit_char - ) - } - - if max_entry < m_limit { - ( - m_limit, - vec![ - get_zero(network_use_binary_prefix, unit_char), - get_k(network_use_binary_prefix, unit_char), - get_m(network_use_binary_prefix, unit_char), - ], - ) - } else if max_entry < g_limit { - ( - g_limit, - vec![ - get_zero(network_use_binary_prefix, unit_char), - get_k(network_use_binary_prefix, unit_char), - get_m(network_use_binary_prefix, unit_char), - get_g(network_use_binary_prefix, unit_char), - ], - ) - } else if max_entry < t_limit { - ( - t_limit, - vec![ - get_zero(network_use_binary_prefix, unit_char), - get_k(network_use_binary_prefix, unit_char), - get_m(network_use_binary_prefix, unit_char), - get_g(network_use_binary_prefix, unit_char), - get_t(network_use_binary_prefix, unit_char), - ], - ) + } + AxisScaling::Linear => { + if network_use_binary_prefix { + KIBI_LIMIT_F64 } else { - // I really doubt anyone's transferring beyond petabyte speeds... - ( - if network_use_binary_prefix { - LOG_PEBI_LIMIT - } else { - LOG_PETA_LIMIT - }, - vec![ - get_zero(network_use_binary_prefix, unit_char), - get_k(network_use_binary_prefix, unit_char), - get_m(network_use_binary_prefix, unit_char), - get_g(network_use_binary_prefix, unit_char), - get_t(network_use_binary_prefix, unit_char), - get_p(network_use_binary_prefix, unit_char), - ], - ) + KILO_LIMIT_F64 } } } } - if let Some(network_widget_state) = app_state.net_state.widget_states.get_mut(&widget_id) { - let network_data_rx: &mut [(f64, f64)] = &mut app_state.canvas_data.network_data_rx; - let network_data_tx: &mut [(f64, f64)] = &mut app_state.canvas_data.network_data_tx; - - let time_start = -(network_widget_state.current_display_time as f64); - - let display_time_labels = vec![ - Span::styled( - format!("{}s", network_widget_state.current_display_time / 1000), - self.colours.graph_style, - ), - Span::styled("0s".to_string(), self.colours.graph_style), - ]; - let x_axis = if app_state.app_config_fields.hide_time - || (app_state.app_config_fields.autohide_time - && network_widget_state.autohide_timer.is_none()) - { - Axis::default().bounds([time_start, 0.0]) - } else if let Some(time) = network_widget_state.autohide_timer { - if std::time::Instant::now().duration_since(time).as_millis() - < AUTOHIDE_TIMEOUT_MILLISECONDS as u128 - { - Axis::default() - .bounds([time_start, 0.0]) - .style(self.colours.graph_style) - .labels(display_time_labels) - } else { - network_widget_state.autohide_timer = None; - Axis::default().bounds([time_start, 0.0]) - } - } else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT { - Axis::default().bounds([time_start, 0.0]) - } else { - Axis::default() - .bounds([time_start, 0.0]) - .style(self.colours.graph_style) - .labels(display_time_labels) - }; - - // Interpolate a point for rx and tx between the last value outside of the left bounds and the first value - // inside it. - // Because we assume it is all in order for... basically all our code, we can't just append it, - // and insertion in the middle seems. So instead, we swap *out* the value that is outside with our - // interpolated point, draw and do whatever calculations, then swap back in the old value! - // - // Note there is some re-used work here! For potential optimizations, we could re-use some work here in/from - // get_max_entry... - let interpolated_rx_point = if let Some(rx_end_pos) = network_data_rx - .iter() - .position(|(time, _data)| *time >= time_start) - { - if rx_end_pos > 1 { - let rx_start_pos = rx_end_pos - 1; - let outside_rx_point = network_data_rx.get(rx_start_pos); - let inside_rx_point = network_data_rx.get(rx_end_pos); - - if let (Some(outside_rx_point), Some(inside_rx_point)) = - (outside_rx_point, inside_rx_point) - { - let old = *outside_rx_point; - - let new_point = ( - time_start, - interpolate_points(outside_rx_point, inside_rx_point, time_start), - ); - - // debug!( - // "Interpolated between {:?} and {:?}, got rx for time {:?}: {:?}", - // outside_rx_point, inside_rx_point, time_start, new_point - // ); - - if let Some(to_replace) = network_data_rx.get_mut(rx_start_pos) { - *to_replace = new_point; - Some((rx_start_pos, old)) - } else { - None // Failed to get mutable reference. - } - } else { - None // Point somehow doesn't exist in our network_data_rx - } - } else { - None // Point is already "leftmost", no need to interpolate. - } - } else { - None // There is no point. - }; - - let interpolated_tx_point = if let Some(tx_end_pos) = network_data_tx - .iter() - .position(|(time, _data)| *time >= time_start) - { - if tx_end_pos > 1 { - let tx_start_pos = tx_end_pos - 1; - let outside_tx_point = network_data_tx.get(tx_start_pos); - let inside_tx_point = network_data_tx.get(tx_end_pos); - - if let (Some(outside_tx_point), Some(inside_tx_point)) = - (outside_tx_point, inside_tx_point) - { - let old = *outside_tx_point; - - let new_point = ( - time_start, - interpolate_points(outside_tx_point, inside_tx_point, time_start), - ); - - if let Some(to_replace) = network_data_tx.get_mut(tx_start_pos) { - *to_replace = new_point; - Some((tx_start_pos, old)) - } else { - None // Failed to get mutable reference. - } - } else { - None // Point somehow doesn't exist in our network_data_tx - } - } else { - None // Point is already "leftmost", no need to interpolate. - } - } else { - None // There is no point. - }; - - // TODO: Cache network results: Only update if: - // - Force update (includes time interval change) - // - Old max time is off screen - // - A new time interval is better and does not fit (check from end of vector to last checked; we only want to update if it is TOO big!) - - // Find the maximal rx/tx so we know how to scale, and return it. - - let (_best_time, max_entry) = get_max_entry( - network_data_rx, - network_data_tx, - time_start, - &app_state.app_config_fields.network_scale_type, - app_state.app_config_fields.network_use_binary_prefix, - ); - - let (max_range, labels) = adjust_network_data_point( - max_entry, - &app_state.app_config_fields.network_scale_type, - &app_state.app_config_fields.network_unit_type, - app_state.app_config_fields.network_use_binary_prefix, - ); - - // Cache results. - // network_widget_state.draw_max_range_cache = max_range; - // network_widget_state.draw_time_start_cache = best_time; - // network_widget_state.draw_labels_cache = labels; - - let y_axis_labels = labels - .iter() - .map(|label| Span::styled(label, self.colours.graph_style)) - .collect::>(); - let y_axis = Axis::default() - .style(self.colours.graph_style) - .bounds([0.0, max_range]) - .labels(y_axis_labels); - - let is_on_widget = widget_id == app_state.current_widget.widget_id; - let border_style = if is_on_widget { - self.colours.highlighted_border_style - } else { - self.colours.border_style - }; - - let title = if app_state.is_expanded { - const TITLE_BASE: &str = " Network ── Esc to go back "; - Spans::from(vec![ - Span::styled(" Network ", self.colours.widget_title_style), - Span::styled( - format!( - "─{}─ Esc to go back ", - "─".repeat(usize::from(draw_loc.width).saturating_sub( - UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2 - )) - ), - border_style, - ), - ]) - } else { - Spans::from(Span::styled(" Network ", self.colours.widget_title_style)) - }; - - let legend_constraints = if hide_legend { - (Constraint::Ratio(0, 1), Constraint::Ratio(0, 1)) - } else { - (Constraint::Ratio(1, 1), Constraint::Ratio(3, 4)) - }; - - // TODO: Add support for clicking on legend to only show that value on chart. - let dataset = if app_state.app_config_fields.use_old_network_legend && !hide_legend { - vec![ - Dataset::default() - .name(format!("RX: {:7}", app_state.canvas_data.rx_display)) - .marker(if app_state.app_config_fields.use_dot { - Marker::Dot - } else { - Marker::Braille - }) - .style(self.colours.rx_style) - .data(network_data_rx) - .graph_type(tui::widgets::GraphType::Line), - Dataset::default() - .name(format!("TX: {:7}", app_state.canvas_data.tx_display)) - .marker(if app_state.app_config_fields.use_dot { - Marker::Dot - } else { - Marker::Braille - }) - .style(self.colours.tx_style) - .data(network_data_tx) - .graph_type(tui::widgets::GraphType::Line), - Dataset::default() - .name(format!( - "Total RX: {:7}", - app_state.canvas_data.total_rx_display - )) - .style(self.colours.total_rx_style), - Dataset::default() - .name(format!( - "Total TX: {:7}", - app_state.canvas_data.total_tx_display - )) - .style(self.colours.total_tx_style), - ] - } else { - vec![ - Dataset::default() - .name(&app_state.canvas_data.rx_display) - .marker(if app_state.app_config_fields.use_dot { - Marker::Dot - } else { - Marker::Braille - }) - .style(self.colours.rx_style) - .data(network_data_rx) - .graph_type(tui::widgets::GraphType::Line), - Dataset::default() - .name(&app_state.canvas_data.tx_display) - .marker(if app_state.app_config_fields.use_dot { - Marker::Dot - } else { - Marker::Braille - }) - .style(self.colours.tx_style) - .data(network_data_tx) - .graph_type(tui::widgets::GraphType::Line), - ] - }; - - f.render_widget( - Chart::new(dataset) - .block( - Block::default() - .title(title) - .borders(Borders::ALL) - .border_style(if app_state.current_widget.widget_id == widget_id { - self.colours.highlighted_border_style - } else { - self.colours.border_style - }), - ) - .x_axis(x_axis) - .y_axis(y_axis) - .hidden_legend_constraints(legend_constraints), - draw_loc, - ); - - // Now if you're done, reset any interpolated points! - if let Some((index, old_value)) = interpolated_rx_point { - if let Some(to_replace) = network_data_rx.get_mut(index) { - *to_replace = old_value; - } - } - - if let Some((index, old_value)) = interpolated_tx_point { - if let Some(to_replace) = network_data_tx.get_mut(index) { - *to_replace = old_value; - } - } - } - } - - fn draw_network_labels( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, widget_id: u64, - ) { - let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { - 0 + // First, let's shorten our ranges to actually look. We can abuse the fact that our rx and tx arrays + // are sorted, so we can short-circuit our search to filter out only the relevant data points... + let filtered_rx = if let (Some(rx_start), Some(rx_end)) = ( + rx.iter().position(|(time, _data)| *time >= time_start), + rx.iter().rposition(|(time, _data)| *time <= 0.0), + ) { + Some(&rx[rx_start..=rx_end]) } else { - app_state.app_config_fields.table_gap + None }; - let rx_display = &app_state.canvas_data.rx_display; - let tx_display = &app_state.canvas_data.tx_display; - let total_rx_display = &app_state.canvas_data.total_rx_display; - let total_tx_display = &app_state.canvas_data.total_tx_display; + let filtered_tx = if let (Some(tx_start), Some(tx_end)) = ( + tx.iter().position(|(time, _data)| *time >= time_start), + tx.iter().rposition(|(time, _data)| *time <= 0.0), + ) { + Some(&tx[tx_start..=tx_end]) + } else { + None + }; - // Gross but I need it to work... - let total_network = vec![vec![ - Text::raw(rx_display), - Text::raw(tx_display), - Text::raw(total_rx_display), - Text::raw(total_tx_display), - ]]; - let mapped_network = total_network - .into_iter() - .map(|val| Row::new(val).style(self.colours.text_style)); + // Then, find the maximal rx/tx so we know how to scale, and return it. + match (filtered_rx, filtered_tx) { + (None, None) => ( + time_start, + calculate_missing_max(network_scale_type, network_use_binary_prefix), + ), + (None, Some(filtered_tx)) => { + match filtered_tx + .iter() + .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) + { + Some((best_time, max_val)) => { + if *max_val == 0.0 { + ( + time_start, + calculate_missing_max( + network_scale_type, + network_use_binary_prefix, + ), + ) + } else { + (*best_time, *max_val) + } + } + None => ( + time_start, + calculate_missing_max(network_scale_type, network_use_binary_prefix), + ), + } + } + (Some(filtered_rx), None) => { + match filtered_rx + .iter() + .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) + { + Some((best_time, max_val)) => { + if *max_val == 0.0 { + ( + time_start, + calculate_missing_max( + network_scale_type, + network_use_binary_prefix, + ), + ) + } else { + (*best_time, *max_val) + } + } + None => ( + time_start, + calculate_missing_max(network_scale_type, network_use_binary_prefix), + ), + } + } + (Some(filtered_rx), Some(filtered_tx)) => { + match filtered_rx + .iter() + .chain(filtered_tx) + .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) + { + Some((best_time, max_val)) => { + if *max_val == 0.0 { + ( + *best_time, + calculate_missing_max( + network_scale_type, + network_use_binary_prefix, + ), + ) + } else { + (*best_time, *max_val) + } + } + None => ( + time_start, + calculate_missing_max(network_scale_type, network_use_binary_prefix), + ), + } + } + } + } - // Calculate widths - let intrinsic_widths = get_column_widths( - draw_loc.width, - &[None, None, None, None], - &(NETWORK_HEADERS_LENS - .iter() - .map(|s| Some(*s)) - .collect::>()), - &[Some(0.25); 4], - &(NETWORK_HEADERS_LENS - .iter() - .map(|s| Some(*s)) - .collect::>()), - true, + /// Returns the required max data point and labels. + fn adjust_network_data_point( + max_entry: f64, network_scale_type: &AxisScaling, network_unit_type: &DataUnit, + network_use_binary_prefix: bool, + ) -> (f64, Vec) { + // So, we're going with an approach like this for linear data: + // - Main goal is to maximize the amount of information displayed given a specific height. + // We don't want to drown out some data if the ranges are too far though! Nor do we want to filter + // out too much data... + // - Change the y-axis unit (kilo/kibi, mega/mebi...) dynamically based on max load. + // + // The idea is we take the top value, build our scale such that each "point" is a scaled version of that. + // So for example, let's say I use 390 Mb/s. If I drew 4 segments, it would be 97.5, 195, 292.5, 390, and + // probably something like 438.75? + // + // So, how do we do this in tui-rs? Well, if we are using intervals that tie in perfectly to the max + // value we want... then it's actually not that hard. Since tui-rs accepts a vector as labels and will + // properly space them all out... we just work with that and space it out properly. + // + // Dynamic chart idea based off of FreeNAS's chart design. + // + // === + // + // For log data, we just use the old method of log intervals (kilo/mega/giga/etc.). Keep it nice and simple. + + // Now just check the largest unit we correspond to... then proceed to build some entries from there! + + let unit_char = match network_unit_type { + DataUnit::Byte => "B", + DataUnit::Bit => "b", + }; + + match network_scale_type { + AxisScaling::Linear => { + let (k_limit, m_limit, g_limit, t_limit) = if network_use_binary_prefix { + ( + KIBI_LIMIT_F64, + MEBI_LIMIT_F64, + GIBI_LIMIT_F64, + TEBI_LIMIT_F64, + ) + } else { + ( + KILO_LIMIT_F64, + MEGA_LIMIT_F64, + GIGA_LIMIT_F64, + TERA_LIMIT_F64, + ) + }; + + let bumped_max_entry = max_entry * 1.5; // We use the bumped up version to calculate our unit type. + let (max_value_scaled, unit_prefix, unit_type): (f64, &str, &str) = + if bumped_max_entry < k_limit { + (max_entry, "", unit_char) + } else if bumped_max_entry < m_limit { + ( + max_entry / k_limit, + if network_use_binary_prefix { "Ki" } else { "K" }, + unit_char, + ) + } else if bumped_max_entry < g_limit { + ( + max_entry / m_limit, + if network_use_binary_prefix { "Mi" } else { "M" }, + unit_char, + ) + } else if bumped_max_entry < t_limit { + ( + max_entry / g_limit, + if network_use_binary_prefix { "Gi" } else { "G" }, + unit_char, + ) + } else { + ( + max_entry / t_limit, + if network_use_binary_prefix { "Ti" } else { "T" }, + unit_char, + ) + }; + + // Finally, build an acceptable range starting from there, using the given height! + // Note we try to put more of a weight on the bottom section vs. the top, since the top has less data. + + let base_unit = max_value_scaled; + let labels: Vec = vec![ + format!("0{}{}", unit_prefix, unit_type), + format!("{:.1}", base_unit * 0.5), + format!("{:.1}", base_unit), + format!("{:.1}", base_unit * 1.5), + ] + .into_iter() + .map(|s| format!("{:>5}", s)) // Pull 5 as the longest legend value is generally going to be 5 digits (if they somehow hit over 5 terabits per second) + .collect(); + + (bumped_max_entry, labels) + } + AxisScaling::Log => { + let (m_limit, g_limit, t_limit) = if network_use_binary_prefix { + (LOG_MEBI_LIMIT, LOG_GIBI_LIMIT, LOG_TEBI_LIMIT) + } else { + (LOG_MEGA_LIMIT, LOG_GIGA_LIMIT, LOG_TERA_LIMIT) + }; + + fn get_zero(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "{}0{}", + if network_use_binary_prefix { " " } else { " " }, + unit_char + ) + } + + fn get_k(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Ki" } else { "K" }, + unit_char + ) + } + + fn get_m(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Mi" } else { "M" }, + unit_char + ) + } + + fn get_g(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Gi" } else { "G" }, + unit_char + ) + } + + fn get_t(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Ti" } else { "T" }, + unit_char + ) + } + + fn get_p(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Pi" } else { "P" }, + unit_char + ) + } + + if max_entry < m_limit { + ( + m_limit, + vec![ + get_zero(network_use_binary_prefix, unit_char), + get_k(network_use_binary_prefix, unit_char), + get_m(network_use_binary_prefix, unit_char), + ], + ) + } else if max_entry < g_limit { + ( + g_limit, + vec![ + get_zero(network_use_binary_prefix, unit_char), + get_k(network_use_binary_prefix, unit_char), + get_m(network_use_binary_prefix, unit_char), + get_g(network_use_binary_prefix, unit_char), + ], + ) + } else if max_entry < t_limit { + ( + t_limit, + vec![ + get_zero(network_use_binary_prefix, unit_char), + get_k(network_use_binary_prefix, unit_char), + get_m(network_use_binary_prefix, unit_char), + get_g(network_use_binary_prefix, unit_char), + get_t(network_use_binary_prefix, unit_char), + ], + ) + } else { + // I really doubt anyone's transferring beyond petabyte speeds... + ( + if network_use_binary_prefix { + LOG_PEBI_LIMIT + } else { + LOG_PETA_LIMIT + }, + vec![ + get_zero(network_use_binary_prefix, unit_char), + get_k(network_use_binary_prefix, unit_char), + get_m(network_use_binary_prefix, unit_char), + get_g(network_use_binary_prefix, unit_char), + get_t(network_use_binary_prefix, unit_char), + get_p(network_use_binary_prefix, unit_char), + ], + ) + } + } + } + } + + if let Some(network_widget_state) = app_state.net_state.widget_states.get_mut(&widget_id) { + let network_data_rx: &mut [(f64, f64)] = &mut app_state.canvas_data.network_data_rx; + let network_data_tx: &mut [(f64, f64)] = &mut app_state.canvas_data.network_data_tx; + + let time_start = -(network_widget_state.current_display_time as f64); + + let display_time_labels = vec![ + Span::styled( + format!("{}s", network_widget_state.current_display_time / 1000), + painter.colours.graph_style, + ), + Span::styled("0s".to_string(), painter.colours.graph_style), + ]; + let x_axis = if app_state.app_config_fields.hide_time + || (app_state.app_config_fields.autohide_time + && network_widget_state.autohide_timer.is_none()) + { + Axis::default().bounds([time_start, 0.0]) + } else if let Some(time) = network_widget_state.autohide_timer { + if std::time::Instant::now().duration_since(time).as_millis() + < AUTOHIDE_TIMEOUT_MILLISECONDS as u128 + { + Axis::default() + .bounds([time_start, 0.0]) + .style(painter.colours.graph_style) + .labels(display_time_labels) + } else { + network_widget_state.autohide_timer = None; + Axis::default().bounds([time_start, 0.0]) + } + } else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT { + Axis::default().bounds([time_start, 0.0]) + } else { + Axis::default() + .bounds([time_start, 0.0]) + .style(painter.colours.graph_style) + .labels(display_time_labels) + }; + + // Interpolate a point for rx and tx between the last value outside of the left bounds and the first value + // inside it. + // Because we assume it is all in order for... basically all our code, we can't just append it, + // and insertion in the middle seems. So instead, we swap *out* the value that is outside with our + // interpolated point, draw and do whatever calculations, then swap back in the old value! + // + // Note there is some re-used work here! For potential optimizations, we could re-use some work here in/from + // get_max_entry... + let interpolated_rx_point = if let Some(rx_end_pos) = network_data_rx + .iter() + .position(|(time, _data)| *time >= time_start) + { + if rx_end_pos > 1 { + let rx_start_pos = rx_end_pos - 1; + let outside_rx_point = network_data_rx.get(rx_start_pos); + let inside_rx_point = network_data_rx.get(rx_end_pos); + + if let (Some(outside_rx_point), Some(inside_rx_point)) = + (outside_rx_point, inside_rx_point) + { + let old = *outside_rx_point; + + let new_point = ( + time_start, + interpolate_points(outside_rx_point, inside_rx_point, time_start), + ); + + // debug!( + // "Interpolated between {:?} and {:?}, got rx for time {:?}: {:?}", + // outside_rx_point, inside_rx_point, time_start, new_point + // ); + + if let Some(to_replace) = network_data_rx.get_mut(rx_start_pos) { + *to_replace = new_point; + Some((rx_start_pos, old)) + } else { + None // Failed to get mutable reference. + } + } else { + None // Point somehow doesn't exist in our network_data_rx + } + } else { + None // Point is already "leftmost", no need to interpolate. + } + } else { + None // There is no point. + }; + + let interpolated_tx_point = if let Some(tx_end_pos) = network_data_tx + .iter() + .position(|(time, _data)| *time >= time_start) + { + if tx_end_pos > 1 { + let tx_start_pos = tx_end_pos - 1; + let outside_tx_point = network_data_tx.get(tx_start_pos); + let inside_tx_point = network_data_tx.get(tx_end_pos); + + if let (Some(outside_tx_point), Some(inside_tx_point)) = + (outside_tx_point, inside_tx_point) + { + let old = *outside_tx_point; + + let new_point = ( + time_start, + interpolate_points(outside_tx_point, inside_tx_point, time_start), + ); + + if let Some(to_replace) = network_data_tx.get_mut(tx_start_pos) { + *to_replace = new_point; + Some((tx_start_pos, old)) + } else { + None // Failed to get mutable reference. + } + } else { + None // Point somehow doesn't exist in our network_data_tx + } + } else { + None // Point is already "leftmost", no need to interpolate. + } + } else { + None // There is no point. + }; + + // TODO: Cache network results: Only update if: + // - Force update (includes time interval change) + // - Old max time is off screen + // - A new time interval is better and does not fit (check from end of vector to last checked; we only want to update if it is TOO big!) + + // Find the maximal rx/tx so we know how to scale, and return it. + + let (_best_time, max_entry) = get_max_entry( + network_data_rx, + network_data_tx, + time_start, + &app_state.app_config_fields.network_scale_type, + app_state.app_config_fields.network_use_binary_prefix, ); - // Draw - f.render_widget( - Table::new(mapped_network) - .header( - Row::new(NETWORK_HEADERS.to_vec()) - .style(self.colours.table_header_style) - .bottom_margin(table_gap), - ) - .block(Block::default().borders(Borders::ALL).border_style( - if app_state.current_widget.widget_id == widget_id { - self.colours.highlighted_border_style - } else { - self.colours.border_style - }, - )) - .style(self.colours.text_style) - .widths( - &(intrinsic_widths - .iter() - .map(|calculated_width| Constraint::Length(*calculated_width as u16)) - .collect::>()), + let (max_range, labels) = adjust_network_data_point( + max_entry, + &app_state.app_config_fields.network_scale_type, + &app_state.app_config_fields.network_unit_type, + app_state.app_config_fields.network_use_binary_prefix, + ); + + // Cache results. + // network_widget_state.draw_max_range_cache = max_range; + // network_widget_state.draw_time_start_cache = best_time; + // network_widget_state.draw_labels_cache = labels; + + let y_axis_labels = labels + .iter() + .map(|label| Span::styled(label, painter.colours.graph_style)) + .collect::>(); + let y_axis = Axis::default() + .style(painter.colours.graph_style) + .bounds([0.0, max_range]) + .labels(y_axis_labels); + + let is_on_widget = widget_id == app_state.current_widget.widget_id; + let border_style = if is_on_widget { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }; + + let title = if app_state.is_expanded { + const TITLE_BASE: &str = " Network ── Esc to go back "; + Spans::from(vec![ + Span::styled(" Network ", painter.colours.widget_title_style), + Span::styled( + format!( + "─{}─ Esc to go back ", + "─".repeat(usize::from(draw_loc.width).saturating_sub( + UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2 + )) + ), + border_style, ), + ]) + } else { + Spans::from(Span::styled( + " Network ", + painter.colours.widget_title_style, + )) + }; + + let legend_constraints = if hide_legend { + (Constraint::Ratio(0, 1), Constraint::Ratio(0, 1)) + } else { + (Constraint::Ratio(1, 1), Constraint::Ratio(3, 4)) + }; + + // TODO: Add support for clicking on legend to only show that value on chart. + let dataset = if app_state.app_config_fields.use_old_network_legend && !hide_legend { + vec![ + Dataset::default() + .name(format!("RX: {:7}", app_state.canvas_data.rx_display)) + .marker(if app_state.app_config_fields.use_dot { + Marker::Dot + } else { + Marker::Braille + }) + .style(painter.colours.rx_style) + .data(network_data_rx) + .graph_type(tui::widgets::GraphType::Line), + Dataset::default() + .name(format!("TX: {:7}", app_state.canvas_data.tx_display)) + .marker(if app_state.app_config_fields.use_dot { + Marker::Dot + } else { + Marker::Braille + }) + .style(painter.colours.tx_style) + .data(network_data_tx) + .graph_type(tui::widgets::GraphType::Line), + Dataset::default() + .name(format!( + "Total RX: {:7}", + app_state.canvas_data.total_rx_display + )) + .style(painter.colours.total_rx_style), + Dataset::default() + .name(format!( + "Total TX: {:7}", + app_state.canvas_data.total_tx_display + )) + .style(painter.colours.total_tx_style), + ] + } else { + vec![ + Dataset::default() + .name(&app_state.canvas_data.rx_display) + .marker(if app_state.app_config_fields.use_dot { + Marker::Dot + } else { + Marker::Braille + }) + .style(painter.colours.rx_style) + .data(network_data_rx) + .graph_type(tui::widgets::GraphType::Line), + Dataset::default() + .name(&app_state.canvas_data.tx_display) + .marker(if app_state.app_config_fields.use_dot { + Marker::Dot + } else { + Marker::Braille + }) + .style(painter.colours.tx_style) + .data(network_data_tx) + .graph_type(tui::widgets::GraphType::Line), + ] + }; + + f.render_widget( + Chart::new(dataset) + .block( + Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(if app_state.current_widget.widget_id == widget_id { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }), + ) + .x_axis(x_axis) + .y_axis(y_axis) + .hidden_legend_constraints(legend_constraints), draw_loc, ); + + // Now if you're done, reset any interpolated points! + if let Some((index, old_value)) = interpolated_rx_point { + if let Some(to_replace) = network_data_rx.get_mut(index) { + *to_replace = old_value; + } + } + + if let Some((index, old_value)) = interpolated_tx_point { + if let Some(to_replace) = network_data_tx.get_mut(index) { + *to_replace = old_value; + } + } } } + +fn draw_network_labels( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + widget_id: u64, +) { + let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { + 0 + } else { + app_state.app_config_fields.table_gap + }; + + let rx_display = &app_state.canvas_data.rx_display; + let tx_display = &app_state.canvas_data.tx_display; + let total_rx_display = &app_state.canvas_data.total_rx_display; + let total_tx_display = &app_state.canvas_data.total_tx_display; + + // Gross but I need it to work... + let total_network = vec![vec![ + Text::raw(rx_display), + Text::raw(tx_display), + Text::raw(total_rx_display), + Text::raw(total_tx_display), + ]]; + let mapped_network = total_network + .into_iter() + .map(|val| Row::new(val).style(painter.colours.text_style)); + + // Calculate widths + let intrinsic_widths = get_column_widths( + draw_loc.width, + &[None, None, None, None], + &(NETWORK_HEADERS_LENS + .iter() + .map(|s| Some(*s)) + .collect::>()), + &[Some(0.25); 4], + &(NETWORK_HEADERS_LENS + .iter() + .map(|s| Some(*s)) + .collect::>()), + true, + ); + + // Draw + f.render_widget( + Table::new(mapped_network) + .header( + Row::new(NETWORK_HEADERS.to_vec()) + .style(painter.colours.table_header_style) + .bottom_margin(table_gap), + ) + .block(Block::default().borders(Borders::ALL).border_style( + if app_state.current_widget.widget_id == widget_id { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }, + )) + .style(painter.colours.text_style) + .widths( + &(intrinsic_widths + .iter() + .map(|calculated_width| Constraint::Length(*calculated_width as u16)) + .collect::>()), + ), + draw_loc, + ); +} diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/widgets/process_table.rs index 87795712..ab639a37 100644 --- a/src/canvas/widgets/process_table.rs +++ b/src/canvas/widgets/process_table.rs @@ -99,378 +99,333 @@ static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: Lazy>> = La ] }); -pub trait ProcessTableWidget { - /// Draws and handles all process-related drawing. Use this. - /// - `widget_id` here represents the widget ID of the process widget itself! - fn draw_process_features( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, - widget_id: u64, - ); +pub fn draw_process_features( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + draw_border: bool, widget_id: u64, +) { + if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) { + let search_height = if draw_border { 5 } else { 3 }; + let is_sort_open = process_widget_state.is_sort_open; + let header_len = process_widget_state.columns.longest_header_len; - /// Draws the process sort box. - /// - `widget_id` represents the widget ID of the process widget itself. - /// - /// This should not be directly called. - fn draw_processes_table( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, - widget_id: u64, - ); + let mut proc_draw_loc = draw_loc; + if process_widget_state.is_search_enabled() { + let processes_chunk = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(search_height)]) + .split(draw_loc); + proc_draw_loc = processes_chunk[0]; - /// Draws the process search field. - /// - `widget_id` represents the widget ID of the search box itself --- NOT the process widget - /// state that is stored. - /// - /// This should not be directly called. - fn draw_search_field( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, - widget_id: u64, - ); + draw_search_field( + painter, + f, + app_state, + processes_chunk[1], + draw_border, + widget_id + 1, + ); + } - /// Draws the process sort box. - /// - `widget_id` represents the widget ID of the sort box itself --- NOT the process widget - /// state that is stored. - /// - /// This should not be directly called. - fn draw_process_sort( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, - widget_id: u64, - ); + if is_sort_open { + let processes_chunk = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(header_len + 4), Constraint::Min(0)]) + .split(proc_draw_loc); + proc_draw_loc = processes_chunk[1]; + + draw_process_sort( + painter, + f, + app_state, + processes_chunk[0], + draw_border, + widget_id + 2, + ); + } + + draw_processes_table(painter, f, app_state, proc_draw_loc, draw_border, widget_id); + } } -impl ProcessTableWidget for Painter { - fn draw_process_features( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, - widget_id: u64, - ) { - if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) { - let search_height = if draw_border { 5 } else { 3 }; - let is_sort_open = process_widget_state.is_sort_open; - let header_len = process_widget_state.columns.longest_header_len; - - let mut proc_draw_loc = draw_loc; - if process_widget_state.is_search_enabled() { - let processes_chunk = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0), Constraint::Length(search_height)]) - .split(draw_loc); - proc_draw_loc = processes_chunk[0]; - - self.draw_search_field( - f, - app_state, - processes_chunk[1], - draw_border, - widget_id + 1, - ); - } - - if is_sort_open { - let processes_chunk = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Length(header_len + 4), Constraint::Min(0)]) - .split(proc_draw_loc); - proc_draw_loc = processes_chunk[1]; - - self.draw_process_sort( - f, - app_state, - processes_chunk[0], - draw_border, - widget_id + 2, - ); - } - - self.draw_processes_table(f, app_state, proc_draw_loc, draw_border, widget_id); +fn draw_processes_table( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + draw_border: bool, widget_id: u64, +) { + let should_get_widget_bounds = app_state.should_get_widget_bounds(); + if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) { + let recalculate_column_widths = + should_get_widget_bounds || proc_widget_state.requires_redraw; + if proc_widget_state.requires_redraw { + proc_widget_state.requires_redraw = false; } - } - fn draw_processes_table( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, - widget_id: u64, - ) { - let should_get_widget_bounds = app_state.should_get_widget_bounds(); - if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) { - let recalculate_column_widths = - should_get_widget_bounds || proc_widget_state.requires_redraw; - if proc_widget_state.requires_redraw { - proc_widget_state.requires_redraw = false; - } + let is_on_widget = widget_id == app_state.current_widget.widget_id; + 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]; - let is_on_widget = widget_id == app_state.current_widget.widget_id; - 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]; + let (border_style, highlight_style) = if is_on_widget { + ( + painter.colours.highlighted_border_style, + painter.colours.currently_selected_text_style, + ) + } else { + (painter.colours.border_style, painter.colours.text_style) + }; - let (border_style, highlight_style) = if is_on_widget { - ( - self.colours.highlighted_border_style, - self.colours.currently_selected_text_style, - ) - } else { - (self.colours.border_style, self.colours.text_style) - }; + let title_base = if app_state.app_config_fields.show_table_scroll_position { + if let Some(finalized_process_data) = app_state + .canvas_data + .finalized_process_data_map + .get(&widget_id) + { + let title = format!( + " Processes ({} of {}) ", + proc_widget_state + .scroll_state + .current_scroll_position + .saturating_add(1), + finalized_process_data.len() + ); - let title_base = if app_state.app_config_fields.show_table_scroll_position { - if let Some(finalized_process_data) = app_state - .canvas_data - .finalized_process_data_map - .get(&widget_id) - { - let title = format!( - " Processes ({} of {}) ", - proc_widget_state - .scroll_state - .current_scroll_position - .saturating_add(1), - finalized_process_data.len() - ); - - if title.len() <= draw_loc.width as usize { - title - } else { - " Processes ".to_string() - } + if title.len() <= draw_loc.width as usize { + title } else { " Processes ".to_string() } } else { " Processes ".to_string() - }; + } + } else { + " Processes ".to_string() + }; - let title = if app_state.is_expanded - && !proc_widget_state - .process_search_state - .search_state - .is_enabled - && !proc_widget_state.is_sort_open - { - const ESCAPE_ENDING: &str = "── Esc to go back "; + let title = if app_state.is_expanded + && !proc_widget_state + .process_search_state + .search_state + .is_enabled + && !proc_widget_state.is_sort_open + { + const ESCAPE_ENDING: &str = "── Esc to go back "; - let (chosen_title_base, expanded_title_base) = { - let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING); + let (chosen_title_base, expanded_title_base) = { + let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING); - if temp_title_base.len() > draw_loc.width as usize { - ( - " Processes ".to_string(), - format!("{}{}", " Processes ".to_string(), 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 process_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) - }; - - if let Some(process_data) = &app_state - .canvas_data - .stringified_process_data_map - .get(&widget_id) - { - let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { - 0 - } else { - app_state.app_config_fields.table_gap - }; - let position = get_start_position( - usize::from( - (draw_loc.height + (1 - table_gap)) - .saturating_sub(self.table_height_offset), - ), - &proc_widget_state.scroll_state.scroll_direction, - &mut proc_widget_state.scroll_state.previous_scroll_position, - proc_widget_state.scroll_state.current_scroll_position, - app_state.is_force_redraw, - ); - - // Sanity check - let start_position = if position >= process_data.len() { - process_data.len().saturating_sub(1) - } else { - position - }; - - let sliced_vec = &process_data[start_position..]; - let processed_sliced_vec = sliced_vec.iter().map(|(data, disabled)| { + if temp_title_base.len() > draw_loc.width as usize { ( - data.iter() - .map(|(entry, _alternative)| entry) - .collect::>(), - disabled, + " Processes ".to_string(), + format!("{}{}", " Processes ".to_string(), ESCAPE_ENDING), ) - }); - - let proc_table_state = &mut proc_widget_state.scroll_state.table_state; - proc_table_state.select(Some( - proc_widget_state - .scroll_state - .current_scroll_position - .saturating_sub(start_position), - )); - - // Draw! - let process_headers = proc_widget_state.columns.get_column_headers( - &proc_widget_state.process_sorting_type, - proc_widget_state.is_process_sort_descending, - ); - - // Calculate widths - // FIXME: See if we can move this into the recalculate block? I want to move column widths into the column widths - let hard_widths = if proc_widget_state.is_grouped { - &*PROCESS_HEADERS_HARD_WIDTH_GROUPED } else { - &*PROCESS_HEADERS_HARD_WIDTH_NO_GROUP - }; + (title_base, temp_title_base) + } + }; - if recalculate_column_widths { - let mut column_widths = process_headers - .iter() - .map(|entry| UnicodeWidthStr::width(entry.as_str()) as u16) - .collect::>(); + Spans::from(vec![ + Span::styled(chosen_title_base, painter.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, painter.colours.widget_title_style)) + }; - let soft_widths_min = column_widths - .iter() - .map(|width| Some(*width)) - .collect::>(); + let process_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(painter.colours.highlighted_border_style) + } else { + Block::default().borders(Borders::NONE) + }; - proc_widget_state.table_width_state.desired_column_widths = { - for (row, _disabled) in processed_sliced_vec.clone() { - for (col, entry) in row.iter().enumerate() { - if let Some(col_width) = column_widths.get_mut(col) { - let grapheme_len = UnicodeWidthStr::width(entry.as_str()); - if grapheme_len as u16 > *col_width { - *col_width = grapheme_len as u16; - } + if let Some(process_data) = &app_state + .canvas_data + .stringified_process_data_map + .get(&widget_id) + { + let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { + 0 + } else { + app_state.app_config_fields.table_gap + }; + let position = get_start_position( + usize::from( + (draw_loc.height + (1 - table_gap)).saturating_sub(painter.table_height_offset), + ), + &proc_widget_state.scroll_state.scroll_direction, + &mut proc_widget_state.scroll_state.previous_scroll_position, + proc_widget_state.scroll_state.current_scroll_position, + app_state.is_force_redraw, + ); + + // Sanity check + let start_position = if position >= process_data.len() { + process_data.len().saturating_sub(1) + } else { + position + }; + + let sliced_vec = &process_data[start_position..]; + let processed_sliced_vec = sliced_vec.iter().map(|(data, disabled)| { + ( + data.iter() + .map(|(entry, _alternative)| entry) + .collect::>(), + disabled, + ) + }); + + let proc_table_state = &mut proc_widget_state.scroll_state.table_state; + proc_table_state.select(Some( + proc_widget_state + .scroll_state + .current_scroll_position + .saturating_sub(start_position), + )); + + // Draw! + let process_headers = proc_widget_state.columns.get_column_headers( + &proc_widget_state.process_sorting_type, + proc_widget_state.is_process_sort_descending, + ); + + // Calculate widths + // FIXME: See if we can move this into the recalculate block? I want to move column widths into the column widths + let hard_widths = if proc_widget_state.is_grouped { + &*PROCESS_HEADERS_HARD_WIDTH_GROUPED + } else { + &*PROCESS_HEADERS_HARD_WIDTH_NO_GROUP + }; + + if recalculate_column_widths { + let mut column_widths = process_headers + .iter() + .map(|entry| UnicodeWidthStr::width(entry.as_str()) as u16) + .collect::>(); + + let soft_widths_min = column_widths + .iter() + .map(|width| Some(*width)) + .collect::>(); + + proc_widget_state.table_width_state.desired_column_widths = { + for (row, _disabled) in processed_sliced_vec.clone() { + for (col, entry) in row.iter().enumerate() { + if let Some(col_width) = column_widths.get_mut(col) { + let grapheme_len = UnicodeWidthStr::width(entry.as_str()); + if grapheme_len as u16 > *col_width { + *col_width = grapheme_len as u16; } } } - column_widths - }; + } + column_widths + }; - proc_widget_state.table_width_state.desired_column_widths = proc_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 - } + proc_widget_state.table_width_state.desired_column_widths = proc_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 } - }) - .collect::>(); - - let soft_widths_max = if proc_widget_state.is_grouped { - // Note grouped trees are not a thing. - - if proc_widget_state.is_using_command { - &*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND } else { - &*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE + *current } - } else if proc_widget_state.is_using_command { - &*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND - } else if proc_widget_state.is_tree_mode { - &*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE + }) + .collect::>(); + + let soft_widths_max = if proc_widget_state.is_grouped { + // Note grouped trees are not a thing. + + if proc_widget_state.is_using_command { + &*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND } else { - &*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE - }; + &*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE + } + } else if proc_widget_state.is_using_command { + &*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND + } else if proc_widget_state.is_tree_mode { + &*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE + } else { + &*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE + }; - proc_widget_state.table_width_state.calculated_column_widths = - get_column_widths( - draw_loc.width, - hard_widths, - &soft_widths_min, - soft_widths_max, - &(proc_widget_state - .table_width_state - .desired_column_widths - .iter() - .map(|width| Some(*width)) - .collect::>()), - true, - ); + proc_widget_state.table_width_state.calculated_column_widths = get_column_widths( + draw_loc.width, + hard_widths, + &soft_widths_min, + soft_widths_max, + &(proc_widget_state + .table_width_state + .desired_column_widths + .iter() + .map(|width| Some(*width)) + .collect::>()), + true, + ); - // debug!( - // "DCW: {:?}", - // proc_widget_state.table_width_state.desired_column_widths - // ); - // debug!( - // "CCW: {:?}", - // proc_widget_state.table_width_state.calculated_column_widths - // ); - } + // debug!( + // "DCW: {:?}", + // proc_widget_state.table_width_state.desired_column_widths + // ); + // debug!( + // "CCW: {:?}", + // proc_widget_state.table_width_state.calculated_column_widths + // ); + } - let dcw = &proc_widget_state.table_width_state.desired_column_widths; - let ccw = &proc_widget_state.table_width_state.calculated_column_widths; + let dcw = &proc_widget_state.table_width_state.desired_column_widths; + let ccw = &proc_widget_state.table_width_state.calculated_column_widths; - let process_rows = sliced_vec.iter().map(|(data, disabled)| { - let truncated_data = data.iter().zip(hard_widths).enumerate().map( - |(itx, ((entry, alternative), width))| { - if let (Some(desired_col_width), Some(calculated_col_width)) = - (dcw.get(itx), ccw.get(itx)) - { - if width.is_none() { - if *desired_col_width > *calculated_col_width - && *calculated_col_width > 0 + let process_rows = sliced_vec.iter().map(|(data, disabled)| { + let truncated_data = data.iter().zip(hard_widths).enumerate().map( + |(itx, ((entry, alternative), width))| { + if let (Some(desired_col_width), Some(calculated_col_width)) = + (dcw.get(itx), ccw.get(itx)) + { + if width.is_none() { + if *desired_col_width > *calculated_col_width + && *calculated_col_width > 0 + { + let graphemes = + UnicodeSegmentation::graphemes(entry.as_str(), true) + .collect::>(); + + if let Some(alternative) = alternative { + Text::raw(alternative) + } else if graphemes.len() > *calculated_col_width as usize + && *calculated_col_width > 1 { - let graphemes = - UnicodeSegmentation::graphemes(entry.as_str(), true) - .collect::>(); - - if let Some(alternative) = alternative { - Text::raw(alternative) - } else if graphemes.len() > *calculated_col_width as usize - && *calculated_col_width > 1 - { - // Truncate with ellipsis - let first_n = graphemes - [..(*calculated_col_width as usize - 1)] - .concat(); - Text::raw(format!("{}…", first_n)) - } else { - Text::raw(entry) - } + // Truncate with ellipsis + let first_n = graphemes + [..(*calculated_col_width as usize - 1)] + .concat(); + Text::raw(format!("{}…", first_n)) } else { Text::raw(entry) } @@ -480,432 +435,428 @@ impl ProcessTableWidget for Painter { } else { Text::raw(entry) } - }, - ); - - if *disabled { - Row::new(truncated_data).style(self.colours.disabled_text_style) - } else { - Row::new(truncated_data) - } - }); - - f.render_stateful_widget( - Table::new(process_rows) - .header( - Row::new(process_headers) - .style(self.colours.table_header_style) - .bottom_margin(table_gap), - ) - .block(process_block) - .highlight_style(highlight_style) - .style(self.colours.text_style) - .widths( - &(proc_widget_state - .table_width_state - .calculated_column_widths - .iter() - .map(|calculated_width| { - Constraint::Length(*calculated_width as u16) - }) - .collect::>()), - ), - margined_draw_loc, - proc_table_state, - ); - } else { - f.render_widget(process_block, margined_draw_loc); - } - - // Check if we need to update columnar bounds... - if recalculate_column_widths - || proc_widget_state.columns.column_header_x_locs.is_none() - || proc_widget_state.columns.column_header_y_loc.is_none() - { - // y location is just the y location of the widget + border size (1 normally, 0 in basic) - proc_widget_state.columns.column_header_y_loc = - Some(draw_loc.y + if draw_border { 1 } else { 0 }); - - // x location is determined using the x locations of the widget; just offset from the left bound - // as appropriate, and use the right bound as limiter. - - let mut current_x_left = draw_loc.x + 1; - let max_x_right = draw_loc.x + draw_loc.width - 1; - - let mut x_locs = vec![]; - - for width in proc_widget_state - .table_width_state - .calculated_column_widths - .iter() - { - let right_bound = current_x_left + width; - - if right_bound < max_x_right { - x_locs.push((current_x_left, right_bound)); - current_x_left = right_bound + 1; - } else { - x_locs.push((current_x_left, max_x_right)); - break; - } - } - - proc_widget_state.columns.column_header_x_locs = Some(x_locs); - } - - if app_state.should_get_widget_bounds() { - // Update draw loc in widget map - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { - widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); - widget.bottom_right_corner = Some(( - margined_draw_loc.x + margined_draw_loc.width, - margined_draw_loc.y + margined_draw_loc.height, - )); - } - } - } - } - - fn draw_search_field( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, - widget_id: u64, - ) { - fn build_query<'a>( - is_on_widget: bool, grapheme_indices: GraphemeIndices<'a>, start_position: usize, - cursor_position: usize, query: &str, currently_selected_text_style: tui::style::Style, - text_style: tui::style::Style, - ) -> Vec> { - let mut current_grapheme_posn = 0; - - if is_on_widget { - let mut res = grapheme_indices - .filter_map(|grapheme| { - current_grapheme_posn += UnicodeWidthStr::width(grapheme.1); - - if current_grapheme_posn <= start_position { - None } else { - let styled = if grapheme.0 == cursor_position { - Span::styled(grapheme.1, currently_selected_text_style) - } else { - Span::styled(grapheme.1, text_style) - }; - Some(styled) + Text::raw(entry) } - }) - .collect::>(); - - if cursor_position == query.len() { - res.push(Span::styled(" ", currently_selected_text_style)) - } - - res - } else { - // This is easier - we just need to get a range of graphemes, rather than - // dealing with possibly inserting a cursor (as none is shown!) - - vec![Span::styled(query.to_string(), text_style)] - } - } - - // TODO: Make the cursor scroll back if there's space! - if let Some(proc_widget_state) = - app_state.proc_state.widget_states.get_mut(&(widget_id - 1)) - { - let is_on_widget = widget_id == app_state.current_widget.widget_id; - let num_columns = usize::from(draw_loc.width); - let search_title = "> "; - - let num_chars_for_text = search_title.len(); - let cursor_position = proc_widget_state.get_search_cursor_position(); - let current_cursor_position = proc_widget_state.get_char_cursor_position(); - - let start_position: usize = get_search_start_position( - num_columns - num_chars_for_text - 5, - &proc_widget_state - .process_search_state - .search_state - .cursor_direction, - &mut proc_widget_state - .process_search_state - .search_state - .cursor_bar, - current_cursor_position, - app_state.is_force_redraw, - ); - - let query = proc_widget_state.get_current_search_query().as_str(); - let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true); - - // TODO: [CURSOR] blank cursor if not selected - // TODO: [CURSOR] blinking cursor? - let query_with_cursor = build_query( - is_on_widget, - grapheme_indices, - start_position, - cursor_position, - query, - self.colours.currently_selected_text_style, - self.colours.text_style, - ); - - let mut search_text = vec![Spans::from({ - let mut search_vec = vec![Span::styled( - search_title, - if is_on_widget { - self.colours.table_header_style - } else { - self.colours.text_style }, - )]; - search_vec.extend(query_with_cursor); + ); - search_vec - })]; - - // Text options shamelessly stolen from VS Code. - let case_style = if !proc_widget_state.process_search_state.is_ignoring_case { - self.colours.currently_selected_text_style - } else { - self.colours.text_style - }; - - let whole_word_style = if proc_widget_state - .process_search_state - .is_searching_whole_word - { - self.colours.currently_selected_text_style - } else { - self.colours.text_style - }; - - let regex_style = if proc_widget_state - .process_search_state - .is_searching_with_regex - { - self.colours.currently_selected_text_style - } else { - self.colours.text_style - }; - - // FIXME: [MOUSE] Mouse support for these in search - // FIXME: [MOVEMENT] Movement support for these in search - let option_text = Spans::from(vec![ - Span::styled( - format!("Case({})", if self.is_mac_os { "F1" } else { "Alt+C" }), - case_style, - ), - Span::raw(" "), - Span::styled( - format!("Whole({})", if self.is_mac_os { "F2" } else { "Alt+W" }), - whole_word_style, - ), - Span::raw(" "), - Span::styled( - format!("Regex({})", if self.is_mac_os { "F3" } else { "Alt+R" }), - regex_style, - ), - ]); - - search_text.push(Spans::from(Span::styled( - if let Some(err) = &proc_widget_state - .process_search_state - .search_state - .error_message - { - err.as_str() + if *disabled { + Row::new(truncated_data).style(painter.colours.disabled_text_style) } else { - "" - }, - self.colours.invalid_query_style, - ))); - search_text.push(option_text); - - let current_border_style = if proc_widget_state - .process_search_state - .search_state - .is_invalid_search - { - self.colours.invalid_query_style - } else if is_on_widget { - self.colours.highlighted_border_style - } else { - self.colours.border_style - }; - - let title = Span::styled( - if draw_border { - const TITLE_BASE: &str = " Esc to close "; - let repeat_num = - usize::from(draw_loc.width).saturating_sub(TITLE_BASE.chars().count() + 2); - format!("{} Esc to close ", "─".repeat(repeat_num)) - } else { - String::new() - }, - current_border_style, - ); - - let process_search_block = if draw_border { - Block::default() - .title(title) - .borders(Borders::ALL) - .border_style(current_border_style) - } else if is_on_widget { - Block::default() - .borders(*SIDE_BORDERS) - .border_style(current_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]; - - f.render_widget( - Paragraph::new(search_text) - .block(process_search_block) - .style(self.colours.text_style) - .alignment(Alignment::Left), - margined_draw_loc, - ); - - if app_state.should_get_widget_bounds() { - // Update draw loc in widget map - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { - widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); - widget.bottom_right_corner = Some(( - margined_draw_loc.x + margined_draw_loc.width, - margined_draw_loc.y + margined_draw_loc.height, - )); + Row::new(truncated_data) } - } - } - } - - fn draw_process_sort( - &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, - widget_id: u64, - ) { - let is_on_widget = widget_id == app_state.current_widget.widget_id; - - if let Some(proc_widget_state) = - app_state.proc_state.widget_states.get_mut(&(widget_id - 2)) - { - let current_scroll_position = proc_widget_state.columns.current_scroll_position; - let sort_string = proc_widget_state - .columns - .ordered_columns - .iter() - .filter(|column_type| { - proc_widget_state - .columns - .column_mapping - .get(column_type) - .unwrap() - .enabled - }) - .map(|column_type| column_type.to_string()) - .collect::>(); - - let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { - 0 - } else { - app_state.app_config_fields.table_gap - }; - let position = get_start_position( - usize::from( - (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset), - ), - &proc_widget_state.columns.scroll_direction, - &mut proc_widget_state.columns.previous_scroll_position, - current_scroll_position, - app_state.is_force_redraw, - ); - - // Sanity check - let start_position = if position >= sort_string.len() { - sort_string.len().saturating_sub(1) - } else { - position - }; - - let sliced_vec = &sort_string[start_position..]; - - let sort_options = sliced_vec - .iter() - .map(|column| Row::new(vec![column.as_str()])); - - let column_state = &mut proc_widget_state.columns.column_state; - column_state.select(Some( - proc_widget_state - .columns - .current_scroll_position - .saturating_sub(start_position), - )); - let current_border_style = if proc_widget_state - .process_search_state - .search_state - .is_invalid_search - { - self.colours.invalid_query_style - } else if is_on_widget { - self.colours.highlighted_border_style - } else { - self.colours.border_style - }; - - let process_sort_block = if draw_border { - Block::default() - .borders(Borders::ALL) - .border_style(current_border_style) - } else if is_on_widget { - Block::default() - .borders(*SIDE_BORDERS) - .border_style(current_border_style) - } else { - Block::default().borders(Borders::NONE) - }; - - let highlight_style = if is_on_widget { - self.colours.currently_selected_text_style - } else { - self.colours.text_style - }; - - 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]; + }); f.render_stateful_widget( - Table::new(sort_options) + Table::new(process_rows) .header( - Row::new(vec!["Sort By"]) - .style(self.colours.table_header_style) + Row::new(process_headers) + .style(painter.colours.table_header_style) .bottom_margin(table_gap), ) - .block(process_sort_block) + .block(process_block) .highlight_style(highlight_style) - .style(self.colours.text_style) - .widths(&[Constraint::Percentage(100)]), + .style(painter.colours.text_style) + .widths( + &(proc_widget_state + .table_width_state + .calculated_column_widths + .iter() + .map(|calculated_width| Constraint::Length(*calculated_width as u16)) + .collect::>()), + ), margined_draw_loc, - column_state, + proc_table_state, ); + } else { + f.render_widget(process_block, margined_draw_loc); + } - if app_state.should_get_widget_bounds() { - // Update draw loc in widget map - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { - widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); - widget.bottom_right_corner = Some(( - margined_draw_loc.x + margined_draw_loc.width, - margined_draw_loc.y + margined_draw_loc.height, - )); + // Check if we need to update columnar bounds... + if recalculate_column_widths + || proc_widget_state.columns.column_header_x_locs.is_none() + || proc_widget_state.columns.column_header_y_loc.is_none() + { + // y location is just the y location of the widget + border size (1 normally, 0 in basic) + proc_widget_state.columns.column_header_y_loc = + Some(draw_loc.y + if draw_border { 1 } else { 0 }); + + // x location is determined using the x locations of the widget; just offset from the left bound + // as appropriate, and use the right bound as limiter. + + let mut current_x_left = draw_loc.x + 1; + let max_x_right = draw_loc.x + draw_loc.width - 1; + + let mut x_locs = vec![]; + + for width in proc_widget_state + .table_width_state + .calculated_column_widths + .iter() + { + let right_bound = current_x_left + width; + + if right_bound < max_x_right { + x_locs.push((current_x_left, right_bound)); + current_x_left = right_bound + 1; + } else { + x_locs.push((current_x_left, max_x_right)); + break; } } + + proc_widget_state.columns.column_header_x_locs = Some(x_locs); + } + + if app_state.should_get_widget_bounds() { + // Update draw loc in widget map + if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); + widget.bottom_right_corner = Some(( + margined_draw_loc.x + margined_draw_loc.width, + margined_draw_loc.y + margined_draw_loc.height, + )); + } + } + } +} + +fn draw_search_field( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + draw_border: bool, widget_id: u64, +) { + fn build_query<'a>( + is_on_widget: bool, grapheme_indices: GraphemeIndices<'a>, start_position: usize, + cursor_position: usize, query: &str, currently_selected_text_style: tui::style::Style, + text_style: tui::style::Style, + ) -> Vec> { + let mut current_grapheme_posn = 0; + + if is_on_widget { + let mut res = grapheme_indices + .filter_map(|grapheme| { + current_grapheme_posn += UnicodeWidthStr::width(grapheme.1); + + if current_grapheme_posn <= start_position { + None + } else { + let styled = if grapheme.0 == cursor_position { + Span::styled(grapheme.1, currently_selected_text_style) + } else { + Span::styled(grapheme.1, text_style) + }; + Some(styled) + } + }) + .collect::>(); + + if cursor_position == query.len() { + res.push(Span::styled(" ", currently_selected_text_style)) + } + + res + } else { + // This is easier - we just need to get a range of graphemes, rather than + // dealing with possibly inserting a cursor (as none is shown!) + + vec![Span::styled(query.to_string(), text_style)] + } + } + + // TODO: Make the cursor scroll back if there's space! + if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&(widget_id - 1)) { + let is_on_widget = widget_id == app_state.current_widget.widget_id; + let num_columns = usize::from(draw_loc.width); + let search_title = "> "; + + let num_chars_for_text = search_title.len(); + let cursor_position = proc_widget_state.get_search_cursor_position(); + let current_cursor_position = proc_widget_state.get_char_cursor_position(); + + let start_position: usize = get_search_start_position( + num_columns - num_chars_for_text - 5, + &proc_widget_state + .process_search_state + .search_state + .cursor_direction, + &mut proc_widget_state + .process_search_state + .search_state + .cursor_bar, + current_cursor_position, + app_state.is_force_redraw, + ); + + let query = proc_widget_state.get_current_search_query().as_str(); + let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true); + + // TODO: [CURSOR] blank cursor if not selected + // TODO: [CURSOR] blinking cursor? + let query_with_cursor = build_query( + is_on_widget, + grapheme_indices, + start_position, + cursor_position, + query, + painter.colours.currently_selected_text_style, + painter.colours.text_style, + ); + + let mut search_text = vec![Spans::from({ + let mut search_vec = vec![Span::styled( + search_title, + if is_on_widget { + painter.colours.table_header_style + } else { + painter.colours.text_style + }, + )]; + search_vec.extend(query_with_cursor); + + search_vec + })]; + + // Text options shamelessly stolen from VS Code. + let case_style = if !proc_widget_state.process_search_state.is_ignoring_case { + painter.colours.currently_selected_text_style + } else { + painter.colours.text_style + }; + + let whole_word_style = if proc_widget_state + .process_search_state + .is_searching_whole_word + { + painter.colours.currently_selected_text_style + } else { + painter.colours.text_style + }; + + let regex_style = if proc_widget_state + .process_search_state + .is_searching_with_regex + { + painter.colours.currently_selected_text_style + } else { + painter.colours.text_style + }; + + // FIXME: [MOUSE] Mouse support for these in search + // FIXME: [MOVEMENT] Movement support for these in search + let option_text = Spans::from(vec![ + Span::styled( + format!("Case({})", if painter.is_mac_os { "F1" } else { "Alt+C" }), + case_style, + ), + Span::raw(" "), + Span::styled( + format!("Whole({})", if painter.is_mac_os { "F2" } else { "Alt+W" }), + whole_word_style, + ), + Span::raw(" "), + Span::styled( + format!("Regex({})", if painter.is_mac_os { "F3" } else { "Alt+R" }), + regex_style, + ), + ]); + + search_text.push(Spans::from(Span::styled( + if let Some(err) = &proc_widget_state + .process_search_state + .search_state + .error_message + { + err.as_str() + } else { + "" + }, + painter.colours.invalid_query_style, + ))); + search_text.push(option_text); + + let current_border_style = if proc_widget_state + .process_search_state + .search_state + .is_invalid_search + { + painter.colours.invalid_query_style + } else if is_on_widget { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }; + + let title = Span::styled( + if draw_border { + const TITLE_BASE: &str = " Esc to close "; + let repeat_num = + usize::from(draw_loc.width).saturating_sub(TITLE_BASE.chars().count() + 2); + format!("{} Esc to close ", "─".repeat(repeat_num)) + } else { + String::new() + }, + current_border_style, + ); + + let process_search_block = if draw_border { + Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(current_border_style) + } else if is_on_widget { + Block::default() + .borders(*SIDE_BORDERS) + .border_style(current_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]; + + f.render_widget( + Paragraph::new(search_text) + .block(process_search_block) + .style(painter.colours.text_style) + .alignment(Alignment::Left), + margined_draw_loc, + ); + + if app_state.should_get_widget_bounds() { + // Update draw loc in widget map + if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); + widget.bottom_right_corner = Some(( + margined_draw_loc.x + margined_draw_loc.width, + margined_draw_loc.y + margined_draw_loc.height, + )); + } + } + } +} + +fn draw_process_sort( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, + draw_border: bool, widget_id: u64, +) { + let is_on_widget = widget_id == app_state.current_widget.widget_id; + + if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&(widget_id - 2)) { + let current_scroll_position = proc_widget_state.columns.current_scroll_position; + let sort_string = proc_widget_state + .columns + .ordered_columns + .iter() + .filter(|column_type| { + proc_widget_state + .columns + .column_mapping + .get(column_type) + .unwrap() + .enabled + }) + .map(|column_type| column_type.to_string()) + .collect::>(); + + let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { + 0 + } else { + app_state.app_config_fields.table_gap + }; + let position = get_start_position( + usize::from( + (draw_loc.height + (1 - table_gap)).saturating_sub(painter.table_height_offset), + ), + &proc_widget_state.columns.scroll_direction, + &mut proc_widget_state.columns.previous_scroll_position, + current_scroll_position, + app_state.is_force_redraw, + ); + + // Sanity check + let start_position = if position >= sort_string.len() { + sort_string.len().saturating_sub(1) + } else { + position + }; + + let sliced_vec = &sort_string[start_position..]; + + let sort_options = sliced_vec + .iter() + .map(|column| Row::new(vec![column.as_str()])); + + let column_state = &mut proc_widget_state.columns.column_state; + column_state.select(Some( + proc_widget_state + .columns + .current_scroll_position + .saturating_sub(start_position), + )); + let current_border_style = if proc_widget_state + .process_search_state + .search_state + .is_invalid_search + { + painter.colours.invalid_query_style + } else if is_on_widget { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }; + + let process_sort_block = if draw_border { + Block::default() + .borders(Borders::ALL) + .border_style(current_border_style) + } else if is_on_widget { + Block::default() + .borders(*SIDE_BORDERS) + .border_style(current_border_style) + } else { + Block::default().borders(Borders::NONE) + }; + + let highlight_style = if is_on_widget { + painter.colours.currently_selected_text_style + } else { + painter.colours.text_style + }; + + 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]; + + f.render_stateful_widget( + Table::new(sort_options) + .header( + Row::new(vec!["Sort By"]) + .style(painter.colours.table_header_style) + .bottom_margin(table_gap), + ) + .block(process_sort_block) + .highlight_style(highlight_style) + .style(painter.colours.text_style) + .widths(&[Constraint::Percentage(100)]), + margined_draw_loc, + column_state, + ); + + if app_state.should_get_widget_bounds() { + // Update draw loc in widget map + if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); + widget.bottom_right_corner = Some(( + margined_draw_loc.x + margined_draw_loc.width, + margined_draw_loc.y + margined_draw_loc.height, + )); + } } } } diff --git a/src/canvas/widgets/temp_table.rs b/src/canvas/widgets/temp_table.rs index eaa9ea2a..17d05842 100644 --- a/src/canvas/widgets/temp_table.rs +++ b/src/canvas/widgets/temp_table.rs @@ -27,105 +27,97 @@ static TEMP_HEADERS_LENS: Lazy> = Lazy::new(|| { .collect::>() }); -pub trait TempTableWidget { - fn draw_temp_table( - &self, f: &mut Frame<'_, B>, app_state: &mut app::AppState, draw_loc: Rect, - draw_border: bool, widget_id: u64, - ); -} +pub fn draw_temp_table( + painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut app::AppState, draw_loc: Rect, + draw_border: bool, widget_id: u64, +) { + 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(painter.table_height_offset), + ), + &temp_widget_state.scroll_state.scroll_direction, + &mut temp_widget_state.scroll_state.previous_scroll_position, + 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..]; -impl TempTableWidget for Painter { - fn draw_temp_table( - &self, f: &mut Frame<'_, B>, app_state: &mut app::AppState, draw_loc: Rect, - draw_border: bool, widget_id: u64, - ) { - 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.previous_scroll_position, - 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..]; - - // 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; - } + // 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, - ); - } + 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)) + 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 { - if *desired_col_width > *calculated_col_width - && *calculated_col_width > 0 - { - let graphemes = - UnicodeSegmentation::graphemes(entry.as_str(), true) - .collect::>(); + let graphemes = + UnicodeSegmentation::graphemes(entry.as_str(), true) + .collect::>(); - if graphemes.len() > *calculated_col_width as usize - && *calculated_col_width > 1 - { - // Truncate with ellipsis - let first_n = graphemes - [..(*calculated_col_width as usize - 1)] - .concat(); - Text::raw(format!("{}…", first_n)) - } else { - Text::raw(entry) - } + if graphemes.len() > *calculated_col_width as usize + && *calculated_col_width > 1 + { + // Truncate with ellipsis + let first_n = graphemes + [..(*calculated_col_width as usize - 1)] + .concat(); + Text::raw(format!("{}…", first_n)) } else { Text::raw(entry) } @@ -135,131 +127,129 @@ impl TempTableWidget for Painter { } else { Text::raw(entry) } - }, - ); + } else { + Text::raw(entry) + } + }); - Row::new(truncated_data) - }); + Row::new(truncated_data) + }); - let (border_style, highlight_style) = if is_on_widget { - ( - self.colours.highlighted_border_style, - self.colours.currently_selected_text_style, - ) - } else { - (self.colours.border_style, self.colours.text_style) - }; + let (border_style, highlight_style) = if is_on_widget { + ( + painter.colours.highlighted_border_style, + painter.colours.currently_selected_text_style, + ) + } else { + (painter.colours.border_style, painter.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 as usize { - 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 as usize { - ( - " Temperatures ".to_string(), - format!("{}{}", " Temperatures ".to_string(), 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 as u16)) - .collect::>()), - ), - margined_draw_loc, - temp_table_state, + 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 app_state.should_get_widget_bounds() { - // Update draw loc in widget map - // Note there is no difference between this and using draw_loc, but I'm too lazy to fix it. - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { - widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); - widget.bottom_right_corner = Some(( - margined_draw_loc.x + margined_draw_loc.width, - margined_draw_loc.y + margined_draw_loc.height, - )); + if title_string.len() <= draw_loc.width as usize { + 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 as usize { + ( + " Temperatures ".to_string(), + format!("{}{}", " Temperatures ".to_string(), ESCAPE_ENDING), + ) + } else { + (title_base, temp_title_base) } + }; + + Spans::from(vec![ + Span::styled(chosen_title_base, painter.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, painter.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(painter.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(painter.colours.table_header_style) + .bottom_margin(table_gap), + ) + .block(temp_block) + .highlight_style(highlight_style) + .style(painter.colours.text_style) + .widths( + &(temp_widget_state + .table_width_state + .calculated_column_widths + .iter() + .map(|calculated_width| Constraint::Length(*calculated_width as u16)) + .collect::>()), + ), + margined_draw_loc, + temp_table_state, + ); + + if app_state.should_get_widget_bounds() { + // Update draw loc in widget map + // Note there is no difference between this and using draw_loc, but I'm too lazy to fix it. + if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); + widget.bottom_right_corner = Some(( + margined_draw_loc.x + margined_draw_loc.width, + margined_draw_loc.y + margined_draw_loc.height, + )); } } } diff --git a/src/lib.rs b/src/lib.rs index c5c792b5..f11a0f63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,6 +64,7 @@ pub enum BottomEvent { KeyInput(KeyEvent), MouseInput(MouseEvent), Update(Box), + Resize { width: u16, height: u16 }, Clean, } @@ -635,7 +636,11 @@ pub fn create_input_thread( } } }, - Event::Resize(_, _) => {} + Event::Resize(width, height) => { + if sender.send(BottomEvent::Resize { width, height }).is_err() { + break; + } + } } } }