refactor: start moving over drawing system

In particular, moving over table-style widgets
This commit is contained in:
ClementTsang 2021-08-26 16:49:20 -04:00
parent dd7e183ec8
commit 0afc371eaa
28 changed files with 829 additions and 595 deletions

View File

@ -1,2 +1,3 @@
cognitive-complexity-threshold = 100
type-complexity-threshold = 500
type-complexity-threshold = 500
too-many-arguments-threshold = 8

View File

@ -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) {

View File

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

View File

@ -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,

View File

@ -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 {

View File

@ -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.

View File

@ -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 {

View File

@ -29,6 +29,7 @@ impl CpuWidgetState {
}
}
#[derive(Default)]
pub struct CpuState {
pub force_update: Option<u64>,
pub widget_states: HashMap<u64, CpuWidgetState>,

View File

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

View File

@ -21,6 +21,7 @@ impl MemWidgetState {
}
}
#[derive(Default)]
pub struct MemState {
pub force_update: Option<u64>,
pub widget_states: HashMap<u64, MemWidgetState>,

View File

@ -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 {

View File

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

View File

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

View File

@ -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,

View File

@ -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,
),
_ => {}
}
}
}
}

View File

@ -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 =