bug: fix bug causing click bounds to fail

There were three bugs:

1. The click bounds calculation was incorrect. I did the silly mistake
   of checking for <= bounds for the bottom and right sections of a
   Rect when checking if the mouse intersected - this is WRONG.

   For example, let's say you want to calculate if an x value of 5 falls
   between something that starts at 0 and is 5 long.  It shouldn't,
   right?  Because it draws from 0 to 4?  But if you just did <=
   Rect.right(), you would get a hit - because it just does (start +
   width), so you get 5, and 5 <= 5!

   So, easy fix, change all far bounds checks to <.

2. The second bug is a mistake where I accidentally did not include
   bounds sets for my memory and net widgets. Instead, they set their
   bounds to the underlying graph representation, which is WRONG, since
   that bound gets updated on draw, and gets set to a slightly smaller
   rect due to borders!

3. A slightly sneakier one. This broke my bounds checks for the CPU
   widget - and it would have broken my process widget too.

   The problem lies in the concept of widgets that handle multiple
   "sub"-blocks internally, and how I was doing click detection
   internally - I would check if the bounds of the internal Components
   were hit.  Say, the CPU, I would check if the internal graph was hit,
   then if the internal table was hit.

   But wait! I said in point 2 that a graph gets its borders updated on
   draw to something slightly smaller, due to borders!  And there's the
   problem - it affected tables too.  I was setting the bounds of
   components to that of the *internal* representation - without borders
   - but my click detection *needed* borders included!

   Solution?  Add another trait function to check bordered bounds, and
   make the default implementation just check the existing bounds. For
   cases like internal Components that may need it, I add a separate
   implementation.

   I also switched over all border bounds checks for Widgets to that,
   since it's a bit more consistent.
This commit is contained in:
ClementTsang 2021-08-29 20:05:17 -04:00
parent 48c572dbaf
commit 3fa50605b3
18 changed files with 204 additions and 69 deletions

View File

