mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-25 22:55:06 +02:00
refactor: start moving over drawing system
In particular, moving over table-style widgets
This commit is contained in:
parent
dd7e183ec8
commit
0afc371eaa
@ -1,2 +1,3 @@
|
||||
cognitive-complexity-threshold = 100
|
||||
type-complexity-threshold = 500
|
||||
type-complexity-threshold = 500
|
||||
too-many-arguments-threshold = 8
|
||||
|
139
src/app.rs
139
src/app.rs
@ -19,8 +19,6 @@ use indextree::{Arena, NodeId};
|
||||
use unicode_segmentation::GraphemeCursor;
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
|
||||
use typed_builder::*;
|
||||
|
||||
use data_farmer::*;
|
||||
use data_harvester::{processes, temperature};
|
||||
pub use filter::*;
|
||||
@ -56,6 +54,35 @@ pub struct UsedWidgets {
|
||||
pub use_battery: bool,
|
||||
}
|
||||
|
||||
impl UsedWidgets {
|
||||
pub fn add(&mut self, widget_type: &BottomWidgetType) {
|
||||
match widget_type {
|
||||
BottomWidgetType::Cpu | BottomWidgetType::BasicCpu => {
|
||||
self.use_cpu = true;
|
||||
}
|
||||
BottomWidgetType::Mem | BottomWidgetType::BasicMem => {
|
||||
self.use_mem = true;
|
||||
}
|
||||
BottomWidgetType::Net | BottomWidgetType::BasicNet => {
|
||||
self.use_net = true;
|
||||
}
|
||||
BottomWidgetType::Proc => {
|
||||
self.use_proc = true;
|
||||
}
|
||||
BottomWidgetType::Temp => {
|
||||
self.use_temp = true;
|
||||
}
|
||||
BottomWidgetType::Disk => {
|
||||
self.use_disk = true;
|
||||
}
|
||||
BottomWidgetType::Battery => {
|
||||
self.use_battery = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// AppConfigFields is meant to cover basic fields that would normally be set
|
||||
/// by config files or launch options.
|
||||
#[derive(Debug)]
|
||||
@ -72,7 +99,7 @@ pub struct AppConfigFields {
|
||||
pub hide_time: bool,
|
||||
pub autohide_time: bool,
|
||||
pub use_old_network_legend: bool,
|
||||
pub table_gap: u16,
|
||||
pub table_gap: u16, // TODO: Just make this a bool...
|
||||
pub disable_click: bool,
|
||||
pub no_write: bool,
|
||||
pub show_table_scroll_position: bool,
|
||||
@ -83,55 +110,32 @@ pub struct AppConfigFields {
|
||||
pub network_use_binary_prefix: bool,
|
||||
}
|
||||
|
||||
// FIXME: Get rid of TypedBuilder here!
|
||||
#[derive(TypedBuilder)]
|
||||
pub struct AppState {
|
||||
#[builder(default, setter(skip))]
|
||||
pub dd_err: Option<String>,
|
||||
|
||||
#[builder(default, setter(skip))]
|
||||
to_delete_process_list: Option<(String, Vec<Pid>)>,
|
||||
|
||||
#[builder(default = false, setter(skip))]
|
||||
pub is_frozen: bool,
|
||||
|
||||
#[builder(default = Instant::now(), setter(skip))]
|
||||
last_key_press: Instant,
|
||||
|
||||
#[builder(default, setter(skip))]
|
||||
pub canvas_data: canvas::DisplayableData,
|
||||
|
||||
#[builder(default, setter(skip))]
|
||||
pub data_collection: DataCollection,
|
||||
|
||||
#[builder(default = false, setter(skip))]
|
||||
pub is_expanded: bool,
|
||||
|
||||
#[builder(default = false, setter(skip))]
|
||||
pub is_force_redraw: bool,
|
||||
|
||||
#[builder(default = false, setter(skip))]
|
||||
pub is_determining_widget_boundary: bool,
|
||||
|
||||
#[builder(default = false, setter(skip))]
|
||||
pub basic_mode_use_percent: bool,
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
#[builder(default, setter(skip))]
|
||||
pub user_table: processes::UserTable,
|
||||
|
||||
pub used_widgets: UsedWidgets,
|
||||
pub filters: DataFilters,
|
||||
pub app_config_fields: AppConfigFields,
|
||||
|
||||
// --- Possibly delete? ---
|
||||
#[builder(default, setter(skip))]
|
||||
// --- Eventually delete/rewrite ---
|
||||
pub delete_dialog_state: AppDeleteDialogState,
|
||||
|
||||
#[builder(default, setter(skip))]
|
||||
pub help_dialog_state: AppHelpDialogState,
|
||||
|
||||
// --- TO DELETE---
|
||||
// --- TO DELETE ---
|
||||
pub cpu_state: CpuState,
|
||||
pub mem_state: MemState,
|
||||
pub net_state: NetState,
|
||||
@ -143,12 +147,18 @@ pub struct AppState {
|
||||
pub widget_map: HashMap<u64, BottomWidget>,
|
||||
pub current_widget: BottomWidget,
|
||||
|
||||
#[builder(default = false, setter(skip))]
|
||||
last_key_press: Instant,
|
||||
|
||||
awaiting_second_char: bool,
|
||||
|
||||
#[builder(default, setter(skip))]
|
||||
second_char: Option<char>,
|
||||
|
||||
pub basic_mode_use_percent: bool,
|
||||
|
||||
pub is_force_redraw: bool,
|
||||
|
||||
pub is_determining_widget_boundary: bool,
|
||||
|
||||
// --- NEW STUFF ---
|
||||
pub selected_widget: NodeId,
|
||||
pub widget_lookup_map: FxHashMap<NodeId, TmpBottomWidget>,
|
||||
@ -159,10 +169,53 @@ pub struct AppState {
|
||||
impl AppState {
|
||||
/// Creates a new [`AppState`].
|
||||
pub fn new(
|
||||
_app_config_fields: AppConfigFields, _filters: DataFilters,
|
||||
_layout_tree_output: LayoutCreationOutput,
|
||||
app_config_fields: AppConfigFields, filters: DataFilters,
|
||||
layout_tree_output: LayoutCreationOutput,
|
||||
) -> Self {
|
||||
todo!()
|
||||
let LayoutCreationOutput {
|
||||
layout_tree,
|
||||
root: layout_tree_root,
|
||||
widget_lookup_map,
|
||||
selected: selected_widget,
|
||||
used_widgets,
|
||||
} = layout_tree_output;
|
||||
|
||||
Self {
|
||||
app_config_fields,
|
||||
filters,
|
||||
used_widgets,
|
||||
selected_widget,
|
||||
widget_lookup_map,
|
||||
layout_tree,
|
||||
layout_tree_root,
|
||||
|
||||
// Use defaults.
|
||||
dd_err: Default::default(),
|
||||
to_delete_process_list: Default::default(),
|
||||
is_frozen: Default::default(),
|
||||
canvas_data: Default::default(),
|
||||
data_collection: Default::default(),
|
||||
is_expanded: Default::default(),
|
||||
user_table: Default::default(),
|
||||
delete_dialog_state: Default::default(),
|
||||
help_dialog_state: Default::default(),
|
||||
cpu_state: Default::default(),
|
||||
mem_state: Default::default(),
|
||||
net_state: Default::default(),
|
||||
proc_state: Default::default(),
|
||||
temp_state: Default::default(),
|
||||
disk_state: Default::default(),
|
||||
battery_state: Default::default(),
|
||||
basic_table_widget_state: Default::default(),
|
||||
widget_map: Default::default(),
|
||||
current_widget: Default::default(),
|
||||
last_key_press: Instant::now(),
|
||||
awaiting_second_char: Default::default(),
|
||||
second_char: Default::default(),
|
||||
basic_mode_use_percent: Default::default(),
|
||||
is_force_redraw: Default::default(),
|
||||
is_determining_widget_boundary: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
@ -248,12 +301,13 @@ impl AppState {
|
||||
|
||||
for (id, widget) in self.widget_lookup_map.iter_mut() {
|
||||
if does_point_intersect_rect(x, y, widget.bounds()) {
|
||||
if self.selected_widget == *id {
|
||||
self.selected_widget = *id;
|
||||
let is_id_selected = self.selected_widget == *id;
|
||||
self.selected_widget = *id;
|
||||
|
||||
if is_id_selected {
|
||||
return widget.handle_mouse_event(event);
|
||||
} else {
|
||||
// If the aren't equal, *force* a redraw.
|
||||
self.selected_widget = *id;
|
||||
widget.handle_mouse_event(event);
|
||||
return EventResult::Redraw;
|
||||
}
|
||||
@ -262,10 +316,10 @@ impl AppState {
|
||||
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
BottomEvent::Update(new_data) => {
|
||||
BottomEvent::Update(_new_data) => {
|
||||
if !self.is_frozen {
|
||||
// TODO: Update all data, and redraw.
|
||||
EventResult::Redraw
|
||||
todo!()
|
||||
} else {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
@ -282,9 +336,14 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles a [`ReturnSignal`], and returns
|
||||
/// Handles a [`ReturnSignal`], and returns an [`EventResult`].
|
||||
pub fn handle_return_signal(&mut self, return_signal: ReturnSignal) -> EventResult {
|
||||
todo!()
|
||||
match return_signal {
|
||||
ReturnSignal::Nothing => EventResult::NoRedraw,
|
||||
ReturnSignal::KillProcess => {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_esc(&mut self) {
|
||||
|
@ -1,5 +1,7 @@
|
||||
use crate::{
|
||||
app::{DiskTable, MemGraph, NetGraph, OldNetGraph, ProcessManager, TempTable},
|
||||
app::{
|
||||
text_table::Column, DiskTable, MemGraph, NetGraph, OldNetGraph, ProcessManager, TempTable,
|
||||
},
|
||||
error::{BottomError, Result},
|
||||
options::layout_options::{Row, RowChildren},
|
||||
};
|
||||
@ -12,7 +14,7 @@ use typed_builder::*;
|
||||
use crate::app::widgets::Widget;
|
||||
use crate::constants::DEFAULT_WIDGET_ID;
|
||||
|
||||
use super::{event::SelectionAction, CpuGraph, TextTable, TimeGraph, TmpBottomWidget};
|
||||
use super::{event::SelectionAction, CpuGraph, TextTable, TimeGraph, TmpBottomWidget, UsedWidgets};
|
||||
|
||||
/// Represents a more usable representation of the layout, derived from the
|
||||
/// config.
|
||||
@ -985,49 +987,17 @@ Supported widget names:
|
||||
// --- New stuff ---
|
||||
|
||||
/// Represents a row in the layout tree.
|
||||
#[derive(PartialEq, Eq)]
|
||||
#[derive(PartialEq, Eq, Default)]
|
||||
pub struct RowLayout {
|
||||
last_selected_index: usize,
|
||||
pub constraint: Constraint,
|
||||
}
|
||||
|
||||
impl Default for RowLayout {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_selected_index: 0,
|
||||
constraint: Constraint::Min(0),
|
||||
}
|
||||
}
|
||||
pub constraints: Vec<Constraint>,
|
||||
}
|
||||
|
||||
/// Represents a column in the layout tree.
|
||||
#[derive(PartialEq, Eq)]
|
||||
#[derive(PartialEq, Eq, Default)]
|
||||
pub struct ColLayout {
|
||||
last_selected_index: usize,
|
||||
pub constraint: Constraint,
|
||||
}
|
||||
|
||||
impl Default for ColLayout {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_selected_index: 0,
|
||||
constraint: Constraint::Min(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a widget in the layout tree.
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct WidgetLayout {
|
||||
pub constraint: Constraint,
|
||||
}
|
||||
|
||||
impl Default for WidgetLayout {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
constraint: Constraint::Min(0),
|
||||
}
|
||||
}
|
||||
pub constraints: Vec<Constraint>,
|
||||
}
|
||||
|
||||
/// A [`LayoutNode`] represents a single node in the overall widget hierarchy. Each node is one of:
|
||||
@ -1038,7 +1008,21 @@ impl Default for WidgetLayout {
|
||||
pub enum LayoutNode {
|
||||
Row(RowLayout),
|
||||
Col(ColLayout),
|
||||
Widget(WidgetLayout),
|
||||
Widget,
|
||||
}
|
||||
|
||||
impl LayoutNode {
|
||||
pub fn set_constraints(&mut self, constraints: Vec<Constraint>) {
|
||||
match self {
|
||||
LayoutNode::Row(row) => {
|
||||
row.constraints = constraints;
|
||||
}
|
||||
LayoutNode::Col(col) => {
|
||||
col.constraints = constraints;
|
||||
}
|
||||
LayoutNode::Widget => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Relative movement direction from the currently selected widget.
|
||||
@ -1054,7 +1038,8 @@ pub struct LayoutCreationOutput {
|
||||
pub layout_tree: Arena<LayoutNode>,
|
||||
pub root: NodeId,
|
||||
pub widget_lookup_map: FxHashMap<NodeId, TmpBottomWidget>,
|
||||
pub selected: Option<NodeId>,
|
||||
pub selected: NodeId,
|
||||
pub used_widgets: UsedWidgets,
|
||||
}
|
||||
|
||||
/// Creates a new [`Arena<LayoutNode>`] from the given config and returns it, along with the [`NodeId`] representing
|
||||
@ -1066,14 +1051,17 @@ pub fn create_layout_tree(
|
||||
app_config_fields: &super::AppConfigFields,
|
||||
) -> Result<LayoutCreationOutput> {
|
||||
fn add_widget_to_map(
|
||||
widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, widget_type: &str,
|
||||
widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, widget_type: BottomWidgetType,
|
||||
widget_id: NodeId, process_defaults: &crate::options::ProcessDefaults,
|
||||
app_config_fields: &super::AppConfigFields,
|
||||
) -> Result<()> {
|
||||
match widget_type.parse::<BottomWidgetType>()? {
|
||||
match widget_type {
|
||||
BottomWidgetType::Cpu => {
|
||||
let graph = TimeGraph::from_config(app_config_fields);
|
||||
let legend = TextTable::new(vec![("CPU", None, false), ("Use%", None, false)]);
|
||||
let legend = TextTable::new(vec![
|
||||
Column::new_flex("CPU", None, false, 0.5),
|
||||
Column::new_flex("Use%", None, false, 0.5),
|
||||
]);
|
||||
let legend_position = super::CpuGraphLegendPosition::Right;
|
||||
|
||||
widget_lookup_map.insert(
|
||||
@ -1100,20 +1088,10 @@ pub fn create_layout_tree(
|
||||
);
|
||||
}
|
||||
BottomWidgetType::Temp => {
|
||||
let table = TextTable::new(vec![("Sensor", None, false), ("Temp", None, false)]);
|
||||
widget_lookup_map.insert(widget_id, TempTable::new(table).into());
|
||||
widget_lookup_map.insert(widget_id, TempTable::default().into());
|
||||
}
|
||||
BottomWidgetType::Disk => {
|
||||
let table = TextTable::new(vec![
|
||||
("Disk", None, false),
|
||||
("Mount", None, false),
|
||||
("Used", None, false),
|
||||
("Free", None, false),
|
||||
("Total", None, false),
|
||||
("R/s", None, false),
|
||||
("W/s", None, false),
|
||||
]);
|
||||
widget_lookup_map.insert(widget_id, DiskTable::new(table).into());
|
||||
widget_lookup_map.insert(widget_id, DiskTable::default().into());
|
||||
}
|
||||
BottomWidgetType::Battery => {}
|
||||
_ => {}
|
||||
@ -1125,19 +1103,20 @@ pub fn create_layout_tree(
|
||||
let mut layout_tree = Arena::new();
|
||||
let root_id = layout_tree.new_node(LayoutNode::Col(ColLayout::default()));
|
||||
let mut widget_lookup_map = FxHashMap::default();
|
||||
let mut selected = None;
|
||||
let mut first_selected = None;
|
||||
let mut first_widget_seen = None; // Backup
|
||||
let mut used_widgets = UsedWidgets::default();
|
||||
|
||||
let row_sum: u32 = rows.iter().map(|row| row.ratio.unwrap_or(1)).sum();
|
||||
let mut root_constraints = Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
let ratio = row.ratio.unwrap_or(1);
|
||||
let layout_node = LayoutNode::Row(RowLayout {
|
||||
constraint: Constraint::Ratio(ratio, row_sum),
|
||||
..Default::default()
|
||||
});
|
||||
root_constraints.push(Constraint::Ratio(row.ratio.unwrap_or(1), row_sum));
|
||||
let layout_node = LayoutNode::Row(RowLayout::default());
|
||||
let row_id = layout_tree.new_node(layout_node);
|
||||
root_id.append(row_id, &mut layout_tree);
|
||||
|
||||
if let Some(cols) = &row.child {
|
||||
let mut row_constraints = Vec::with_capacity(cols.len());
|
||||
let col_sum: u32 = cols
|
||||
.iter()
|
||||
.map(|col| match col {
|
||||
@ -1149,18 +1128,24 @@ pub fn create_layout_tree(
|
||||
for col in cols {
|
||||
match col {
|
||||
RowChildren::Widget(widget) => {
|
||||
let widget_node = LayoutNode::Widget(WidgetLayout {
|
||||
constraint: Constraint::Ratio(widget.ratio.unwrap_or(1), col_sum),
|
||||
});
|
||||
let widget_id = layout_tree.new_node(widget_node);
|
||||
row_constraints.push(Constraint::Ratio(widget.ratio.unwrap_or(1), col_sum));
|
||||
let widget_id = layout_tree.new_node(LayoutNode::Widget);
|
||||
row_id.append(widget_id, &mut layout_tree);
|
||||
|
||||
if let Some(true) = widget.default {
|
||||
selected = Some(widget_id);
|
||||
first_selected = Some(widget_id);
|
||||
}
|
||||
|
||||
if first_widget_seen.is_none() {
|
||||
first_widget_seen = Some(widget_id);
|
||||
}
|
||||
|
||||
let widget_type = widget.widget_type.parse::<BottomWidgetType>()?;
|
||||
used_widgets.add(&widget_type);
|
||||
|
||||
add_widget_to_map(
|
||||
&mut widget_lookup_map,
|
||||
&widget.widget_type,
|
||||
widget_type,
|
||||
widget_id,
|
||||
&process_defaults,
|
||||
app_config_fields,
|
||||
@ -1170,45 +1155,73 @@ pub fn create_layout_tree(
|
||||
ratio,
|
||||
child: children,
|
||||
} => {
|
||||
let col_node = LayoutNode::Col(ColLayout {
|
||||
constraint: Constraint::Ratio(ratio.unwrap_or(1), col_sum),
|
||||
..Default::default()
|
||||
});
|
||||
row_constraints.push(Constraint::Ratio(ratio.unwrap_or(1), col_sum));
|
||||
let col_node = LayoutNode::Col(ColLayout::default());
|
||||
let col_id = layout_tree.new_node(col_node);
|
||||
row_id.append(col_id, &mut layout_tree);
|
||||
|
||||
let child_sum: u32 =
|
||||
children.iter().map(|child| child.ratio.unwrap_or(1)).sum();
|
||||
|
||||
let mut col_constraints = Vec::with_capacity(children.len());
|
||||
for child in children {
|
||||
let widget_node = LayoutNode::Widget(WidgetLayout {
|
||||
constraint: Constraint::Ratio(child.ratio.unwrap_or(1), child_sum),
|
||||
});
|
||||
let widget_id = layout_tree.new_node(widget_node);
|
||||
col_constraints
|
||||
.push(Constraint::Ratio(child.ratio.unwrap_or(1), child_sum));
|
||||
let widget_id = layout_tree.new_node(LayoutNode::Widget);
|
||||
col_id.append(widget_id, &mut layout_tree);
|
||||
|
||||
if let Some(true) = child.default {
|
||||
selected = Some(widget_id);
|
||||
first_selected = Some(widget_id);
|
||||
}
|
||||
|
||||
if first_widget_seen.is_none() {
|
||||
first_widget_seen = Some(widget_id);
|
||||
}
|
||||
|
||||
let widget_type = child.widget_type.parse::<BottomWidgetType>()?;
|
||||
used_widgets.add(&widget_type);
|
||||
|
||||
add_widget_to_map(
|
||||
&mut widget_lookup_map,
|
||||
&child.widget_type,
|
||||
widget_type,
|
||||
widget_id,
|
||||
&process_defaults,
|
||||
app_config_fields,
|
||||
)?;
|
||||
}
|
||||
layout_tree[col_id]
|
||||
.get_mut()
|
||||
.set_constraints(col_constraints);
|
||||
}
|
||||
}
|
||||
}
|
||||
layout_tree[row_id]
|
||||
.get_mut()
|
||||
.set_constraints(row_constraints);
|
||||
}
|
||||
}
|
||||
layout_tree[root_id]
|
||||
.get_mut()
|
||||
.set_constraints(root_constraints);
|
||||
|
||||
let selected: NodeId;
|
||||
|
||||
if let Some(first_selected) = first_selected {
|
||||
selected = first_selected;
|
||||
} else if let Some(first_widget_seen) = first_widget_seen {
|
||||
selected = first_widget_seen;
|
||||
} else {
|
||||
return Err(BottomError::ConfigError(
|
||||
"A layout cannot contain zero widgets!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(LayoutCreationOutput {
|
||||
layout_tree,
|
||||
root: root_id,
|
||||
widget_lookup_map,
|
||||
selected,
|
||||
used_widgets,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1252,7 +1265,7 @@ pub fn move_widget_selection(
|
||||
.and_then(|(parent_id, parent_node)| match parent_node.get() {
|
||||
LayoutNode::Row(_) => Some((parent_id, current_id)),
|
||||
LayoutNode::Col(_) => find_first_row(layout_tree, parent_id),
|
||||
LayoutNode::Widget(_) => None,
|
||||
LayoutNode::Widget => None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1273,7 +1286,7 @@ pub fn move_widget_selection(
|
||||
.and_then(|(parent_id, parent_node)| match parent_node.get() {
|
||||
LayoutNode::Row(_) => find_first_col(layout_tree, parent_id),
|
||||
LayoutNode::Col(_) => Some((parent_id, current_id)),
|
||||
LayoutNode::Widget(_) => None,
|
||||
LayoutNode::Widget => None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1283,11 +1296,11 @@ pub fn move_widget_selection(
|
||||
match current_node.get() {
|
||||
LayoutNode::Row(RowLayout {
|
||||
last_selected_index,
|
||||
constraint: _,
|
||||
constraints: _,
|
||||
})
|
||||
| LayoutNode::Col(ColLayout {
|
||||
last_selected_index,
|
||||
constraint: _,
|
||||
constraints: _,
|
||||
}) => {
|
||||
if let Some(next_child) =
|
||||
current_id.children(layout_tree).nth(*last_selected_index)
|
||||
@ -1297,7 +1310,7 @@ pub fn move_widget_selection(
|
||||
current_id
|
||||
}
|
||||
}
|
||||
LayoutNode::Widget(_) => {
|
||||
LayoutNode::Widget => {
|
||||
// Halt!
|
||||
current_id
|
||||
}
|
||||
|
@ -2,13 +2,19 @@ use std::time::Instant;
|
||||
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use enum_dispatch::enum_dispatch;
|
||||
use tui::{layout::Rect, widgets::TableState};
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::Rect,
|
||||
widgets::{Block, TableState},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
event::{EventResult, SelectionAction},
|
||||
layout_manager::BottomWidgetType,
|
||||
},
|
||||
canvas::{DisplayableData, Painter},
|
||||
constants,
|
||||
};
|
||||
|
||||
@ -64,6 +70,7 @@ pub trait Component {
|
||||
|
||||
/// A trait for actual fully-fledged widgets to be displayed in bottom.
|
||||
#[enum_dispatch]
|
||||
#[allow(unused_variables)]
|
||||
pub trait Widget {
|
||||
/// Updates a [`Widget`] given some data. Defaults to doing nothing.
|
||||
fn update(&mut self) {}
|
||||
@ -92,10 +99,21 @@ pub trait Widget {
|
||||
SelectionAction::NotHandled
|
||||
}
|
||||
|
||||
/// Returns a [`Widget`]'s "pretty" display name.
|
||||
fn get_pretty_name(&self) -> &'static str;
|
||||
|
||||
/// Draws a [`Widget`]. Defaults to doing nothing.
|
||||
fn draw<B: Backend>(
|
||||
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, block: Block<'_>,
|
||||
data: &DisplayableData,
|
||||
) {
|
||||
// TODO: Remove the default implementation in the future!
|
||||
// TODO: Do another pass on ALL of the draw code - currently it's just glue, it should eventually be done properly!
|
||||
}
|
||||
}
|
||||
|
||||
/// The "main" widgets that are used by bottom to display information!
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[enum_dispatch(Component, Widget)]
|
||||
pub enum TmpBottomWidget {
|
||||
MemGraph,
|
||||
|
@ -131,13 +131,17 @@ impl Scrollable {
|
||||
self.num_items = num_items;
|
||||
|
||||
if num_items <= self.current_index {
|
||||
self.current_index = num_items - 1;
|
||||
self.current_index = num_items.saturating_sub(1);
|
||||
}
|
||||
|
||||
if num_items <= self.previous_index {
|
||||
self.previous_index = num_items - 1;
|
||||
self.previous_index = num_items.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn num_items(&self) -> usize {
|
||||
self.num_items
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Scrollable {
|
||||
|
@ -1,32 +1,132 @@
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use tui::layout::Rect;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{max, min},
|
||||
};
|
||||
|
||||
use crate::app::{event::EventResult, Component, Scrollable};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
|
||||
use tui::{
|
||||
layout::{Constraint, Rect},
|
||||
text::Text,
|
||||
widgets::{Table, TableState},
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::{
|
||||
app::{event::EventResult, Component, Scrollable},
|
||||
canvas::Painter,
|
||||
constants::TABLE_GAP_HEIGHT_LIMIT,
|
||||
};
|
||||
|
||||
/// Represents the desired widths a column tries to have.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DesiredColumnWidth {
|
||||
Hard(u16),
|
||||
Flex { desired: u16, max_percentage: f64 },
|
||||
}
|
||||
|
||||
/// A [`Column`] represents some column in a [`TextTable`].
|
||||
#[derive(Debug)]
|
||||
pub struct Column {
|
||||
pub name: &'static str,
|
||||
pub shortcut: Option<KeyEvent>,
|
||||
pub shortcut: Option<(KeyEvent, String)>,
|
||||
pub default_descending: bool,
|
||||
|
||||
// TODO: I would remove these in the future, storing them here feels weird...
|
||||
pub desired_column_width: u16,
|
||||
pub calculated_column_width: u16,
|
||||
pub desired_width: DesiredColumnWidth,
|
||||
pub x_bounds: (u16, u16),
|
||||
}
|
||||
|
||||
impl Column {
|
||||
/// Creates a new [`Column`], given a name and optional shortcut.
|
||||
pub fn new(name: &'static str, shortcut: Option<KeyEvent>, default_descending: bool) -> Self {
|
||||
/// Creates a new [`Column`].
|
||||
pub fn new(
|
||||
name: &'static str, shortcut: Option<KeyEvent>, default_descending: bool,
|
||||
desired_width: DesiredColumnWidth,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
desired_column_width: 0,
|
||||
calculated_column_width: 0,
|
||||
x_bounds: (0, 0),
|
||||
shortcut,
|
||||
shortcut: shortcut.map(|e| {
|
||||
let modifier = if e.modifiers.is_empty() {
|
||||
""
|
||||
} else if let KeyModifiers::ALT = e.modifiers {
|
||||
"Alt+"
|
||||
} else if let KeyModifiers::SHIFT = e.modifiers {
|
||||
"Shift+"
|
||||
} else if let KeyModifiers::CONTROL = e.modifiers {
|
||||
"Ctrl+"
|
||||
} else {
|
||||
// For now, that's all we support, though combos/more could be added.
|
||||
""
|
||||
};
|
||||
|
||||
let key: Cow<'static, str> = match e.code {
|
||||
KeyCode::Backspace => "Backspace".into(),
|
||||
KeyCode::Enter => "Enter".into(),
|
||||
KeyCode::Left => "Left".into(),
|
||||
KeyCode::Right => "Right".into(),
|
||||
KeyCode::Up => "Up".into(),
|
||||
KeyCode::Down => "Down".into(),
|
||||
KeyCode::Home => "Home".into(),
|
||||
KeyCode::End => "End".into(),
|
||||
KeyCode::PageUp => "PgUp".into(),
|
||||
KeyCode::PageDown => "PgDown".into(),
|
||||
KeyCode::Tab => "Tab".into(),
|
||||
KeyCode::BackTab => "BackTab".into(),
|
||||
KeyCode::Delete => "Del".into(),
|
||||
KeyCode::Insert => "Insert".into(),
|
||||
KeyCode::F(num) => format!("F{}", num).into(),
|
||||
KeyCode::Char(c) => format!("{}", c).into(),
|
||||
KeyCode::Null => "Null".into(),
|
||||
KeyCode::Esc => "Esc".into(),
|
||||
};
|
||||
|
||||
let shortcut_name = format!("({}{})", modifier, key);
|
||||
|
||||
(e, shortcut_name)
|
||||
}),
|
||||
default_descending,
|
||||
desired_width,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`Column`] with a hard desired width. If none is specified,
|
||||
/// it will instead use the name's length.
|
||||
pub fn new_hard(
|
||||
name: &'static str, shortcut: Option<KeyEvent>, default_descending: bool,
|
||||
hard_length: Option<u16>,
|
||||
) -> Self {
|
||||
Column::new(
|
||||
name,
|
||||
shortcut,
|
||||
default_descending,
|
||||
DesiredColumnWidth::Hard(hard_length.unwrap_or(name.len() as u16)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new [`Column`] with a flexible desired width.
|
||||
pub fn new_flex(
|
||||
name: &'static str, shortcut: Option<KeyEvent>, default_descending: bool,
|
||||
max_percentage: f64,
|
||||
) -> Self {
|
||||
Column::new(
|
||||
name,
|
||||
shortcut,
|
||||
default_descending,
|
||||
DesiredColumnWidth::Flex {
|
||||
desired: name.len() as u16,
|
||||
max_percentage,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum CachedColumnWidths {
|
||||
Uncached,
|
||||
Cached {
|
||||
cached_area: Rect,
|
||||
cached_data: Vec<u16>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A sortable, scrollable table with columns.
|
||||
@ -37,6 +137,9 @@ pub struct TextTable {
|
||||
/// The columns themselves.
|
||||
columns: Vec<Column>,
|
||||
|
||||
/// Cached column width data.
|
||||
cached_column_widths: CachedColumnWidths,
|
||||
|
||||
/// Whether to show a gap between the column headers and the columns.
|
||||
show_gap: bool,
|
||||
|
||||
@ -48,30 +151,30 @@ pub struct TextTable {
|
||||
|
||||
/// Whether we're sorting by ascending order.
|
||||
sort_ascending: bool,
|
||||
|
||||
/// Whether we draw columns from left-to-right.
|
||||
left_to_right: bool,
|
||||
}
|
||||
|
||||
impl TextTable {
|
||||
pub fn new(columns: Vec<(&'static str, Option<KeyEvent>, bool)>) -> Self {
|
||||
pub fn new(columns: Vec<Column>) -> Self {
|
||||
Self {
|
||||
scrollable: Scrollable::new(0),
|
||||
columns: columns
|
||||
.into_iter()
|
||||
.map(|(name, shortcut, default_descending)| Column {
|
||||
name,
|
||||
desired_column_width: 0,
|
||||
calculated_column_width: 0,
|
||||
x_bounds: (0, 0),
|
||||
shortcut,
|
||||
default_descending,
|
||||
})
|
||||
.collect(),
|
||||
columns,
|
||||
cached_column_widths: CachedColumnWidths::Uncached,
|
||||
show_gap: true,
|
||||
bounds: Rect::default(),
|
||||
sort_index: 0,
|
||||
sort_ascending: true,
|
||||
left_to_right: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left_to_right(mut self, ltr: bool) -> Self {
|
||||
self.left_to_right = ltr;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn try_show_gap(mut self, show_gap: bool) -> Self {
|
||||
self.show_gap = show_gap;
|
||||
self
|
||||
@ -82,28 +185,48 @@ impl TextTable {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn update_bounds(&mut self, new_bounds: Rect) {
|
||||
self.bounds = new_bounds;
|
||||
}
|
||||
|
||||
pub fn update_calculated_column_bounds(&mut self, calculated_bounds: &[u16]) {
|
||||
self.columns
|
||||
.iter_mut()
|
||||
.zip(calculated_bounds.iter())
|
||||
.for_each(|(column, bound)| column.calculated_column_width = *bound);
|
||||
}
|
||||
|
||||
pub fn desired_column_bounds(&self) -> Vec<u16> {
|
||||
self.columns
|
||||
.iter()
|
||||
.map(|column| column.desired_column_width)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn column_names(&self) -> Vec<&'static str> {
|
||||
self.columns.iter().map(|column| column.name).collect()
|
||||
}
|
||||
|
||||
pub fn sorted_column_names(&self) -> Vec<String> {
|
||||
const UP_ARROW: char = '▲';
|
||||
const DOWN_ARROW: char = '▼';
|
||||
|
||||
self.columns
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, column)| {
|
||||
if index == self.sort_index {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
column.name,
|
||||
if let Some(shortcut) = &column.shortcut {
|
||||
shortcut.1.as_str()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if self.sort_ascending {
|
||||
UP_ARROW
|
||||
} else {
|
||||
DOWN_ARROW
|
||||
}
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{}{}",
|
||||
column.name,
|
||||
if let Some(shortcut) = &column.shortcut {
|
||||
shortcut.1.as_str()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn update_num_items(&mut self, num_items: usize) {
|
||||
self.scrollable.update_num_items(num_items);
|
||||
}
|
||||
@ -114,18 +237,216 @@ impl TextTable {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_columns(&mut self, columns: Vec<Column>) {
|
||||
self.columns = columns;
|
||||
if self.columns.len() <= self.sort_index {
|
||||
self.sort_index = self.columns.len() - 1;
|
||||
pub fn get_desired_column_widths(
|
||||
columns: &[Column], data: &[Vec<String>],
|
||||
) -> Vec<DesiredColumnWidth> {
|
||||
columns
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(column_index, c)| match c.desired_width {
|
||||
DesiredColumnWidth::Hard(width) => {
|
||||
let max_len = data
|
||||
.iter()
|
||||
.filter_map(|c| c.get(column_index))
|
||||
.max_by(|x, y| x.len().cmp(&y.len()))
|
||||
.map(|s| s.len())
|
||||
.unwrap_or(0) as u16;
|
||||
|
||||
DesiredColumnWidth::Hard(max(max_len, width))
|
||||
}
|
||||
DesiredColumnWidth::Flex {
|
||||
desired: _,
|
||||
max_percentage: _,
|
||||
} => c.desired_width.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn get_cache(&mut self, area: Rect, data: &[Vec<String>]) -> Vec<u16> {
|
||||
fn calculate_column_widths(
|
||||
left_to_right: bool, mut desired_widths: Vec<DesiredColumnWidth>, total_width: u16,
|
||||
) -> Vec<u16> {
|
||||
debug!("OG desired widths: {:?}", desired_widths);
|
||||
let mut total_width_left = total_width;
|
||||
if !left_to_right {
|
||||
desired_widths.reverse();
|
||||
}
|
||||
debug!("Desired widths: {:?}", desired_widths);
|
||||
|
||||
let mut column_widths: Vec<u16> = Vec::with_capacity(desired_widths.len());
|
||||
for width in desired_widths {
|
||||
match width {
|
||||
DesiredColumnWidth::Hard(width) => {
|
||||
if width > total_width_left {
|
||||
break;
|
||||
} else {
|
||||
column_widths.push(width);
|
||||
total_width_left = total_width_left.saturating_sub(width + 1);
|
||||
}
|
||||
}
|
||||
DesiredColumnWidth::Flex {
|
||||
desired,
|
||||
max_percentage,
|
||||
} => {
|
||||
if desired > total_width_left {
|
||||
break;
|
||||
} else {
|
||||
let calculated_width = min(
|
||||
max(desired, (max_percentage * total_width as f64).ceil() as u16),
|
||||
total_width_left,
|
||||
);
|
||||
|
||||
column_widths.push(calculated_width);
|
||||
total_width_left =
|
||||
total_width_left.saturating_sub(calculated_width + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("Initial column widths: {:?}", column_widths);
|
||||
|
||||
if !column_widths.is_empty() {
|
||||
let amount_per_slot = total_width_left / column_widths.len() as u16;
|
||||
total_width_left %= column_widths.len() as u16;
|
||||
for (itx, width) in column_widths.iter_mut().enumerate() {
|
||||
if (itx as u16) < total_width_left {
|
||||
*width += amount_per_slot + 1;
|
||||
} else {
|
||||
*width += amount_per_slot;
|
||||
}
|
||||
}
|
||||
|
||||
if !left_to_right {
|
||||
column_widths.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Column widths: {:?}", column_widths);
|
||||
|
||||
column_widths
|
||||
}
|
||||
|
||||
// If empty, do NOT save the cache! We have to get it again when it updates.
|
||||
if data.is_empty() {
|
||||
vec![0; self.columns.len()]
|
||||
} else {
|
||||
match &mut self.cached_column_widths {
|
||||
CachedColumnWidths::Uncached => {
|
||||
// Always recalculate.
|
||||
let desired_widths = TextTable::get_desired_column_widths(&self.columns, data);
|
||||
let calculated_widths =
|
||||
calculate_column_widths(self.left_to_right, desired_widths, area.width);
|
||||
self.cached_column_widths = CachedColumnWidths::Cached {
|
||||
cached_area: area,
|
||||
cached_data: calculated_widths.clone(),
|
||||
};
|
||||
|
||||
calculated_widths
|
||||
}
|
||||
CachedColumnWidths::Cached {
|
||||
cached_area,
|
||||
cached_data,
|
||||
} => {
|
||||
if *cached_area != area {
|
||||
// Recalculate!
|
||||
let desired_widths =
|
||||
TextTable::get_desired_column_widths(&self.columns, data);
|
||||
let calculated_widths =
|
||||
calculate_column_widths(self.left_to_right, desired_widths, area.width);
|
||||
*cached_area = area;
|
||||
*cached_data = calculated_widths.clone();
|
||||
|
||||
calculated_widths
|
||||
} else {
|
||||
cached_data.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`Table`] given the [`TextTable`] and the given data, along with its
|
||||
/// widths (because for some reason a [`Table`] only borrows the constraints...?)
|
||||
/// and [`TableState`] (so we know which row is selected).
|
||||
///
|
||||
/// Note if the number of columns don't match in the [`TextTable`] and data,
|
||||
/// it will only create as many columns as it can grab data from both sources from.
|
||||
pub fn create_draw_table(
|
||||
&mut self, painter: &Painter, data: &[Vec<String>], area: Rect,
|
||||
) -> (Table<'_>, Vec<Constraint>, TableState) {
|
||||
// TODO: Change data: &[Vec<String>] to &[Vec<Cow<'static, str>>]
|
||||
use tui::widgets::Row;
|
||||
|
||||
let table_gap = if !self.show_gap || area.height < TABLE_GAP_HEIGHT_LIMIT {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
self.set_bounds(area);
|
||||
let scrollable_height = area.height.saturating_sub(1 + table_gap);
|
||||
self.scrollable.set_bounds(Rect::new(
|
||||
area.x,
|
||||
area.y + 1 + table_gap,
|
||||
area.width,
|
||||
scrollable_height,
|
||||
));
|
||||
self.update_num_items(data.len());
|
||||
|
||||
// Calculate widths first, since we need them later.
|
||||
let calculated_widths = self.get_cache(area, data);
|
||||
let widths = calculated_widths
|
||||
.iter()
|
||||
.map(|column| Constraint::Length(*column))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Then calculate rows. We truncate the amount of data read based on height,
|
||||
// as well as truncating some entries based on available width.
|
||||
let data_slice = {
|
||||
let start = self.scrollable.index();
|
||||
let end = std::cmp::min(
|
||||
self.scrollable.num_items(),
|
||||
start + scrollable_height as usize,
|
||||
);
|
||||
&data[start..end]
|
||||
};
|
||||
let rows = data_slice.iter().map(|row| {
|
||||
Row::new(row.iter().zip(&calculated_widths).map(|(cell, width)| {
|
||||
let width = *width as usize;
|
||||
let graphemes =
|
||||
UnicodeSegmentation::graphemes(cell.as_str(), true).collect::<Vec<&str>>();
|
||||
let grapheme_width = graphemes.len();
|
||||
if width < grapheme_width && width > 1 {
|
||||
Text::raw(format!("{}…", graphemes[..(width - 1)].concat()))
|
||||
} else {
|
||||
Text::raw(cell.to_owned())
|
||||
}
|
||||
}))
|
||||
});
|
||||
|
||||
// Now build up our headers...
|
||||
let header = Row::new(self.sorted_column_names())
|
||||
.style(painter.colours.table_header_style)
|
||||
.bottom_margin(table_gap);
|
||||
|
||||
// And return tui-rs's [`TableState`].
|
||||
let mut tui_state = TableState::default();
|
||||
tui_state.select(Some(self.scrollable.index()));
|
||||
|
||||
(
|
||||
Table::new(rows)
|
||||
.header(header)
|
||||
.style(painter.colours.text_style),
|
||||
widths,
|
||||
tui_state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for TextTable {
|
||||
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
||||
for (index, column) in self.columns.iter().enumerate() {
|
||||
if let Some(shortcut) = column.shortcut {
|
||||
if let Some((shortcut, _)) = column.shortcut {
|
||||
if shortcut == event {
|
||||
if self.sort_index == index {
|
||||
// Just flip the sort if we're already sorting by this.
|
||||
|
@ -10,6 +10,7 @@ pub struct BatteryWidgetState {
|
||||
pub tab_click_locs: Option<Vec<((u16, u16), (u16, u16))>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BatteryState {
|
||||
pub widget_states: HashMap<u64, BatteryWidgetState>,
|
||||
}
|
||||
@ -30,17 +31,29 @@ impl BatteryState {
|
||||
|
||||
// TODO: Implement battery widget.
|
||||
/// A table displaying battery information on a per-battery basis.
|
||||
#[derive(Default)]
|
||||
pub struct BatteryTable {
|
||||
bounds: Rect,
|
||||
selected_index: usize,
|
||||
batteries: Vec<String>,
|
||||
}
|
||||
|
||||
impl BatteryTable {
|
||||
/// Creates a new [`BatteryTable`].
|
||||
pub fn new() -> Self {
|
||||
pub fn new(batteries: Vec<String>) -> Self {
|
||||
Self {
|
||||
bounds: Rect::default(),
|
||||
batteries,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
pub fn batteries(&self) -> &[String] {
|
||||
&self.batteries
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for BatteryTable {
|
||||
|
@ -29,6 +29,7 @@ impl CpuWidgetState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CpuState {
|
||||
pub force_update: Option<u64>,
|
||||
pub widget_states: HashMap<u64, CpuWidgetState>,
|
||||
|
@ -1,9 +1,12 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use tui::layout::Rect;
|
||||
use tui::{backend::Backend, layout::Rect, widgets::Block, Frame};
|
||||
|
||||
use crate::app::event::EventResult;
|
||||
use crate::{
|
||||
app::{event::EventResult, text_table::Column},
|
||||
canvas::{DisplayableData, Painter},
|
||||
};
|
||||
|
||||
use super::{AppScrollWidgetState, CanvasTableWidthState, Component, TextTable, Widget};
|
||||
|
||||
@ -21,6 +24,7 @@ impl DiskWidgetState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DiskState {
|
||||
pub widget_states: HashMap<u64, DiskWidgetState>,
|
||||
}
|
||||
@ -45,9 +49,18 @@ pub struct DiskTable {
|
||||
bounds: Rect,
|
||||
}
|
||||
|
||||
impl DiskTable {
|
||||
/// Creates a new [`DiskTable`].
|
||||
pub fn new(table: TextTable) -> Self {
|
||||
impl Default for DiskTable {
|
||||
fn default() -> Self {
|
||||
let table = TextTable::new(vec![
|
||||
Column::new_flex("Disk", None, false, 0.2),
|
||||
Column::new_flex("Mount", None, false, 0.2),
|
||||
Column::new_hard("Used", None, false, Some(4)),
|
||||
Column::new_hard("Free", None, false, Some(6)),
|
||||
Column::new_hard("Total", None, false, Some(6)),
|
||||
Column::new_hard("R/s", None, false, Some(7)),
|
||||
Column::new_hard("W/s", None, false, Some(7)),
|
||||
]);
|
||||
|
||||
Self {
|
||||
table,
|
||||
bounds: Rect::default(),
|
||||
@ -77,4 +90,16 @@ impl Widget for DiskTable {
|
||||
fn get_pretty_name(&self) -> &'static str {
|
||||
"Disk"
|
||||
}
|
||||
|
||||
fn draw<B: Backend>(
|
||||
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, block: Block<'_>,
|
||||
data: &DisplayableData,
|
||||
) {
|
||||
let draw_area = block.inner(area);
|
||||
let (table, widths, mut tui_state) =
|
||||
self.table
|
||||
.create_draw_table(painter, &data.disk_data, draw_area);
|
||||
|
||||
f.render_stateful_widget(table.block(block).widths(&widths), area, &mut tui_state);
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ impl MemWidgetState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MemState {
|
||||
pub force_update: Option<u64>,
|
||||
pub widget_states: HashMap<u64, MemWidgetState>,
|
||||
|
@ -33,6 +33,8 @@ impl NetWidgetState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NetState {
|
||||
pub force_update: Option<u64>,
|
||||
pub widget_states: HashMap<u64, NetWidgetState>,
|
||||
@ -65,7 +67,10 @@ pub struct NetGraphCache {
|
||||
|
||||
enum NetGraphCacheState {
|
||||
Uncached,
|
||||
Cached(NetGraphCache),
|
||||
Cached {
|
||||
cached_area: Rect,
|
||||
data: NetGraphCache,
|
||||
},
|
||||
}
|
||||
|
||||
/// A widget denoting network usage via a graph. This version is self-contained within a single [`TimeGraph`];
|
||||
@ -90,37 +95,54 @@ impl NetGraph {
|
||||
}
|
||||
|
||||
/// Updates the associated cache on a [`NetGraph`].
|
||||
pub fn set_cache(&mut self, max_range: f64, labels: Vec<String>, time_start: f64) {
|
||||
self.draw_cache = NetGraphCacheState::Cached(NetGraphCache {
|
||||
max_range,
|
||||
labels,
|
||||
time_start,
|
||||
})
|
||||
pub fn set_cache(&mut self, area: Rect, max_range: f64, labels: Vec<String>, time_start: f64) {
|
||||
self.draw_cache = NetGraphCacheState::Cached {
|
||||
cached_area: area,
|
||||
data: NetGraphCache {
|
||||
max_range,
|
||||
labels,
|
||||
time_start,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the [`NetGraph`] contains a cache from drawing.
|
||||
pub fn is_cached(&self) -> bool {
|
||||
match self.draw_cache {
|
||||
pub fn is_cache_valid(&self, area: Rect) -> bool {
|
||||
match &self.draw_cache {
|
||||
NetGraphCacheState::Uncached => false,
|
||||
NetGraphCacheState::Cached(_) => true,
|
||||
NetGraphCacheState::Cached {
|
||||
cached_area,
|
||||
data: _,
|
||||
} => *cached_area == area,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the [`NetGraphCache`] tied to the [`NetGraph`] if there is one.
|
||||
/// Returns a reference to the [`NetGraphCache`] tied to the [`NetGraph`].
|
||||
pub fn get_cache(&self) -> Option<&NetGraphCache> {
|
||||
match &self.draw_cache {
|
||||
NetGraphCacheState::Uncached => None,
|
||||
NetGraphCacheState::Cached(cache) => Some(cache),
|
||||
NetGraphCacheState::Cached {
|
||||
cached_area: _,
|
||||
data,
|
||||
} => Some(data),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an owned copy of the [`NetGraphCache`] tied to the [`NetGraph`] if there is one.
|
||||
/// Returns the [`NetGraphCache`] tied to the [`NetGraph`].
|
||||
pub fn get_cache_owned(&self) -> Option<NetGraphCache> {
|
||||
match &self.draw_cache {
|
||||
NetGraphCacheState::Uncached => None,
|
||||
NetGraphCacheState::Cached(cache) => Some(cache.clone()),
|
||||
NetGraphCacheState::Cached {
|
||||
cached_area: _,
|
||||
data,
|
||||
} => Some(data.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper function around checking the cache validity and setting/getting the cache.
|
||||
pub fn check_get_cache(&mut self) -> NetGraphCache {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for NetGraph {
|
||||
|
@ -3,13 +3,19 @@ use std::collections::HashMap;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
|
||||
use unicode_segmentation::GraphemeCursor;
|
||||
|
||||
use tui::{layout::Rect, widgets::TableState};
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::Rect,
|
||||
widgets::{Block, TableState},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
event::{does_point_intersect_rect, EventResult, MultiKey, MultiKeyResult},
|
||||
query::*,
|
||||
},
|
||||
canvas::{DisplayableData, Painter},
|
||||
data_harvester::processes::{self, ProcessSorting},
|
||||
};
|
||||
use ProcessSorting::*;
|
||||
@ -590,6 +596,7 @@ impl ProcWidgetState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ProcState {
|
||||
pub widget_states: HashMap<u64, ProcWidgetState>,
|
||||
pub force_update: Option<u64>,
|
||||
@ -815,4 +822,22 @@ impl Widget for ProcessManager {
|
||||
fn get_pretty_name(&self) -> &'static str {
|
||||
"Processes"
|
||||
}
|
||||
|
||||
fn draw<B: Backend>(
|
||||
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, block: Block<'_>,
|
||||
data: &DisplayableData,
|
||||
) {
|
||||
let draw_area = block.inner(area);
|
||||
let (process_table, widths, mut tui_state) = self.process_table.create_draw_table(
|
||||
painter,
|
||||
&vec![], // TODO: Fix this
|
||||
draw_area,
|
||||
);
|
||||
|
||||
f.render_stateful_widget(
|
||||
process_table.block(block).widths(&widths),
|
||||
area,
|
||||
&mut tui_state,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use tui::layout::Rect;
|
||||
use tui::{backend::Backend, layout::Rect, widgets::Block, Frame};
|
||||
|
||||
use crate::app::event::EventResult;
|
||||
use crate::{
|
||||
app::{event::EventResult, text_table::Column},
|
||||
canvas::{DisplayableData, Painter},
|
||||
};
|
||||
|
||||
use super::{AppScrollWidgetState, CanvasTableWidthState, Component, TextTable, Widget};
|
||||
|
||||
@ -21,6 +24,7 @@ impl TempWidgetState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TempState {
|
||||
pub widget_states: HashMap<u64, TempWidgetState>,
|
||||
}
|
||||
@ -45,9 +49,14 @@ pub struct TempTable {
|
||||
bounds: Rect,
|
||||
}
|
||||
|
||||
impl TempTable {
|
||||
/// Creates a new [`TempTable`].
|
||||
pub fn new(table: TextTable) -> Self {
|
||||
impl Default for TempTable {
|
||||
fn default() -> Self {
|
||||
let table = TextTable::new(vec![
|
||||
Column::new_flex("Sensor", None, false, 1.0),
|
||||
Column::new_hard("Temp", None, false, Some(4)),
|
||||
])
|
||||
.left_to_right(false);
|
||||
|
||||
Self {
|
||||
table,
|
||||
bounds: Rect::default(),
|
||||
@ -77,4 +86,16 @@ impl Widget for TempTable {
|
||||
fn get_pretty_name(&self) -> &'static str {
|
||||
"Temperature"
|
||||
}
|
||||
|
||||
fn draw<B: Backend>(
|
||||
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, block: Block<'_>,
|
||||
data: &DisplayableData,
|
||||
) {
|
||||
let draw_area = block.inner(area);
|
||||
let (table, widths, mut tui_state) =
|
||||
self.table
|
||||
.create_draw_table(painter, &data.temp_sensor_data, draw_area);
|
||||
|
||||
f.render_stateful_widget(table.block(block).widths(&widths), area, &mut tui_state);
|
||||
}
|
||||
}
|
||||
|
@ -38,17 +38,11 @@ fn main() -> Result<()> {
|
||||
let mut config: Config = create_or_get_config(&config_path)
|
||||
.context("Unable to properly parse or create the config file.")?;
|
||||
|
||||
// Get widget layout separately
|
||||
let (widget_layout, _default_widget_id, _default_widget_type_option) =
|
||||
get_widget_layout(&matches, &config)
|
||||
.context("Found an issue while trying to build the widget layout.")?;
|
||||
|
||||
// Create "app" struct, which will control most of the program and store settings/state
|
||||
let mut app = build_app(&matches, &mut config)?;
|
||||
|
||||
// Create painter and set colours.
|
||||
let mut painter = canvas::Painter::init(
|
||||
widget_layout,
|
||||
app.app_config_fields.table_gap,
|
||||
app.app_config_fields.use_basic_mode,
|
||||
&config,
|
||||
|
480
src/canvas.rs
480
src/canvas.rs
@ -1,11 +1,12 @@
|
||||
use itertools::izip;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use fxhash::FxHashMap;
|
||||
use indextree::{Arena, NodeId};
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
text::{Span, Spans},
|
||||
widgets::Paragraph,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
@ -13,14 +14,9 @@ use tui::{
|
||||
|
||||
use canvas_colours::*;
|
||||
use dialogs::*;
|
||||
use widgets::*;
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
self,
|
||||
layout_manager::{BottomColRow, BottomLayout, BottomWidgetType},
|
||||
AppState,
|
||||
},
|
||||
app::{self, layout_manager::LayoutNode, widgets::Widget, TmpBottomWidget},
|
||||
constants::*,
|
||||
data_conversion::{ConvertedBatteryData, ConvertedCpuData, ConvertedProcessData},
|
||||
options::Config,
|
||||
@ -31,8 +27,8 @@ use crate::{
|
||||
|
||||
mod canvas_colours;
|
||||
mod dialogs;
|
||||
pub mod drawing; // TODO: Remove pub access at some point!
|
||||
mod drawing_utils;
|
||||
mod widgets;
|
||||
|
||||
/// Point is of time, data
|
||||
type Point = (f64, f64);
|
||||
@ -92,102 +88,23 @@ impl FromStr for ColourScheme {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the canvas' state. TODO: [OPT] implement this.
|
||||
/// Handles the canvas' state.
|
||||
pub struct Painter {
|
||||
pub colours: CanvasColours,
|
||||
styled_help_text: Vec<Spans<'static>>,
|
||||
is_mac_os: bool, // FIXME: This feels out of place...
|
||||
row_constraints: Vec<Constraint>,
|
||||
col_constraints: Vec<Vec<Constraint>>,
|
||||
col_row_constraints: Vec<Vec<Vec<Constraint>>>,
|
||||
layout_constraints: Vec<Vec<Vec<Vec<Constraint>>>>,
|
||||
derived_widget_draw_locs: Vec<Vec<Vec<Vec<Rect>>>>,
|
||||
widget_layout: BottomLayout,
|
||||
|
||||
table_height_offset: u16,
|
||||
}
|
||||
|
||||
impl Painter {
|
||||
pub fn init(
|
||||
widget_layout: BottomLayout, table_gap: u16, is_basic_mode: bool, config: &Config,
|
||||
colour_scheme: ColourScheme,
|
||||
table_gap: u16, is_basic_mode: bool, config: &Config, colour_scheme: ColourScheme,
|
||||
) -> anyhow::Result<Self> {
|
||||
// Now for modularity; we have to also initialize the base layouts!
|
||||
// We want to do this ONCE and reuse; after this we can just construct
|
||||
// based on the console size.
|
||||
|
||||
let mut row_constraints = Vec::new();
|
||||
let mut col_constraints = Vec::new();
|
||||
let mut col_row_constraints = Vec::new();
|
||||
let mut layout_constraints = Vec::new();
|
||||
|
||||
widget_layout.rows.iter().for_each(|row| {
|
||||
if row.canvas_handle_height {
|
||||
row_constraints.push(Constraint::Length(0));
|
||||
} else {
|
||||
row_constraints.push(Constraint::Ratio(
|
||||
row.row_height_ratio,
|
||||
widget_layout.total_row_height_ratio,
|
||||
));
|
||||
}
|
||||
|
||||
let mut new_col_constraints = Vec::new();
|
||||
let mut new_widget_constraints = Vec::new();
|
||||
let mut new_col_row_constraints = Vec::new();
|
||||
row.children.iter().for_each(|col| {
|
||||
if col.canvas_handle_width {
|
||||
new_col_constraints.push(Constraint::Length(0));
|
||||
} else {
|
||||
new_col_constraints
|
||||
.push(Constraint::Ratio(col.col_width_ratio, row.total_col_ratio));
|
||||
}
|
||||
|
||||
let mut new_new_col_row_constraints = Vec::new();
|
||||
let mut new_new_widget_constraints = Vec::new();
|
||||
col.children.iter().for_each(|col_row| {
|
||||
if col_row.canvas_handle_height {
|
||||
new_new_col_row_constraints.push(Constraint::Length(0));
|
||||
} else if col_row.flex_grow {
|
||||
new_new_col_row_constraints.push(Constraint::Min(0));
|
||||
} else {
|
||||
new_new_col_row_constraints.push(Constraint::Ratio(
|
||||
col_row.col_row_height_ratio,
|
||||
col.total_col_row_ratio,
|
||||
));
|
||||
}
|
||||
|
||||
let mut new_new_new_widget_constraints = Vec::new();
|
||||
col_row.children.iter().for_each(|widget| {
|
||||
if widget.canvas_handle_width {
|
||||
new_new_new_widget_constraints.push(Constraint::Length(0));
|
||||
} else if widget.flex_grow {
|
||||
new_new_new_widget_constraints.push(Constraint::Min(0));
|
||||
} else {
|
||||
new_new_new_widget_constraints.push(Constraint::Ratio(
|
||||
widget.width_ratio,
|
||||
col_row.total_widget_ratio,
|
||||
));
|
||||
}
|
||||
});
|
||||
new_new_widget_constraints.push(new_new_new_widget_constraints);
|
||||
});
|
||||
new_col_row_constraints.push(new_new_col_row_constraints);
|
||||
new_widget_constraints.push(new_new_widget_constraints);
|
||||
});
|
||||
col_row_constraints.push(new_col_row_constraints);
|
||||
layout_constraints.push(new_widget_constraints);
|
||||
col_constraints.push(new_col_constraints);
|
||||
});
|
||||
|
||||
let mut painter = Painter {
|
||||
colours: CanvasColours::default(),
|
||||
styled_help_text: Vec::default(),
|
||||
is_mac_os: cfg!(target_os = "macos"),
|
||||
row_constraints,
|
||||
col_constraints,
|
||||
col_row_constraints,
|
||||
layout_constraints,
|
||||
widget_layout,
|
||||
derived_widget_draw_locs: Vec::default(),
|
||||
table_height_offset: if is_basic_mode { 2 } else { 4 } + table_gap,
|
||||
};
|
||||
|
||||
@ -295,10 +212,8 @@ impl Painter {
|
||||
pub fn draw_data<B: Backend>(
|
||||
&mut self, terminal: &mut Terminal<B>, app_state: &mut app::AppState,
|
||||
) -> error::Result<()> {
|
||||
use BottomWidgetType::*;
|
||||
|
||||
terminal.draw(|mut f| {
|
||||
let (terminal_size, frozen_draw_loc) = if app_state.is_frozen {
|
||||
let (draw_area, frozen_draw_loc) = if app_state.is_frozen {
|
||||
let split_loc = Layout::default()
|
||||
.constraints([Constraint::Min(0), Constraint::Length(1)])
|
||||
.split(f.size());
|
||||
@ -306,8 +221,8 @@ impl Painter {
|
||||
} else {
|
||||
(f.size(), None)
|
||||
};
|
||||
let terminal_height = terminal_size.height;
|
||||
let terminal_width = terminal_size.width;
|
||||
let terminal_height = draw_area.height;
|
||||
let terminal_width = draw_area.width;
|
||||
|
||||
if app_state.help_dialog_state.is_showing_help {
|
||||
let gen_help_len = GENERAL_HELP_TEXT.len() as u16 + 3;
|
||||
@ -319,7 +234,7 @@ impl Painter {
|
||||
Constraint::Length(gen_help_len),
|
||||
Constraint::Length(border_len),
|
||||
])
|
||||
.split(terminal_size);
|
||||
.split(draw_area);
|
||||
|
||||
let middle_dialog_chunk = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
@ -402,7 +317,7 @@ impl Painter {
|
||||
Constraint::Length(text_height),
|
||||
Constraint::Length(vertical_bordering),
|
||||
])
|
||||
.split(terminal_size);
|
||||
.split(draw_area);
|
||||
|
||||
let horizontal_bordering = terminal_width.saturating_sub(text_width) / 2;
|
||||
let middle_dialog_chunk = Layout::default()
|
||||
@ -422,257 +337,98 @@ impl Painter {
|
||||
self.draw_frozen_indicator(&mut f, frozen_draw_loc);
|
||||
}
|
||||
|
||||
let rect = Layout::default()
|
||||
.margin(0)
|
||||
.constraints([Constraint::Percentage(100)])
|
||||
.split(terminal_size);
|
||||
match &app_state.current_widget.widget_type {
|
||||
Cpu => draw_cpu(
|
||||
self,
|
||||
&mut f,
|
||||
app_state,
|
||||
rect[0],
|
||||
app_state.current_widget.widget_id,
|
||||
),
|
||||
CpuLegend => draw_cpu(
|
||||
self,
|
||||
&mut f,
|
||||
app_state,
|
||||
rect[0],
|
||||
app_state.current_widget.widget_id - 1,
|
||||
),
|
||||
Mem | BasicMem => draw_memory_graph(
|
||||
self,
|
||||
&mut f,
|
||||
app_state,
|
||||
rect[0],
|
||||
app_state.current_widget.widget_id,
|
||||
),
|
||||
Disk => draw_disk_table(
|
||||
self,
|
||||
&mut f,
|
||||
app_state,
|
||||
rect[0],
|
||||
true,
|
||||
app_state.current_widget.widget_id,
|
||||
),
|
||||
Temp => draw_temp_table(
|
||||
self,
|
||||
&mut f,
|
||||
app_state,
|
||||
rect[0],
|
||||
true,
|
||||
app_state.current_widget.widget_id,
|
||||
),
|
||||
Net => draw_network_graph(
|
||||
self,
|
||||
&mut f,
|
||||
app_state,
|
||||
rect[0],
|
||||
app_state.current_widget.widget_id,
|
||||
false,
|
||||
),
|
||||
Proc | ProcSearch | ProcSort => {
|
||||
let widget_id = app_state.current_widget.widget_id
|
||||
- match &app_state.current_widget.widget_type {
|
||||
ProcSearch => 1,
|
||||
ProcSort => 2,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
draw_process_features(self, &mut f, app_state, rect[0], true, widget_id);
|
||||
}
|
||||
Battery => draw_battery_display(
|
||||
self,
|
||||
&mut f,
|
||||
app_state,
|
||||
rect[0],
|
||||
true,
|
||||
app_state.current_widget.widget_id,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
} else if app_state.app_config_fields.use_basic_mode {
|
||||
// Basic mode. This basically removes all graphs but otherwise
|
||||
// the same info.
|
||||
if let Some(frozen_draw_loc) = frozen_draw_loc {
|
||||
self.draw_frozen_indicator(&mut f, frozen_draw_loc);
|
||||
}
|
||||
|
||||
let actual_cpu_data_len = app_state.canvas_data.cpu_data.len().saturating_sub(1);
|
||||
|
||||
// This fixes #397, apparently if the height is 1, it can't render the CPU bars...
|
||||
let cpu_height = {
|
||||
let c = (actual_cpu_data_len / 4) as u16
|
||||
+ (if actual_cpu_data_len % 4 == 0 { 0 } else { 1 });
|
||||
|
||||
if c <= 1 {
|
||||
1
|
||||
} else {
|
||||
c
|
||||
}
|
||||
};
|
||||
|
||||
let vertical_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints([
|
||||
Constraint::Length(cpu_height),
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(2),
|
||||
Constraint::Min(5),
|
||||
])
|
||||
.split(terminal_size);
|
||||
|
||||
let middle_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(vertical_chunks[1]);
|
||||
draw_basic_cpu(self, &mut f, app_state, vertical_chunks[0], 1);
|
||||
draw_basic_memory(self, &mut f, app_state, middle_chunks[0], 2);
|
||||
draw_basic_network(self, &mut f, app_state, middle_chunks[1], 3);
|
||||
|
||||
let mut later_widget_id: Option<u64> = None;
|
||||
if let Some(basic_table_widget_state) = &app_state.basic_table_widget_state {
|
||||
let widget_id = basic_table_widget_state.currently_displayed_widget_id;
|
||||
later_widget_id = Some(widget_id);
|
||||
match basic_table_widget_state.currently_displayed_widget_type {
|
||||
Disk => draw_disk_table(
|
||||
self,
|
||||
&mut f,
|
||||
app_state,
|
||||
vertical_chunks[3],
|
||||
false,
|
||||
widget_id,
|
||||
),
|
||||
Proc | ProcSort => {
|
||||
let wid = widget_id
|
||||
- match basic_table_widget_state.currently_displayed_widget_type {
|
||||
ProcSearch => 1,
|
||||
ProcSort => 2,
|
||||
_ => 0,
|
||||
};
|
||||
draw_process_features(
|
||||
self,
|
||||
&mut f,
|
||||
app_state,
|
||||
vertical_chunks[3],
|
||||
false,
|
||||
wid,
|
||||
);
|
||||
}
|
||||
Temp => draw_temp_table(
|
||||
self,
|
||||
&mut f,
|
||||
app_state,
|
||||
vertical_chunks[3],
|
||||
false,
|
||||
widget_id,
|
||||
),
|
||||
Battery => draw_battery_display(
|
||||
self,
|
||||
&mut f,
|
||||
app_state,
|
||||
vertical_chunks[3],
|
||||
false,
|
||||
widget_id,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(widget_id) = later_widget_id {
|
||||
draw_basic_table_arrows(self, &mut f, app_state, vertical_chunks[2], widget_id);
|
||||
let canvas_data = &app_state.canvas_data;
|
||||
if let Some(current_widget) = app_state
|
||||
.widget_lookup_map
|
||||
.get_mut(&app_state.selected_widget)
|
||||
{
|
||||
let block = Block::default()
|
||||
.border_style(self.colours.highlighted_border_style)
|
||||
.borders(Borders::ALL);
|
||||
current_widget.draw(self, f, draw_area, block, canvas_data);
|
||||
}
|
||||
} else {
|
||||
// Draws using the passed in (or default) layout.
|
||||
/// A simple traversal through the `arena`.
|
||||
fn traverse_and_draw_tree<B: Backend>(
|
||||
node: NodeId, arena: &Arena<LayoutNode>, area: Rect, f: &mut Frame<'_, B>,
|
||||
lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, painter: &Painter,
|
||||
canvas_data: &DisplayableData, selected_id: NodeId,
|
||||
) {
|
||||
if let Some(layout_node) = arena.get(node).map(|n| n.get()) {
|
||||
match layout_node {
|
||||
LayoutNode::Row(row) => {
|
||||
let split_area = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(row.constraints.clone())
|
||||
.split(area);
|
||||
|
||||
for (child, child_area) in node.children(arena).zip(split_area) {
|
||||
traverse_and_draw_tree(
|
||||
child,
|
||||
arena,
|
||||
child_area,
|
||||
f,
|
||||
lookup_map,
|
||||
painter,
|
||||
canvas_data,
|
||||
selected_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
LayoutNode::Col(col) => {
|
||||
let split_area = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(col.constraints.clone())
|
||||
.split(area);
|
||||
|
||||
for (child, child_area) in node.children(arena).zip(split_area) {
|
||||
traverse_and_draw_tree(
|
||||
child,
|
||||
arena,
|
||||
child_area,
|
||||
f,
|
||||
lookup_map,
|
||||
painter,
|
||||
canvas_data,
|
||||
selected_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
LayoutNode::Widget => {
|
||||
if let Some(widget) = lookup_map.get_mut(&node) {
|
||||
let block = Block::default()
|
||||
.border_style(if selected_id == node {
|
||||
painter.colours.highlighted_border_style
|
||||
} else {
|
||||
painter.colours.border_style
|
||||
})
|
||||
.borders(Borders::ALL);
|
||||
widget.draw(painter, f, area, block, canvas_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(frozen_draw_loc) = frozen_draw_loc {
|
||||
self.draw_frozen_indicator(&mut f, frozen_draw_loc);
|
||||
}
|
||||
|
||||
if self.derived_widget_draw_locs.is_empty() || app_state.is_force_redraw {
|
||||
let draw_locs = Layout::default()
|
||||
.margin(0)
|
||||
.constraints(self.row_constraints.as_ref())
|
||||
.direction(Direction::Vertical)
|
||||
.split(terminal_size);
|
||||
|
||||
self.derived_widget_draw_locs = izip!(
|
||||
draw_locs,
|
||||
&self.col_constraints,
|
||||
&self.col_row_constraints,
|
||||
&self.layout_constraints,
|
||||
&self.widget_layout.rows
|
||||
)
|
||||
.map(
|
||||
|(
|
||||
draw_loc,
|
||||
col_constraint,
|
||||
col_row_constraint,
|
||||
row_constraint_vec,
|
||||
cols,
|
||||
)| {
|
||||
izip!(
|
||||
Layout::default()
|
||||
.constraints(col_constraint.as_ref())
|
||||
.direction(Direction::Horizontal)
|
||||
.split(draw_loc)
|
||||
.into_iter(),
|
||||
col_row_constraint,
|
||||
row_constraint_vec,
|
||||
&cols.children
|
||||
)
|
||||
.map(|(split_loc, constraint, col_constraint_vec, col_rows)| {
|
||||
izip!(
|
||||
Layout::default()
|
||||
.constraints(constraint.as_ref())
|
||||
.direction(Direction::Vertical)
|
||||
.split(split_loc)
|
||||
.into_iter(),
|
||||
col_constraint_vec,
|
||||
&col_rows.children
|
||||
)
|
||||
.map(|(draw_loc, col_row_constraint_vec, widgets)| {
|
||||
// Note that col_row_constraint_vec CONTAINS the widget constraints
|
||||
let widget_draw_locs = Layout::default()
|
||||
.constraints(col_row_constraint_vec.as_ref())
|
||||
.direction(Direction::Horizontal)
|
||||
.split(draw_loc);
|
||||
|
||||
// Side effect, draw here.
|
||||
self.draw_widgets_with_constraints(
|
||||
&mut f,
|
||||
app_state,
|
||||
widgets,
|
||||
&widget_draw_locs,
|
||||
);
|
||||
|
||||
widget_draw_locs
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
} else {
|
||||
self.widget_layout
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| &row.children)
|
||||
.flatten()
|
||||
.map(|col| &col.children)
|
||||
.flatten()
|
||||
.zip(self.derived_widget_draw_locs.iter().flatten().flatten())
|
||||
.for_each(|(widgets, widget_draw_locs)| {
|
||||
self.draw_widgets_with_constraints(
|
||||
&mut f,
|
||||
app_state,
|
||||
widgets,
|
||||
widget_draw_locs,
|
||||
);
|
||||
});
|
||||
}
|
||||
let root = &app_state.layout_tree_root;
|
||||
let arena = &app_state.layout_tree;
|
||||
let canvas_data = &app_state.canvas_data;
|
||||
let selected_id = app_state.selected_widget;
|
||||
let lookup_map = &mut app_state.widget_lookup_map;
|
||||
traverse_and_draw_tree(
|
||||
*root,
|
||||
arena,
|
||||
draw_area,
|
||||
f,
|
||||
lookup_map,
|
||||
self,
|
||||
canvas_data,
|
||||
selected_id,
|
||||
);
|
||||
}
|
||||
})?;
|
||||
|
||||
@ -681,42 +437,4 @@ impl Painter {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_widgets_with_constraints<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut AppState, widgets: &BottomColRow,
|
||||
widget_draw_locs: &[Rect],
|
||||
) {
|
||||
use BottomWidgetType::*;
|
||||
for (widget, widget_draw_loc) in widgets.children.iter().zip(widget_draw_locs) {
|
||||
match &widget.widget_type {
|
||||
Empty => {}
|
||||
Cpu => draw_cpu(self, f, app_state, *widget_draw_loc, widget.widget_id),
|
||||
Mem => draw_memory_graph(self, f, app_state, *widget_draw_loc, widget.widget_id),
|
||||
Net => draw_network(self, f, app_state, *widget_draw_loc, widget.widget_id),
|
||||
Temp => {
|
||||
draw_temp_table(self, f, app_state, *widget_draw_loc, true, widget.widget_id)
|
||||
}
|
||||
Disk => {
|
||||
draw_disk_table(self, f, app_state, *widget_draw_loc, true, widget.widget_id)
|
||||
}
|
||||
Proc => draw_process_features(
|
||||
self,
|
||||
f,
|
||||
app_state,
|
||||
*widget_draw_loc,
|
||||
true,
|
||||
widget.widget_id,
|
||||
),
|
||||
Battery => draw_battery_display(
|
||||
self,
|
||||
f,
|
||||
app_state,
|
||||
*widget_draw_loc,
|
||||
true,
|
||||
widget.widget_id,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -242,16 +242,14 @@ pub fn build_app(matches: &clap::ArgMatches<'static>, config: &mut Config) -> Re
|
||||
todo!()
|
||||
} else if let Some(row) = &config.row {
|
||||
create_layout_tree(row, process_defaults, &app_config_fields)?
|
||||
} else if get_use_battery(matches, config) {
|
||||
let rows = toml::from_str::<Config>(DEFAULT_BATTERY_LAYOUT)?
|
||||
.row
|
||||
.unwrap();
|
||||
create_layout_tree(&rows, process_defaults, &app_config_fields)?
|
||||
} else {
|
||||
if get_use_battery(matches, config) {
|
||||
let rows = toml::from_str::<Config>(DEFAULT_BATTERY_LAYOUT)?
|
||||
.row
|
||||
.unwrap();
|
||||
create_layout_tree(&rows, process_defaults, &app_config_fields)?
|
||||
} else {
|
||||
let rows = toml::from_str::<Config>(DEFAULT_LAYOUT)?.row.unwrap();
|
||||
create_layout_tree(&rows, process_defaults, &app_config_fields)?
|
||||
}
|
||||
let rows = toml::from_str::<Config>(DEFAULT_LAYOUT)?.row.unwrap();
|
||||
create_layout_tree(&rows, process_defaults, &app_config_fields)?
|
||||
};
|
||||
|
||||
let disk_filter =
|
||||
|
Loading…
x
Reference in New Issue
Block a user