mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-27 07:34:27 +02:00
Merge pull request #710 from ClementTsang/consolidate_component_drawing
Cleans up some drawing code and unifies all time graph drawing.
This commit is contained in:
commit
cddee9d923
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -232,6 +232,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
"clap_mangen",
|
"clap_mangen",
|
||||||
|
"concat-string",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"dirs",
|
"dirs",
|
||||||
@ -354,6 +355,12 @@ dependencies = [
|
|||||||
"roff",
|
"roff",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "concat-string"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7439becb5fafc780b6f4de382b1a7a3e70234afe783854a4702ee8adbb838609"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
|
@ -42,6 +42,7 @@ crossterm = "0.18.2"
|
|||||||
ctrlc = { version = "3.1.9", features = ["termination"] }
|
ctrlc = { version = "3.1.9", features = ["termination"] }
|
||||||
clap = { version = "3.1.12", features = ["default", "cargo", "wrap_help"] }
|
clap = { version = "3.1.12", features = ["default", "cargo", "wrap_help"] }
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
|
concat-string = "1.0.1"
|
||||||
dirs = "4.0.0"
|
dirs = "4.0.0"
|
||||||
futures = "0.3.21"
|
futures = "0.3.21"
|
||||||
futures-timer = "3.0.2"
|
futures-timer = "3.0.2"
|
||||||
|
34
src/app.rs
34
src/app.rs
@ -127,9 +127,6 @@ pub struct App {
|
|||||||
#[builder(default = false, setter(skip))]
|
#[builder(default = false, setter(skip))]
|
||||||
pub basic_mode_use_percent: bool,
|
pub basic_mode_use_percent: bool,
|
||||||
|
|
||||||
#[builder(default = false, setter(skip))]
|
|
||||||
pub is_config_open: bool,
|
|
||||||
|
|
||||||
#[builder(default = false, setter(skip))]
|
#[builder(default = false, setter(skip))]
|
||||||
pub did_config_fail_to_save: bool,
|
pub did_config_fail_to_save: bool,
|
||||||
|
|
||||||
@ -218,8 +215,6 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.is_force_redraw = true;
|
self.is_force_redraw = true;
|
||||||
} else if self.is_config_open {
|
|
||||||
self.close_config_screen();
|
|
||||||
} else {
|
} else {
|
||||||
match self.current_widget.widget_type {
|
match self.current_widget.widget_type {
|
||||||
BottomWidgetType::Proc => {
|
BottomWidgetType::Proc => {
|
||||||
@ -297,7 +292,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ignore_normal_keybinds(&self) -> bool {
|
fn ignore_normal_keybinds(&self) -> bool {
|
||||||
self.is_config_open || self.is_in_dialog()
|
self.is_in_dialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_tab(&mut self) {
|
pub fn on_tab(&mut self) {
|
||||||
@ -910,8 +905,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_up_key(&mut self) {
|
pub fn on_up_key(&mut self) {
|
||||||
if self.is_config_open {
|
if !self.is_in_dialog() {
|
||||||
} else if !self.is_in_dialog() {
|
|
||||||
self.decrement_position_count();
|
self.decrement_position_count();
|
||||||
} else if self.help_dialog_state.is_showing_help {
|
} else if self.help_dialog_state.is_showing_help {
|
||||||
self.help_scroll_up();
|
self.help_scroll_up();
|
||||||
@ -932,8 +926,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_down_key(&mut self) {
|
pub fn on_down_key(&mut self) {
|
||||||
if self.is_config_open {
|
if !self.is_in_dialog() {
|
||||||
} else if !self.is_in_dialog() {
|
|
||||||
self.increment_position_count();
|
self.increment_position_count();
|
||||||
} else if self.help_dialog_state.is_showing_help {
|
} else if self.help_dialog_state.is_showing_help {
|
||||||
self.help_scroll_down();
|
self.help_scroll_down();
|
||||||
@ -954,8 +947,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_left_key(&mut self) {
|
pub fn on_left_key(&mut self) {
|
||||||
if self.is_config_open {
|
if !self.is_in_dialog() {
|
||||||
} else if !self.is_in_dialog() {
|
|
||||||
match self.current_widget.widget_type {
|
match self.current_widget.widget_type {
|
||||||
BottomWidgetType::ProcSearch => {
|
BottomWidgetType::ProcSearch => {
|
||||||
let is_in_search_widget = self.is_in_search_widget();
|
let is_in_search_widget = self.is_in_search_widget();
|
||||||
@ -1026,8 +1018,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_right_key(&mut self) {
|
pub fn on_right_key(&mut self) {
|
||||||
if self.is_config_open {
|
if !self.is_in_dialog() {
|
||||||
} else if !self.is_in_dialog() {
|
|
||||||
match self.current_widget.widget_type {
|
match self.current_widget.widget_type {
|
||||||
BottomWidgetType::ProcSearch => {
|
BottomWidgetType::ProcSearch => {
|
||||||
let is_in_search_widget = self.is_in_search_widget();
|
let is_in_search_widget = self.is_in_search_widget();
|
||||||
@ -1191,7 +1182,6 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if self.is_config_open {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1238,7 +1228,6 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if self.is_config_open {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1490,7 +1479,6 @@ impl App {
|
|||||||
'G' => self.skip_to_last(),
|
'G' => self.skip_to_last(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
} else if self.is_config_open {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1673,16 +1661,6 @@ impl App {
|
|||||||
|
|
||||||
pub fn on_space(&mut self) {}
|
pub fn on_space(&mut self) {}
|
||||||
|
|
||||||
pub fn open_config_screen(&mut self) {
|
|
||||||
self.is_config_open = true;
|
|
||||||
self.is_force_redraw = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close_config_screen(&mut self) {
|
|
||||||
self.is_config_open = false;
|
|
||||||
self.is_force_redraw = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// TODO: Disabled.
|
/// TODO: Disabled.
|
||||||
/// Call this whenever the config value is updated!
|
/// Call this whenever the config value is updated!
|
||||||
// fn update_config_file(&mut self) -> anyhow::Result<()> {
|
// fn update_config_file(&mut self) -> anyhow::Result<()> {
|
||||||
@ -2264,7 +2242,6 @@ impl App {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
self.reset_multi_tap_keys();
|
self.reset_multi_tap_keys();
|
||||||
} else if self.is_config_open {
|
|
||||||
} else if self.help_dialog_state.is_showing_help {
|
} else if self.help_dialog_state.is_showing_help {
|
||||||
self.help_dialog_state.scroll_state.current_scroll_index = 0;
|
self.help_dialog_state.scroll_state.current_scroll_index = 0;
|
||||||
} else if self.delete_dialog_state.is_showing_dd {
|
} else if self.delete_dialog_state.is_showing_dd {
|
||||||
@ -2343,7 +2320,6 @@ impl App {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
self.reset_multi_tap_keys();
|
self.reset_multi_tap_keys();
|
||||||
} else if self.is_config_open {
|
|
||||||
} else if self.help_dialog_state.is_showing_help {
|
} else if self.help_dialog_state.is_showing_help {
|
||||||
self.help_dialog_state.scroll_state.current_scroll_index = self
|
self.help_dialog_state.scroll_state.current_scroll_index = self
|
||||||
.help_dialog_state
|
.help_dialog_state
|
||||||
|
@ -9,12 +9,7 @@ use tui::{
|
|||||||
Frame, Terminal,
|
Frame, Terminal,
|
||||||
};
|
};
|
||||||
|
|
||||||
// use ordered_float::OrderedFloat;
|
|
||||||
|
|
||||||
use canvas_colours::*;
|
use canvas_colours::*;
|
||||||
use dialogs::*;
|
|
||||||
use screens::*;
|
|
||||||
use widgets::*;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{
|
app::{
|
||||||
@ -30,15 +25,14 @@ use crate::{
|
|||||||
Pid,
|
Pid,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use self::components::Point;
|
||||||
|
|
||||||
mod canvas_colours;
|
mod canvas_colours;
|
||||||
|
mod components;
|
||||||
mod dialogs;
|
mod dialogs;
|
||||||
mod drawing_utils;
|
mod drawing_utils;
|
||||||
mod screens;
|
|
||||||
mod widgets;
|
mod widgets;
|
||||||
|
|
||||||
/// Point is of time, data
|
|
||||||
type Point = (f64, f64);
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct DisplayableData {
|
pub struct DisplayableData {
|
||||||
pub rx_display: String,
|
pub rx_display: String,
|
||||||
@ -207,6 +201,16 @@ impl Painter {
|
|||||||
Ok(painter)
|
Ok(painter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determines the border style.
|
||||||
|
pub fn get_border_style(&self, widget_id: u64, selected_widget_id: u64) -> tui::style::Style {
|
||||||
|
let is_on_widget = widget_id == selected_widget_id;
|
||||||
|
if is_on_widget {
|
||||||
|
self.colours.highlighted_border_style
|
||||||
|
} else {
|
||||||
|
self.colours.border_style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn generate_config_colours(&mut self, config: &Config) -> anyhow::Result<()> {
|
fn generate_config_colours(&mut self, config: &Config) -> anyhow::Result<()> {
|
||||||
if let Some(colours) = &config.colors {
|
if let Some(colours) = &config.colors {
|
||||||
self.colours.set_colours_from_palette(colours)?;
|
self.colours.set_colours_from_palette(colours)?;
|
||||||
@ -513,13 +517,6 @@ impl Painter {
|
|||||||
),
|
),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
} else if app_state.is_config_open {
|
|
||||||
let rect = Layout::default()
|
|
||||||
.margin(0)
|
|
||||||
.constraints([Constraint::Percentage(100)])
|
|
||||||
.split(f.size())[0];
|
|
||||||
|
|
||||||
self.draw_config_screen(f, app_state, rect)
|
|
||||||
} else if app_state.app_config_fields.use_basic_mode {
|
} else if app_state.app_config_fields.use_basic_mode {
|
||||||
// Basic mode. This basically removes all graphs but otherwise
|
// Basic mode. This basically removes all graphs but otherwise
|
||||||
// the same info.
|
// the same info.
|
||||||
|
10
src/canvas/components.rs
Normal file
10
src/canvas/components.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
//! Some common components to reuse when drawing widgets.
|
||||||
|
|
||||||
|
pub mod time_chart;
|
||||||
|
pub use time_chart::*;
|
||||||
|
|
||||||
|
pub mod time_graph;
|
||||||
|
pub use time_graph::*;
|
||||||
|
|
||||||
|
pub mod text_table;
|
||||||
|
pub use text_table::*;
|
1
src/canvas/components/text_table.rs
Normal file
1
src/canvas/components/text_table.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
701
src/canvas/components/time_chart.rs
Normal file
701
src/canvas/components/time_chart.rs
Normal file
@ -0,0 +1,701 @@
|
|||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
cmp::{max, Ordering},
|
||||||
|
};
|
||||||
|
use tui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Constraint, Rect},
|
||||||
|
style::{Color, Style},
|
||||||
|
symbols,
|
||||||
|
text::{Span, Spans},
|
||||||
|
widgets::{
|
||||||
|
canvas::{Canvas, Line, Points},
|
||||||
|
Block, Borders, GraphType, Widget,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
/// An X or Y axis for the chart widget
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Axis<'a> {
|
||||||
|
/// Title displayed next to axis end
|
||||||
|
pub title: Option<Spans<'a>>,
|
||||||
|
/// Bounds for the axis (all data points outside these limits will not be represented)
|
||||||
|
pub bounds: [f64; 2],
|
||||||
|
/// A list of labels to put to the left or below the axis
|
||||||
|
pub labels: Option<Vec<Span<'a>>>,
|
||||||
|
/// The style used to draw the axis itself
|
||||||
|
pub style: Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for Axis<'a> {
|
||||||
|
fn default() -> Axis<'a> {
|
||||||
|
Axis {
|
||||||
|
title: None,
|
||||||
|
bounds: [0.0, 0.0],
|
||||||
|
labels: None,
|
||||||
|
style: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl<'a> Axis<'a> {
|
||||||
|
pub fn title<T>(mut self, title: T) -> Axis<'a>
|
||||||
|
where
|
||||||
|
T: Into<Spans<'a>>,
|
||||||
|
{
|
||||||
|
self.title = Some(title.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> {
|
||||||
|
self.bounds = bounds;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn labels(mut self, labels: Vec<Span<'a>>) -> Axis<'a> {
|
||||||
|
self.labels = Some(labels);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn style(mut self, style: Style) -> Axis<'a> {
|
||||||
|
self.style = style;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A group of data points
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Dataset<'a> {
|
||||||
|
/// Name of the dataset (used in the legend if shown)
|
||||||
|
name: Cow<'a, str>,
|
||||||
|
/// A reference to the actual data
|
||||||
|
data: &'a [(f64, f64)],
|
||||||
|
/// Symbol used for each points of this dataset
|
||||||
|
marker: symbols::Marker,
|
||||||
|
/// Determines graph type used for drawing points
|
||||||
|
graph_type: GraphType,
|
||||||
|
/// Style used to plot this dataset
|
||||||
|
style: Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for Dataset<'a> {
|
||||||
|
fn default() -> Dataset<'a> {
|
||||||
|
Dataset {
|
||||||
|
name: Cow::from(""),
|
||||||
|
data: &[],
|
||||||
|
marker: symbols::Marker::Dot,
|
||||||
|
graph_type: GraphType::Scatter,
|
||||||
|
style: Style::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl<'a> Dataset<'a> {
|
||||||
|
pub fn name<S>(mut self, name: S) -> Dataset<'a>
|
||||||
|
where
|
||||||
|
S: Into<Cow<'a, str>>,
|
||||||
|
{
|
||||||
|
self.name = name.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data(mut self, data: &'a [(f64, f64)]) -> Dataset<'a> {
|
||||||
|
self.data = data;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> {
|
||||||
|
self.marker = marker;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a> {
|
||||||
|
self.graph_type = graph_type;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn style(mut self, style: Style) -> Dataset<'a> {
|
||||||
|
self.style = style;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A container that holds all the infos about where to display each elements of the chart (axis,
|
||||||
|
/// labels, legend, ...).
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq)]
|
||||||
|
struct ChartLayout {
|
||||||
|
/// Location of the title of the x axis
|
||||||
|
title_x: Option<(u16, u16)>,
|
||||||
|
/// Location of the title of the y axis
|
||||||
|
title_y: Option<(u16, u16)>,
|
||||||
|
/// Location of the first label of the x axis
|
||||||
|
label_x: Option<u16>,
|
||||||
|
/// Location of the first label of the y axis
|
||||||
|
label_y: Option<u16>,
|
||||||
|
/// Y coordinate of the horizontal axis
|
||||||
|
axis_x: Option<u16>,
|
||||||
|
/// X coordinate of the vertical axis
|
||||||
|
axis_y: Option<u16>,
|
||||||
|
/// Area of the legend
|
||||||
|
legend_area: Option<Rect>,
|
||||||
|
/// Area of the graph
|
||||||
|
graph_area: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A "custom" chart, just a slightly tweaked [`tui::widgets::Chart`] from tui-rs, but with greater control over the
|
||||||
|
/// legend, and built with the idea of drawing data points relative to a time-based x-axis.
|
||||||
|
///
|
||||||
|
/// Main changes:
|
||||||
|
/// - Styling option for the legend box
|
||||||
|
/// - Automatically trimming out redundant draws in the x-bounds.
|
||||||
|
/// - Automatic interpolation to points that fall *just* outside of the screen.
|
||||||
|
///
|
||||||
|
/// TODO: Support for putting the legend on the left side.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TimeChart<'a> {
|
||||||
|
/// A block to display around the widget eventually
|
||||||
|
block: Option<Block<'a>>,
|
||||||
|
/// The horizontal axis
|
||||||
|
x_axis: Axis<'a>,
|
||||||
|
/// The vertical axis
|
||||||
|
y_axis: Axis<'a>,
|
||||||
|
/// A reference to the datasets
|
||||||
|
datasets: Vec<Dataset<'a>>,
|
||||||
|
/// The widget base style
|
||||||
|
style: Style,
|
||||||
|
/// The legend's style
|
||||||
|
legend_style: Style,
|
||||||
|
/// Constraints used to determine whether the legend should be shown or not
|
||||||
|
hidden_legend_constraints: (Constraint, Constraint),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const DEFAULT_LEGEND_CONSTRAINTS: (Constraint, Constraint) =
|
||||||
|
(Constraint::Ratio(1, 4), Constraint::Ratio(1, 4));
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl<'a> TimeChart<'a> {
|
||||||
|
/// Creates a new [`TimeChart`].
|
||||||
|
///
|
||||||
|
/// **Note:** `datasets` **must** be sorted!
|
||||||
|
pub fn new(datasets: Vec<Dataset<'a>>) -> TimeChart<'a> {
|
||||||
|
TimeChart {
|
||||||
|
block: None,
|
||||||
|
x_axis: Axis::default(),
|
||||||
|
y_axis: Axis::default(),
|
||||||
|
style: Default::default(),
|
||||||
|
legend_style: Default::default(),
|
||||||
|
datasets,
|
||||||
|
hidden_legend_constraints: DEFAULT_LEGEND_CONSTRAINTS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn block(mut self, block: Block<'a>) -> TimeChart<'a> {
|
||||||
|
self.block = Some(block);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn style(mut self, style: Style) -> TimeChart<'a> {
|
||||||
|
self.style = style;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn legend_style(mut self, legend_style: Style) -> TimeChart<'a> {
|
||||||
|
self.legend_style = legend_style;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn x_axis(mut self, axis: Axis<'a>) -> TimeChart<'a> {
|
||||||
|
self.x_axis = axis;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn y_axis(mut self, axis: Axis<'a>) -> TimeChart<'a> {
|
||||||
|
self.y_axis = axis;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the constraints used to determine whether the legend should be shown or not.
|
||||||
|
pub fn hidden_legend_constraints(
|
||||||
|
mut self, constraints: (Constraint, Constraint),
|
||||||
|
) -> TimeChart<'a> {
|
||||||
|
self.hidden_legend_constraints = constraints;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the internal layout of the chart given the area. If the area is too small some
|
||||||
|
/// elements may be automatically hidden
|
||||||
|
fn layout(&self, area: Rect) -> ChartLayout {
|
||||||
|
let mut layout = ChartLayout::default();
|
||||||
|
if area.height == 0 || area.width == 0 {
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
let mut x = area.left();
|
||||||
|
let mut y = area.bottom() - 1;
|
||||||
|
|
||||||
|
if self.x_axis.labels.is_some() && y > area.top() {
|
||||||
|
layout.label_x = Some(y);
|
||||||
|
y -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.label_y = self.y_axis.labels.as_ref().and(Some(x));
|
||||||
|
x += self.max_width_of_labels_left_of_y_axis(area);
|
||||||
|
|
||||||
|
if self.x_axis.labels.is_some() && y > area.top() {
|
||||||
|
layout.axis_x = Some(y);
|
||||||
|
y -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.y_axis.labels.is_some() && x + 1 < area.right() {
|
||||||
|
layout.axis_y = Some(x);
|
||||||
|
x += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if x < area.right() && y > 1 {
|
||||||
|
layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref title) = self.x_axis.title {
|
||||||
|
let w = title.width() as u16;
|
||||||
|
if w < layout.graph_area.width && layout.graph_area.height > 2 {
|
||||||
|
layout.title_x = Some((x + layout.graph_area.width - w, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref title) = self.y_axis.title {
|
||||||
|
let w = title.width() as u16;
|
||||||
|
if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
|
||||||
|
layout.title_y = Some((x, area.top()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(inner_width) = self.datasets.iter().map(|d| d.name.width() as u16).max() {
|
||||||
|
let legend_width = inner_width + 2;
|
||||||
|
let legend_height = self.datasets.len() as u16 + 2;
|
||||||
|
let max_legend_width = self
|
||||||
|
.hidden_legend_constraints
|
||||||
|
.0
|
||||||
|
.apply(layout.graph_area.width);
|
||||||
|
let max_legend_height = self
|
||||||
|
.hidden_legend_constraints
|
||||||
|
.1
|
||||||
|
.apply(layout.graph_area.height);
|
||||||
|
if inner_width > 0
|
||||||
|
&& legend_width < max_legend_width
|
||||||
|
&& legend_height < max_legend_height
|
||||||
|
{
|
||||||
|
layout.legend_area = Some(Rect::new(
|
||||||
|
layout.graph_area.right() - legend_width,
|
||||||
|
layout.graph_area.top(),
|
||||||
|
legend_width,
|
||||||
|
legend_height,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layout
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_width_of_labels_left_of_y_axis(&self, area: Rect) -> u16 {
|
||||||
|
let mut max_width = self
|
||||||
|
.y_axis
|
||||||
|
.labels
|
||||||
|
.as_ref()
|
||||||
|
.map(|l| l.iter().map(Span::width).max().unwrap_or_default() as u16)
|
||||||
|
.unwrap_or_default();
|
||||||
|
if let Some(ref x_labels) = self.x_axis.labels {
|
||||||
|
if !x_labels.is_empty() {
|
||||||
|
max_width = max(max_width, x_labels[0].content.width() as u16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// labels of y axis and first label of x axis can take at most 1/3rd of the total width
|
||||||
|
max_width.min(area.width / 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_x_labels(
|
||||||
|
&mut self, buf: &mut Buffer, layout: &ChartLayout, chart_area: Rect, graph_area: Rect,
|
||||||
|
) {
|
||||||
|
let y = match layout.label_x {
|
||||||
|
Some(y) => y,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let labels = self.x_axis.labels.as_ref().unwrap();
|
||||||
|
let labels_len = labels.len() as u16;
|
||||||
|
if labels_len < 2 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let width_between_ticks = graph_area.width / (labels_len - 1);
|
||||||
|
for (i, label) in labels.iter().enumerate() {
|
||||||
|
let label_width = label.width() as u16;
|
||||||
|
let label_width = if i == 0 {
|
||||||
|
// the first label is put between the left border of the chart and the y axis.
|
||||||
|
graph_area
|
||||||
|
.left()
|
||||||
|
.saturating_sub(chart_area.left())
|
||||||
|
.min(label_width)
|
||||||
|
} else {
|
||||||
|
// other labels are put on the left of each tick on the x axis
|
||||||
|
width_between_ticks.min(label_width)
|
||||||
|
};
|
||||||
|
buf.set_span(
|
||||||
|
graph_area.left() + i as u16 * width_between_ticks - label_width,
|
||||||
|
y,
|
||||||
|
label,
|
||||||
|
label_width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_y_labels(
|
||||||
|
&mut self, buf: &mut Buffer, layout: &ChartLayout, chart_area: Rect, graph_area: Rect,
|
||||||
|
) {
|
||||||
|
let x = match layout.label_y {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let labels = self.y_axis.labels.as_ref().unwrap();
|
||||||
|
let labels_len = labels.len() as u16;
|
||||||
|
let label_width = graph_area.left().saturating_sub(chart_area.left());
|
||||||
|
for (i, label) in labels.iter().enumerate() {
|
||||||
|
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
|
||||||
|
if dy < graph_area.bottom() {
|
||||||
|
buf.set_span(x, graph_area.bottom() - 1 - dy, label, label_width as u16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for TimeChart<'a> {
|
||||||
|
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||||
|
if area.area() == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buf.set_style(area, self.style);
|
||||||
|
// Sample the style of the entire widget. This sample will be used to reset the style of
|
||||||
|
// the cells that are part of the components put on top of the graph area (i.e legend and
|
||||||
|
// axis names).
|
||||||
|
let original_style = buf.get(area.left(), area.top()).style();
|
||||||
|
|
||||||
|
let chart_area = match self.block.take() {
|
||||||
|
Some(b) => {
|
||||||
|
let inner_area = b.inner(area);
|
||||||
|
b.render(area, buf);
|
||||||
|
inner_area
|
||||||
|
}
|
||||||
|
None => area,
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout = self.layout(chart_area);
|
||||||
|
let graph_area = layout.graph_area;
|
||||||
|
if graph_area.width < 1 || graph_area.height < 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.render_x_labels(buf, &layout, chart_area, graph_area);
|
||||||
|
self.render_y_labels(buf, &layout, chart_area, graph_area);
|
||||||
|
|
||||||
|
if let Some(y) = layout.axis_x {
|
||||||
|
for x in graph_area.left()..graph_area.right() {
|
||||||
|
buf.get_mut(x, y)
|
||||||
|
.set_symbol(symbols::line::HORIZONTAL)
|
||||||
|
.set_style(self.x_axis.style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = layout.axis_y {
|
||||||
|
for y in graph_area.top()..graph_area.bottom() {
|
||||||
|
buf.get_mut(x, y)
|
||||||
|
.set_symbol(symbols::line::VERTICAL)
|
||||||
|
.set_style(self.y_axis.style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(y) = layout.axis_x {
|
||||||
|
if let Some(x) = layout.axis_y {
|
||||||
|
buf.get_mut(x, y)
|
||||||
|
.set_symbol(symbols::line::BOTTOM_LEFT)
|
||||||
|
.set_style(self.x_axis.style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for dataset in &self.datasets {
|
||||||
|
Canvas::default()
|
||||||
|
.background_color(self.style.bg.unwrap_or(Color::Reset))
|
||||||
|
.x_bounds(self.x_axis.bounds)
|
||||||
|
.y_bounds(self.y_axis.bounds)
|
||||||
|
.marker(dataset.marker)
|
||||||
|
.paint(|ctx| {
|
||||||
|
let start_bound = self.x_axis.bounds[0];
|
||||||
|
let end_bound = self.x_axis.bounds[1];
|
||||||
|
|
||||||
|
let (start_index, interpolate_start) = get_start(dataset, start_bound);
|
||||||
|
let (end_index, interpolate_end) = get_end(dataset, end_bound);
|
||||||
|
|
||||||
|
let data_slice = &dataset.data[start_index..end_index];
|
||||||
|
|
||||||
|
ctx.draw(&Points {
|
||||||
|
coords: data_slice,
|
||||||
|
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(interpolate_start) = interpolate_start {
|
||||||
|
if let (Some(older_point), Some(newer_point)) = (
|
||||||
|
dataset.data.get(interpolate_start),
|
||||||
|
dataset.data.get(interpolate_start + 1),
|
||||||
|
) {
|
||||||
|
let interpolated_point = (
|
||||||
|
self.x_axis.bounds[0],
|
||||||
|
interpolate_point(older_point, newer_point, self.x_axis.bounds[0]),
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.draw(&Points {
|
||||||
|
coords: &[interpolated_point],
|
||||||
|
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||||
|
});
|
||||||
|
|
||||||
|
if let GraphType::Line = dataset.graph_type {
|
||||||
|
ctx.draw(&Line {
|
||||||
|
x1: interpolated_point.0,
|
||||||
|
y1: interpolated_point.1,
|
||||||
|
x2: newer_point.0,
|
||||||
|
y2: newer_point.1,
|
||||||
|
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let GraphType::Line = dataset.graph_type {
|
||||||
|
for data in data_slice.windows(2) {
|
||||||
|
ctx.draw(&Line {
|
||||||
|
x1: data[0].0,
|
||||||
|
y1: data[0].1,
|
||||||
|
x2: data[1].0,
|
||||||
|
y2: data[1].1,
|
||||||
|
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(interpolate_end) = interpolate_end {
|
||||||
|
if let (Some(older_point), Some(newer_point)) = (
|
||||||
|
dataset.data.get(interpolate_end - 1),
|
||||||
|
dataset.data.get(interpolate_end),
|
||||||
|
) {
|
||||||
|
let interpolated_point = (
|
||||||
|
self.x_axis.bounds[1],
|
||||||
|
interpolate_point(older_point, newer_point, self.x_axis.bounds[1]),
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.draw(&Points {
|
||||||
|
coords: &[interpolated_point],
|
||||||
|
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||||
|
});
|
||||||
|
|
||||||
|
if let GraphType::Line = dataset.graph_type {
|
||||||
|
ctx.draw(&Line {
|
||||||
|
x1: older_point.0,
|
||||||
|
y1: older_point.1,
|
||||||
|
x2: interpolated_point.0,
|
||||||
|
y2: interpolated_point.1,
|
||||||
|
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.render(graph_area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(legend_area) = layout.legend_area {
|
||||||
|
buf.set_style(legend_area, original_style);
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(self.legend_style)
|
||||||
|
.render(legend_area, buf);
|
||||||
|
for (i, dataset) in self.datasets.iter().enumerate() {
|
||||||
|
buf.set_string(
|
||||||
|
legend_area.x + 1,
|
||||||
|
legend_area.y + 1 + i as u16,
|
||||||
|
&dataset.name,
|
||||||
|
dataset.style,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((x, y)) = layout.title_x {
|
||||||
|
let title = self.x_axis.title.unwrap();
|
||||||
|
let width = graph_area.right().saturating_sub(x);
|
||||||
|
buf.set_style(
|
||||||
|
Rect {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
original_style,
|
||||||
|
);
|
||||||
|
buf.set_spans(x, y, &title, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((x, y)) = layout.title_y {
|
||||||
|
let title = self.y_axis.title.unwrap();
|
||||||
|
let width = graph_area.right().saturating_sub(x);
|
||||||
|
buf.set_style(
|
||||||
|
Rect {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
original_style,
|
||||||
|
);
|
||||||
|
buf.set_spans(x, y, &title, width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bin_cmp(a: &f64, b: &f64) -> Ordering {
|
||||||
|
// TODO: Switch to `total_cmp` on 1.62
|
||||||
|
a.partial_cmp(b).unwrap_or(Ordering::Equal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the start index and potential interpolation index given the start time and the dataset.
|
||||||
|
fn get_start(dataset: &Dataset<'_>, start_bound: f64) -> (usize, Option<usize>) {
|
||||||
|
match dataset
|
||||||
|
.data
|
||||||
|
.binary_search_by(|(x, _y)| bin_cmp(x, &start_bound))
|
||||||
|
{
|
||||||
|
Ok(index) => (index, None),
|
||||||
|
Err(index) => (index, index.checked_sub(1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the end position and potential interpolation index given the end time and the dataset.
|
||||||
|
fn get_end(dataset: &Dataset<'_>, end_bound: f64) -> (usize, Option<usize>) {
|
||||||
|
match dataset
|
||||||
|
.data
|
||||||
|
.binary_search_by(|(x, _y)| bin_cmp(x, &end_bound))
|
||||||
|
{
|
||||||
|
// In the success case, this means we found an index. Add one since we want to include this index and we
|
||||||
|
// expect to use the returned index as part of a (m..n) range.
|
||||||
|
Ok(index) => (index.saturating_add(1), None),
|
||||||
|
// In the fail case, this means we did not find an index, and the returned index is where one would *insert*
|
||||||
|
// the location. This index is where one would insert to fit inside the dataset - and since this is an end
|
||||||
|
// bound, index is, in a sense, already "+1" for our range later.
|
||||||
|
Err(index) => (index, {
|
||||||
|
let sum = index.checked_add(1);
|
||||||
|
match sum {
|
||||||
|
Some(s) if s < dataset.data.len() => sum,
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the y-axis value for a given `x`, given two points to draw a line between.
|
||||||
|
fn interpolate_point(older_point: &(f64, f64), newer_point: &(f64, f64), x: f64) -> f64 {
|
||||||
|
let delta_x = newer_point.0 - older_point.0;
|
||||||
|
let delta_y = newer_point.1 - older_point.1;
|
||||||
|
let slope = delta_y / delta_x;
|
||||||
|
|
||||||
|
(older_point.1 + (x - older_point.0) * slope).max(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn time_chart_test_interpolation() {
|
||||||
|
let data = [(-3.0, 8.0), (-1.0, 6.0), (0.0, 5.0)];
|
||||||
|
|
||||||
|
assert_eq!(interpolate_point(&data[1], &data[2], 0.0), 5.0);
|
||||||
|
assert_eq!(interpolate_point(&data[1], &data[2], -0.25), 5.25);
|
||||||
|
assert_eq!(interpolate_point(&data[1], &data[2], -0.5), 5.5);
|
||||||
|
assert_eq!(interpolate_point(&data[0], &data[1], -1.0), 6.0);
|
||||||
|
assert_eq!(interpolate_point(&data[0], &data[1], -1.5), 6.5);
|
||||||
|
assert_eq!(interpolate_point(&data[0], &data[1], -2.0), 7.0);
|
||||||
|
assert_eq!(interpolate_point(&data[0], &data[1], -2.5), 7.5);
|
||||||
|
assert_eq!(interpolate_point(&data[0], &data[1], -3.0), 8.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn time_chart_test_data_trimming() {
|
||||||
|
// Quick test on a completely empty dataset...
|
||||||
|
{
|
||||||
|
let data = [];
|
||||||
|
let dataset = Dataset::default().data(&data);
|
||||||
|
|
||||||
|
assert_eq!(get_start(&dataset, -100.0), (0, None));
|
||||||
|
assert_eq!(get_start(&dataset, -3.0), (0, None));
|
||||||
|
|
||||||
|
assert_eq!(get_end(&dataset, 0.0), (0, None));
|
||||||
|
assert_eq!(get_end(&dataset, 100.0), (0, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = [
|
||||||
|
(-3.0, 8.0),
|
||||||
|
(-2.5, 15.0),
|
||||||
|
(-2.0, 9.0),
|
||||||
|
(-1.0, 6.0),
|
||||||
|
(0.0, 5.0),
|
||||||
|
];
|
||||||
|
let dataset = Dataset::default().data(&data);
|
||||||
|
|
||||||
|
// Test start point cases (miss and hit)
|
||||||
|
assert_eq!(get_start(&dataset, -100.0), (0, None));
|
||||||
|
assert_eq!(get_start(&dataset, -3.0), (0, None));
|
||||||
|
assert_eq!(get_start(&dataset, -2.8), (1, Some(0)));
|
||||||
|
assert_eq!(get_start(&dataset, -2.5), (1, None));
|
||||||
|
assert_eq!(get_start(&dataset, -2.4), (2, Some(1)));
|
||||||
|
|
||||||
|
// Test end point cases (miss and hit)
|
||||||
|
assert_eq!(get_end(&dataset, -2.5), (2, None));
|
||||||
|
assert_eq!(get_end(&dataset, -2.4), (2, Some(3)));
|
||||||
|
assert_eq!(get_end(&dataset, -1.4), (3, Some(4)));
|
||||||
|
assert_eq!(get_end(&dataset, -1.0), (4, None));
|
||||||
|
assert_eq!(get_end(&dataset, 0.0), (5, None));
|
||||||
|
assert_eq!(get_end(&dataset, 1.0), (5, None));
|
||||||
|
assert_eq!(get_end(&dataset, 100.0), (5, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LegendTestCase {
|
||||||
|
chart_area: Rect,
|
||||||
|
hidden_legend_constraints: (Constraint, Constraint),
|
||||||
|
legend_area: Option<Rect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test from the original tui-rs [`Chart`](tui::widgets::Chart).
|
||||||
|
#[test]
|
||||||
|
fn it_should_hide_the_legend() {
|
||||||
|
let data = [(0.0, 5.0), (1.0, 6.0), (3.0, 7.0)];
|
||||||
|
let cases = [
|
||||||
|
LegendTestCase {
|
||||||
|
chart_area: Rect::new(0, 0, 100, 100),
|
||||||
|
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
|
||||||
|
legend_area: Some(Rect::new(88, 0, 12, 12)),
|
||||||
|
},
|
||||||
|
LegendTestCase {
|
||||||
|
chart_area: Rect::new(0, 0, 100, 100),
|
||||||
|
hidden_legend_constraints: (Constraint::Ratio(1, 10), Constraint::Ratio(1, 4)),
|
||||||
|
legend_area: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for case in &cases {
|
||||||
|
let datasets = (0..10)
|
||||||
|
.map(|i| {
|
||||||
|
let name = format!("Dataset #{}", i);
|
||||||
|
Dataset::default().name(name).data(&data)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let chart = TimeChart::new(datasets)
|
||||||
|
.x_axis(Axis::default().title("X axis"))
|
||||||
|
.y_axis(Axis::default().title("Y axis"))
|
||||||
|
.hidden_legend_constraints(case.hidden_legend_constraints);
|
||||||
|
let layout = chart.layout(case.chart_area);
|
||||||
|
assert_eq!(layout.legend_area, case.legend_area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
273
src/canvas/components/time_graph.rs
Normal file
273
src/canvas/components/time_graph.rs
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use tui::{
|
||||||
|
backend::Backend,
|
||||||
|
layout::{Constraint, Rect},
|
||||||
|
style::Style,
|
||||||
|
symbols::Marker,
|
||||||
|
text::{Span, Spans},
|
||||||
|
widgets::{Block, Borders, GraphType},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
use concat_string::concat_string;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
use super::{Axis, Dataset, TimeChart};
|
||||||
|
|
||||||
|
/// A single graph point.
|
||||||
|
pub type Point = (f64, f64);
|
||||||
|
|
||||||
|
/// Represents the data required by the [`TimeGraph`].
|
||||||
|
pub struct GraphData<'a> {
|
||||||
|
pub points: &'a [Point],
|
||||||
|
pub style: Style,
|
||||||
|
pub name: Option<Cow<'a, str>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TimeGraph<'a> {
|
||||||
|
/// Whether to use a dot marker over the default braille markers.
|
||||||
|
pub use_dot: bool,
|
||||||
|
|
||||||
|
/// The min and max x boundaries. Expects a f64 representing the time range in milliseconds.
|
||||||
|
pub x_bounds: [u64; 2],
|
||||||
|
|
||||||
|
/// Whether to hide the time/x-labels.
|
||||||
|
pub hide_x_labels: bool,
|
||||||
|
|
||||||
|
/// The min and max y boundaries.
|
||||||
|
pub y_bounds: [f64; 2],
|
||||||
|
|
||||||
|
/// Any y-labels.
|
||||||
|
pub y_labels: &'a [Cow<'a, str>],
|
||||||
|
|
||||||
|
/// The graph style.
|
||||||
|
pub graph_style: Style,
|
||||||
|
|
||||||
|
/// The border style.
|
||||||
|
pub border_style: Style,
|
||||||
|
|
||||||
|
/// The graph title.
|
||||||
|
pub title: Cow<'a, str>,
|
||||||
|
|
||||||
|
/// Whether this graph is expanded.
|
||||||
|
pub is_expanded: bool,
|
||||||
|
|
||||||
|
/// The title style.
|
||||||
|
pub title_style: Style,
|
||||||
|
|
||||||
|
/// Any legend constraints.
|
||||||
|
pub legend_constraints: Option<(Constraint, Constraint)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TimeGraph<'a> {
|
||||||
|
/// Generates the [`Axis`] for the x-axis.
|
||||||
|
fn generate_x_axis(&self) -> Axis<'_> {
|
||||||
|
// Due to how we display things, we need to adjust the time bound values.
|
||||||
|
let time_start = -(self.x_bounds[1] as f64);
|
||||||
|
let adjusted_x_bounds = [time_start, 0.0];
|
||||||
|
|
||||||
|
if self.hide_x_labels {
|
||||||
|
Axis::default().bounds(adjusted_x_bounds)
|
||||||
|
} else {
|
||||||
|
let x_labels = vec![
|
||||||
|
Span::raw(concat_string!((self.x_bounds[1] / 1000).to_string(), "s")),
|
||||||
|
Span::raw(concat_string!((self.x_bounds[0] / 1000).to_string(), "s")),
|
||||||
|
];
|
||||||
|
|
||||||
|
Axis::default()
|
||||||
|
.bounds(adjusted_x_bounds)
|
||||||
|
.labels(x_labels)
|
||||||
|
.style(self.graph_style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates the [`Axis`] for the y-axis.
|
||||||
|
fn generate_y_axis(&self) -> Axis<'_> {
|
||||||
|
Axis::default()
|
||||||
|
.bounds(self.y_bounds)
|
||||||
|
.style(self.graph_style)
|
||||||
|
.labels(
|
||||||
|
self.y_labels
|
||||||
|
.iter()
|
||||||
|
.map(|label| Span::raw(label.clone()))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a title for the [`TimeGraph`] widget, given the available space.
|
||||||
|
fn generate_title(&self, draw_loc: Rect) -> Spans<'_> {
|
||||||
|
if self.is_expanded {
|
||||||
|
let title_base = concat_string!(self.title, "── Esc to go back ");
|
||||||
|
Spans::from(vec![
|
||||||
|
Span::styled(self.title.as_ref(), self.title_style),
|
||||||
|
Span::styled(
|
||||||
|
concat_string!(
|
||||||
|
"─",
|
||||||
|
"─".repeat(usize::from(draw_loc.width).saturating_sub(
|
||||||
|
UnicodeSegmentation::graphemes(title_base.as_str(), true).count() + 2
|
||||||
|
)),
|
||||||
|
"─ Esc to go back "
|
||||||
|
),
|
||||||
|
self.border_style,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
Spans::from(Span::styled(self.title.as_ref(), self.title_style))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a time graph at [`Rect`] location provided by `draw_loc`. A time graph is used to display data points
|
||||||
|
/// throughout time in the x-axis.
|
||||||
|
///
|
||||||
|
/// This time graph:
|
||||||
|
/// - Draws with the higher time value on the left, and lower on the right.
|
||||||
|
/// - Expects a [`TimeGraph`] to be passed in, which details how to draw the graph.
|
||||||
|
/// - Expects `graph_data`, which represents *what* data to draw, and various details like style and optional legends.
|
||||||
|
pub fn draw_time_graph<B: Backend>(
|
||||||
|
&self, f: &mut Frame<'_, B>, draw_loc: Rect, graph_data: &[GraphData<'_>],
|
||||||
|
) {
|
||||||
|
let x_axis = self.generate_x_axis();
|
||||||
|
let y_axis = self.generate_y_axis();
|
||||||
|
|
||||||
|
// This is some ugly manual loop unswitching. Maybe unnecessary.
|
||||||
|
let data = if self.use_dot {
|
||||||
|
graph_data
|
||||||
|
.iter()
|
||||||
|
.map(|data| create_dataset(data, Marker::Dot))
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
graph_data
|
||||||
|
.iter()
|
||||||
|
.map(|data| create_dataset(data, Marker::Braille))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
f.render_widget(
|
||||||
|
TimeChart::new(data)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(self.generate_title(draw_loc))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(self.border_style),
|
||||||
|
)
|
||||||
|
.x_axis(x_axis)
|
||||||
|
.y_axis(y_axis)
|
||||||
|
.hidden_legend_constraints(
|
||||||
|
self.legend_constraints
|
||||||
|
.unwrap_or(super::DEFAULT_LEGEND_CONSTRAINTS),
|
||||||
|
),
|
||||||
|
draw_loc,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Dataset`].
|
||||||
|
fn create_dataset<'a>(data: &'a GraphData<'a>, marker: Marker) -> Dataset<'a> {
|
||||||
|
let GraphData {
|
||||||
|
points,
|
||||||
|
style,
|
||||||
|
name,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
let dataset = Dataset::default()
|
||||||
|
.style(*style)
|
||||||
|
.data(points)
|
||||||
|
.graph_type(GraphType::Line)
|
||||||
|
.marker(marker);
|
||||||
|
|
||||||
|
if let Some(name) = name {
|
||||||
|
dataset.name(name.as_ref())
|
||||||
|
} else {
|
||||||
|
dataset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use tui::{
|
||||||
|
layout::Rect,
|
||||||
|
style::{Color, Style},
|
||||||
|
text::{Span, Spans},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::canvas::components::Axis;
|
||||||
|
|
||||||
|
use super::TimeGraph;
|
||||||
|
|
||||||
|
const Y_LABELS: [Cow<'static, str>; 3] = [
|
||||||
|
Cow::Borrowed("0%"),
|
||||||
|
Cow::Borrowed("50%"),
|
||||||
|
Cow::Borrowed("100%"),
|
||||||
|
];
|
||||||
|
|
||||||
|
fn create_time_graph() -> TimeGraph<'static> {
|
||||||
|
TimeGraph {
|
||||||
|
title: " Network ".into(),
|
||||||
|
use_dot: true,
|
||||||
|
x_bounds: [0, 15000],
|
||||||
|
hide_x_labels: false,
|
||||||
|
y_bounds: [0.0, 100.5],
|
||||||
|
y_labels: &Y_LABELS,
|
||||||
|
graph_style: Style::default().fg(Color::Red),
|
||||||
|
border_style: Style::default().fg(Color::Blue),
|
||||||
|
is_expanded: false,
|
||||||
|
title_style: Style::default().fg(Color::Cyan),
|
||||||
|
legend_constraints: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn time_graph_gen_x_axis() {
|
||||||
|
let tg = create_time_graph();
|
||||||
|
|
||||||
|
let x_axis = tg.generate_x_axis();
|
||||||
|
let actual = Axis::default()
|
||||||
|
.bounds([-15000.0, 0.0])
|
||||||
|
.labels(vec![Span::raw("15s"), Span::raw("0s")])
|
||||||
|
.style(Style::default().fg(Color::Red));
|
||||||
|
assert_eq!(x_axis.bounds, actual.bounds);
|
||||||
|
assert_eq!(x_axis.labels, actual.labels);
|
||||||
|
assert_eq!(x_axis.style, actual.style);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn time_graph_gen_y_axis() {
|
||||||
|
let tg = create_time_graph();
|
||||||
|
|
||||||
|
let y_axis = tg.generate_y_axis();
|
||||||
|
let actual = Axis::default()
|
||||||
|
.bounds([0.0, 100.5])
|
||||||
|
.labels(vec![Span::raw("0%"), Span::raw("50%"), Span::raw("100%")])
|
||||||
|
.style(Style::default().fg(Color::Red));
|
||||||
|
|
||||||
|
assert_eq!(y_axis.bounds, actual.bounds);
|
||||||
|
assert_eq!(y_axis.labels, actual.labels);
|
||||||
|
assert_eq!(y_axis.style, actual.style);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn time_graph_gen_title() {
|
||||||
|
let mut tg = create_time_graph();
|
||||||
|
let draw_loc = Rect::new(0, 0, 32, 100);
|
||||||
|
|
||||||
|
let title = tg.generate_title(draw_loc);
|
||||||
|
assert_eq!(
|
||||||
|
title,
|
||||||
|
Spans::from(Span::styled(" Network ", Style::default().fg(Color::Cyan)))
|
||||||
|
);
|
||||||
|
|
||||||
|
tg.is_expanded = true;
|
||||||
|
let title = tg.generate_title(draw_loc);
|
||||||
|
assert_eq!(
|
||||||
|
title,
|
||||||
|
Spans::from(vec![
|
||||||
|
Span::styled(" Network ", Style::default().fg(Color::Cyan)),
|
||||||
|
Span::styled("───── Esc to go back ", Style::default().fg(Color::Blue))
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,2 @@
|
|||||||
pub mod dd_dialog;
|
pub mod dd_dialog;
|
||||||
pub mod help_dialog;
|
pub mod help_dialog;
|
||||||
|
|
||||||
pub use dd_dialog::KillDialog;
|
|
||||||
pub use help_dialog::HelpDialog;
|
|
||||||
|
@ -16,20 +16,8 @@ use crate::{
|
|||||||
const DD_BASE: &str = " Confirm Kill Process ── Esc to close ";
|
const DD_BASE: &str = " Confirm Kill Process ── Esc to close ";
|
||||||
const DD_ERROR_BASE: &str = " Error ── Esc to close ";
|
const DD_ERROR_BASE: &str = " Error ── Esc to close ";
|
||||||
|
|
||||||
pub trait KillDialog {
|
impl Painter {
|
||||||
fn get_dd_spans(&self, app_state: &App) -> Option<Text<'_>>;
|
pub fn get_dd_spans(&self, app_state: &App) -> Option<Text<'_>> {
|
||||||
|
|
||||||
fn draw_dd_confirm_buttons<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, button_draw_loc: &Rect, app_state: &mut App,
|
|
||||||
);
|
|
||||||
|
|
||||||
fn draw_dd_dialog<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, dd_text: Option<Text<'_>>, app_state: &mut App, draw_loc: Rect,
|
|
||||||
) -> bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KillDialog for Painter {
|
|
||||||
fn get_dd_spans(&self, app_state: &App) -> Option<Text<'_>> {
|
|
||||||
if let Some(dd_err) = &app_state.dd_err {
|
if let Some(dd_err) = &app_state.dd_err {
|
||||||
return Some(Text::from(vec![
|
return Some(Text::from(vec![
|
||||||
Spans::default(),
|
Spans::default(),
|
||||||
@ -317,7 +305,7 @@ impl KillDialog for Painter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_dd_dialog<B: Backend>(
|
pub fn draw_dd_dialog<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, dd_text: Option<Text<'_>>, app_state: &mut App, draw_loc: Rect,
|
&self, f: &mut Frame<'_, B>, dd_text: Option<Text<'_>>, app_state: &mut App, draw_loc: Rect,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if let Some(dd_text) = dd_text {
|
if let Some(dd_text) = dd_text {
|
||||||
|
@ -12,15 +12,9 @@ use tui::{
|
|||||||
|
|
||||||
const HELP_BASE: &str = " Help ── Esc to close ";
|
const HELP_BASE: &str = " Help ── Esc to close ";
|
||||||
|
|
||||||
pub trait HelpDialog {
|
|
||||||
fn draw_help_dialog<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: [REFACTOR] Make generic dialog boxes to build off of instead?
|
// TODO: [REFACTOR] Make generic dialog boxes to build off of instead?
|
||||||
impl HelpDialog for Painter {
|
impl Painter {
|
||||||
fn draw_help_dialog<B: Backend>(
|
pub fn draw_help_dialog<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect,
|
||||||
) {
|
) {
|
||||||
let help_title = Spans::from(vec![
|
let help_title = Spans::from(vec![
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
|
use tui::layout::Rect;
|
||||||
|
|
||||||
use crate::app;
|
use crate::app;
|
||||||
use std::cmp::{max, min};
|
use std::{
|
||||||
|
cmp::{max, min},
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
|
||||||
/// Return a (hard)-width vector for column widths.
|
/// Return a (hard)-width vector for column widths.
|
||||||
///
|
///
|
||||||
@ -186,8 +191,7 @@ pub fn get_start_position(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate how many bars are to be
|
/// Calculate how many bars are to be drawn within basic mode's components.
|
||||||
/// drawn within basic mode's components.
|
|
||||||
pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize {
|
pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize {
|
||||||
std::cmp::min(
|
std::cmp::min(
|
||||||
(num_bars_available as f64 * use_percentage / 100.0).round() as usize,
|
(num_bars_available as f64 * use_percentage / 100.0).round() as usize,
|
||||||
@ -195,21 +199,214 @@ pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Interpolates between two points. Mainly used to help fill in tui-rs blanks in certain situations.
|
/// Determine whether a graph x-label should be hidden.
|
||||||
/// It is expected point_one is "further left" compared to point_two.
|
pub fn should_hide_x_label(
|
||||||
/// A point is two floats, in (x, y) form. x is time, y is value.
|
always_hide_time: bool, autohide_time: bool, timer: &mut Option<Instant>, draw_loc: Rect,
|
||||||
pub fn interpolate_points(point_one: &(f64, f64), point_two: &(f64, f64), time: f64) -> f64 {
|
) -> bool {
|
||||||
let delta_x = point_two.0 - point_one.0;
|
use crate::constants::*;
|
||||||
let delta_y = point_two.1 - point_one.1;
|
|
||||||
let slope = delta_y / delta_x;
|
|
||||||
|
|
||||||
(point_one.1 + (time - point_one.0) * slope).max(0.0)
|
if always_hide_time || (autohide_time && timer.is_none()) {
|
||||||
|
true
|
||||||
|
} else if let Some(time) = timer {
|
||||||
|
if Instant::now().duration_since(*time).as_millis() < AUTOHIDE_TIMEOUT_MILLISECONDS.into() {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
*timer = None;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
draw_loc.height < TIME_LABEL_HEIGHT_LIMIT
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_start_position() {
|
||||||
|
use crate::app::ScrollDirection;
|
||||||
|
|
||||||
|
// Scrolling down from start
|
||||||
|
{
|
||||||
|
let mut bar = 0;
|
||||||
|
assert_eq!(
|
||||||
|
get_start_position(10, &ScrollDirection::Down, &mut bar, 0, false),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(bar, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple scrolling down
|
||||||
|
{
|
||||||
|
let mut bar = 0;
|
||||||
|
assert_eq!(
|
||||||
|
get_start_position(10, &ScrollDirection::Down, &mut bar, 1, false),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(bar, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrolling down from the middle high up
|
||||||
|
{
|
||||||
|
let mut bar = 0;
|
||||||
|
assert_eq!(
|
||||||
|
get_start_position(10, &ScrollDirection::Down, &mut bar, 5, false),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(bar, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrolling down into boundary
|
||||||
|
{
|
||||||
|
let mut bar = 0;
|
||||||
|
assert_eq!(
|
||||||
|
get_start_position(10, &ScrollDirection::Down, &mut bar, 11, false),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert_eq!(bar, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrolling down from the with non-zero bar
|
||||||
|
{
|
||||||
|
let mut bar = 5;
|
||||||
|
assert_eq!(
|
||||||
|
get_start_position(10, &ScrollDirection::Down, &mut bar, 15, false),
|
||||||
|
5
|
||||||
|
);
|
||||||
|
assert_eq!(bar, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force redraw scrolling down (e.g. resize)
|
||||||
|
{
|
||||||
|
let mut bar = 5;
|
||||||
|
assert_eq!(
|
||||||
|
get_start_position(15, &ScrollDirection::Down, &mut bar, 15, true),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(bar, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test jumping down
|
||||||
|
{
|
||||||
|
let mut bar = 1;
|
||||||
|
assert_eq!(
|
||||||
|
get_start_position(10, &ScrollDirection::Down, &mut bar, 20, true),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
assert_eq!(bar, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrolling up from bottom
|
||||||
|
{
|
||||||
|
let mut bar = 10;
|
||||||
|
assert_eq!(
|
||||||
|
get_start_position(10, &ScrollDirection::Up, &mut bar, 20, false),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
assert_eq!(bar, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple scrolling up
|
||||||
|
{
|
||||||
|
let mut bar = 10;
|
||||||
|
assert_eq!(
|
||||||
|
get_start_position(10, &ScrollDirection::Up, &mut bar, 19, false),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
assert_eq!(bar, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrolling up from the middle
|
||||||
|
{
|
||||||
|
let mut bar = 10;
|
||||||
|
assert_eq!(
|
||||||
|
get_start_position(10, &ScrollDirection::Up, &mut bar, 10, false),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
assert_eq!(bar, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrolling up into boundary
|
||||||
|
{
|
||||||
|
let mut bar = 10;
|
||||||
|
assert_eq!(
|
||||||
|
get_start_position(10, &ScrollDirection::Up, &mut bar, 9, false),
|
||||||
|
9
|
||||||
|
);
|
||||||
|
assert_eq!(bar, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force redraw scrolling up (e.g. resize)
|
||||||
|
{
|
||||||
|
let mut bar = 5;
|
||||||
|
assert_eq!(
|
||||||
|
get_start_position(10, &ScrollDirection::Up, &mut bar, 15, true),
|
||||||
|
5
|
||||||
|
);
|
||||||
|
assert_eq!(bar, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test jumping up
|
||||||
|
{
|
||||||
|
let mut bar = 10;
|
||||||
|
assert_eq!(
|
||||||
|
get_start_position(10, &ScrollDirection::Up, &mut bar, 0, false),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(bar, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_calculate_basic_use_bars() {
|
||||||
|
// Testing various breakpoints and edge cases.
|
||||||
|
assert_eq!(calculate_basic_use_bars(0.0, 15), 0);
|
||||||
|
assert_eq!(calculate_basic_use_bars(1.0, 15), 0);
|
||||||
|
assert_eq!(calculate_basic_use_bars(5.0, 15), 1);
|
||||||
|
assert_eq!(calculate_basic_use_bars(10.0, 15), 2);
|
||||||
|
assert_eq!(calculate_basic_use_bars(40.0, 15), 6);
|
||||||
|
assert_eq!(calculate_basic_use_bars(45.0, 15), 7);
|
||||||
|
assert_eq!(calculate_basic_use_bars(50.0, 15), 8);
|
||||||
|
assert_eq!(calculate_basic_use_bars(100.0, 15), 15);
|
||||||
|
assert_eq!(calculate_basic_use_bars(150.0, 15), 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_hide_x_label() {
|
||||||
|
use crate::constants::*;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use tui::layout::Rect;
|
||||||
|
|
||||||
|
let rect = Rect::new(0, 0, 10, 10);
|
||||||
|
let small_rect = Rect::new(0, 0, 10, 6);
|
||||||
|
|
||||||
|
let mut under_timer = Some(Instant::now());
|
||||||
|
let mut over_timer =
|
||||||
|
Instant::now().checked_sub(Duration::from_millis(AUTOHIDE_TIMEOUT_MILLISECONDS + 100));
|
||||||
|
|
||||||
|
assert!(should_hide_x_label(true, false, &mut None, rect));
|
||||||
|
assert!(should_hide_x_label(false, true, &mut None, rect));
|
||||||
|
assert!(should_hide_x_label(false, false, &mut None, small_rect));
|
||||||
|
|
||||||
|
assert!(!should_hide_x_label(
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
&mut under_timer,
|
||||||
|
small_rect
|
||||||
|
));
|
||||||
|
assert!(under_timer.is_some());
|
||||||
|
|
||||||
|
assert!(should_hide_x_label(
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
&mut over_timer,
|
||||||
|
small_rect
|
||||||
|
));
|
||||||
|
assert!(over_timer.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_zero_width() {
|
fn test_zero_width() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -222,7 +419,6 @@ mod test {
|
|||||||
true
|
true
|
||||||
),
|
),
|
||||||
vec![],
|
vec![],
|
||||||
"vector should be empty"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,7 +434,6 @@ mod test {
|
|||||||
true
|
true
|
||||||
),
|
),
|
||||||
vec![],
|
vec![],
|
||||||
"vector should be empty"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,7 +449,6 @@ mod test {
|
|||||||
true
|
true
|
||||||
),
|
),
|
||||||
vec![2, 2, 7],
|
vec![2, 2, 7],
|
||||||
"vector should not be empty"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
pub mod config_screen;
|
|
||||||
|
|
||||||
pub use config_screen::*;
|
|
@ -1,33 +0,0 @@
|
|||||||
#![allow(unused_variables)] //FIXME: Remove this
|
|
||||||
#![allow(unused_imports)] //FIXME: Remove this
|
|
||||||
use crate::{app::App, canvas::Painter, constants};
|
|
||||||
use tui::{
|
|
||||||
backend::Backend,
|
|
||||||
layout::Constraint,
|
|
||||||
layout::Direction,
|
|
||||||
layout::Layout,
|
|
||||||
layout::{Alignment, Rect},
|
|
||||||
terminal::Frame,
|
|
||||||
text::Span,
|
|
||||||
widgets::{Block, Borders, Paragraph},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub trait ConfigScreen {
|
|
||||||
fn draw_config_screen<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigScreen for Painter {
|
|
||||||
fn draw_config_screen<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect,
|
|
||||||
) {
|
|
||||||
let config_block = Block::default()
|
|
||||||
.title(Span::styled(" Config ", self.colours.widget_title_style))
|
|
||||||
.style(self.colours.border_style)
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(self.colours.border_style);
|
|
||||||
|
|
||||||
f.render_widget(config_block, draw_loc);
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,15 +9,3 @@ pub mod network_basic;
|
|||||||
pub mod network_graph;
|
pub mod network_graph;
|
||||||
pub mod process_table;
|
pub mod process_table;
|
||||||
pub mod temp_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;
|
|
||||||
|
@ -12,14 +12,8 @@ use tui::{
|
|||||||
widgets::{Block, Paragraph},
|
widgets::{Block, Paragraph},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait BasicTableArrows {
|
impl Painter {
|
||||||
fn draw_basic_table_arrows<B: Backend>(
|
pub fn draw_basic_table_arrows<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BasicTableArrows for Painter {
|
|
||||||
fn draw_basic_table_arrows<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||||
) {
|
) {
|
||||||
if let Some(current_table) = app_state.widget_map.get(&widget_id) {
|
if let Some(current_table) = app_state.widget_map.get(&widget_id) {
|
||||||
|
@ -13,15 +13,8 @@ use tui::{
|
|||||||
};
|
};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
pub trait BatteryDisplayWidget {
|
impl Painter {
|
||||||
fn draw_battery_display<B: Backend>(
|
pub fn draw_battery_display<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
|
||||||
widget_id: u64,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BatteryDisplayWidget for Painter {
|
|
||||||
fn draw_battery_display<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||||
widget_id: u64,
|
widget_id: u64,
|
||||||
) {
|
) {
|
||||||
|
@ -15,14 +15,8 @@ use tui::{
|
|||||||
widgets::{Block, Paragraph},
|
widgets::{Block, Paragraph},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait CpuBasicWidget {
|
impl Painter {
|
||||||
fn draw_basic_cpu<B: Backend>(
|
pub fn draw_basic_cpu<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CpuBasicWidget for Painter {
|
|
||||||
fn draw_basic_cpu<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||||
) {
|
) {
|
||||||
// Skip the first element, it's the "all" element
|
// Skip the first element, it's the "all" element
|
||||||
|
@ -1,51 +1,35 @@
|
|||||||
use once_cell::sync::Lazy;
|
use std::borrow::Cow;
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{layout_manager::WidgetDirection, App},
|
app::{layout_manager::WidgetDirection, App},
|
||||||
canvas::{
|
canvas::{
|
||||||
drawing_utils::{get_column_widths, get_start_position, interpolate_points},
|
components::{GraphData, TimeGraph},
|
||||||
|
drawing_utils::{get_column_widths, get_start_position, should_hide_x_label},
|
||||||
Painter,
|
Painter,
|
||||||
},
|
},
|
||||||
constants::*,
|
constants::*,
|
||||||
data_conversion::ConvertedCpuData,
|
data_conversion::ConvertedCpuData,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use concat_string::concat_string;
|
||||||
|
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
symbols::Marker,
|
|
||||||
terminal::Frame,
|
terminal::Frame,
|
||||||
text::Span,
|
text::Text,
|
||||||
text::{Spans, Text},
|
widgets::{Block, Borders, Row, Table},
|
||||||
widgets::{Axis, Block, Borders, Chart, Dataset, Row, Table},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"];
|
const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"];
|
||||||
const AVG_POSITION: usize = 1;
|
const AVG_POSITION: usize = 1;
|
||||||
const ALL_POSITION: usize = 0;
|
const ALL_POSITION: usize = 0;
|
||||||
|
|
||||||
static CPU_LEGEND_HEADER_LENS: Lazy<Vec<u16>> = Lazy::new(|| {
|
static CPU_LEGEND_HEADER_LENS: [usize; 2] =
|
||||||
CPU_LEGEND_HEADER
|
[CPU_LEGEND_HEADER[0].len(), CPU_LEGEND_HEADER[1].len()];
|
||||||
.iter()
|
|
||||||
.map(|entry| entry.len() as u16)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
});
|
|
||||||
|
|
||||||
pub trait CpuGraphWidget {
|
impl Painter {
|
||||||
fn draw_cpu<B: Backend>(
|
pub fn draw_cpu<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
|
||||||
);
|
|
||||||
fn draw_cpu_graph<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
|
||||||
);
|
|
||||||
fn draw_cpu_legend<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CpuGraphWidget for Painter {
|
|
||||||
fn draw_cpu<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||||
) {
|
) {
|
||||||
if draw_loc.width as f64 * 0.15 <= 6.0 {
|
if draw_loc.width as f64 * 0.15 <= 6.0 {
|
||||||
@ -134,250 +118,93 @@ impl CpuGraphWidget for Painter {
|
|||||||
fn draw_cpu_graph<B: Backend>(
|
fn draw_cpu_graph<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||||
) {
|
) {
|
||||||
|
const Y_BOUNDS: [f64; 2] = [0.0, 100.5];
|
||||||
|
const Y_LABELS: [Cow<'static, str>; 2] = [Cow::Borrowed(" 0%"), Cow::Borrowed("100%")];
|
||||||
|
|
||||||
if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&widget_id) {
|
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 cpu_data = &app_state.canvas_data.cpu_data;
|
||||||
|
let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
|
||||||
let display_time_labels = vec![
|
let x_bounds = [0, cpu_widget_state.current_display_time];
|
||||||
Span::styled(
|
let hide_x_labels = should_hide_x_label(
|
||||||
format!("{}s", cpu_widget_state.current_display_time / 1000),
|
app_state.app_config_fields.hide_time,
|
||||||
self.colours.graph_style,
|
app_state.app_config_fields.autohide_time,
|
||||||
),
|
&mut cpu_widget_state.autohide_timer,
|
||||||
Span::styled("0s".to_string(), self.colours.graph_style),
|
draw_loc,
|
||||||
];
|
|
||||||
|
|
||||||
let y_axis_labels = vec![
|
|
||||||
Span::styled(" 0%", self.colours.graph_style),
|
|
||||||
Span::styled("100%", self.colours.graph_style),
|
|
||||||
];
|
|
||||||
|
|
||||||
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())
|
|
||||||
{
|
|
||||||
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.into()
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
.labels(display_time_labels)
|
|
||||||
};
|
|
||||||
|
|
||||||
let y_axis = Axis::default()
|
|
||||||
.style(self.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 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 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 show_avg_cpu = app_state.app_config_fields.show_average_cpu;
|
||||||
if let Some(to_replace) = cpu.cpu_data.get_mut(start_pos) {
|
let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 };
|
||||||
*to_replace = new_point;
|
let points = {
|
||||||
Some((start_pos, old))
|
let current_scroll_position = cpu_widget_state.scroll_state.current_scroll_position;
|
||||||
} else {
|
if current_scroll_position == ALL_POSITION {
|
||||||
None // Failed to get mutable reference.
|
// This case ensures the other cases cannot have the position be equal to 0.
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None // Point somehow doesn't exist in our data
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None // Point is already "leftmost", no need to interpolate.
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None // There is no point.
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let dataset_vector: Vec<Dataset<'_>> = if current_scroll_position == ALL_POSITION {
|
|
||||||
cpu_data
|
cpu_data
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.rev()
|
.rev()
|
||||||
.map(|(itx, cpu)| {
|
.map(|(itx, cpu)| {
|
||||||
Dataset::default()
|
let style = if show_avg_cpu && itx == AVG_POSITION {
|
||||||
.marker(if use_dot {
|
|
||||||
Marker::Dot
|
|
||||||
} else {
|
|
||||||
Marker::Braille
|
|
||||||
})
|
|
||||||
.style(if show_avg_cpu && itx == AVG_POSITION {
|
|
||||||
self.colours.avg_colour_style
|
self.colours.avg_colour_style
|
||||||
} else if itx == ALL_POSITION {
|
} else if itx == ALL_POSITION {
|
||||||
self.colours.all_colour_style
|
self.colours.all_colour_style
|
||||||
} else {
|
} else {
|
||||||
self.colours.cpu_colour_styles[(itx - 1 // Because of the all position
|
let offset_position = itx - 1; // Because of the all position
|
||||||
- (if show_avg_cpu {
|
self.colours.cpu_colour_styles[(offset_position - show_avg_offset)
|
||||||
AVG_POSITION
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}))
|
|
||||||
% self.colours.cpu_colour_styles.len()]
|
% self.colours.cpu_colour_styles.len()]
|
||||||
|
};
|
||||||
|
|
||||||
|
GraphData {
|
||||||
|
points: &cpu.cpu_data[..],
|
||||||
|
style,
|
||||||
|
name: None,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.data(&cpu.cpu_data[..])
|
.collect::<Vec<_>>()
|
||||||
.graph_type(tui::widgets::GraphType::Line)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
} else if let Some(cpu) = cpu_data.get(current_scroll_position) {
|
} else if let Some(cpu) = cpu_data.get(current_scroll_position) {
|
||||||
vec![Dataset::default()
|
let style = if show_avg_cpu && current_scroll_position == AVG_POSITION {
|
||||||
.marker(if use_dot {
|
|
||||||
Marker::Dot
|
|
||||||
} else {
|
|
||||||
Marker::Braille
|
|
||||||
})
|
|
||||||
.style(if show_avg_cpu && current_scroll_position == AVG_POSITION {
|
|
||||||
self.colours.avg_colour_style
|
self.colours.avg_colour_style
|
||||||
} else {
|
} else {
|
||||||
self.colours.cpu_colour_styles[(cpu_widget_state
|
let offset_position = current_scroll_position - 1; // Because of the all position
|
||||||
.scroll_state
|
self.colours.cpu_colour_styles[(offset_position - show_avg_offset)
|
||||||
.current_scroll_position
|
|
||||||
- 1 // Because of the all position
|
|
||||||
- (if show_avg_cpu {
|
|
||||||
AVG_POSITION
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}))
|
|
||||||
% self.colours.cpu_colour_styles.len()]
|
% self.colours.cpu_colour_styles.len()]
|
||||||
})
|
};
|
||||||
.data(&cpu.cpu_data[..])
|
|
||||||
.graph_type(tui::widgets::GraphType::Line)]
|
vec![GraphData {
|
||||||
|
points: &cpu.cpu_data[..],
|
||||||
|
style,
|
||||||
|
name: None,
|
||||||
|
}]
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
// TODO: Maybe hide load avg if too long? Or maybe the CPU part.
|
||||||
let border_style = if is_on_widget {
|
|
||||||
self.colours.highlighted_border_style
|
|
||||||
} else {
|
|
||||||
self.colours.border_style
|
|
||||||
};
|
|
||||||
|
|
||||||
let title = if cfg!(target_family = "unix") {
|
let title = if cfg!(target_family = "unix") {
|
||||||
let load_avg = app_state.canvas_data.load_avg_data;
|
let load_avg = app_state.canvas_data.load_avg_data;
|
||||||
let load_avg_str = format!(
|
let load_avg_str = format!(
|
||||||
"─ {:.2} {:.2} {:.2} ",
|
"─ {:.2} {:.2} {:.2} ",
|
||||||
load_avg[0], load_avg[1], load_avg[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 {
|
concat_string!(" CPU ", load_avg_str).into()
|
||||||
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 {
|
} else {
|
||||||
Spans::from(vec![
|
" CPU ".into()
|
||||||
Span::styled(" CPU ", self.colours.widget_title_style),
|
|
||||||
Span::styled(load_avg_str, self.colours.widget_title_style),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
} else 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(
|
|
||||||
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 ", self.colours.widget_title_style)])
|
|
||||||
};
|
};
|
||||||
|
|
||||||
f.render_widget(
|
TimeGraph {
|
||||||
Chart::new(dataset_vector)
|
use_dot: app_state.app_config_fields.use_dot,
|
||||||
.block(
|
x_bounds,
|
||||||
Block::default()
|
hide_x_labels,
|
||||||
.title(title)
|
y_bounds: Y_BOUNDS,
|
||||||
.borders(Borders::ALL)
|
y_labels: &Y_LABELS,
|
||||||
.border_style(border_style),
|
graph_style: self.colours.graph_style,
|
||||||
)
|
border_style,
|
||||||
.x_axis(x_axis)
|
title,
|
||||||
.y_axis(y_axis),
|
is_expanded: app_state.is_expanded,
|
||||||
draw_loc,
|
title_style: self.colours.widget_title_style,
|
||||||
);
|
legend_constraints: None,
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
.draw_time_graph(f, draw_loc, &points);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,7 +255,7 @@ impl CpuGraphWidget for Painter {
|
|||||||
&[None, None],
|
&[None, None],
|
||||||
&(CPU_LEGEND_HEADER_LENS
|
&(CPU_LEGEND_HEADER_LENS
|
||||||
.iter()
|
.iter()
|
||||||
.map(|width| Some(*width))
|
.map(|width| Some(*width as u16))
|
||||||
.collect::<Vec<_>>()),
|
.collect::<Vec<_>>()),
|
||||||
&[Some(0.5), Some(0.5)],
|
&[Some(0.5), Some(0.5)],
|
||||||
&(cpu_widget_state
|
&(cpu_widget_state
|
||||||
|
@ -27,15 +27,8 @@ static DISK_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
|
|
||||||
pub trait DiskTableWidget {
|
impl Painter {
|
||||||
fn draw_disk_table<B: Backend>(
|
pub fn draw_disk_table<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool,
|
|
||||||
widget_id: u64,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DiskTableWidget for Painter {
|
|
||||||
fn draw_disk_table<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool,
|
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool,
|
||||||
widget_id: u64,
|
widget_id: u64,
|
||||||
) {
|
) {
|
||||||
|
@ -13,14 +13,8 @@ use tui::{
|
|||||||
widgets::{Block, Paragraph},
|
widgets::{Block, Paragraph},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait MemBasicWidget {
|
impl Painter {
|
||||||
fn draw_basic_memory<B: Backend>(
|
pub fn draw_basic_memory<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MemBasicWidget for Painter {
|
|
||||||
fn draw_basic_memory<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||||
) {
|
) {
|
||||||
let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data;
|
let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data;
|
||||||
|
@ -1,240 +1,72 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
canvas::{drawing_utils::interpolate_points, Painter},
|
canvas::{
|
||||||
constants::*,
|
components::{GraphData, TimeGraph},
|
||||||
|
drawing_utils::should_hide_x_label,
|
||||||
|
Painter,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::{Constraint, Rect},
|
layout::{Constraint, Rect},
|
||||||
symbols::Marker,
|
|
||||||
terminal::Frame,
|
terminal::Frame,
|
||||||
text::Span,
|
|
||||||
text::Spans,
|
|
||||||
widgets::{Axis, Block, Borders, Chart, Dataset},
|
|
||||||
};
|
};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
|
||||||
|
|
||||||
pub trait MemGraphWidget {
|
impl Painter {
|
||||||
fn draw_memory_graph<B: Backend>(
|
pub fn draw_memory_graph<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MemGraphWidget for Painter {
|
|
||||||
fn draw_memory_graph<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||||
) {
|
) {
|
||||||
|
const Y_BOUNDS: [f64; 2] = [0.0, 100.5];
|
||||||
|
const Y_LABELS: [Cow<'static, str>; 2] = [Cow::Borrowed(" 0%"), Cow::Borrowed("100%")];
|
||||||
|
|
||||||
if let Some(mem_widget_state) = app_state.mem_state.widget_states.get_mut(&widget_id) {
|
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 border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
|
||||||
let swap_data: &mut [(f64, f64)] = &mut app_state.canvas_data.swap_data;
|
let x_bounds = [0, mem_widget_state.current_display_time];
|
||||||
|
let hide_x_labels = should_hide_x_label(
|
||||||
let time_start = -(mem_widget_state.current_display_time as f64);
|
app_state.app_config_fields.hide_time,
|
||||||
|
app_state.app_config_fields.autohide_time,
|
||||||
let display_time_labels = vec![
|
&mut mem_widget_state.autohide_timer,
|
||||||
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())
|
|
||||||
{
|
|
||||||
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.into()
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
.labels(display_time_labels)
|
|
||||||
};
|
|
||||||
|
|
||||||
let y_axis = Axis::default()
|
|
||||||
.style(self.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);
|
|
||||||
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
|
|
||||||
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.
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None // Point somehow doesn't exist in our data
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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);
|
|
||||||
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
|
|
||||||
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.
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None // Point somehow doesn't exist in our data
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None // Point is already "leftmost", no need to interpolate.
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None // There is no point.
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut mem_canvas_vec: Vec<Dataset<'_>> = 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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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,
|
draw_loc,
|
||||||
);
|
);
|
||||||
|
let points = {
|
||||||
// Now if you're done, reset any interpolated points!
|
let mut points = Vec::with_capacity(2);
|
||||||
if let Some((index, old_value)) = interpolated_mem_point {
|
if let Some((label_percent, label_frac)) = &app_state.canvas_data.mem_labels {
|
||||||
if let Some(to_replace) = mem_data.get_mut(index) {
|
let mem_label = format!("RAM:{}{}", label_percent, label_frac);
|
||||||
*to_replace = old_value;
|
points.push(GraphData {
|
||||||
|
points: &app_state.canvas_data.mem_data,
|
||||||
|
style: self.colours.ram_style,
|
||||||
|
name: Some(mem_label.into()),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
if let Some((label_percent, label_frac)) = &app_state.canvas_data.swap_labels {
|
||||||
|
let swap_label = format!("SWP:{}{}", label_percent, label_frac);
|
||||||
|
points.push(GraphData {
|
||||||
|
points: &app_state.canvas_data.swap_data,
|
||||||
|
style: self.colours.swap_style,
|
||||||
|
name: Some(swap_label.into()),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((index, old_value)) = interpolated_swap_point {
|
points
|
||||||
if let Some(to_replace) = swap_data.get_mut(index) {
|
};
|
||||||
*to_replace = old_value;
|
|
||||||
}
|
TimeGraph {
|
||||||
|
use_dot: app_state.app_config_fields.use_dot,
|
||||||
|
x_bounds,
|
||||||
|
hide_x_labels,
|
||||||
|
y_bounds: Y_BOUNDS,
|
||||||
|
y_labels: &Y_LABELS,
|
||||||
|
graph_style: self.colours.graph_style,
|
||||||
|
border_style,
|
||||||
|
title: " Memory ".into(),
|
||||||
|
is_expanded: app_state.is_expanded,
|
||||||
|
title_style: self.colours.widget_title_style,
|
||||||
|
legend_constraints: Some((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))),
|
||||||
}
|
}
|
||||||
|
.draw_time_graph(f, draw_loc, &points);
|
||||||
}
|
}
|
||||||
|
|
||||||
if app_state.should_get_widget_bounds() {
|
if app_state.should_get_widget_bounds() {
|
||||||
|
@ -8,14 +8,8 @@ use tui::{
|
|||||||
widgets::{Block, Paragraph},
|
widgets::{Block, Paragraph},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait NetworkBasicWidget {
|
impl Painter {
|
||||||
fn draw_basic_network<B: Backend>(
|
pub fn draw_basic_network<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetworkBasicWidget for Painter {
|
|
||||||
fn draw_basic_network<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||||
) {
|
) {
|
||||||
let divided_loc = Layout::default()
|
let divided_loc = Layout::default()
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{App, AxisScaling},
|
app::{App, AxisScaling},
|
||||||
canvas::{
|
canvas::{
|
||||||
drawing_utils::{get_column_widths, interpolate_points},
|
components::{GraphData, TimeGraph},
|
||||||
Painter,
|
drawing_utils::{get_column_widths, should_hide_x_label},
|
||||||
|
Painter, Point,
|
||||||
},
|
},
|
||||||
constants::*,
|
constants::*,
|
||||||
units::data_units::DataUnit,
|
units::data_units::DataUnit,
|
||||||
@ -16,11 +16,9 @@ use crate::{
|
|||||||
use tui::{
|
use tui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
symbols::Marker,
|
|
||||||
terminal::Frame,
|
terminal::Frame,
|
||||||
text::Span,
|
text::Text,
|
||||||
text::{Spans, Text},
|
widgets::{Block, Borders, Row, Table},
|
||||||
widgets::{Axis, Block, Borders, Chart, Dataset, Row, Table},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const NETWORK_HEADERS: [&str; 4] = ["RX", "TX", "Total RX", "Total TX"];
|
const NETWORK_HEADERS: [&str; 4] = ["RX", "TX", "Total RX", "Total TX"];
|
||||||
@ -32,23 +30,8 @@ static NETWORK_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
|
|
||||||
pub trait NetworkGraphWidget {
|
impl Painter {
|
||||||
fn draw_network<B: Backend>(
|
pub fn draw_network<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
|
||||||
);
|
|
||||||
|
|
||||||
fn draw_network_graph<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
|
||||||
hide_legend: bool,
|
|
||||||
);
|
|
||||||
|
|
||||||
fn draw_network_labels<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetworkGraphWidget for Painter {
|
|
||||||
fn draw_network<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||||
) {
|
) {
|
||||||
if app_state.app_config_fields.use_old_network_legend {
|
if app_state.app_config_fields.use_old_network_legend {
|
||||||
@ -79,12 +62,182 @@ impl NetworkGraphWidget for Painter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_network_graph<B: Backend>(
|
pub fn draw_network_graph<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||||
hide_legend: bool,
|
hide_legend: bool,
|
||||||
) {
|
) {
|
||||||
/// Point is of time, data
|
if let Some(network_widget_state) = app_state.net_state.widget_states.get_mut(&widget_id) {
|
||||||
type Point = (f64, f64);
|
let network_data_rx: &[(f64, f64)] = &app_state.canvas_data.network_data_rx;
|
||||||
|
let network_data_tx: &[(f64, f64)] = &app_state.canvas_data.network_data_tx;
|
||||||
|
let time_start = -(network_widget_state.current_display_time as f64);
|
||||||
|
let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
|
||||||
|
let x_bounds = [0, network_widget_state.current_display_time];
|
||||||
|
let hide_x_labels = should_hide_x_label(
|
||||||
|
app_state.app_config_fields.hide_time,
|
||||||
|
app_state.app_config_fields.autohide_time,
|
||||||
|
&mut network_widget_state.autohide_timer,
|
||||||
|
draw_loc,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
);
|
||||||
|
|
||||||
|
let y_labels = labels.iter().map(|label| label.into()).collect::<Vec<_>>();
|
||||||
|
let y_bounds = [0.0, max_range];
|
||||||
|
|
||||||
|
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 points = if app_state.app_config_fields.use_old_network_legend && !hide_legend {
|
||||||
|
vec![
|
||||||
|
GraphData {
|
||||||
|
points: network_data_rx,
|
||||||
|
style: self.colours.rx_style,
|
||||||
|
name: Some(format!("RX: {:7}", app_state.canvas_data.rx_display).into()),
|
||||||
|
},
|
||||||
|
GraphData {
|
||||||
|
points: network_data_tx,
|
||||||
|
style: self.colours.tx_style,
|
||||||
|
name: Some(format!("TX: {:7}", app_state.canvas_data.tx_display).into()),
|
||||||
|
},
|
||||||
|
GraphData {
|
||||||
|
points: &[],
|
||||||
|
style: self.colours.total_rx_style,
|
||||||
|
name: Some(
|
||||||
|
format!("Total RX: {:7}", app_state.canvas_data.total_rx_display)
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
GraphData {
|
||||||
|
points: &[],
|
||||||
|
style: self.colours.total_tx_style,
|
||||||
|
name: Some(
|
||||||
|
format!("Total TX: {:7}", app_state.canvas_data.total_tx_display)
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
GraphData {
|
||||||
|
points: network_data_rx,
|
||||||
|
style: self.colours.rx_style,
|
||||||
|
name: Some((&app_state.canvas_data.rx_display).into()),
|
||||||
|
},
|
||||||
|
GraphData {
|
||||||
|
points: network_data_tx,
|
||||||
|
style: self.colours.tx_style,
|
||||||
|
name: Some((&app_state.canvas_data.tx_display).into()),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
TimeGraph {
|
||||||
|
use_dot: app_state.app_config_fields.use_dot,
|
||||||
|
x_bounds,
|
||||||
|
hide_x_labels,
|
||||||
|
y_bounds,
|
||||||
|
y_labels: &y_labels,
|
||||||
|
graph_style: self.colours.graph_style,
|
||||||
|
border_style,
|
||||||
|
title: " Network ".into(),
|
||||||
|
is_expanded: app_state.is_expanded,
|
||||||
|
title_style: self.colours.widget_title_style,
|
||||||
|
legend_constraints: Some(legend_constraints),
|
||||||
|
}
|
||||||
|
.draw_time_graph(f, draw_loc, &points);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_network_labels<B: Backend>(
|
||||||
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, 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(self.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::<Vec<_>>()),
|
||||||
|
&[Some(0.25); 4],
|
||||||
|
&(NETWORK_HEADERS_LENS
|
||||||
|
.iter()
|
||||||
|
.map(|s| Some(*s))
|
||||||
|
.collect::<Vec<_>>()),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
.collect::<Vec<_>>()),
|
||||||
|
),
|
||||||
|
draw_loc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the max data point and time given a time.
|
/// Returns the max data point and time given a time.
|
||||||
fn get_max_entry(
|
fn get_max_entry(
|
||||||
@ -148,10 +301,7 @@ impl NetworkGraphWidget for Painter {
|
|||||||
if *max_val == 0.0 {
|
if *max_val == 0.0 {
|
||||||
(
|
(
|
||||||
time_start,
|
time_start,
|
||||||
calculate_missing_max(
|
calculate_missing_max(network_scale_type, network_use_binary_prefix),
|
||||||
network_scale_type,
|
|
||||||
network_use_binary_prefix,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(*best_time, *max_val)
|
(*best_time, *max_val)
|
||||||
@ -172,10 +322,7 @@ impl NetworkGraphWidget for Painter {
|
|||||||
if *max_val == 0.0 {
|
if *max_val == 0.0 {
|
||||||
(
|
(
|
||||||
time_start,
|
time_start,
|
||||||
calculate_missing_max(
|
calculate_missing_max(network_scale_type, network_use_binary_prefix),
|
||||||
network_scale_type,
|
|
||||||
network_use_binary_prefix,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(*best_time, *max_val)
|
(*best_time, *max_val)
|
||||||
@ -197,10 +344,7 @@ impl NetworkGraphWidget for Painter {
|
|||||||
if *max_val == 0.0 {
|
if *max_val == 0.0 {
|
||||||
(
|
(
|
||||||
*best_time,
|
*best_time,
|
||||||
calculate_missing_max(
|
calculate_missing_max(network_scale_type, network_use_binary_prefix),
|
||||||
network_scale_type,
|
|
||||||
network_use_binary_prefix,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(*best_time, *max_val)
|
(*best_time, *max_val)
|
||||||
@ -417,354 +561,3 @@ impl NetworkGraphWidget for Painter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.into()
|
|
||||||
{
|
|
||||||
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::<Vec<_>>();
|
|
||||||
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<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, 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(self.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::<Vec<_>>()),
|
|
||||||
&[Some(0.25); 4],
|
|
||||||
&(NETWORK_HEADERS_LENS
|
|
||||||
.iter()
|
|
||||||
.map(|s| Some(*s))
|
|
||||||
.collect::<Vec<_>>()),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
.collect::<Vec<_>>()),
|
|
||||||
),
|
|
||||||
draw_loc,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -87,46 +87,10 @@ const PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: &[Option<f64>] = &[
|
|||||||
Some(0.2),
|
Some(0.2),
|
||||||
];
|
];
|
||||||
|
|
||||||
pub trait ProcessTableWidget {
|
impl Painter {
|
||||||
/// Draws and handles all process-related drawing. Use this.
|
/// Draws and handles all process-related drawing. Use this.
|
||||||
/// - `widget_id` here represents the widget ID of the process widget itself!
|
/// - `widget_id` here represents the widget ID of the process widget itself!
|
||||||
fn draw_process_features<B: Backend>(
|
pub fn draw_process_features<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
|
||||||
widget_id: u64,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// 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<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
|
||||||
widget_id: u64,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// 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<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
|
||||||
widget_id: u64,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// 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<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
|
||||||
widget_id: u64,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProcessTableWidget for Painter {
|
|
||||||
fn draw_process_features<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||||
widget_id: u64,
|
widget_id: u64,
|
||||||
) {
|
) {
|
||||||
@ -172,6 +136,10 @@ impl ProcessTableWidget for Painter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<B: Backend>(
|
fn draw_processes_table<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||||
widget_id: u64,
|
widget_id: u64,
|
||||||
@ -554,6 +522,11 @@ impl ProcessTableWidget for Painter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<B: Backend>(
|
fn draw_search_field<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||||
widget_id: u64,
|
widget_id: u64,
|
||||||
@ -773,6 +746,11 @@ impl ProcessTableWidget for Painter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<B: Backend>(
|
fn draw_process_sort<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||||
widget_id: u64,
|
widget_id: u64,
|
||||||
|
@ -27,15 +27,8 @@ static TEMP_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
|
|
||||||
pub trait TempTableWidget {
|
impl Painter {
|
||||||
fn draw_temp_table<B: Backend>(
|
pub fn draw_temp_table<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool,
|
|
||||||
widget_id: u64,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TempTableWidget for Painter {
|
|
||||||
fn draw_temp_table<B: Backend>(
|
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool,
|
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool,
|
||||||
widget_id: u64,
|
widget_id: u64,
|
||||||
) {
|
) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user