diff options
Diffstat (limited to 'src/app/widgets/bottom_widgets/net.rs')
-rw-r--r-- | src/app/widgets/bottom_widgets/net.rs | 782 |
1 files changed, 782 insertions, 0 deletions
diff --git a/src/app/widgets/bottom_widgets/net.rs b/src/app/widgets/bottom_widgets/net.rs new file mode 100644 index 00000000..00a93c8e --- /dev/null +++ b/src/app/widgets/bottom_widgets/net.rs @@ -0,0 +1,782 @@ +use std::{borrow::Cow, collections::HashMap, time::Instant}; + +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout, Rect}, + widgets::{Block, Borders}, + Frame, +}; + +use crate::{ + app::{ + data_farmer::DataCollection, text_table::SimpleColumn, time_graph::TimeGraphData, + AppConfigFields, AxisScaling, Component, TextTable, TimeGraph, Widget, + }, + canvas::Painter, + data_conversion::convert_network_data_points, + options::layout_options::LayoutRule, + units::data_units::DataUnit, + utils::gen_util::*, +}; + +pub struct NetWidgetState { + pub current_display_time: u64, + pub autohide_timer: Option<Instant>, + // pub draw_max_range_cache: f64, + // pub draw_labels_cache: Vec<String>, + // pub draw_time_start_cache: f64, + // TODO: Re-enable these when we move net details state-side! + // pub unit_type: DataUnitTypes, + // pub scale_type: AxisScaling, +} + +impl NetWidgetState { + pub fn init( + current_display_time: u64, + autohide_timer: Option<Instant>, + // unit_type: DataUnitTypes, + // scale_type: AxisScaling, + ) -> Self { + NetWidgetState { + current_display_time, + autohide_timer, + // draw_max_range_cache: 0.0, + // draw_labels_cache: vec![], + // draw_time_start_cache: 0.0, + // unit_type, + // scale_type, + } + } +} + +#[derive(Default)] +pub struct NetState { + pub force_update: Option<u64>, + pub widget_states: HashMap<u64, NetWidgetState>, +} + +impl NetState { + pub fn init(widget_states: HashMap<u64, NetWidgetState>) -> Self { + NetState { + force_update: None, + widget_states, + } + } + + pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut NetWidgetState> { + self.widget_states.get_mut(&widget_id) + } + + pub fn get_widget_state(&self, widget_id: u64) -> Option<&NetWidgetState> { + self.widget_states.get(&widget_id) + } +} + +// --- NEW STUFF BELOW --- + +/// Returns the max data point and time given a time. +fn get_max_entry( + rx: &[(f64, f64)], tx: &[(f64, f64)], 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<String>) { + // 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<String> = 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), + ], + ) + } + } + } +} + +/// A struct containing useful cached information for a [`NetGraph`]. +#[derive(Clone)] +pub struct NetGraphCache { + max_value: f64, + cached_upper_bound: f64, + labels: Vec<Cow<'static, str>>, +} + +enum NetGraphCacheState { + Uncached, + Cached(NetGraphCache), +} + +/// A widget denoting network usage via a graph. This version is self-contained within a single [`TimeGraph`]; +/// if you need the older one that splits into two sections, use [`OldNetGraph`], which is built on a [`NetGraph`]. +/// +/// As of now, this is essentially just a wrapper around a [`TimeGraph`]. +pub struct NetGraph { + /// The graph itself. Just a [`TimeGraph`]. + graph: TimeGraph, + + // Cached details for drawing purposes; probably want to move at some point... + draw_cache: NetGraphCacheState, + + pub rx_display: String, + pub tx_display: String, + pub total_rx_display: String, + pub total_tx_display: String, + pub network_data_rx: Vec<(f64, f64)>, + pub network_data_tx: Vec<(f64, f64)>, + + pub scale_type: AxisScaling, + pub unit_type: DataUnit, + pub use_binary_prefix: bool, + + hide_legend: bool, + + bounds: Rect, + width: LayoutRule, + height: LayoutRule, +} + +impl NetGraph { + /// Creates a new [`NetGraph`] given a [`AppConfigFields`]. + pub fn from_config(app_config_fields: &AppConfigFields) -> Self { + let graph = TimeGraph::from_config(app_config_fields); + + Self { + graph, + draw_cache: NetGraphCacheState::Uncached, + rx_display: Default::default(), + tx_display: Default::default(), + total_rx_display: Default::default(), + total_tx_display: Default::default(), + network_data_rx: Default::default(), + network_data_tx: Default::default(), + scale_type: app_config_fields.network_scale_type.clone(), + unit_type: app_config_fields.network_unit_type.clone(), + use_binary_prefix: app_config_fields.network_use_binary_prefix, + hide_legend: false, + bounds: Rect::default(), + width: LayoutRule::default(), + height: LayoutRule::default(), + } + } + + /// Hides the legend. Only really useful for [`OldNetGraph`]. + pub fn hide_legend(mut self) -> Self { + self.hide_legend = true; + self + } + + /// 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 + } + + /// Sets the draw cache for a [`NetGraph`]. + pub fn set_draw_cache(&mut self) { + let current_time = -(self.graph.get_current_display_time() as f64); + let (_current_max_time, current_max_value) = get_max_entry( + &self.network_data_rx, + &self.network_data_tx, + current_time, + &self.scale_type, + self.use_binary_prefix, + ); + + match &mut self.draw_cache { + NetGraphCacheState::Uncached => { + let (cached_upper_bound, labels) = adjust_network_data_point( + current_max_value, + &self.scale_type, + &self.unit_type, + self.use_binary_prefix, + ); + + let labels: Vec<Cow<'static, str>> = labels.into_iter().map(Into::into).collect(); + + self.draw_cache = NetGraphCacheState::Cached(NetGraphCache { + max_value: current_max_value, + cached_upper_bound, + labels: labels.clone(), + }); + } + NetGraphCacheState::Cached(cache) => { + if (current_max_value - cache.max_value).abs() > f64::EPSILON { + // Invalidated. + let (upper_bound, labels) = adjust_network_data_point( + current_max_value, + &self.scale_type, + &self.unit_type, + self.use_binary_prefix, + ); + + *cache = NetGraphCache { + max_value: current_max_value, + cached_upper_bound: upper_bound, + labels: labels.into_iter().map(Into::into).collect(), + }; + } + } + } + } +} + +impl Component for NetGraph { + 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: crossterm::event::KeyEvent, + ) -> crate::app::event::WidgetEventResult { + self.graph.handle_key_event(event) + } + + fn handle_mouse_event( + &mut self, event: crossterm::event::MouseEvent, + ) -> crate::app::event::WidgetEventResult { + self.graph.handle_mouse_event(event) + } +} + +impl Widget for NetGraph { + 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() + .border_style(if selected { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }) + .borders(Borders::ALL); + + self.set_draw_cache(); + + let chart_data = vec![ + TimeGraphData { + data: &self.network_data_rx, + label: if self.hide_legend { + None + } else { + Some(self.rx_display.clone().into()) + }, + style: painter.colours.rx_style, + }, + TimeGraphData { + data: &self.network_data_tx, + label: if self.hide_legend { + None + } else { + Some(self.tx_display.clone().into()) + }, + style: painter.colours.tx_style, + }, + ]; + + let (y_bounds, y_bound_labels) = match &self.draw_cache { + NetGraphCacheState::Cached(cache) => ([0.0, cache.cached_upper_bound], &cache.labels), + NetGraphCacheState::Uncached => unreachable!(), + }; + + self.graph.draw_tui_chart( + painter, + f, + &chart_data, + y_bound_labels, + y_bounds, + false, + block, + 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 + false, + &self.scale_type, + &self.unit_type, + self.use_binary_prefix, + ); + self.network_data_rx = network_data.rx; + self.network_data_tx = network_data.tx; + self.rx_display = network_data.rx_display; + self.tx_display = network_data.tx_display; + if let Some(total_rx_display) = network_data.total_rx_display { + self.total_rx_display = total_rx_display; + } + if let Some(total_tx_display) = network_data.total_tx_display { + self.total_tx_display = total_tx_display; + } + } + + fn width(&self) -> LayoutRule { + self.width + } + + fn height(&self) -> LayoutRule { + self.height + } +} + +/// A widget denoting network usage via a graph and a separate, single row table. This is built on [`NetGraph`], +/// and the main difference is that it also contains a bounding box for the graph + text. +pub struct OldNetGraph { + net_graph: NetGraph, + table: TextTable, + bounds: Rect, + width: LayoutRule, + height: LayoutRule, +} + +impl OldNetGraph { + /// Creates a new [`OldNetGraph`] from a [`AppConfigFields`]. + pub fn from_config(config: &AppConfigFields) -> Self { + Self { + net_graph: NetGraph::from_config(config).hide_legend(), + table: TextTable::new(vec![ + SimpleColumn::new_flex("RX".into(), 0.25), + SimpleColumn::new_flex("TX".into(), 0.25), + SimpleColumn::new_flex("Total RX".into(), 0.25), + SimpleColumn::new_flex("Total TX".into(), 0.25), + ]) + .unselectable(), + bounds: Rect::default(), + 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 OldNetGraph { + 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: crossterm::event::KeyEvent, + ) -> crate::app::event::WidgetEventResult { + self.net_graph.handle_key_event(event) + } + + fn handle_mouse_event( + &mut self, event: crossterm::event::MouseEvent, + ) -> crate::app::event::WidgetEventResult { + self.net_graph.handle_mouse_event(event) + } +} + +impl Widget for OldNetGraph { + fn get_pretty_name(&self) -> &'static str { + "Network" + } + + fn draw<B: Backend>( + &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool, + ) { + const CONSTRAINTS: [Constraint; 2] = [Constraint::Min(0), Constraint::Length(4)]; + + let split_area = Layout::default() + .direction(Direction::Vertical) + .constraints(CONSTRAINTS) + .split(area); + + let graph_area = split_area[0]; + let table_area = split_area[1]; + + self.net_graph.draw(painter, f, graph_area, selected); + + let table_block = Block::default() + .border_style(if selected { + painter.colours.highlighted_border_style + } else { + painter.colours.border_style + }) + .borders(Borders::ALL); + self.table.draw_tui_table( + painter, + f, + &[vec![ + ( + self.net_graph.rx_display.clone().into(), + None, + Some(painter.colours.rx_style), + ), + ( + self.net_graph.tx_display.clone().into(), + None, + Some(painter.colours.tx_style), + ), + (self.net_graph.total_rx_display.clone().into(), None, None), + (self.net_graph.total_tx_display.clone().into(), None, None), + ]], + table_block, + table_area, + selected, + ); + } + + 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, + &self.net_graph.scale_type, + &self.net_graph.unit_type, + self.net_graph.use_binary_prefix, + ); + self.net_graph.network_data_rx = network_data.rx; + self.net_graph.network_data_tx = network_data.tx; + self.net_graph.rx_display = network_data.rx_display; + self.net_graph.tx_display = network_data.tx_display; + if let Some(total_rx_display) = network_data.total_rx_display { + self.net_graph.total_rx_display = total_rx_display; + } + if let Some(total_tx_display) = network_data.total_tx_display { + self.net_graph.total_tx_display = total_tx_display; + } + } + + fn width(&self) -> LayoutRule { + self.width + } + + fn height(&self) -> LayoutRule { + self.height + } +} |