@ -353,11 +353,11 @@ impl AppState {
} }
} else { } else {
for (id, widget) in self.widget_lookup_map.iter_mut() { for (id, widget) in self.widget_lookup_map.iter_mut() {
if widget.does_intersect_mouse(&event) { if widget.does_border_intersect_mouse(&event) {
let is_id_selected = self.selected_widget == *id; let was_id_already_selected = self.selected_widget == *id;
self.selected_widget = *id; self.selected_widget = *id;
if is_id_selected { if was_id_already_selected {
let result = widget.handle_mouse_event(event); let result = widget.handle_mouse_event(event);
return self.convert_widget_event_result(result); return self.convert_widget_event_result(result);
} else { } else {

View File

@ -4,6 +4,7 @@ const MAX_TIMEOUT: Duration = Duration::from_millis(400);
/// These are "signals" that are sent along with an [`WidgetEventResult`] to signify a potential additional action /// These are "signals" that are sent along with an [`WidgetEventResult`] to signify a potential additional action
/// that the caller must do, along with the "core" result of either drawing or redrawing. /// that the caller must do, along with the "core" result of either drawing or redrawing.
#[derive(Debug)]
pub enum ReturnSignal { pub enum ReturnSignal {
/// A signal returned when some process widget was told to try to kill a process (or group of processes). /// A signal returned when some process widget was told to try to kill a process (or group of processes).
/// ///
@ -18,6 +19,7 @@ pub enum ReturnSignal {
} }
/// The results of handling an event by the [`AppState`]. /// The results of handling an event by the [`AppState`].
#[derive(Debug)]
pub enum EventResult { pub enum EventResult {
/// Kill the program. /// Kill the program.
Quit, Quit,
@ -29,6 +31,7 @@ pub enum EventResult {
/// The results of a widget handling some event, like a mouse or key event, /// The results of a widget handling some event, like a mouse or key event,
/// signifying what the program should then do next. /// signifying what the program should then do next.
#[derive(Debug)]
pub enum WidgetEventResult { pub enum WidgetEventResult {
/// Kill the program. /// Kill the program.
Quit, Quit,

View File

@ -1001,13 +1001,16 @@ pub struct ColLayout {
} }
/// A [`LayoutNode`] represents a single node in the overall widget hierarchy. Each node is one of: /// A [`LayoutNode`] represents a single node in the overall widget hierarchy. Each node is one of:
/// - [`LayoutNode::Row`] (a a non-leaf that distributes its children horizontally) /// - [`LayoutNode::Row`] (a non-leaf that distributes its children horizontally)
/// - [`LayoutNode::Col`] (a non-leaf node that distributes its children vertically) /// - [`LayoutNode::Col`] (a non-leaf node that distributes its children vertically)
/// - [`LayoutNode::Widget`] (a leaf node that contains the ID of the widget it is associated with) /// - [`LayoutNode::Widget`] (a leaf node that contains the ID of the widget it is associated with)
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
pub enum LayoutNode { pub enum LayoutNode {
/// A non-leaf that distributes its children horizontally
Row(RowLayout), Row(RowLayout),
/// A non-leaf node that distributes its children vertically
Col(ColLayout), Col(ColLayout),
/// A leaf node that contains the ID of the widget it is associated with
Widget, Widget,
} }

View File

@ -6,7 +6,7 @@ use tui::{backend::Backend, layout::Rect, widgets::TableState, Frame};
use crate::{ use crate::{
app::{ app::{
event::{WidgetEventResult, SelectionAction}, event::{SelectionAction, WidgetEventResult},
layout_manager::BottomWidgetType, layout_manager::BottomWidgetType,
}, },
canvas::Painter, canvas::Painter,
@ -63,15 +63,36 @@ pub trait Component {
/// coordinates. /// coordinates.
fn bounds(&self) -> Rect; fn bounds(&self) -> Rect;
/// Updates a [`Component`]s bounding box to `new_bounds`. /// Updates a [`Component`]'s bounding box to `new_bounds`.
fn set_bounds(&mut self, new_bounds: Rect); fn set_bounds(&mut self, new_bounds: Rect);
/// Returns whether a [`MouseEvent`] intersects a [`Component`]. /// Returns a [`Component`]'s bounding box, *including the border*. Defaults to just returning the normal bounds.
fn does_intersect_mouse(&self, event: &MouseEvent) -> bool { /// Note that these are defined in *global*, *absolute* coordinates.
fn border_bounds(&self) -> Rect {
self.bounds()
}
/// Updates a [`Component`]'s bounding box to `new_bounds`. Defaults to just setting the normal bounds.
fn set_border_bounds(&mut self, new_bounds: Rect) {
self.set_bounds(new_bounds);
}
/// Returns whether a [`MouseEvent`] intersects a [`Component`]'s bounds.
fn does_bounds_intersect_mouse(&self, event: &MouseEvent) -> bool {
let x = event.column; let x = event.column;
let y = event.row; let y = event.row;
let rect = self.bounds(); let bounds = self.bounds();
x >= rect.left() && x <= rect.right() && y >= rect.top() && y <= rect.bottom()
x >= bounds.left() && x < bounds.right() && y >= bounds.top() && y < bounds.bottom()
}
/// Returns whether a [`MouseEvent`] intersects a [`Component`]'s bounds, including any borders, if there are.
fn does_border_intersect_mouse(&self, event: &MouseEvent) -> bool {
let x = event.column;
let y = event.row;
let bounds = self.border_bounds();
x >= bounds.left() && x < bounds.right() && y >= bounds.top() && y < bounds.bottom()
} }
} }

View File

@ -224,7 +224,7 @@ impl Component for Scrollable {
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult { fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
match event.kind { match event.kind {
MouseEventKind::Down(MouseButton::Left) => { MouseEventKind::Down(MouseButton::Left) => {
if self.does_intersect_mouse(&event) { if self.does_bounds_intersect_mouse(&event) {
// This requires a bit of fancy calculation. The main trick is remembering that // This requires a bit of fancy calculation. The main trick is remembering that
// we are using a *visual* index here - not what is the actual index! Luckily, we keep track of that // we are using a *visual* index here - not what is the actual index! Luckily, we keep track of that
// inside our linked copy of TableState! // inside our linked copy of TableState!

View File

@ -1,7 +1,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use tui::{backend::Backend, layout::Rect, widgets::Block}; use tui::{backend::Backend, layout::Rect, widgets::Block, Frame};
use crate::{ use crate::{
app::{ app::{
@ -60,6 +60,9 @@ pub enum SortStatus {
/// A trait for sortable columns. /// A trait for sortable columns.
pub trait SortableColumn { pub trait SortableColumn {
/// Returns the original name of the column.
fn original_name(&self) -> &Cow<'static, str>;
/// Returns the shortcut for the column, if it exists. /// Returns the shortcut for the column, if it exists.
fn shortcut(&self) -> &Option<(KeyEvent, String)>; fn shortcut(&self) -> &Option<(KeyEvent, String)>;
@ -73,12 +76,18 @@ pub trait SortableColumn {
/// Sets the sorting status. /// Sets the sorting status.
fn set_sorting_status(&mut self, sorting_status: SortStatus); fn set_sorting_status(&mut self, sorting_status: SortStatus);
// ----- The following are required since SortableColumn implements TableColumn -----
/// Returns the displayed name on the column when drawing.
fn display_name(&self) -> Cow<'static, str>; fn display_name(&self) -> Cow<'static, str>;
/// Returns the desired width of the column when drawing.
fn get_desired_width(&self) -> &DesiredColumnWidth; fn get_desired_width(&self) -> &DesiredColumnWidth;
/// Returns the x bounds of a column. The y is assumed to be 0, relative to the table..
fn get_x_bounds(&self) -> Option<(u16, u16)>; fn get_x_bounds(&self) -> Option<(u16, u16)>;
/// Sets the x bounds of a column.
fn set_x_bounds(&mut self, x_bounds: Option<(u16, u16)>); fn set_x_bounds(&mut self, x_bounds: Option<(u16, u16)>);
} }
@ -106,8 +115,11 @@ where
/// A [`SimpleSortableColumn`] represents some column in a [`SortableTextTable`]. /// A [`SimpleSortableColumn`] represents some column in a [`SortableTextTable`].
#[derive(Debug)] #[derive(Debug)]
pub struct SimpleSortableColumn { pub struct SimpleSortableColumn {
original_name: Cow<'static, str>,
pub shortcut: Option<(KeyEvent, String)>, pub shortcut: Option<(KeyEvent, String)>,
pub default_descending: bool, pub default_descending: bool,
x_bounds: Option<(u16, u16)>,
pub internal: SimpleColumn, pub internal: SimpleColumn,
/// Whether this column is currently selected for sorting, and which direction. /// Whether this column is currently selected for sorting, and which direction.
@ -117,12 +129,15 @@ pub struct SimpleSortableColumn {
impl SimpleSortableColumn { impl SimpleSortableColumn {
/// Creates a new [`SimpleSortableColumn`]. /// Creates a new [`SimpleSortableColumn`].
fn new( fn new(
full_name: Cow<'static, str>, shortcut: Option<(KeyEvent, String)>, original_name: Cow<'static, str>, full_name: Cow<'static, str>,
default_descending: bool, desired_width: DesiredColumnWidth, shortcut: Option<(KeyEvent, String)>, default_descending: bool,
desired_width: DesiredColumnWidth,
) -> Self { ) -> Self {
Self { Self {
original_name,
shortcut, shortcut,
default_descending, default_descending,
x_bounds: None,
internal: SimpleColumn::new(full_name, desired_width), internal: SimpleColumn::new(full_name, desired_width),
sorting_status: SortStatus::NotSorting, sorting_status: SortStatus::NotSorting,
} }
@ -141,11 +156,12 @@ impl SimpleSortableColumn {
Some((shortcut, shortcut_name)), Some((shortcut, shortcut_name)),
) )
} else { } else {
(name, None) (name.clone(), None)
}; };
let full_name_len = full_name.len(); let full_name_len = full_name.len();
SimpleSortableColumn::new( SimpleSortableColumn::new(
name,
full_name, full_name,
shortcut, shortcut,
default_descending, default_descending,
@ -165,11 +181,12 @@ impl SimpleSortableColumn {
Some((shortcut, shortcut_name)), Some((shortcut, shortcut_name)),
) )
} else { } else {
(name, None) (name.clone(), None)
}; };
let full_name_len = full_name.len(); let full_name_len = full_name.len();
SimpleSortableColumn::new( SimpleSortableColumn::new(
name,
full_name, full_name,
shortcut, shortcut,
default_descending, default_descending,
@ -182,6 +199,10 @@ impl SimpleSortableColumn {
} }
impl SortableColumn for SimpleSortableColumn { impl SortableColumn for SimpleSortableColumn {
fn original_name(&self) -> &Cow<'static, str> {
&self.original_name
}
fn shortcut(&self) -> &Option<(KeyEvent, String)> { fn shortcut(&self) -> &Option<(KeyEvent, String)> {
&self.shortcut &self.shortcut
} }
@ -236,6 +257,9 @@ where
/// The underlying [`TextTable`]. /// The underlying [`TextTable`].
pub table: TextTable<S>, pub table: TextTable<S>,
/// A corresponding "sort" menu.
pub sort_menu: TextTable,
} }
impl<S> SortableTextTable<S> impl<S> SortableTextTable<S>
@ -244,9 +268,15 @@ where
{ {
/// Creates a new [`SortableTextTable`]. Note that `columns` cannot be empty. /// Creates a new [`SortableTextTable`]. Note that `columns` cannot be empty.
pub fn new(columns: Vec<S>) -> Self { pub fn new(columns: Vec<S>) -> Self {
let sort_menu_columns = columns
.iter()
.map(|column| SimpleColumn::new_hard(column.original_name().clone(), None))
.collect::<Vec<_>>();
let mut st = Self { let mut st = Self {
sort_index: 0, sort_index: 0,
table: TextTable::new(columns), table: TextTable::new(columns),
sort_menu: TextTable::new(sort_menu_columns),
}; };
st.set_sort_index(0); st.set_sort_index(0);
st st
@ -317,15 +347,21 @@ where
/// Draws a [`tui::widgets::Table`] on screen. /// Draws a [`tui::widgets::Table`] on screen.
/// ///
/// Note if the number of columns don't match in the [`TextTable`] and data, /// Note if the number of columns don't match in the [`SortableTextTable`] and data,
/// it will only create as many columns as it can grab data from both sources from. /// it will only create as many columns as it can grab data from both sources from.
pub fn draw_tui_table<B: Backend>( pub fn draw_tui_table<B: Backend>(
&mut self, painter: &Painter, f: &mut tui::Frame<'_, B>, data: &TextTableData, &mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableData, block: Block<'_>,
block: Block<'_>, block_area: Rect, show_selected_entry: bool, block_area: Rect, show_selected_entry: bool,
) { ) {
self.table self.table
.draw_tui_table(painter, f, data, block, block_area, show_selected_entry); .draw_tui_table(painter, f, data, block, block_area, show_selected_entry);
} }
/// Draws a [`tui::widgets::Table`] on screen corresponding to the sort columns of this [`SortableTextTable`].
pub fn draw_sort_table<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, block: Block<'_>, block_area: Rect,
) {
}
} }
impl<S> Component for SortableTextTable<S> impl<S> Component for SortableTextTable<S>
@ -347,7 +383,7 @@ where
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult { fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
if let MouseEventKind::Down(MouseButton::Left) = event.kind { if let MouseEventKind::Down(MouseButton::Left) = event.kind {
if !self.does_intersect_mouse(&event) { if !self.does_bounds_intersect_mouse(&event) {
return WidgetEventResult::NoRedraw; return WidgetEventResult::NoRedraw;
} }
@ -373,10 +409,18 @@ where
} }
fn bounds(&self) -> Rect { fn bounds(&self) -> Rect {
self.table.bounds self.table.bounds()
} }
fn set_bounds(&mut self, new_bounds: Rect) { fn set_bounds(&mut self, new_bounds: Rect) {
self.table.bounds = new_bounds; self.table.set_bounds(new_bounds)
}
fn border_bounds(&self) -> Rect {
self.table.border_bounds()
}
fn set_border_bounds(&mut self, new_bounds: Rect) {
self.table.set_border_bounds(new_bounds)
} }
} }

View File

@ -12,6 +12,7 @@ pub struct TextInput {
text: String, text: String,
cursor_index: usize, cursor_index: usize,
bounds: Rect, bounds: Rect,
border_bounds: Rect,
} }
impl TextInput { impl TextInput {
@ -92,6 +93,14 @@ impl Component for TextInput {
self.bounds = new_bounds; self.bounds = new_bounds;
} }
fn border_bounds(&self) -> Rect {
self.border_bounds
}
fn set_border_bounds(&mut self, new_bounds: Rect) {
self.border_bounds = new_bounds;
}
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult { fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
if event.modifiers.is_empty() { if event.modifiers.is_empty() {
match event.code { match event.code {

View File

@ -131,6 +131,9 @@ where
/// The bounding box of the [`TextTable`]. /// The bounding box of the [`TextTable`].
pub bounds: Rect, // TODO: Consider moving bounds to something else??? pub bounds: Rect, // TODO: Consider moving bounds to something else???
/// The bounds including the border, if there is one.
pub border_bounds: Rect,
/// Whether we draw columns from left-to-right. /// Whether we draw columns from left-to-right.
pub left_to_right: bool, pub left_to_right: bool,
@ -149,6 +152,7 @@ where
cached_column_widths: CachedColumnWidths::Uncached, cached_column_widths: CachedColumnWidths::Uncached,
show_gap: true, show_gap: true,
bounds: Rect::default(), bounds: Rect::default(),
border_bounds: Rect::default(),
left_to_right: true, left_to_right: true,
selectable: true, selectable: true,
} }
@ -342,7 +346,7 @@ where
} }
} }
/// Draws a [`Table`] on screen.. /// Draws a [`Table`] on screen corresponding to the [`TextTable`].
/// ///
/// Note if the number of columns don't match in the [`TextTable`] and data, /// 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. /// it will only create as many columns as it can grab data from both sources from.
@ -353,7 +357,6 @@ where
use tui::widgets::Row; use tui::widgets::Row;
let inner_area = block.inner(block_area); let inner_area = block.inner(block_area);
let table_gap = if !self.show_gap || inner_area.height < TABLE_GAP_HEIGHT_LIMIT { let table_gap = if !self.show_gap || inner_area.height < TABLE_GAP_HEIGHT_LIMIT {
0 0
} else { } else {
@ -361,6 +364,7 @@ where
}; };
self.set_num_items(data.len()); self.set_num_items(data.len());
self.set_border_bounds(block_area);
self.set_bounds(inner_area); self.set_bounds(inner_area);
let table_extras = 1 + table_gap; let table_extras = 1 + table_gap;
let scrollable_height = inner_area.height.saturating_sub(table_extras); let scrollable_height = inner_area.height.saturating_sub(table_extras);
@ -466,4 +470,12 @@ where
fn set_bounds(&mut self, new_bounds: Rect) { fn set_bounds(&mut self, new_bounds: Rect) {
self.bounds = new_bounds; self.bounds = new_bounds;
} }
fn border_bounds(&self) -> Rect {
self.border_bounds
}
fn set_border_bounds(&mut self, new_bounds: Rect) {
self.border_bounds = new_bounds;
}
} }

View File

@ -111,6 +111,7 @@ pub struct TimeGraph {
time_interval: u64, time_interval: u64,
bounds: Rect, bounds: Rect,
border_bounds: Rect,
use_dot: bool, use_dot: bool,
} }
@ -129,6 +130,7 @@ impl TimeGraph {
max_duration, max_duration,
time_interval, time_interval,
bounds: Rect::default(), bounds: Rect::default(),
border_bounds: Rect::default(),
use_dot, use_dot,
} }
} }
@ -236,6 +238,7 @@ impl TimeGraph {
) { ) {
let inner_area = block.inner(block_area); let inner_area = block.inner(block_area);
self.set_border_bounds(block_area);
self.set_bounds(inner_area); self.set_bounds(inner_area);
let time_start = -(self.current_display_time as f64); let time_start = -(self.current_display_time as f64);
@ -324,4 +327,12 @@ impl Component for TimeGraph {
fn set_bounds(&mut self, new_bounds: Rect) { fn set_bounds(&mut self, new_bounds: Rect) {
self.bounds = new_bounds; self.bounds = new_bounds;
} }
fn border_bounds(&self) -> Rect {
self.border_bounds
}
fn set_border_bounds(&mut self, new_bounds: Rect) {
self.border_bounds = new_bounds;
}
} }

View File

@ -16,9 +16,7 @@ use crate::{
data_conversion::{convert_cpu_data_points, ConvertedCpuData}, data_conversion::{convert_cpu_data_points, ConvertedCpuData},
}; };
use super::{ use super::{AppScrollWidgetState, CanvasTableWidthState, Component, TextTable, TimeGraph, Widget};
AppScrollWidgetState, CanvasTableWidthState, Component, SortableTextTable, TimeGraph, Widget,
};
pub struct CpuWidgetState { pub struct CpuWidgetState {
pub current_display_time: u64, pub current_display_time: u64,
@ -77,10 +75,10 @@ pub enum CpuGraphLegendPosition {
Right, Right,
} }
/// A widget designed to show CPU usage via a graph, along with a side legend implemented as a [`TextTable`]. /// A widget designed to show CPU usage via a graph, along with a side legend in a table.
pub struct CpuGraph { pub struct CpuGraph {
graph: TimeGraph, graph: TimeGraph,
legend: SortableTextTable, legend: TextTable<SimpleSortableColumn>,
legend_position: CpuGraphLegendPosition, legend_position: CpuGraphLegendPosition,
showing_avg: bool, showing_avg: bool,
@ -95,7 +93,7 @@ impl CpuGraph {
/// Creates a new [`CpuGraph`] from a config. /// Creates a new [`CpuGraph`] from a config.
pub fn from_config(app_config_fields: &AppConfigFields) -> Self { pub fn from_config(app_config_fields: &AppConfigFields) -> Self {
let graph = TimeGraph::from_config(app_config_fields); let graph = TimeGraph::from_config(app_config_fields);
let legend = SortableTextTable::new(vec![ let legend = TextTable::new(vec![
SimpleSortableColumn::new_flex("CPU".into(), None, false, 0.5), SimpleSortableColumn::new_flex("CPU".into(), None, false, 0.5),
SimpleSortableColumn::new_flex("Use%".into(), None, false, 0.5), SimpleSortableColumn::new_flex("Use%".into(), None, false, 0.5),
]); ]);
@ -129,7 +127,7 @@ impl Component for CpuGraph {
} }
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult { fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
if self.graph.does_intersect_mouse(&event) { if self.graph.does_border_intersect_mouse(&event) {
if let CpuGraphSelection::Graph = self.selected { if let CpuGraphSelection::Graph = self.selected {
self.graph.handle_mouse_event(event) self.graph.handle_mouse_event(event)
} else { } else {
@ -137,7 +135,7 @@ impl Component for CpuGraph {
self.graph.handle_mouse_event(event); self.graph.handle_mouse_event(event);
WidgetEventResult::Redraw WidgetEventResult::Redraw
} }
} else if self.legend.does_intersect_mouse(&event) { } else if self.legend.does_border_intersect_mouse(&event) {
if let CpuGraphSelection::Legend = self.selected { if let CpuGraphSelection::Legend = self.selected {
self.legend.handle_mouse_event(event) self.legend.handle_mouse_event(event)
} else { } else {
@ -176,11 +174,16 @@ impl Widget for CpuGraph {
} }
}; };
// debug!("Area: {:?}", area);
let split_area = Layout::default() let split_area = Layout::default()
.margin(0)
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints(constraints) .constraints(constraints)
.split(area); .split(area);
// debug!("Split area: {:?}", split_area);
const Y_BOUNDS: [f64; 2] = [0.0, 100.5]; const Y_BOUNDS: [f64; 2] = [0.0, 100.5];
let y_bound_labels: [Cow<'static, str>; 2] = ["0%".into(), "100%".into()]; let y_bound_labels: [Cow<'static, str>; 2] = ["0%".into(), "100%".into()];

View File

@ -9,14 +9,17 @@ use tui::{
}; };
use crate::{ use crate::{
app::{data_farmer::DataCollection, event::WidgetEventResult, sort_text_table::SimpleSortableColumn}, app::{
data_farmer::DataCollection, event::WidgetEventResult,
sort_text_table::SimpleSortableColumn,
},
canvas::Painter, canvas::Painter,
data_conversion::convert_disk_row, data_conversion::convert_disk_row,
}; };
use super::{ use super::{
text_table::TextTableData, AppScrollWidgetState, CanvasTableWidthState, Component, text_table::TextTableData, AppScrollWidgetState, CanvasTableWidthState, Component, TextTable,
SortableTextTable, Widget, Widget,
}; };
pub struct DiskWidgetState { pub struct DiskWidgetState {
@ -52,9 +55,9 @@ impl DiskState {
} }
} }
/// A table displaying disk data. Essentially a wrapper around a [`TextTable`]. /// A table displaying disk data.
pub struct DiskTable { pub struct DiskTable {
table: SortableTextTable, table: TextTable<SimpleSortableColumn>,
bounds: Rect, bounds: Rect,
display_data: TextTableData, display_data: TextTableData,
@ -62,7 +65,7 @@ pub struct DiskTable {
impl Default for DiskTable { impl Default for DiskTable {
fn default() -> Self { fn default() -> Self {
let table = SortableTextTable::new(vec![ let table = TextTable::new(vec![
SimpleSortableColumn::new_flex("Disk".into(), None, false, 0.2), SimpleSortableColumn::new_flex("Disk".into(), None, false, 0.2),
SimpleSortableColumn::new_flex("Mount".into(), None, false, 0.2), SimpleSortableColumn::new_flex("Mount".into(), None, false, 0.2),
SimpleSortableColumn::new_hard("Used".into(), None, false, Some(5)), SimpleSortableColumn::new_hard("Used".into(), None, false, Some(5)),
@ -115,7 +118,6 @@ impl Widget for DiskTable {
.borders(Borders::ALL); .borders(Borders::ALL);
self.table self.table
.table
.draw_tui_table(painter, f, &self.display_data, block, area, selected); .draw_tui_table(painter, f, &self.display_data, block, area, selected);
} }

View File

@ -59,6 +59,7 @@ pub struct MemGraph {
swap_labels: Option<(String, String)>, swap_labels: Option<(String, String)>,
mem_data: Vec<(f64, f64)>, mem_data: Vec<(f64, f64)>,
swap_data: Vec<(f64, f64)>, swap_data: Vec<(f64, f64)>,
bounds: Rect,
} }
impl MemGraph { impl MemGraph {
@ -70,6 +71,7 @@ impl MemGraph {
swap_labels: Default::default(), swap_labels: Default::default(),
mem_data: Default::default(), mem_data: Default::default(),
swap_data: Default::default(), swap_data: Default::default(),
bounds: Rect::default(),
} }
} }
} }
@ -84,11 +86,11 @@ impl Component for MemGraph {
} }
fn bounds(&self) -> Rect { fn bounds(&self) -> Rect {
self.graph.bounds() self.bounds
} }
fn set_bounds(&mut self, new_bounds: Rect) { fn set_bounds(&mut self, new_bounds: Rect) {
self.graph.set_bounds(new_bounds); self.bounds = new_bounds;
} }
} }

View File

@ -434,6 +434,8 @@ pub struct NetGraph {
pub use_binary_prefix: bool, pub use_binary_prefix: bool,
hide_legend: bool, hide_legend: bool,
bounds: Rect,
} }
impl NetGraph { impl NetGraph {
@ -454,6 +456,7 @@ impl NetGraph {
unit_type: app_config_fields.network_unit_type.clone(), unit_type: app_config_fields.network_unit_type.clone(),
use_binary_prefix: app_config_fields.network_use_binary_prefix, use_binary_prefix: app_config_fields.network_use_binary_prefix,
hide_legend: false, hide_legend: false,
bounds: Rect::default(),
} }
} }
@ -514,11 +517,11 @@ impl NetGraph {
impl Component for NetGraph { impl Component for NetGraph {
fn bounds(&self) -> Rect { fn bounds(&self) -> Rect {
self.graph.bounds() self.bounds
} }
fn set_bounds(&mut self, new_bounds: Rect) { fn set_bounds(&mut self, new_bounds: Rect) {
self.graph.set_bounds(new_bounds); self.bounds = new_bounds;
} }
fn handle_key_event( fn handle_key_event(

View File

@ -1,4 +1,4 @@
use std::collections::HashMap; use std::{borrow::Cow, collections::HashMap};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use float_ord::FloatOrd; use float_ord::FloatOrd;
@ -799,6 +799,10 @@ impl ProcessSortColumn {
} }
impl SortableColumn for ProcessSortColumn { impl SortableColumn for ProcessSortColumn {
fn original_name(&self) -> &Cow<'static, str> {
self.sortable_column.original_name()
}
fn shortcut(&self) -> &Option<(KeyEvent, String)> { fn shortcut(&self) -> &Option<(KeyEvent, String)> {
self.sortable_column.shortcut() self.sortable_column.shortcut()
} }
@ -815,7 +819,7 @@ impl SortableColumn for ProcessSortColumn {
self.sortable_column.set_sorting_status(sorting_status) self.sortable_column.set_sorting_status(sorting_status)
} }
fn display_name(&self) -> std::borrow::Cow<'static, str> { fn display_name(&self) -> Cow<'static, str> {
self.sortable_column.display_name() self.sortable_column.display_name()
} }
@ -1024,7 +1028,7 @@ impl Component for ProcessManager {
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult { fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
match &event.kind { match &event.kind {
MouseEventKind::Down(MouseButton::Left) => { MouseEventKind::Down(MouseButton::Left) => {
if self.process_table.does_intersect_mouse(&event) { if self.process_table.does_border_intersect_mouse(&event) {
if let ProcessManagerSelection::Processes = self.selected { if let ProcessManagerSelection::Processes = self.selected {
self.process_table.handle_mouse_event(event) self.process_table.handle_mouse_event(event)
} else { } else {
@ -1037,7 +1041,7 @@ impl Component for ProcessManager {
WidgetEventResult::Signal(s) => WidgetEventResult::Signal(s), WidgetEventResult::Signal(s) => WidgetEventResult::Signal(s),
} }
} }
} else if self.sort_table.does_intersect_mouse(&event) { } else if self.sort_table.does_border_intersect_mouse(&event) {
if let ProcessManagerSelection::Sort = self.selected { if let ProcessManagerSelection::Sort = self.selected {
self.sort_table.handle_mouse_event(event) self.sort_table.handle_mouse_event(event)
} else { } else {
@ -1045,7 +1049,7 @@ impl Component for ProcessManager {
self.sort_table.handle_mouse_event(event); self.sort_table.handle_mouse_event(event);
WidgetEventResult::Redraw WidgetEventResult::Redraw
} }
} else if self.search_input.does_intersect_mouse(&event) { } else if self.search_input.does_border_intersect_mouse(&event) {
if let ProcessManagerSelection::Search = self.selected { if let ProcessManagerSelection::Search = self.selected {
self.search_input.handle_mouse_event(event) self.search_input.handle_mouse_event(event)
} else { } else {

View File

@ -18,8 +18,8 @@ use crate::{
}; };
use super::{ use super::{
text_table::TextTableData, AppScrollWidgetState, CanvasTableWidthState, Component, text_table::TextTableData, AppScrollWidgetState, CanvasTableWidthState, Component, TextTable,
SortableTextTable, Widget, Widget,
}; };
pub struct TempWidgetState { pub struct TempWidgetState {
@ -55,9 +55,9 @@ impl TempState {
} }
} }
/// A table displaying disk data. Essentially a wrapper around a [`TextTable`]. /// A table displaying disk data..
pub struct TempTable { pub struct TempTable {
table: SortableTextTable, table: TextTable<SimpleSortableColumn>,
bounds: Rect, bounds: Rect,
display_data: TextTableData, display_data: TextTableData,
temp_type: TemperatureType, temp_type: TemperatureType,
@ -65,7 +65,7 @@ pub struct TempTable {
impl Default for TempTable { impl Default for TempTable {
fn default() -> Self { fn default() -> Self {
let table = SortableTextTable::new(vec![ let table = TextTable::new(vec![
SimpleSortableColumn::new_flex("Sensor".into(), None, false, 0.8), SimpleSortableColumn::new_flex("Sensor".into(), None, false, 0.8),
SimpleSortableColumn::new_hard("Temp".into(), None, false, Some(5)), SimpleSortableColumn::new_hard("Temp".into(), None, false, Some(5)),
]) ])
@ -123,7 +123,6 @@ impl Widget for TempTable {
.borders(Borders::ALL); // TODO: Also do the scrolling indicator! .borders(Borders::ALL); // TODO: Also do the scrolling indicator!
self.table self.table
.table
.draw_tui_table(painter, f, &self.display_data, block, area, selected); .draw_tui_table(painter, f, &self.display_data, block, area, selected);
} }

View File

@ -116,8 +116,11 @@ fn main() -> Result<()> {
ist_clone.store(true, Ordering::SeqCst); ist_clone.store(true, Ordering::SeqCst);
})?; })?;
// Paint once first.
try_drawing(&mut terminal, &mut app, &mut painter)?;
while !is_terminated.load(Ordering::SeqCst) { while !is_terminated.load(Ordering::SeqCst) {
if let Ok(recv) = receiver.recv_timeout(Duration::from_millis(TICK_RATE_IN_MILLISECONDS)) { if let Ok(recv) = receiver.recv() {
match app.handle_event(recv) { match app.handle_event(recv) {
EventResult::Quit => { EventResult::Quit => {
break; break;
@ -129,6 +132,8 @@ fn main() -> Result<()> {
continue; continue;
} }
} }
} else {
break;
} }
} }

View File

@ -360,10 +360,16 @@ impl Painter {
match layout_node { match layout_node {
LayoutNode::Row(row) => { LayoutNode::Row(row) => {
let split_area = Layout::default() let split_area = Layout::default()
.margin(0)
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints(row.constraints.clone()) .constraints(row.constraints.clone())
.split(area); .split(area);
// debug!(
// "Row - constraints: {:#?}, split_area: {:#?}",
// row.constraints, split_area
// );
for (child, child_area) in node.children(arena).zip(split_area) { for (child, child_area) in node.children(arena).zip(split_area) {
traverse_and_draw_tree( traverse_and_draw_tree(
child, child,
@ -379,10 +385,16 @@ impl Painter {
} }
LayoutNode::Col(col) => { LayoutNode::Col(col) => {
let split_area = Layout::default() let split_area = Layout::default()
.margin(0)
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints(col.constraints.clone()) .constraints(col.constraints.clone())
.split(area); .split(area);
// debug!(
// "Col - constraints: {:#?}, split_area: {:#?}",
// col.constraints, split_area
// );
for (child, child_area) in node.children(arena).zip(split_area) { for (child, child_area) in node.children(arena).zip(split_area) {
traverse_and_draw_tree( traverse_and_draw_tree(
child, child,
@ -397,6 +409,8 @@ impl Painter {
} }
} }
LayoutNode::Widget => { LayoutNode::Widget => {
// debug!("Widget - area: {:#?}", area);
if let Some(widget) = lookup_map.get_mut(&node) { if let Some(widget) = lookup_map.get_mut(&node) {
widget.set_bounds(area); widget.set_bounds(area);
widget.draw(painter, f, area, selected_id == node); widget.draw(painter, f, area, selected_id == node);

View File

@ -31,7 +31,7 @@ use crossterm::{
use app::{ use app::{
data_harvester::{self, processes::ProcessSorting}, data_harvester::{self, processes::ProcessSorting},
event::WidgetEventResult, event::EventResult,
layout_manager::WidgetDirection, layout_manager::WidgetDirection,
AppState, UsedWidgets, AppState, UsedWidgets,
}; };
@ -76,27 +76,27 @@ pub enum ThreadControlEvent {
UpdateUpdateTime(u64), UpdateUpdateTime(u64),
} }
pub fn handle_mouse_event(event: MouseEvent, app: &mut AppState) -> WidgetEventResult { pub fn handle_mouse_event(event: MouseEvent, app: &mut AppState) -> EventResult {
match event.kind { match event.kind {
MouseEventKind::Down(MouseButton::Left) => { MouseEventKind::Down(MouseButton::Left) => {
app.on_left_mouse_up(event.column, event.row); app.on_left_mouse_up(event.column, event.row);
WidgetEventResult::Redraw EventResult::Redraw
} }
MouseEventKind::ScrollUp => { MouseEventKind::ScrollUp => {
app.handle_scroll_up(); app.handle_scroll_up();
WidgetEventResult::Redraw EventResult::Redraw
} }
MouseEventKind::ScrollDown => { MouseEventKind::ScrollDown => {
app.handle_scroll_down(); app.handle_scroll_down();
WidgetEventResult::Redraw EventResult::Redraw
} }
_ => WidgetEventResult::NoRedraw, _ => EventResult::NoRedraw,
} }
} }
pub fn handle_key_event( pub fn handle_key_event(
event: KeyEvent, app: &mut AppState, reset_sender: &std::sync::mpsc::Sender<ThreadControlEvent>, event: KeyEvent, app: &mut AppState, reset_sender: &std::sync::mpsc::Sender<ThreadControlEvent>,
) -> WidgetEventResult { ) -> EventResult {
// debug!("KeyEvent: {:?}", event); // debug!("KeyEvent: {:?}", event);
// TODO: [PASTE] Note that this does NOT support some emojis like flags. This is due to us // TODO: [PASTE] Note that this does NOT support some emojis like flags. This is due to us
@ -107,7 +107,7 @@ pub fn handle_key_event(
if event.modifiers.is_empty() { if event.modifiers.is_empty() {
// Required catch for searching - otherwise you couldn't search with q. // Required catch for searching - otherwise you couldn't search with q.
if event.code == KeyCode::Char('q') && !app.is_in_search_widget() { if event.code == KeyCode::Char('q') && !app.is_in_search_widget() {
return WidgetEventResult::Quit; return EventResult::Quit;
} }
match event.code { match event.code {
KeyCode::End => app.skip_to_last(), KeyCode::End => app.skip_to_last(),
@ -129,7 +129,7 @@ pub fn handle_key_event(
KeyCode::F(6) => app.toggle_sort(), KeyCode::F(6) => app.toggle_sort(),
KeyCode::F(9) => app.start_killing_process(), KeyCode::F(9) => app.start_killing_process(),
_ => { _ => {
return WidgetEventResult::NoRedraw; return EventResult::NoRedraw;
} }
} }
} else { } else {
@ -145,7 +145,7 @@ pub fn handle_key_event(
} }
} else if let KeyModifiers::CONTROL = event.modifiers { } else if let KeyModifiers::CONTROL = event.modifiers {
if event.code == KeyCode::Char('c') { if event.code == KeyCode::Char('c') {
return WidgetEventResult::Quit; return EventResult::Quit;
} }
match event.code { match event.code {
@ -172,7 +172,7 @@ pub fn handle_key_event(
// are hard to iter while truncating last (eloquently). // are hard to iter while truncating last (eloquently).
// KeyCode::Backspace => app.skip_word_backspace(), // KeyCode::Backspace => app.skip_word_backspace(),
_ => { _ => {
return WidgetEventResult::NoRedraw; return EventResult::NoRedraw;
} }
} }
} else if let KeyModifiers::SHIFT = event.modifiers { } else if let KeyModifiers::SHIFT = event.modifiers {
@ -183,13 +183,13 @@ pub fn handle_key_event(
KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down), KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down),
KeyCode::Char(caught_char) => app.on_char_key(caught_char), KeyCode::Char(caught_char) => app.on_char_key(caught_char),
_ => { _ => {
return WidgetEventResult::NoRedraw; return EventResult::NoRedraw;
} }
} }
} }
} }
WidgetEventResult::Redraw EventResult::Redraw
} }
pub fn read_config(config_location: Option<&str>) -> error::Result<Option<PathBuf>> { pub fn read_config(config_location: Option<&str>) -> error::Result<Option<PathBuf>> {
@ -474,7 +474,7 @@ fn update_final_process_list(_app: &mut AppState, _widget_id: u64) {
// } // }
} }
fn sort_process_data( fn _sort_process_data(
to_sort_vec: &mut Vec<ConvertedProcessData>, proc_widget_state: &app::ProcWidgetState, to_sort_vec: &mut Vec<ConvertedProcessData>, proc_widget_state: &app::ProcWidgetState,
) { ) {
to_sort_vec.sort_by_cached_key(|c| c.name.to_lowercase()); to_sort_vec.sort_by_cached_key(|c| c.name.to_lowercase());