diff options
Diffstat (limited to 'src/app/widgets/bottom_widgets')
-rw-r--r-- | src/app/widgets/bottom_widgets/basic_cpu.rs | 205 | ||||
-rw-r--r-- | src/app/widgets/bottom_widgets/basic_mem.rs | 164 | ||||
-rw-r--r-- | src/app/widgets/bottom_widgets/basic_net.rs | 134 | ||||
-rw-r--r-- | src/app/widgets/bottom_widgets/battery.rs | 121 | ||||
-rw-r--r-- | src/app/widgets/bottom_widgets/carousel.rs | 209 | ||||
-rw-r--r-- | src/app/widgets/bottom_widgets/cpu.rs | 333 | ||||
-rw-r--r-- | src/app/widgets/bottom_widgets/disk.rs | 162 | ||||
-rw-r--r-- | src/app/widgets/bottom_widgets/empty.rs | 58 | ||||
-rw-r--r-- | src/app/widgets/bottom_widgets/mem.rs | 179 | ||||
-rw-r--r-- | src/app/widgets/bottom_widgets/net.rs | 782 | ||||
-rw-r--r-- | src/app/widgets/bottom_widgets/process.rs | 1529 | ||||
-rw-r--r-- | src/app/widgets/bottom_widgets/temp.rs | 164 |
12 files changed, 4040 insertions, 0 deletions
diff --git a/src/app/widgets/bottom_widgets/basic_cpu.rs b/src/app/widgets/bottom_widgets/basic_cpu.rs new file mode 100644 index 00000000..5b862367 --- /dev/null +++ b/src/app/widgets/bottom_widgets/basic_cpu.rs @@ -0,0 +1,205 @@ +use std::cmp::max; + +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout, Rect}, + widgets::Block, + Frame, +}; + +use crate::{ + app::{widgets::tui_widgets::PipeGauge, AppConfigFields, Component, DataCollection, Widget}, + canvas::Painter, + constants::SIDE_BORDERS, + options::layout_options::LayoutRule, +}; + +const REQUIRED_COLUMNS: usize = 4; + +#[derive(Debug)] +pub struct BasicCpu { + bounds: Rect, + display_data: Vec<(f64, String, String)>, + width: LayoutRule, + showing_avg: bool, +} + +impl BasicCpu { + /// Creates a new [`BasicCpu`] given a [`AppConfigFields`]. + pub fn from_config(app_config_fields: &AppConfigFields) -> Self { + Self { + bounds: Default::default(), + display_data: Default::default(), + width: Default::default(), + showing_avg: app_config_fields.show_average_cpu, + } + } + + /// Sets the width. + pub fn width(mut self, width: LayoutRule) -> Self { + self.width = width; + self + } +} + +impl Component for BasicCpu { + fn bounds(&self) -> Rect { + self.bounds + } + + fn set_bounds(&mut self, new_bounds: Rect) { + self.bounds = new_bounds; + } +} + +impl Widget for BasicCpu { + fn get_pretty_name(&self) -> &'static str { + "CPU" + } + + fn draw<B: Backend>( + &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool, + ) { + const CONSTRAINTS: [Constraint; 2 * REQUIRED_COLUMNS - 1] = [ + Constraint::Ratio(1, REQUIRED_COLUMNS as u32), + Constraint::Length(2), + Constraint::Ratio(1, REQUIRED_COLUMNS as u32), + Constraint::Length(2), + Constraint::Ratio(1, REQUIRED_COLUMNS as u32), + Constraint::Length(2), + Constraint::Ratio(1, REQUIRED_COLUMNS as u32), + ]; + let block = Block::default() + .borders(*SIDE_BORDERS) + .border_style(painter.colours.highlighted_border_style); + let inner_area = block.inner(area); + let split_area = Layout::default() + .direction(Direction::Horizontal) + .constraints(CONSTRAINTS) + .split(inner_area) + .into_iter() + .enumerate() + .filter_map( + |(index, rect)| { + if index % 2 == 0 { + Some(rect) + } else { + None + } + }, + ); + + let display_data_len = self.display_data.len(); + let length = display_data_len / REQUIRED_COLUMNS; + let largest_height = max( + 1, + length + + (if display_data_len % REQUIRED_COLUMNS == 0 { + 0 + } else { + 1 + }), + ); + let mut leftover = display_data_len % REQUIRED_COLUMNS; + let column_heights = (0..REQUIRED_COLUMNS).map(|_| { + if leftover > 0 { + leftover -= 1; + length + 1 + } else { + length + } + }); + + if selected { + f.render_widget(block, area); + } + + let mut index_offset = 0; + split_area + .into_iter() + .zip(column_heights) + .for_each(|(area, height)| { + let column_areas = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Length(1); largest_height]) + .split(area); + + let num_entries = if index_offset + height < display_data_len { + height + } else { + display_data_len - index_offset + }; + let end = index_offset + num_entries; + + self.display_data[index_offset..end] + .iter() + .zip(column_areas) + .enumerate() + .for_each(|(column_index, ((percent, label, usage_label), area))| { + let cpu_index = index_offset + column_index; + let style = if cpu_index == 0 { + painter.colours.avg_colour_style + } else { + let cpu_style_index = if self.showing_avg { + cpu_index - 1 + } else { + cpu_index + }; + painter.colours.cpu_colour_styles + [cpu_style_index % painter.colours.cpu_colour_styles.len()] + }; + + f.render_widget( + PipeGauge::default() + .ratio(*percent) + .style(style) + .gauge_style(style) + .start_label(label.clone()) + .end_label(usage_label.clone()), + area, + ); + }); + + index_offset = end; + }); + } + + fn update_data(&mut self, data_collection: &DataCollection) { + self.display_data = data_collection + .cpu_harvest + .iter() + .map(|data| { + ( + data.cpu_usage / 100.0, + format!( + "{:3}", + data.cpu_count + .map(|c| c.to_string()) + .unwrap_or(data.cpu_prefix.clone()) + ), + format!("{:3.0}%", data.cpu_usage.round()), + ) + }) + .collect::<Vec<_>>(); + + } + + fn width(&self) -> LayoutRule { + self.width + } + + fn height(&self) -> LayoutRule { + let display_data_len = self.display_data.len(); + let length = max( + 1, + (display_data_len / REQUIRED_COLUMNS) as u16 + + (if display_data_len % REQUIRED_COLUMNS == 0 { + 0 + } else { + 1 + }), + ); + + LayoutRule::Length { length } + } +} diff --git a/src/app/widgets/bottom_widgets/basic_mem.rs b/src/app/widgets/bottom_widgets/basic_mem.rs new file mode 100644 index 00000000..7fbd1fd0 --- /dev/null +++ b/src/app/widgets/bottom_widgets/basic_mem.rs @@ -0,0 +1,164 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use tui::{ + backend::Backend, + layout::{Constraint, Layout, Rect}, + widgets::Block, + Frame, +}; + +use crate::{ + app::{ + event::WidgetEventResult, widgets::tui_widgets::PipeGauge, Component, DataCollection, + Widget, + }, + canvas::Painter, + constants::SIDE_BORDERS, + data_conversion::{convert_mem_data_points, convert_mem_labels, convert_swap_data_points}, + options::layout_options::LayoutRule, +}; + +#[derive(Debug)] +pub struct BasicMem { + bounds: Rect, + width: LayoutRule, + mem_data: (f64, String, String), + swap_data: Option<(f64, String, String)>, + use_percent: bool, +} + +impl Default for BasicMem { + fn default() -> Self { + Self { + bounds: Default::default(), + width: Default::default(), + mem_data: (0.0, "0.0B/0.0B".to_string(), "0%".to_string()), + swap_data: None, + use_percent: false, + } + } +} + +impl BasicMem { + /// Sets the width. + pub fn width(mut self, width: LayoutRule) -> Self { + self.width = width; + self + } +} + +impl Component for BasicMem { + fn bounds(&self) -> Rect { + self.bounds + } + + fn set_bounds(&mut self, new_bounds: Rect) { + self.bounds = new_bounds; + } + + fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult { + match event.code { + KeyCode::Char('%') if event.modifiers.is_empty() => { + self.use_percent = !self.use_percent; + WidgetEventResult::Redraw + } + _ => WidgetEventResult::NoRedraw, + } + } +} + +impl Widget for BasicMem { + fn get_pretty_name(&self) -> &'static str { + "Memory" + } + + fn draw<B: Backend>( + &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool, + ) { + let block = Block::default() + .borders(*SIDE_BORDERS) + .border_style(painter.colours.highlighted_border_style); + let inner_area = block.inner(area); + const CONSTRAINTS: [Constraint; 2] = [Constraint::Ratio(1, 2); 2]; + let split_area = Layout::default() + .direction(tui::layout::Direction::Vertical) + .constraints(CONSTRAINTS) + .split(inner_area); + + if selected { + f.render_widget(block, area); + } + + let mut use_percentage = + self.use_percent || (split_area[0].width as usize) < self.mem_data.1.len() + 7; + + if let Some(swap_data) = &self.swap_data { + use_percentage = + use_percentage || (split_area[1].width as usize) < swap_data.1.len() + 7; + + f.render_widget( + PipeGauge::default() + .ratio(swap_data.0) + .style(painter.colours.swap_style) + .gauge_style(painter.colours.swap_style) + .start_label("SWP") + .end_label(if use_percentage { + swap_data.2.clone() + } else { + swap_data.1.clone() + }), + split_area[1], + ); + } + f.render_widget( + PipeGauge::default() + .ratio(self.mem_data.0) + .style(painter.colours.ram_style) + .gauge_style(painter.colours.ram_style) + .start_label("RAM") + .end_label(if use_percentage { + self.mem_data.2.clone() + } else { + self.mem_data.1.clone() + }), + split_area[0], + ); + } + + fn update_data(&mut self, data_collection: &DataCollection) { + let (memory_labels, swap_labels) = convert_mem_labels(data_collection); + + // TODO: [Data update optimization] Probably should just make another function altogether for basic mode. + self.mem_data = if let (Some(data), Some((_, fraction))) = ( + convert_mem_data_points(data_collection, false).last(), + memory_labels, + ) { + ( + data.1 / 100.0, + fraction.trim().to_string(), + format!("{:3.0}%", data.1.round()), + ) + } else { + (0.0, "0.0B/0.0B".to_string(), "0%".to_string()) + }; + self.swap_data = if let (Some(data), Some((_, fraction))) = ( + convert_swap_data_points(data_collection, false).last(), + swap_labels, + ) { + Some(( + data.1 / 100.0, + fraction.trim().to_string(), + format!("{:3.0}%", data.1.round()), + )) + } else { + None + }; + } + + fn width(&self) -> LayoutRule { + self.width + } + + fn height(&self) -> LayoutRule { + LayoutRule::Length { length: 2 } + } +} diff --git a/src/app/widgets/bottom_widgets/basic_net.rs b/src/app/widgets/bottom_widgets/basic_net.rs new file mode 100644 index 00000000..60b0c44b --- /dev/null +++ b/src/app/widgets/bottom_widgets/basic_net.rs @@ -0,0 +1,134 @@ +use tui::{ + backend::Backend, + layout::{Constraint, Layout, Rect}, + text::{Span, Spans}, + widgets::{Block, Paragraph}, + Frame, +}; + +use crate::{ + app::{AppConfigFields, AxisScaling, Component, DataCollection, Widget}, + canvas::Painter, + constants::SIDE_BORDERS, + data_conversion::convert_network_data_points, + options::layout_options::LayoutRule, + units::data_units::DataUnit, +}; + +#[derive(Debug)] +pub struct BasicNet { + bounds: Rect, + width: LayoutRule, + + rx_display: String, + tx_display: String, + total_rx_display: String, + total_tx_display: String, + + pub unit_type: DataUnit, + pub use_binary_prefix: bool, +} + +impl BasicNet { + /// Creates a new [`BasicNet`] given a [`AppConfigFields`]. + pub fn from_config(app_config_fields: &AppConfigFields) -> Self { + Self { + bounds: Default::default(), + width: Default::default(), + rx_display: "RX: 0b/s".to_string(), + tx_display: "TX: 0b/s".to_string(), + total_rx_display: "Total RX: 0B".to_string(), + total_tx_display: "Total TX: 0B".to_string(), + unit_type: app_config_fields.network_unit_type.clone(), + use_binary_prefix: app_config_fields.network_use_binary_prefix, + } + } + + /// Sets the width. + pub fn width(mut self, width: LayoutRule) -> Self { + self.width = width; + self + } +} + +impl Component for BasicNet { + fn bounds(&self) -> Rect { + self.bounds + } + + fn set_bounds(&mut self, new_bounds: Rect) { + self.bounds = new_bounds; + } +} + +impl Widget for BasicNet { + fn get_pretty_name(&self) -> &'static str { + "Network" + } + + fn draw<B: Backend>( + &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool, + ) { + let block = Block::default() + .borders(*SIDE_BORDERS) + .border_style(painter.colours.highlighted_border_style); + + let inner_area = block.inner(area); + const CONSTRAINTS: [Constraint; 2] = [Constraint::Ratio(1, 2); 2]; + let split_area = Layout::default() + .direction(tui::layout::Direction::Horizontal) + .constraints(CONSTRAINTS) + .split(inner_area); + let texts = [ + [ + Spans::from(Span::styled(&self.rx_display, painter.colours.rx_style)), + Spans::from(Span::styled(&self.tx_display, painter.colours.tx_style)), + ], + [ + Spans::from(Span::styled( + &self.total_rx_display, + painter.colours.total_rx_style, + )), + Spans::from(Span::styled( + &self.total_tx_display, + painter.colours.total_tx_style, + )), + ], + ]; + + if selected { + f.render_widget(block, area); + } + + IntoIterator::into_iter(texts) + .zip(split_area) + .for_each(|(text, area)| f.render_widget(Paragraph::new(text.to_vec()), area)); + } + + fn update_data(&mut self, data_collection: &DataCollection) { + let network_data = convert_network_data_points( + data_collection, + false, // TODO: I think the is_frozen here is also useless; see mem and cpu + true, + &AxisScaling::Linear, + &self.unit_type, + self.use_binary_prefix, + ); + self.rx_display = format!("RX: {}", network_data.rx_display); + self.tx_display = format!("TX: {}", network_data.tx_display); + if let Some(total_rx_display) = network_data.total_rx_display { + self.total_rx_display = format!("Total RX: {}", total_rx_display); + } + if let Some(total_tx_display) = network_data.total_tx_display { + self.total_tx_display = format!("Total TX: {}", total_tx_display); + } + } + + fn width(&self) -> LayoutRule { + self.width + } + + fn height(&self) -> LayoutRule { + LayoutRule::Length { length: 2 } + } +} diff --git a/src/app/widgets/bottom_widgets/battery.rs b/src/app/widgets/bottom_widgets/battery.rs new file mode 100644 index 00000000..2680a8d7 --- /dev/null +++ b/src/app/widgets/bottom_widgets/battery.rs @@ -0,0 +1,121 @@ +use std::collections::HashMap; + +use tui::{layout::Rect, widgets::Borders}; + +use crate::{ + app::{data_farmer::DataCollection, Component, Widget}, + data_conversion::{convert_battery_harvest, ConvertedBatteryData}, + options::layout_options::LayoutRule, +}; + +#[derive(Default)] +pub struct BatteryWidgetState { + pub currently_selected_battery_index: usize, + pub tab_click_locs: Option<Vec<((u16, u16), (u16, u16))>>, +} + +#[derive(Default)] +pub struct BatteryState { + pub widget_states: HashMap<u64, BatteryWidgetState>, +} + +impl BatteryState { + pub fn init(widget_states: HashMap<u64, BatteryWidgetState>) -> Self { + BatteryState { widget_states } + } + + pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut BatteryWidgetState> { + self.widget_states.get_mut(&widget_id) + } + + pub fn get_widget_state(&self, widget_id: u64) -> Option<&BatteryWidgetState> { + self.widget_states.get(&widget_id) + } +} + +// TODO: Implement battery widget. +/// A table displaying battery information on a per-battery basis. +pub struct BatteryTable { + bounds: Rect, + selected_index: usize, + batteries: Vec<String>, + battery_data: Vec<ConvertedBatteryData>, + width: LayoutRule, + height: LayoutRule, + block_border: Borders, +} + +impl Default for BatteryTable { + fn default() -> Self { + Self { + batteries: vec![], + bounds: Default::default(), + selected_index: 0, + battery_data: Default::default(), + width: LayoutRule::default(), + height: LayoutRule::default(), + block_border: Borders::ALL, + } + } +} + +impl BatteryTable { + /// Sets the width. + pub fn width(mut self, width: LayoutRule) -> Self { + self.width = width; + self + } + + /// Sets the height. + pub fn height(mut self, height: LayoutRule) -> Self { + self.height = height; + self + } + + /// Returns the index of the currently selected battery. + pub fn index(&self) -> usize { + self.selected_index + } + + /// Returns a reference to the battery names. + pub fn batteries(&self) -> &[String] { + &self.batteries + } + + /// Sets the block border style. + pub fn basic_mode(mut self, basic_mode: bool) -> Self { + if basic_mode { + self.block_border = *crate::constants::SIDE_BORDERS; + } + + self + } +} + +impl Component for BatteryTable { + fn bounds(&self) -> tui::layout::Rect { + self.bounds + } + + fn set_bounds(&mut self, new_bounds: tui::layout::Rect) { + self.bounds = new_bounds; + } +} + +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); + } + + fn width(&self) -> LayoutRule { + self.width + } + + fn height(&self) -> LayoutRule { + self.height + } +} diff --git a/src/app/widgets/bottom_widgets/carousel.rs b/src/app/widgets/bottom_widgets/carousel.rs new file mode 100644 index 00000000..0f91c212 --- /dev/null +++ b/src/app/widgets/bottom_widgets/carousel.rs @@ -0,0 +1,209 @@ +use std::borrow::Cow; + +use crossterm::event::MouseEvent; +use indextree::NodeId; +use tui::{ + backend::Backend, + layout::{Constraint, Layout, Rect}, + text::{Span, Spans}, + widgets::Paragraph, + Frame, +}; + +use crate::{ + app::{ + does_bound_intersect_coordinate, event::WidgetEventResult, Component, SelectableType, + Widget, + }, + canvas::Painter, + options::layout_options::LayoutRule, +}; + +/// A container that "holds"" multiple [`BottomWidget`]s through their [`NodeId`]s. +#[derive(PartialEq, Eq)] +pub struct Carousel { + index: usize, + children: Vec<(NodeId, Cow<'static, str>)>, + bounds: Rect, + width: LayoutRule, + height: LayoutRule, + left_button_bounds: Rect, + right_button_bounds: Rect, +} + +impl Carousel { + /// Creates a new [`Carousel`] with the specified children. + pub fn new(children: Vec<(NodeId, Cow<'static, str>)>) -> Self { + Self { + index: 0, + children, + bounds: Default::default(), + width: Default::default(), + height: Default::default(), + left_button_bounds: Default::default(), + right_button_bounds: Default::default(), + } + } + + /// Sets the width. + pub fn width(mut self, width: LayoutRule) -> Self { + self.width = width; + self + } + + /// Sets the height. + pub fn height(mut self, height: LayoutRule) -> Self { + self.height = height; + self + } + + /// Adds a new child to a [`Carousel`]. + pub fn add_child(&mut self, child: NodeId, name: Cow<'static, str>) { + self.children.push((child, name)); + } + + /// Returns the currently selected [`NodeId`] if possible. + pub fn get_currently_selected(&self) -> Option<NodeId> { + self.children.get(self.index).map(|i| i.0.clone()) + } + + fn get_next(&self) -> Option<&(NodeId, Cow<'static, str>)> { + self.children.get(if self.index + 1 == self.children.len() { + 0 + } else { + self.index + 1 + }) + } + + fn get_prev(&self) -> Option<&(NodeId, Cow<'static, str>)> { + self.children.get(if self.index > 0 { + self.index - 1 + } else { + self.children.len().saturating_sub(1) + }) + } + + fn increment_index(&mut self) { + if self.index + 1 == self.children.len() { + self.index = 0; + } else { + self.index += 1; + } + } + + fn decrement_index(&mut self) { + if self.index > 0 { + self.index -= 1; + } else { + self.index = self.children.len().saturating_sub(1); + } + } + + /// Draws the [`Carousel`] arrows, and returns back the remaining [`Rect`] to draw the child with. + pub fn draw_carousel<B: Backend>( + &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, + ) -> Rect { + const CONSTRAINTS: [Constraint; 2] = [Constraint::Length(1), Constraint::Min(0)]; + let split_area = Layout::default() + .constraints(CONSTRAINTS) + .direction(tui::layout::Direction::Vertical) + .split(area); + + self.set_bounds(split_area[0]); + + if let Some((_prev_id, prev_element_name)) = self.get_prev() { + let prev_arrow_text = Spans::from(Span::styled( + format!("◄ {}", prev_element_name), + painter.colours.text_style, + )); + + self.left_button_bounds = Rect::new( + split_area[0].x, + split_area[0].y, + prev_arrow_text.width() as u16, + split_area[0].height, + ); + + f.render_widget( + Paragraph::new(vec![prev_arrow_text]).alignment(tui::layout::Alignment::Left), + split_area[0], + ); + } + + if let Some((_next_id, next_element_name)) = self.get_next() { + let next_arrow_text = Spans::from(Span::styled( + format!("{} ►", next_element_name), + painter.colours.text_style, + )); + + let width = next_arrow_text.width() as u16; + + self.right_button_bounds = Rect::new( + split_area[0].right().saturating_sub(width + 1), + split_area[0].y, + width, + split_area[0].height, + ); + + f.render_widget( + Paragraph::new(vec![next_arrow_text]).alignment(tui::layout::Alignment::Right), + split_area[0], + ); + } + + split_area[1] + } +} + +impl Component for Carousel { + fn bounds(&self) -> Rect { + self.bounds + } + + fn set_bounds(&mut self, new_bounds: Rect) { + self.bounds = new_bounds; + } + + fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult { + match event.kind { + crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left) => { + let x = event.column; + let y = event.row; + + if does_bound_intersect_coordinate(x, y, self.left_button_bounds) { + self.decrement_index(); + WidgetEventResult::Redraw + } else if does_bound_intersect_coordinate(x, y, self.right_button_bounds) { + self.increment_index(); + WidgetEventResult::Redraw + } else { + WidgetEventResult::NoRedraw + } + } + _ => WidgetEventResult::NoRedraw, + } + } +} + +impl Widget for Carousel { + fn get_pretty_name(&self) -> &'static str { + "Carousel" + } + + fn width(&self) -> LayoutRule { + self.width + } + + fn height(&self) -> LayoutRule { + self.height + } + + fn selectable_type(&self) -> SelectableType { + if let Some(node) = self.get_currently_selected() { + debug!("node: {:?}", node); + SelectableType::Redirect(node) + } else { + SelectableType::Unselectable + } + } +} diff --git a/src/app/widgets/bottom_widgets/cpu.rs b/src/app/widgets/bottom_widgets/cpu.rs new file mode 100644 index 00000000..753142f5 --- /dev/null +++ b/src/app/widgets/bottom_widgets/cpu.rs @@ -0,0 +1,333 @@ +use std::{borrow::Cow, collections::HashMap, time::Instant}; + +use crossterm::event::{KeyEvent, MouseEvent}; +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout, Rect}, + widgets::{Block, Borders}, + Frame, +}; + +use crate::{ + app::{ + event::WidgetEventResult, sort_text_table::SimpleSortableColumn, time_graph::TimeGraphData, + AppConfigFields, AppScrollWidgetState, CanvasTableWidthState, Component, DataCollection, + TextTable, TimeGraph, Widget, + }, + canvas::Painter, + data_conversion::{convert_cpu_data_points, ConvertedCpuData}, + options::layout_options::LayoutRule, +}; + +pub struct CpuWidgetState { + pub current_display_time: u64, + pub is_legend_hidden: bool, + pub autohide_timer: Option<Instant>, + pub scroll_state: AppScrollWidgetState, + pub is_multi_graph_mode: bool, + pub table_width_state: CanvasTableWidthState, +} + +impl CpuWidgetState { + pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self { + CpuWidgetState { + current_display_time, + is_legend_hidden: false, + autohide_timer, + scroll_state: AppScrollWidgetState::default(), + is_multi_graph_mode: false, + table_width_state: CanvasTableWidthState::default(), + } + } +} + +#[derive(Default)] +pub struct CpuState { + pub force_update: Option<u64>, + pub widget_states: HashMap<u64, CpuWidgetState>, +} + +impl CpuState { + pub fn init(widget_states: HashMap<u64, CpuWidgetState>) -> Self { + CpuState { + force_update: None, + widget_states, + } + } + + pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut CpuWidgetState> { + self.widget_states.get_mut(&widget_id) + } + + pub fn get_widget_state(&self, widget_id: u64) -> Option<&CpuWidgetState> { + self.widget_states.get(&widget_id) + } +} + +enum CpuGraphSelection { + Graph, + Legend, + None, +} + +/// Whether the [`CpuGraph`]'s legend is placed on the left or right. +pub enum CpuGraphLegendPosition { + Left, + Right, +} + +/// A widget designed to show CPU usage via a graph, along with a side legend in a table. +pub struct CpuGraph { + graph: TimeGraph, + legend: TextTable<SimpleSortableColumn>, + legend_position: CpuGraphLegendPosition, + showing_avg: bool, + + bounds: Rect, + selected: CpuGraphSelection, + + display_data: Vec<ConvertedCpuData>, + load_avg_data: [f32; 3], + + width: LayoutRule, + height: LayoutRule, +} + +impl CpuGraph { + /// 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 = TextTable::new(vec![ + SimpleSortableColumn::new_flex("CPU".into(), None, false, 0.5), + SimpleSortableColumn::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], + width: LayoutRule::default(), + height: LayoutRule::default(), + } + } + + /// Sets the width. + pub fn width(mut self, width: LayoutRule) -> Self { + self.width = width; + self + } + + /// Sets the height. + pub fn height(mut self, height: LayoutRule) -> Self { + self.height = height; + self + } +} + +impl Component for CpuGraph { + fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult { + match self.selected { |