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:
Clement Tsang 2022-04-29 01:12:14 -04:00 committed by GitHub
commit cddee9d923
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1775 additions and 1299 deletions

7
Cargo.lock generated
View File

@ -232,6 +232,7 @@ dependencies = [
"clap",
"clap_complete",
"clap_mangen",
"concat-string",
"crossterm",
"ctrlc",
"dirs",
@ -354,6 +355,12 @@ dependencies = [
"roff",
]
[[package]]
name = "concat-string"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7439becb5fafc780b6f4de382b1a7a3e70234afe783854a4702ee8adbb838609"
[[package]]
name = "concurrent-queue"
version = "1.2.2"

View File

@ -42,6 +42,7 @@ crossterm = "0.18.2"
ctrlc = { version = "3.1.9", features = ["termination"] }
clap = { version = "3.1.12", features = ["default", "cargo", "wrap_help"] }
cfg-if = "1.0.0"
concat-string = "1.0.1"
dirs = "4.0.0"
futures = "0.3.21"
futures-timer = "3.0.2"

View File

@ -127,9 +127,6 @@ pub struct App {
#[builder(default = false, setter(skip))]
pub basic_mode_use_percent: bool,
#[builder(default = false, setter(skip))]
pub is_config_open: bool,
#[builder(default = false, setter(skip))]
pub did_config_fail_to_save: bool,
@ -218,8 +215,6 @@ impl App {
}
self.is_force_redraw = true;
} else if self.is_config_open {
self.close_config_screen();
} else {
match self.current_widget.widget_type {
BottomWidgetType::Proc => {
@ -297,7 +292,7 @@ impl App {
}
fn ignore_normal_keybinds(&self) -> bool {
self.is_config_open || self.is_in_dialog()
self.is_in_dialog()
}
pub fn on_tab(&mut self) {
@ -910,8 +905,7 @@ impl App {
}
pub fn on_up_key(&mut self) {
if self.is_config_open {
} else if !self.is_in_dialog() {
if !self.is_in_dialog() {
self.decrement_position_count();
} else if self.help_dialog_state.is_showing_help {
self.help_scroll_up();
@ -932,8 +926,7 @@ impl App {
}
pub fn on_down_key(&mut self) {
if self.is_config_open {
} else if !self.is_in_dialog() {
if !self.is_in_dialog() {
self.increment_position_count();
} else if self.help_dialog_state.is_showing_help {
self.help_scroll_down();
@ -954,8 +947,7 @@ impl App {
}
pub fn on_left_key(&mut self) {
if self.is_config_open {
} else if !self.is_in_dialog() {
if !self.is_in_dialog() {
match self.current_widget.widget_type {
BottomWidgetType::ProcSearch => {
let is_in_search_widget = self.is_in_search_widget();
@ -1026,8 +1018,7 @@ impl App {
}
pub fn on_right_key(&mut self) {
if self.is_config_open {
} else if !self.is_in_dialog() {
if !self.is_in_dialog() {
match self.current_widget.widget_type {
BottomWidgetType::ProcSearch => {
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(),
_ => {}
}
} else if self.is_config_open {
}
}
@ -1673,16 +1661,6 @@ impl App {
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.
/// Call this whenever the config value is updated!
// fn update_config_file(&mut self) -> anyhow::Result<()> {
@ -2264,7 +2242,6 @@ impl App {
_ => {}
}
self.reset_multi_tap_keys();
} else if self.is_config_open {
} else if self.help_dialog_state.is_showing_help {
self.help_dialog_state.scroll_state.current_scroll_index = 0;
} else if self.delete_dialog_state.is_showing_dd {
@ -2343,7 +2320,6 @@ impl App {
_ => {}
}
self.reset_multi_tap_keys();
} else if self.is_config_open {
} else if self.help_dialog_state.is_showing_help {
self.help_dialog_state.scroll_state.current_scroll_index = self
.help_dialog_state

View File

@ -9,12 +9,7 @@ use tui::{
Frame, Terminal,
};
// use ordered_float::OrderedFloat;
use canvas_colours::*;
use dialogs::*;
use screens::*;
use widgets::*;
use crate::{
app::{
@ -30,15 +25,14 @@ use crate::{
Pid,
};
pub use self::components::Point;
mod canvas_colours;
mod components;
mod dialogs;
mod drawing_utils;
mod screens;
mod widgets;
/// Point is of time, data
type Point = (f64, f64);
#[derive(Default)]
pub struct DisplayableData {
pub rx_display: String,
@ -207,6 +201,16 @@ impl 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<()> {
if let Some(colours) = &config.colors {
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 {
// Basic mode. This basically removes all graphs but otherwise
// the same info.

10
src/canvas/components.rs Normal file
View 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::*;

View File

@ -0,0 +1 @@

View 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);
}
}
}

View 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))
])
);
}
}

View File

@ -1,5 +1,2 @@
pub mod dd_dialog;
pub mod help_dialog;
pub use dd_dialog::KillDialog;
pub use help_dialog::HelpDialog;

View File

@ -16,20 +16,8 @@ use crate::{
const DD_BASE: &str = " Confirm Kill Process ── Esc to close ";
const DD_ERROR_BASE: &str = " Error ── Esc to close ";
pub trait KillDialog {
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<'_>> {
impl Painter {
pub fn get_dd_spans(&self, app_state: &App) -> Option<Text<'_>> {
if let Some(dd_err) = &app_state.dd_err {
return Some(Text::from(vec![
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,
) -> bool {
if let Some(dd_text) = dd_text {

View File

@ -12,15 +12,9 @@ use tui::{
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?
impl HelpDialog for Painter {
fn draw_help_dialog<B: Backend>(
impl Painter {
pub fn draw_help_dialog<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect,
) {
let help_title = Spans::from(vec![

View File

@ -1,5 +1,10 @@
use tui::layout::Rect;
use crate::app;
use std::cmp::{max, min};
use std::{
cmp::{max, min},
time::Instant,
};
/// Return a (hard)-width vector for column widths.
///
@ -186,8 +191,7 @@ pub fn get_start_position(
}
}
/// Calculate how many bars are to be
/// drawn within basic mode's components.
/// Calculate how many bars are to be drawn within basic mode's components.
pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize {
std::cmp::min(
(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.
/// It is expected point_one is "further left" compared to point_two.
/// A point is two floats, in (x, y) form. x is time, y is value.
pub fn interpolate_points(point_one: &(f64, f64), point_two: &(f64, f64), time: f64) -> f64 {
let delta_x = point_two.0 - point_one.0;
let delta_y = point_two.1 - point_one.1;
let slope = delta_y / delta_x;
/// Determine whether a graph x-label should be hidden.
pub fn should_hide_x_label(
always_hide_time: bool, autohide_time: bool, timer: &mut Option<Instant>, draw_loc: Rect,
) -> bool {
use crate::constants::*;
(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)]
mod test {
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]
fn test_zero_width() {
assert_eq!(
@ -222,7 +419,6 @@ mod test {
true
),
vec![],
"vector should be empty"
);
}
@ -238,7 +434,6 @@ mod test {
true
),
vec![],
"vector should be empty"
);
}
@ -254,7 +449,6 @@ mod test {
true
),
vec![2, 2, 7],
"vector should not be empty"
);
}
}

View File

@ -1,3 +0,0 @@
pub mod config_screen;
pub use config_screen::*;

View File

@ -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);
}
}

View File

@ -9,15 +9,3 @@ pub mod network_basic;
pub mod network_graph;
pub mod process_table;
pub mod temp_table;
pub use basic_table_arrows::BasicTableArrows;
pub use battery_display::BatteryDisplayWidget;
pub use cpu_basic::CpuBasicWidget;
pub use cpu_graph::CpuGraphWidget;
pub use disk_table::DiskTableWidget;
pub use mem_basic::MemBasicWidget;
pub use mem_graph::MemGraphWidget;
pub use network_basic::NetworkBasicWidget;
pub use network_graph::NetworkGraphWidget;
pub use process_table::ProcessTableWidget;
pub use temp_table::TempTableWidget;

View File

@ -12,14 +12,8 @@ use tui::{
widgets::{Block, Paragraph},
};
pub trait BasicTableArrows {
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>(
impl Painter {
pub fn draw_basic_table_arrows<B: Backend>(
&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) {

View File

@ -13,15 +13,8 @@ use tui::{
};
use unicode_segmentation::UnicodeSegmentation;
pub trait BatteryDisplayWidget {
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>(
impl Painter {
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,
) {

View File

@ -15,14 +15,8 @@ use tui::{
widgets::{Block, Paragraph},
};
pub trait CpuBasicWidget {
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>(
impl Painter {
pub fn draw_basic_cpu<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) {
// Skip the first element, it's the "all" element

View File

@ -1,51 +1,35 @@
use once_cell::sync::Lazy;
use unicode_segmentation::UnicodeSegmentation;
use std::borrow::Cow;
use crate::{
app::{layout_manager::WidgetDirection, App},
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,
},
constants::*,
data_conversion::ConvertedCpuData,
};
use concat_string::concat_string;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
symbols::Marker,
terminal::Frame,
text::Span,
text::{Spans, Text},
widgets::{Axis, Block, Borders, Chart, Dataset, Row, Table},
text::Text,
widgets::{Block, Borders, Row, Table},
};
const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"];
const AVG_POSITION: usize = 1;
const ALL_POSITION: usize = 0;
static CPU_LEGEND_HEADER_LENS: Lazy<Vec<u16>> = Lazy::new(|| {
CPU_LEGEND_HEADER
.iter()
.map(|entry| entry.len() as u16)
.collect::<Vec<_>>()
});
static CPU_LEGEND_HEADER_LENS: [usize; 2] =
[CPU_LEGEND_HEADER[0].len(), CPU_LEGEND_HEADER[1].len()];
pub trait CpuGraphWidget {
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>(
impl Painter {
pub fn draw_cpu<B: Backend>(
&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 {
@ -134,250 +118,93 @@ impl CpuGraphWidget for Painter {
fn draw_cpu_graph<B: Backend>(
&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) {
let cpu_data: &mut [ConvertedCpuData] = &mut app_state.canvas_data.cpu_data;
let display_time_labels = vec![
Span::styled(
format!("{}s", cpu_widget_state.current_display_time / 1000),
self.colours.graph_style,
),
Span::styled("0s".to_string(), self.colours.graph_style),
];
let 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 cpu_data = &app_state.canvas_data.cpu_data;
let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
let x_bounds = [0, cpu_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 cpu_widget_state.autohide_timer,
draw_loc,
);
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),
);
if let Some(to_replace) = cpu.cpu_data.get_mut(start_pos) {
*to_replace = new_point;
Some((start_pos, old))
} else {
None // Failed to get mutable reference.
}
} 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
.iter()
.enumerate()
.rev()
.map(|(itx, cpu)| {
Dataset::default()
.marker(if use_dot {
Marker::Dot
} else {
Marker::Braille
})
.style(if show_avg_cpu && itx == AVG_POSITION {
let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 };
let points = {
let current_scroll_position = cpu_widget_state.scroll_state.current_scroll_position;
if current_scroll_position == ALL_POSITION {
// This case ensures the other cases cannot have the position be equal to 0.
cpu_data
.iter()
.enumerate()
.rev()
.map(|(itx, cpu)| {
let style = if show_avg_cpu && itx == AVG_POSITION {
self.colours.avg_colour_style
} else if itx == ALL_POSITION {
self.colours.all_colour_style
} else {
self.colours.cpu_colour_styles[(itx - 1 // Because of the all position
- (if show_avg_cpu {
AVG_POSITION
} else {
0
}))
let offset_position = itx - 1; // Because of the all position
self.colours.cpu_colour_styles[(offset_position - show_avg_offset)
% self.colours.cpu_colour_styles.len()]
})
.data(&cpu.cpu_data[..])
.graph_type(tui::widgets::GraphType::Line)
})
.collect()
} else if let Some(cpu) = cpu_data.get(current_scroll_position) {
vec![Dataset::default()
.marker(if use_dot {
Marker::Dot
} else {
Marker::Braille
})
.style(if show_avg_cpu && current_scroll_position == AVG_POSITION {
};
GraphData {
points: &cpu.cpu_data[..],
style,
name: None,
}
})
.collect::<Vec<_>>()
} else if let Some(cpu) = cpu_data.get(current_scroll_position) {
let style = if show_avg_cpu && current_scroll_position == AVG_POSITION {
self.colours.avg_colour_style
} else {
self.colours.cpu_colour_styles[(cpu_widget_state
.scroll_state
.current_scroll_position
- 1 // Because of the all position
- (if show_avg_cpu {
AVG_POSITION
} else {
0
}))
let offset_position = current_scroll_position - 1; // Because of the all position
self.colours.cpu_colour_styles[(offset_position - show_avg_offset)
% self.colours.cpu_colour_styles.len()]
})
.data(&cpu.cpu_data[..])
.graph_type(tui::widgets::GraphType::Line)]
} else {
vec![]
};
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let border_style = if is_on_widget {
self.colours.highlighted_border_style
} else {
self.colours.border_style
};
vec![GraphData {
points: &cpu.cpu_data[..],
style,
name: None,
}]
} else {
vec![]
}
};
// TODO: Maybe hide load avg if too long? Or maybe the CPU part.
let title = if cfg!(target_family = "unix") {
let load_avg = app_state.canvas_data.load_avg_data;
let load_avg_str = format!(
"─ {:.2} {:.2} {:.2} ",
load_avg[0], load_avg[1], load_avg[2]
);
let load_avg_str_size =
UnicodeSegmentation::graphemes(load_avg_str.as_str(), true).count();
if app_state.is_expanded {
const TITLE_BASE: &str = " CPU ── Esc to go back ";
Spans::from(vec![
Span::styled(" CPU ", self.colours.widget_title_style),
Span::styled(load_avg_str, self.colours.widget_title_style),
Span::styled(
format!(
"─{}─ Esc to go back ",
"".repeat(usize::from(draw_loc.width).saturating_sub(
load_avg_str_size
+ UnicodeSegmentation::graphemes(TITLE_BASE, true).count()
+ 2
))
),
border_style,
),
])
} else {
Spans::from(vec![
Span::styled(" CPU ", self.colours.widget_title_style),
Span::styled(load_avg_str, self.colours.widget_title_style),
])
}
} else if app_state.is_expanded {
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,
),
])
concat_string!(" CPU ", load_avg_str).into()
} else {
Spans::from(vec![Span::styled(" CPU ", self.colours.widget_title_style)])
" CPU ".into()
};
f.render_widget(
Chart::new(dataset_vector)
.block(
Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(border_style),
)
.x_axis(x_axis)
.y_axis(y_axis),
draw_loc,
);
// Reset interpolated points
cpu_data
.iter_mut()
.zip(interpolated_cpu_points)
.for_each(|(cpu, interpolation)| {
if let Some((index, old_value)) = interpolation {
if let Some(to_replace) = cpu.cpu_data.get_mut(index) {
*to_replace = old_value;
}
}
});
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,
is_expanded: app_state.is_expanded,
title_style: self.colours.widget_title_style,
legend_constraints: None,
}
.draw_time_graph(f, draw_loc, &points);
}
}
@ -428,7 +255,7 @@ impl CpuGraphWidget for Painter {
&[None, None],
&(CPU_LEGEND_HEADER_LENS
.iter()
.map(|width| Some(*width))
.map(|width| Some(*width as u16))
.collect::<Vec<_>>()),
&[Some(0.5), Some(0.5)],
&(cpu_widget_state

View File

@ -27,15 +27,8 @@ static DISK_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| {
.collect::<Vec<_>>()
});
pub trait DiskTableWidget {
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>(
impl Painter {
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,
) {

View File

@ -13,14 +13,8 @@ use tui::{
widgets::{Block, Paragraph},
};
pub trait MemBasicWidget {
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>(
impl Painter {
pub fn draw_basic_memory<B: Backend>(
&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;

View File

@ -1,240 +1,72 @@
use std::borrow::Cow;
use crate::{
app::App,
canvas::{drawing_utils::interpolate_points, Painter},
constants::*,
canvas::{
components::{GraphData, TimeGraph},
drawing_utils::should_hide_x_label,
Painter,
},
};
use tui::{
backend::Backend,
layout::{Constraint, Rect},
symbols::Marker,
terminal::Frame,
text::Span,
text::Spans,
widgets::{Axis, Block, Borders, Chart, Dataset},
};
use unicode_segmentation::UnicodeSegmentation;
pub trait MemGraphWidget {
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>(
impl Painter {
pub fn draw_memory_graph<B: Backend>(
&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) {
let mem_data: &mut [(f64, f64)] = &mut app_state.canvas_data.mem_data;
let swap_data: &mut [(f64, f64)] = &mut app_state.canvas_data.swap_data;
let time_start = -(mem_widget_state.current_display_time as f64);
let display_time_labels = vec![
Span::styled(
format!("{}s", mem_widget_state.current_display_time / 1000),
self.colours.graph_style,
),
Span::styled("0s".to_string(), self.colours.graph_style),
];
let y_axis_label = vec![
Span::styled(" 0%", self.colours.graph_style),
Span::styled("100%", self.colours.graph_style),
];
let x_axis = if app_state.app_config_fields.hide_time
|| (app_state.app_config_fields.autohide_time
&& mem_widget_state.autohide_timer.is_none())
{
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))),
let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
let x_bounds = [0, mem_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 mem_widget_state.autohide_timer,
draw_loc,
);
// Now if you're done, reset any interpolated points!
if let Some((index, old_value)) = interpolated_mem_point {
if let Some(to_replace) = mem_data.get_mut(index) {
*to_replace = old_value;
let points = {
let mut points = Vec::with_capacity(2);
if let Some((label_percent, label_frac)) = &app_state.canvas_data.mem_labels {
let mem_label = format!("RAM:{}{}", label_percent, label_frac);
points.push(GraphData {
points: &app_state.canvas_data.mem_data,
style: self.colours.ram_style,
name: Some(mem_label.into()),
});
}
}
if let Some((index, old_value)) = interpolated_swap_point {
if let Some(to_replace) = swap_data.get_mut(index) {
*to_replace = old_value;
if let Some((label_percent, label_frac)) = &app_state.canvas_data.swap_labels {
let swap_label = format!("SWP:{}{}", label_percent, label_frac);
points.push(GraphData {
points: &app_state.canvas_data.swap_data,
style: self.colours.swap_style,
name: Some(swap_label.into()),
});
}
points
};
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() {

View File

@ -8,14 +8,8 @@ use tui::{
widgets::{Block, Paragraph},
};
pub trait NetworkBasicWidget {
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>(
impl Painter {
pub fn draw_basic_network<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) {
let divided_loc = Layout::default()

File diff suppressed because it is too large Load Diff

View File

@ -87,46 +87,10 @@ const PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: &[Option<f64>] = &[
Some(0.2),
];
pub trait ProcessTableWidget {
impl Painter {
/// Draws and handles all process-related drawing. Use this.
/// - `widget_id` here represents the widget ID of the process widget itself!
fn draw_process_features<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>(
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,
) {
@ -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>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
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>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
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>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,

View File

@ -27,15 +27,8 @@ static TEMP_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| {
.collect::<Vec<_>>()
});
pub trait TempTableWidget {
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>(
impl Painter {
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,
) {