diff --git a/clippy.toml b/clippy.toml index b3a62dba..e25ae33d 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,2 +1,3 @@ cognitive-complexity-threshold = 100 -type-complexity-threshold = 500 \ No newline at end of file +type-complexity-threshold = 500 +too-many-arguments-threshold = 8 diff --git a/src/app.rs b/src/app.rs index fe4e44da..5c1e9b90 100644 --- a/src/app.rs +++ b/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, - #[builder(default, setter(skip))] to_delete_process_list: Option<(String, Vec)>, - #[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, pub current_widget: BottomWidget, - #[builder(default = false, setter(skip))] + last_key_press: Instant, + awaiting_second_char: bool, - #[builder(default, setter(skip))] second_char: Option, + 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, @@ -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) { diff --git a/src/app/layout_manager.rs b/src/app/layout_manager.rs index 065b89fb..a2a70a42 100644 --- a/src/app/layout_manager.rs +++ b/src/app/layout_manager.rs @@ -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, } /// 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, } /// 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) { + 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, pub root: NodeId, pub widget_lookup_map: FxHashMap, - pub selected: Option, + pub selected: NodeId, + pub used_widgets: UsedWidgets, } /// Creates a new [`Arena`] 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 { fn add_widget_to_map( - widget_lookup_map: &mut FxHashMap, widget_type: &str, + widget_lookup_map: &mut FxHashMap, widget_type: BottomWidgetType, widget_id: NodeId, process_defaults: &crate::options::ProcessDefaults, app_config_fields: &super::AppConfigFields, ) -> Result<()> { - match widget_type.parse::()? { + 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::()?; + 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::()?; + 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 } diff --git a/src/app/widgets.rs b/src/app/widgets.rs index d5ba3586..a99fe7b8 100644 --- a/src/app/widgets.rs +++ b/src/app/widgets.rs @@ -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( + &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, diff --git a/src/app/widgets/base/scrollable.rs b/src/app/widgets/base/scrollable.rs index 4606b02b..c180192a 100644 --- a/src/app/widgets/base/scrollable.rs +++ b/src/app/widgets/base/scrollable.rs @@ -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 { diff --git a/src/app/widgets/base/text_table.rs b/src/app/widgets/base/text_table.rs index d5ff3d2c..fef2e31f 100644 --- a/src/app/widgets/base/text_table.rs +++ b/src/app/widgets/base/text_table.rs @@ -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, + 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, default_descending: bool) -> Self { + /// Creates a new [`Column`]. + pub fn new( + name: &'static str, shortcut: Option, 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, default_descending: bool, + hard_length: Option, + ) -> 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, 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, + }, } /// A sortable, scrollable table with columns. @@ -37,6 +137,9 @@ pub struct TextTable { /// The columns themselves. columns: Vec, + /// 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, bool)>) -> Self { + pub fn new(columns: Vec) -> 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 { - 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 { + 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) { - 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], + ) -> Vec { + 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::>() + } + + fn get_cache(&mut self, area: Rect, data: &[Vec]) -> Vec { + fn calculate_column_widths( + left_to_right: bool, mut desired_widths: Vec, total_width: u16, + ) -> Vec { + 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 = 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], area: Rect, + ) -> (Table<'_>, Vec, TableState) { + // TODO: Change data: &[Vec] to &[Vec>] + 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::>(); + + // 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::>(); + 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. diff --git a/src/app/widgets/battery.rs b/src/app/widgets/battery.rs index 11459870..5ef6742a 100644 --- a/src/app/widgets/battery.rs +++ b/src/app/widgets/battery.rs @@ -10,6 +10,7 @@ pub struct BatteryWidgetState { pub tab_click_locs: Option>, } +#[derive(Default)] pub struct BatteryState { pub widget_states: HashMap, } @@ -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, } impl BatteryTable { /// Creates a new [`BatteryTable`]. - pub fn new() -> Self { + pub fn new(batteries: Vec) -> 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 { diff --git a/src/app/widgets/cpu.rs b/src/app/widgets/cpu.rs index 75f02c4a..9009bfe9 100644 --- a/src/app/widgets/cpu.rs +++ b/src/app/widgets/cpu.rs @@ -29,6 +29,7 @@ impl CpuWidgetState { } } +#[derive(Default)] pub struct CpuState { pub force_update: Option, pub widget_states: HashMap, diff --git a/src/app/widgets/disk.rs b/src/app/widgets/disk.rs index 18fd7704..119c071d 100644 --- a/src/app/widgets/disk.rs +++ b/src/app/widgets/disk.rs @@ -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, } @@ -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( + &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); + } } diff --git a/src/app/widgets/mem.rs b/src/app/widgets/mem.rs index dc23f244..ead790ce 100644 --- a/src/app/widgets/mem.rs +++ b/src/app/widgets/mem.rs @@ -21,6 +21,7 @@ impl MemWidgetState { } } +#[derive(Default)] pub struct MemState { pub force_update: Option, pub widget_states: HashMap, diff --git a/src/app/widgets/net.rs b/src/app/widgets/net.rs index 5d90f9cc..15366bb1 100644 --- a/src/app/widgets/net.rs +++ b/src/app/widgets/net.rs @@ -33,6 +33,8 @@ impl NetWidgetState { } } } + +#[derive(Default)] pub struct NetState { pub force_update: Option, pub widget_states: HashMap, @@ -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, 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, 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 { 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 { diff --git a/src/app/widgets/process.rs b/src/app/widgets/process.rs index dfdfaa19..0addb543 100644 --- a/src/app/widgets/process.rs +++ b/src/app/widgets/process.rs @@ -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, pub force_update: Option, @@ -815,4 +822,22 @@ impl Widget for ProcessManager { fn get_pretty_name(&self) -> &'static str { "Processes" } + + fn draw( + &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, + ); + } } diff --git a/src/app/widgets/temp.rs b/src/app/widgets/temp.rs index ef54dab6..18a02584 100644 --- a/src/app/widgets/temp.rs +++ b/src/app/widgets/temp.rs @@ -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, } @@ -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( + &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); + } } diff --git a/src/bin/main.rs b/src/bin/main.rs index 37431389..f6517167 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -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, diff --git a/src/canvas.rs b/src/canvas.rs index 5d9fe89f..0f98829a 100644 --- a/src/canvas.rs +++ b/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>, is_mac_os: bool, // FIXME: This feels out of place... - row_constraints: Vec, - col_constraints: Vec>, - col_row_constraints: Vec>>, - layout_constraints: Vec>>>, - derived_widget_draw_locs: Vec>>>, - 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 { - // 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( &mut self, terminal: &mut Terminal, 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 = 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( + node: NodeId, arena: &Arena, area: Rect, f: &mut Frame<'_, B>, + lookup_map: &mut FxHashMap, 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( - &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, - ), - _ => {} - } - } - } } diff --git a/src/canvas/widgets.rs b/src/canvas/drawing.rs similarity index 100% rename from src/canvas/widgets.rs rename to src/canvas/drawing.rs diff --git a/src/canvas/widgets/basic_table_arrows.rs b/src/canvas/drawing/basic_table_arrows.rs similarity index 100% rename from src/canvas/widgets/basic_table_arrows.rs rename to src/canvas/drawing/basic_table_arrows.rs diff --git a/src/canvas/widgets/battery_display.rs b/src/canvas/drawing/battery_display.rs similarity index 100% rename from src/canvas/widgets/battery_display.rs rename to src/canvas/drawing/battery_display.rs diff --git a/src/canvas/widgets/cpu_basic.rs b/src/canvas/drawing/cpu_basic.rs similarity index 100% rename from src/canvas/widgets/cpu_basic.rs rename to src/canvas/drawing/cpu_basic.rs diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/drawing/cpu_graph.rs similarity index 100% rename from src/canvas/widgets/cpu_graph.rs rename to src/canvas/drawing/cpu_graph.rs diff --git a/src/canvas/widgets/disk_table.rs b/src/canvas/drawing/disk_table.rs similarity index 100% rename from src/canvas/widgets/disk_table.rs rename to src/canvas/drawing/disk_table.rs diff --git a/src/canvas/widgets/mem_basic.rs b/src/canvas/drawing/mem_basic.rs similarity index 100% rename from src/canvas/widgets/mem_basic.rs rename to src/canvas/drawing/mem_basic.rs diff --git a/src/canvas/widgets/mem_graph.rs b/src/canvas/drawing/mem_graph.rs similarity index 100% rename from src/canvas/widgets/mem_graph.rs rename to src/canvas/drawing/mem_graph.rs diff --git a/src/canvas/widgets/network_basic.rs b/src/canvas/drawing/network_basic.rs similarity index 100% rename from src/canvas/widgets/network_basic.rs rename to src/canvas/drawing/network_basic.rs diff --git a/src/canvas/widgets/network_graph.rs b/src/canvas/drawing/network_graph.rs similarity index 100% rename from src/canvas/widgets/network_graph.rs rename to src/canvas/drawing/network_graph.rs diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/drawing/process_table.rs similarity index 100% rename from src/canvas/widgets/process_table.rs rename to src/canvas/drawing/process_table.rs diff --git a/src/canvas/widgets/temp_table.rs b/src/canvas/drawing/temp_table.rs similarity index 100% rename from src/canvas/widgets/temp_table.rs rename to src/canvas/drawing/temp_table.rs diff --git a/src/options.rs b/src/options.rs index da3102b1..a238c354 100644 --- a/src/options.rs +++ b/src/options.rs @@ -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::(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::(DEFAULT_BATTERY_LAYOUT)? - .row - .unwrap(); - create_layout_tree(&rows, process_defaults, &app_config_fields)? - } else { - let rows = toml::from_str::(DEFAULT_LAYOUT)?.row.unwrap(); - create_layout_tree(&rows, process_defaults, &app_config_fields)? - } + let rows = toml::from_str::(DEFAULT_LAYOUT)?.row.unwrap(); + create_layout_tree(&rows, process_defaults, &app_config_fields)? }; let disk_filter =