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
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
34
src/app.rs
34
src/app.rs
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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::*;
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 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_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 {
|
||||
|
|
|
@ -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![
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
|
|
Loading…
Reference in New Issue