diff options
author | ClementTsang <cjhtsang@uwaterloo.ca> | 2021-08-28 04:15:36 -0400 |
---|---|---|
committer | ClementTsang <cjhtsang@uwaterloo.ca> | 2021-08-28 20:09:00 -0400 |
commit | 2bff04d8a4080be1af48e10d13d9d0b9dc9e1e1e (patch) | |
tree | 174ab7c00f9fa9010d07406bfbb214baaea40a1a /src/app | |
parent | b72e76aa71ef6fc4790ffd5b47ea1fb7c07bd464 (diff) |
refactor: port over graph widgets
Things working as of now:
- Actually drawing
- Interpolation
- Styling
Diffstat (limited to 'src/app')
-rw-r--r-- | src/app/layout_manager.rs | 40 | ||||
-rw-r--r-- | src/app/widgets.rs | 16 | ||||
-rw-r--r-- | src/app/widgets/base/scrollable.rs | 6 | ||||
-rw-r--r-- | src/app/widgets/base/sort_text_table.rs | 26 | ||||
-rw-r--r-- | src/app/widgets/base/text_table.rs | 154 | ||||
-rw-r--r-- | src/app/widgets/base/time_graph.rs | 141 | ||||
-rw-r--r-- | src/app/widgets/battery.rs | 10 | ||||
-rw-r--r-- | src/app/widgets/cpu.rs | 203 | ||||
-rw-r--r-- | src/app/widgets/custom_tui/custom_legend_chart.rs | 335 | ||||
-rw-r--r-- | src/app/widgets/disk.rs | 35 | ||||
-rw-r--r-- | src/app/widgets/mem.rs | 79 | ||||
-rw-r--r-- | src/app/widgets/net.rs | 542 | ||||
-rw-r--r-- | src/app/widgets/process.rs | 63 | ||||
-rw-r--r-- | src/app/widgets/temp.rs | 47 | ||||
-rw-r--r-- | src/app/widgets/tui_widgets.rs | 2 | ||||
-rw-r--r-- | src/app/widgets/tui_widgets/custom_legend_chart.rs | 596 |
16 files changed, 2105 insertions, 190 deletions
diff --git a/src/app/layout_manager.rs b/src/app/layout_manager.rs index 36a4e5d6..d2927a35 100644 --- a/src/app/layout_manager.rs +++ b/src/app/layout_manager.rs @@ -1,8 +1,5 @@ use crate::{ - app::{ - sort_text_table::SortableColumn, DiskTable, MemGraph, NetGraph, OldNetGraph, - ProcessManager, TempTable, - }, + app::{DiskTable, MemGraph, NetGraph, OldNetGraph, ProcessManager, TempTable}, error::{BottomError, Result}, options::layout_options::{Row, RowChildren}, }; @@ -16,7 +13,7 @@ use crate::app::widgets::Widget; use crate::constants::DEFAULT_WIDGET_ID; use super::{ - event::SelectionAction, CpuGraph, SortableTextTable, TimeGraph, TmpBottomWidget, UsedWidgets, + event::SelectionAction, AppConfigFields, CpuGraph, TimeGraph, TmpBottomWidget, UsedWidgets, }; /// Represents a more usable representation of the layout, derived from the @@ -1051,44 +1048,43 @@ pub struct LayoutCreationOutput { // FIXME: This is currently jury-rigged "glue" just to work with the existing config system! We are NOT keeping it like this, it's too awful to keep like this! pub fn create_layout_tree( rows: &[Row], process_defaults: crate::options::ProcessDefaults, - app_config_fields: &super::AppConfigFields, + app_config_fields: &AppConfigFields, ) -> Result<LayoutCreationOutput> { fn add_widget_to_map( widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, widget_type: BottomWidgetType, widget_id: NodeId, process_defaults: &crate::options::ProcessDefaults, - app_config_fields: &super::AppConfigFields, + app_config_fields: &AppConfigFields, ) -> Result<()> { match widget_type { BottomWidgetType::Cpu => { - let graph = TimeGraph::from_config(app_config_fields); - let legend = SortableTextTable::new(vec![ - SortableColumn::new_flex("CPU".into(), None, false, 0.5), - SortableColumn::new_flex("Use%".into(), None, false, 0.5), - ]); - let legend_position = super::CpuGraphLegendPosition::Right; - - widget_lookup_map.insert( - widget_id, - CpuGraph::new(graph, legend, legend_position).into(), - ); + widget_lookup_map + .insert(widget_id, CpuGraph::from_config(app_config_fields).into()); } BottomWidgetType::Mem => { let graph = TimeGraph::from_config(app_config_fields); widget_lookup_map.insert(widget_id, MemGraph::new(graph).into()); } BottomWidgetType::Net => { - let graph = TimeGraph::from_config(app_config_fields); if app_config_fields.use_old_network_legend { - widget_lookup_map.insert(widget_id, OldNetGraph::new(graph).into()); + widget_lookup_map.insert( + widget_id, + OldNetGraph::from_config(app_config_fields).into(), + ); } else { - widget_lookup_map.insert(widget_id, NetGraph::new(graph).into()); + widget_lookup_map + .insert(widget_id, NetGraph::from_config(app_config_fields).into()); } } BottomWidgetType::Proc => { widget_lookup_map.insert(widget_id, ProcessManager::new(process_defaults).into()); } BottomWidgetType::Temp => { - widget_lookup_map.insert(widget_id, TempTable::default().into()); + widget_lookup_map.insert( + widget_id, + TempTable::default() + .set_temp_type(app_config_fields.temperature_type.clone()) + .into(), + ); } BottomWidgetType::Disk => { widget_lookup_map.insert(widget_id, DiskTable::default().into()); diff --git a/src/app/widgets.rs b/src/app/widgets.rs index ad24eb00..34009939 100644 --- a/src/app/widgets.rs +++ b/src/app/widgets.rs @@ -9,10 +9,12 @@ use crate::{ event::{EventResult, SelectionAction}, layout_manager::BottomWidgetType, }, - canvas::{DisplayableData, Painter}, + canvas::Painter, constants, }; +mod tui_widgets; + pub mod base; pub use base::*; @@ -37,6 +39,8 @@ pub use self::battery::*; pub mod temp; pub use temp::*; +use super::data_farmer::DataCollection; + /// A trait for things that are drawn with state. #[enum_dispatch] #[allow(unused_variables)] @@ -75,9 +79,6 @@ pub trait Component { #[enum_dispatch] #[allow(unused_variables)] pub trait Widget { - /// Updates a [`Widget`] given some data. Defaults to doing nothing. - fn update(&mut self) {} - /// Handles what to do when trying to respond to a widget selection movement to the left. /// Defaults to just moving to the next-possible widget in that direction. fn handle_widget_selection_left(&mut self) -> SelectionAction { @@ -107,12 +108,13 @@ pub trait Widget { /// Draws a [`Widget`]. Defaults to doing nothing. fn draw<B: Backend>( - &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, data: &DisplayableData, - selected: bool, + &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool, ) { // TODO: Remove the default implementation in the future! - // TODO: Do another pass on ALL of the draw code - currently it's just glue, it should eventually be done properly! } + + /// How a [`Widget`] updates its internal displayed data. Defaults to doing nothing. + fn update_data(&mut self, data_collection: &DataCollection) {} } /// The "main" widgets that are used by bottom to display information! diff --git a/src/app/widgets/base/scrollable.rs b/src/app/widgets/base/scrollable.rs index 4b5f395f..963839e0 100644 --- a/src/app/widgets/base/scrollable.rs +++ b/src/app/widgets/base/scrollable.rs @@ -61,7 +61,7 @@ impl Scrollable { } /// Returns the currently selected index of the [`Scrollable`]. - pub fn index(&self) -> usize { + pub fn current_index(&self) -> usize { self.current_index } @@ -195,8 +195,8 @@ impl Scrollable { self.num_items } - pub fn tui_state(&self) -> TableState { - self.tui_state.clone() + pub fn tui_state(&mut self) -> &mut TableState { + &mut self.tui_state } } diff --git a/src/app/widgets/base/sort_text_table.rs b/src/app/widgets/base/sort_text_table.rs index 0c282d49..0f99cd4c 100644 --- a/src/app/widgets/base/sort_text_table.rs +++ b/src/app/widgets/base/sort_text_table.rs @@ -2,13 +2,17 @@ use std::borrow::Cow; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; use tui::{ + backend::Backend, layout::Rect, - widgets::{Table, TableState}, + widgets::{Block, Table, TableState}, }; -use crate::app::{event::EventResult, Component, TextTable}; +use crate::{ + app::{event::EventResult, Component, TextTable}, + canvas::Painter, +}; -use super::text_table::{DesiredColumnWidth, SimpleColumn, TableColumn}; +use super::text_table::{DesiredColumnWidth, SimpleColumn, TableColumn, TextTableData}; fn get_shortcut_name(e: &KeyEvent) -> String { let modifier = if e.modifiers.is_empty() { @@ -182,6 +186,10 @@ impl SortableTextTable { self } + pub fn current_index(&self) -> usize { + self.table.current_index() + } + fn set_sort_index(&mut self, new_index: usize) { if new_index == self.sort_index { if let Some(column) = self.table.columns.get_mut(self.sort_index) { @@ -218,6 +226,18 @@ impl SortableTextTable { } } + /// Draws a [`Table`] given the [`TextTable`] and the given data. + /// + /// Note if the number of columns don't match in the [`TextTable`] and data, + /// it will only create as many columns as it can grab data from both sources from. + pub fn draw_tui_table<B: Backend>( + &mut self, painter: &Painter, f: &mut tui::Frame<'_, B>, data: &TextTableData, + block: Block<'_>, block_area: Rect, show_selected_entry: bool, + ) { + self.table + .draw_tui_table(painter, f, data, block, block_area, show_selected_entry); + } + /// Creates a [`Table`] representing the sort list. pub fn create_sort_list(&mut self) -> (Table<'_>, TableState) { todo!() diff --git a/src/app/widgets/base/text_table.rs b/src/app/widgets/base/text_table.rs index 0edda286..3ce3092d 100644 --- a/src/app/widgets/base/text_table.rs +++ b/src/app/widgets/base/text_table.rs @@ -5,9 +5,12 @@ use std::{ use crossterm::event::{KeyEvent, MouseEvent}; use tui::{ + backend::Backend, layout::{Constraint, Rect}, + style::Style, text::Text, - widgets::{Table, TableState}, + widgets::{Block, Table}, + Frame, }; use unicode_segmentation::UnicodeSegmentation; @@ -36,6 +39,8 @@ pub trait TableColumn { fn set_x_bounds(&mut self, x_bounds: Option<(u16, u16)>); } +pub type TextTableData = Vec<Vec<(Cow<'static, str>, Option<Cow<'static, str>>, Option<Style>)>>; + /// A [`SimpleColumn`] represents some column in a [`TextTable`]. #[derive(Debug)] pub struct SimpleColumn { @@ -128,6 +133,9 @@ where /// Whether we draw columns from left-to-right. pub left_to_right: bool, + + /// Whether to enable selection. + pub selectable: bool, } impl<C> TextTable<C> @@ -142,6 +150,7 @@ where show_gap: true, bounds: Rect::default(), left_to_right: true, + selectable: true, } } @@ -155,6 +164,11 @@ where self } + pub fn unselectable(mut self) -> Self { + self.selectable = false; + self + } + pub fn displayed_column_names(&self) -> Vec<Cow<'static, str>> { self.columns .iter() @@ -172,8 +186,12 @@ where } } + pub fn current_index(&self) -> usize { + self.scrollable.current_index() + } + pub fn get_desired_column_widths( - columns: &[C], data: &[Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>], + columns: &[C], data: &TextTableData, ) -> Vec<DesiredColumnWidth> { columns .iter() @@ -183,22 +201,22 @@ where let max_len = data .iter() .filter_map(|c| c.get(column_index)) - .max_by(|(x, short_x), (y, short_y)| { - let x = if let Some(short_x) = short_x { - short_x + .max_by(|(a, short_a, _a_style), (b, short_b, _b_style)| { + let a_len = if let Some(short_a) = short_a { + short_a.len() } else { - x + a.len() }; - let y = if let Some(short_y) = short_y { - short_y + let b_len = if let Some(short_b) = short_b { + short_b.len() } else { - y + b.len() }; - x.len().cmp(&y.len()) + a_len.cmp(&b_len) }) - .map(|(s, _)| s.len()) + .map(|(longest_data_str, _, _)| longest_data_str.len()) .unwrap_or(0) as u16; DesiredColumnWidth::Hard(max(max_len, *width)) @@ -211,9 +229,7 @@ where .collect::<Vec<_>>() } - fn get_cache( - &mut self, area: Rect, data: &[Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>], - ) -> Vec<u16> { + fn get_cache(&mut self, area: Rect, data: &TextTableData) -> Vec<u16> { fn calculate_column_widths( left_to_right: bool, mut desired_widths: Vec<DesiredColumnWidth>, total_width: u16, ) -> Vec<u16> { @@ -326,37 +342,37 @@ where } } - /// Creates a [`Table`] given the [`TextTable`] and the given data, along with its - /// widths (because for some reason a [`Table`] only borrows the constraints...?) - /// and [`TableState`] (so we know which row is selected). + /// Draws a [`Table`] given the [`TextTable`] and the given data. /// /// Note if the number of columns don't match in the [`TextTable`] and data, /// it will only create as many columns as it can grab data from both sources from. - pub fn create_draw_table( - &mut self, painter: &Painter, data: &[Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>], - area: Rect, - ) -> (Table<'_>, Vec<Constraint>, TableState) { + pub fn draw_tui_table<B: Backend>( + &mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableData, block: Block<'_>, + block_area: Rect, show_selected_entry: bool, + ) { use tui::widgets::Row; - let table_gap = if !self.show_gap || area.height < TABLE_GAP_HEIGHT_LIMIT { + let inner_area = block.inner(block_area); + + let table_gap = if !self.show_gap || inner_area.height < TABLE_GAP_HEIGHT_LIMIT { 0 } else { 1 }; self.update_num_items(data.len()); - self.set_bounds(area); + self.set_bounds(inner_area); let table_extras = 1 + table_gap; - let scrollable_height = area.height.saturating_sub(table_extras); + let scrollable_height = inner_area.height.saturating_sub(table_extras); self.scrollable.set_bounds(Rect::new( - area.x, - area.y + table_extras, - area.width, + inner_area.x, + inner_area.y + table_extras, + inner_area.width, scrollable_height, )); // Calculate widths first, since we need them later. - let calculated_widths = self.get_cache(area, data); + let calculated_widths = self.get_cache(inner_area, data); let widths = calculated_widths .iter() .map(|column| Constraint::Length(*column)) @@ -373,25 +389,28 @@ where &data[start..end] }; let rows = data_slice.iter().map(|row| { - Row::new( - row.iter() - .zip(&calculated_widths) - .map(|((text, shrunk_text), width)| { - let width = *width as usize; - let graphemes = UnicodeSegmentation::graphemes(text.as_ref(), true) - .collect::<Vec<&str>>(); - let grapheme_width = graphemes.len(); - if width < grapheme_width && width > 1 { - if let Some(shrunk_text) = shrunk_text { - Text::raw(shrunk_text.clone()) - } else { - Text::raw(format!("{}…", graphemes[..(width - 1)].concat())) - } + Row::new(row.iter().zip(&calculated_widths).map( + |((text, shrunk_text, opt_style), width)| { + let text_style = opt_style.unwrap_or(painter.colours.text_style); + + let width = *width as usize; + let graphemes = + UnicodeSegmentation::graphemes(text.as_ref(), true).collect::<Vec<&str>>(); + let grapheme_width = graphemes.len(); + if width < grapheme_width && width > 1 { + if let Some(shrunk_text) = shrunk_text { + Text::styled(shrunk_text.clone(), text_style) } else { - Text::raw(text.to_owned()) + Text::styled( + format!("{}…", graphemes[..(width - 1)].concat()), + text_style, + ) } - }), - ) + } else { + Text::styled(text.to_owned(), text_style) + } + }, + )) }); // Now build up our headers... @@ -399,21 +418,24 @@ where .style(painter.colours.table_header_style) .bottom_margin(table_gap); - // And return tui-rs's [`TableState`]. - let tui_state = self.scrollable.tui_state(); - - ( - Table::new(rows) - .header(header) - .style(painter.colours.text_style), - widths, - tui_state, - ) - } - - /// Creates a [`Table`] representing the sort list. - pub fn create_sort_list(&mut self) -> (Table<'_>, TableState) { - todo!() + let table = Table::new(rows) + .header(header) + .style(painter.colours.text_style) + .highlight_style(if show_selected_entry { + painter.colours.currently_selected_text_style + } else { + painter.colours.text_style + }); + + if self.selectable { + f.render_stateful_widget( + table.block(block).widths(&widths), + block_area, + self.scrollable.tui_state(), + ); + } else { + f.render_widget(table.block(block).widths(&widths), block_area); + } } } @@ -422,11 +444,19 @@ where C: TableColumn, { fn handle_key_event(&mut self, event: KeyEvent) -> EventResult { - self.scrollable.handle_key_event(event) + if self.selectable { + self.scrollable.handle_key_event(event) + } else { + EventResult::NoRedraw + } } fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { - self.scrollable.handle_mouse_event(event) + if self.selectable { + self.scrollable.handle_mouse_event(event) + } else { + EventResult::NoRedraw + } } fn bounds(&self) -> Rect { diff --git a/src/app/widgets/base/time_graph.rs b/src/app/widgets/base/time_graph.rs index a0054c38..7a3ddd16 100644 --- a/src/app/widgets/base/time_graph.rs +++ b/src/app/widgets/base/time_graph.rs @@ -1,10 +1,28 @@ -use std::time::{Duration, Instant}; +use std::{ + borrow::Cow, + time::{Duration, Instant}, +}; -use crossterm::event::{KeyEvent, KeyModifiers, MouseEvent}; -use tui::layout::Rect; +use crossterm::event::{KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + style::Style, + symbols::Marker, + text::Span, + widgets::{Block, GraphType}, +}; use crate::{ - app::{event::EventResult, AppConfigFields, Component}, + app::{ + event::EventResult, + widgets::tui_widgets::{ + custom_legend_chart::{Axis, Dataset}, + TimeChart, + }, + AppConfigFields, Component, + }, + canvas::Painter, constants::{AUTOHIDE_TIMEOUT_MILLISECONDS, STALE_MAX_MILLISECONDS, STALE_MIN_MILLISECONDS}, }; @@ -24,6 +42,7 @@ pub enum AutohideTimer { }, } +// TODO: [AUTOHIDE] Not a fan of how this is done, as this should really "trigger" a draw when it's done. impl AutohideTimer { fn start_display_timer(&mut self) { match self { @@ -57,6 +76,27 @@ impl AutohideTimer { }, } } + + pub fn is_showing(&mut self) -> bool { + self.update_display_timer(); + match self { + AutohideTimer::AlwaysShow => true, + AutohideTimer::AlwaysHide => false, + AutohideTimer::Enabled { + state, + show_duration: _, + } => match state { + AutohideTimerState::Hidden => false, + AutohideTimerState::Running(_) => true, + }, + } + } +} + +pub struct TimeGraphData<'d> { + pub data: &'d [(f64, f64)], + pub label: Option<Cow<'static, str>>, + pub style: Style, } /// A graph widget with controllable time ranges along the x-axis. @@ -71,13 +111,15 @@ pub struct TimeGraph { time_interval: u64, bounds: Rect, + + use_dot: bool, } impl TimeGraph { /// Creates a new [`TimeGraph`]. All time values are in milliseconds. pub fn new( start_value: u64, autohide_timer: AutohideTimer, min_duration: u64, max_duration: u64, - time_interval: u64, + time_interval: u64, use_dot: bool, ) -> Self { Self { current_display_time: start_value, @@ -87,6 +129,7 @@ impl TimeGraph { max_duration, time_interval, bounds: Rect::default(), + use_dot, } } @@ -107,6 +150,7 @@ impl TimeGraph { STALE_MIN_MILLISECONDS, STALE_MAX_MILLISECONDS, app_config_fields.time_interval, + app_config_fields.use_dot, ) } @@ -165,6 +209,89 @@ impl TimeGraph { EventResult::Redraw } } + + fn get_x_axis_labels(&self, painter: &Painter) -> Vec<Span<'_>> { + vec![ + Span::styled( + format!("{}s", self.current_display_time / 1000), + painter.colours.graph_style, + ), + Span::styled("0s", painter.colours.graph_style), + ] + } + + pub fn get_current_display_time(&self) -> u64 { + self.current_display_time + } + + /// Creates a [`Chart`]. + /// + /// The `reverse_order` parameter is mostly used for cases where you want the first entry to be drawn on + /// top - note that this will also reverse the naturally generated legend, if shown! + pub fn draw_tui_chart<B: Backend>( + &mut self, painter: &Painter, f: &mut tui::Frame<'_, B>, data: &'_ [TimeGraphData<'_>], + y_bound_labels: &[Cow<'static, str>], y_bounds: [f64; 2], reverse_order: bool, + block: Block<'_>, block_area: Rect, + ) { + let inner_area = block.inner(block_area); + + self.set_bounds(inner_area); + + let time_start = -(self.current_display_time as f64); + let x_axis = { + let x_axis = Axis::default() + .bounds([time_start, 0.0]) + .style(painter.colours.graph_style); + if self.autohide_timer.is_showing() { + x_axis.labels(self.get_x_axis_labels(painter)) + } else { + x_axis + } + }; + let y_axis = Axis::default() + .bounds(y_bounds) + .style(painter.colours.graph_style) + .labels( + y_bound_labels + .into_iter() + .map(|label| Span::styled(label.clone(), painter.colours.graph_style)) + .collect(), + ); + + let mut datasets: Vec<Dataset<'_>> = data + .iter() + .map(|time_graph_data| { + let mut dataset = Dataset::default() + .data(time_graph_data.data) + .style(time_graph_data.style) + .marker(if self.use_dot { + Marker::Dot + } else { + Marker::Braille + }) + .graph_type(GraphType::Line); + + if let Some(label) = &time_graph_data.label { + dataset = dataset.name(label.clone()); + } + + dataset + }) + .collect(); + + if reverse_order { + datasets.reverse(); + } + + let chart = TimeChart::new(datasets) + .x_axis(x_axis) + .y_axis(y_axis) + .style(painter.colours.graph_style) + .legend_style(painter.colours.graph_style) + .hidden_legend_constraints((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))); + + f.render_widget(chart.block(block), block_area); + } } impl Component for TimeGraph { @@ -183,8 +310,8 @@ impl Component for TimeGraph { fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { match event.kind { - crossterm::event::MouseEventKind::ScrollDown => self.zoom_out(), - crossterm::event::MouseEventKind::ScrollUp => self.zoom_in(), + MouseEventKind::ScrollDown => self.zoom_out(), + MouseEventKind::ScrollUp => self.zoom_in(), _ => EventResult::NoRedraw, } } diff --git a/src/app/widgets/battery.rs b/src/app/widgets/battery.rs index 5ef6742a..e12e2fe6 100644 --- a/src/app/widgets/battery.rs +++ b/src/app/widgets/battery.rs @@ -2,6 +2,11 @@ use std::collections::HashMap; use tui::layout::Rect; +use crate::{ + app::data_farmer::DataCollection, + data_conversion::{convert_battery_harvest, ConvertedBatteryData}, +}; + use super::{Component, Widget}; #[derive(Default)] @@ -36,6 +41,7 @@ pub struct BatteryTable { bounds: Rect, selected_index: usize, batteries: Vec<String>, + battery_data: Vec<ConvertedBatteryData>, } impl BatteryTable { @@ -70,4 +76,8 @@ impl Widget for BatteryTable { fn get_pretty_name(&self) -> &'static str { "Battery" } + + fn update_data(&mut self, data_collection: &DataCollection) { + self.battery_data = convert_battery_harvest(data_collection); + } } diff --git a/src/app/widgets/cpu.rs b/src/app/widgets/cpu.rs index 15fe9efe..40c2fa89 100644 --- a/src/app/widgets/cpu.rs +++ b/src/app/widgets/cpu.rs @@ -1,9 +1,20 @@ -use std::{collections::HashMap, time::Instant}; +use std::{borrow::Cow, collections::HashMap, time::Instant}; use crossterm::event::{KeyEvent, MouseEvent}; -use tui::layout::Rect; +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout, Rect}, + widgets::{Block, Borders}, +}; -use crate::app::event::EventResult; +use crate::{ + app::{ + event::EventResult, sort_text_table::SortableColumn, time_graph::TimeGraphData, + AppConfigFields, DataCollection, + }, + canvas::Painter, + data_conversion::{convert_cpu_data_points, ConvertedCpuData}, +}; use super::{ AppScrollWidgetState, CanvasTableWidthState, Component, SortableTextTable, TimeGraph, Widget, @@ -70,23 +81,40 @@ pub enum CpuGraphLegendPosition { pub struct CpuGraph { graph: TimeGraph, legend: SortableTextTable, - pub legend_position: CpuGraphLegendPosition, + legend_position: CpuGraphLegendPosition, + showing_avg: bool, bounds: Rect, selected: CpuGraphSelection, + + display_data: Vec<ConvertedCpuData>, + load_avg_data: [f32; 3], } impl CpuGraph { - /// Creates a new [`CpuGraph`]. - pub fn new( - graph: TimeGraph, legend: SortableTextTable, legend_position: CpuGraphLegendPosition, - ) -> Self { + /// Creates a new [`CpuGraph`] from a config. + pub fn from_config(app_config_fields: &AppConfigFields) -> Self { + let graph = TimeGraph::from_config(app_config_fields); + let legend = SortableTextTable::new(vec![ + SortableColumn::new_flex("CPU".into(), None, false, 0.5), + SortableColumn::new_flex("Use%".into(), None, false, 0.5), + ]); + let legend_position = if app_config_fields.left_legend { + CpuGraphLegendPosition::Left + } else { + CpuGraphLegendPosition::Right + }; + let showing_avg = app_config_fields.show_average_cpu; + Self { graph, legend, legend_position, + showing_avg, bounds: Rect::default(), selected: CpuGraphSelection::None, + display_data: Default::default(), + load_avg_data: [0.0; 3], } } } @@ -102,11 +130,21 @@ impl Component for CpuGraph { fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { if self.graph.does_intersect_mouse(&event) { - self.selected = CpuGraphSelection::Graph; - self.graph.handle_mouse_event(event) + if let CpuGraphSelection::Graph = self.selected { + self.graph.handle_mouse_event(event) + } else { + self.selected = CpuGraphSelection::Graph; + self.graph.handle_mouse_event(event); + EventResult::Redraw + } } else if self.legend.does_intersect_mouse(&event) { - self.selected = CpuGraphSelection::Legend; - self.legend.handle_mouse_event(event) + if let CpuGraphSelection::Legend = self.selected { + self.legend.handle_mouse_event(event) + } else { + self.selected = CpuGraphSelection::Legend; + self.legend.handle_mouse_event(event); + EventResult::Redraw + } } else { EventResult::NoRedraw } @@ -125,4 +163,145 @@ impl Widget for CpuGraph { fn get_pretty_name(&self) -> &'static str { "CPU" } + + fn draw<B: Backend>( + &mut self, painter: &Painter, f: &mut tui::Frame<'_, B>, area: Rect, selected: bool, + ) { + let constraints = match self.legend_position { + CpuGraphLegendPosition::Left => { + [Constraint::Percentage(15), Constraint::Percentage(85)] + } + CpuGraphLegendPosition::Right => { + [Constraint::Percentage(85), Constraint::Percentage(15)] + } + }; + + let split_area = Layout::default() + |