feature: Add mouse click support for moving between widgets (#208)

Adds mouse support to the application, to move between widgets and click on elements.

List of things to added:

- Click to move between widgets
- Click to move between widgets in basic mode
- Click on widget entries
- Ability to disable mouse if you don't like it, I guess
This commit is contained in:
Clement Tsang 2020-08-29 18:54:18 -04:00 committed by GitHub
parent b6363096b4
commit 3d2fc76aa2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 901 additions and 333 deletions

View File

@ -1,5 +1,7 @@
{
"cSpell.words": [
"Artem",
"COPR",
"DWORD",
"Deque",
"EINVAL",
@ -14,8 +16,10 @@
"MSRV",
"Mahmoud",
"Marcin",
"Mousebindings",
"Nonexhaustive",
"PKGBUILD",
"Polishchuk",
"Qudsi",
"SIGTERM",
"TEBI",
@ -26,6 +30,7 @@
"WASD",
"Wojnarowski",
"andys",
"atim",
"choco",
"cmdline",
"commandline",

View File

@ -11,11 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#206](https://github.com/ClementTsang/bottom/pull/206): Adaptive network graphs --- prior to this update, graphs were stuck at a range from 0B to 1GiB. Now, they adjust to your current usage and time span, so if you're using, say, less than a MiB, it will cap at a MiB. If you're using 10GiB, then the graph will reflect that and span to a bit greater than 10GiB.
- [#208](https://github.com/ClementTsang/bottom/pull/208): Mouse support for tables and moving to widgets.
### Changes
### Bug Fixes
- [211](https://github.com/ClementTsang/bottom/pull/211): Fixes a bug where you could move down in the process widget even if the process widget search was closed.
- [#211](https://github.com/ClementTsang/bottom/pull/211): Fixes a bug where you could move down in the process widget even if the process widget search was closed.
## [0.4.7] - 2020-08-26

View File

@ -25,7 +25,6 @@ A cross-platform graphical process/system monitor with a customizable interface
- [Options](#options)
- [Keybindings](#keybindings)
- [General](#general)
- [CPU bindings](#cpu-bindings)
- [Process bindings](#process-bindings)
- [Process search bindings](#process-search-bindings)
- [Process sort bindings](#process-sort-bindings)
@ -35,6 +34,9 @@ A cross-platform graphical process/system monitor with a customizable interface
- [Supported comparison operators](#supported-comparison-operators)
- [Supported logical operators](#supported-logical-operators)
- [Supported units](#supported-units)
- [Mousebindings](#mousebindings)
- [General](#general-1)
- [CPU bindings](#cpu-bindings)
- [Features](#features)
- [Processes](#processes)
- [Process searching](#process-searching)
@ -43,7 +45,7 @@ A cross-platform graphical process/system monitor with a customizable interface
- [Expanding](#expanding)
- [Basic mode](#basic-mode)
- [Config files](#config-files)
- [Config flags](#config-flags)
- [Config flags and options](#config-flags-and-options)
- [Theming](#theming)
- [Layout](#layout)
- [Battery](#battery)
@ -176,6 +178,7 @@ Run using `btm`.
--use_old_network_legend Use the older (pre-0.4) network legend which is separate from the network chart
--hide_table_gap Hides the spacing between table headers and data
--battery Displays the battery widget for default and basic layouts
--disable_click Disables mouse clicks from interacting with the program
```
### Options
@ -193,34 +196,27 @@ Run using `btm`.
#### General
| | |
| ------------------------------------------- | ---------------------------------------------------------------------------- |
| `q`, `Ctrl-c` | Quit |
| `Esc` | Close dialog windows, search, widgets, or exit expanded mode |
| `Ctrl-r` | Reset display and any collected data |
| `f` | Freeze/unfreeze updating with new data |
| `Ctrl-Left`<br>`Shift-Left`<br>`H`<br>`A` | Move widget selection left |
| `Ctrl-Right`<br>`Shift-Right`<br>`L`<br>`D` | Move widget selection right |
| `Ctrl-Up`<br>`Shift-Up`<br>`K`<br>`W` | Move widget selection up |
| `Ctrl-Down`<br>`Shift-Down`<br>`J`<br>`S` | Move widget selection down |
| `Left`, `h` | Move left within widget |
| `Down`, `j` | Move down within widget |
| `Up`,`k` | Move up within widget |
| `Right`, `l` | Move right within widget |
| `?` | Open help menu |
| `gg`, `Home` | Jump to the first entry |
| `Shift-g`, `End` | Jump to the last entry |
| `e` | Toggle expanding the currently selected widget |
| `+` | Zoom in on chart (decrease time range) |
| `-` | Zoom out on chart (increase time range) |
| `=` | Reset zoom |
| Mouse scroll | Table: Scroll<br>Chart: Zooms in or out by scrolling up or down respectively |
#### CPU bindings
| | |
| ------------ | --------------------------------------------------------------------- |
| Mouse scroll | Scrolling over an CPU core/average shows only that entry on the chart |
| | |
| ------------------------------------------- | ------------------------------------------------------------ |
| `q`, `Ctrl-c` | Quit |
| `Esc` | Close dialog windows, search, widgets, or exit expanded mode |
| `Ctrl-r` | Reset display and any collected data |
| `f` | Freeze/unfreeze updating with new data |
| `Ctrl-Left`<br>`Shift-Left`<br>`H`<br>`A` | Move widget selection left |
| `Ctrl-Right`<br>`Shift-Right`<br>`L`<br>`D` | Move widget selection right |
| `Ctrl-Up`<br>`Shift-Up`<br>`K`<br>`W` | Move widget selection up |
| `Ctrl-Down`<br>`Shift-Down`<br>`J`<br>`S` | Move widget selection down |
| `Left`, `h` | Move left within widget |
| `Down`, `j` | Move down within widget |
| `Up`,`k` | Move up within widget |
| `Right`, `l` | Move right within widget |
| `?` | Open help menu |
| `gg`, `Home` | Jump to the first entry |
| `Shift-g`, `End` | Jump to the last entry |
| `e` | Toggle expanding the currently selected widget |
| `+` | Zoom in on chart (decrease time range) |
| `-` | Zoom out on chart (increase time range) |
| `=` | Reset zoom |
#### Process bindings
@ -340,6 +336,21 @@ Note that the `and` operator takes precedence over the `or` operator.
| -------- | ---------------------------------------------------- | -------------------------- |
| `()` | `(<CONDITION 1> AND <CONDITION 2>) OR <CONDITION 3>` | Group together a condition |
### Mousebindings
#### General
| | |
| ------------ | --------------------------------------------------------------------------------------------------------------------- |
| Mouse scroll | Table: Scroll<br>Chart: Zooms in or out by scrolling up or down respectively |
| Mouse click | Selects the clicked widget. For tables, clicking can also select a specific entry. Can be disabled via options/flags. |
#### CPU bindings
| | |
| ------------ | --------------------------------------------------------------------- |
| Mouse scroll | Scrolling over an CPU core/average shows only that entry on the chart |
## Features
As yet _another_ process/system visualization and management application, bottom supports the typical features:
@ -434,11 +445,11 @@ By default, bottom will look at (based on [dirs](https://github.com/dirs-dev/dir
Note that if a config file does not exist at either the default location or the passed in location via `-C` or `--config`, one is automatically created with no settings applied.
#### Config flags
#### Config flags and options
The following options can be set under `[flags]` to achieve the same effect as passing in a flag on runtime. Note that if a flag is given, it will override the config file.
These are the following supported flag config values:
These are the following supported flag config values, which correspond to the flag of the same name described in [Flags](#flags) and [Options](#options):
| Field | Type |
| ------------------------ | ------------------------------------------------------------------------------------- |
@ -461,6 +472,7 @@ These are the following supported flag config values:
| `temperature_type` | String (one of ["k", "f", "c", "kelvin", "fahrenheit", "celsius"]) |
| `default_widget_type` | String (one of ["cpu", "proc", "net", "temp", "mem", "disk"], same as layout options) |
| `default_widget_count` | Unsigned Int (represents which `default_widget_type`) |
| `disable_click` | Boolean |
#### Theming
@ -614,6 +626,7 @@ Thanks to all contributors ([emoji key](https://allcontributors.org/docs/en/emoj
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
## Thanks

View File

@ -40,6 +40,7 @@ pub struct AppConfigFields {
pub autohide_time: bool,
pub use_old_network_legend: bool,
pub table_gap: u16,
pub disable_click: bool,
}
#[derive(TypedBuilder)]
@ -80,6 +81,9 @@ pub struct App {
#[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,
@ -90,9 +94,7 @@ pub struct App {
pub temp_state: TempState,
pub disk_state: DiskState,
pub battery_state: BatteryState,
pub basic_table_widget_state: Option<BasicTableWidgetState>,
pub app_config_fields: AppConfigFields,
pub widget_map: HashMap<u64, BottomWidget>,
pub current_widget: BottomWidget,
@ -133,6 +135,10 @@ impl App {
self.data_collection.reset();
}
pub fn should_get_widget_bounds(&self) -> bool {
self.is_force_redraw || self.is_determining_widget_boundary
}
fn close_dd(&mut self) {
self.delete_dialog_state.is_showing_dd = false;
self.delete_dialog_state.is_on_yes = false;
@ -279,11 +285,9 @@ impl App {
}
}
/// "On space" if we don't want to treat is as a character.
pub fn on_space(&mut self) {}
pub fn on_slash(&mut self) {
if !self.is_in_dialog() {
// FIXME: Add ProcSort too, it's annoying
if let BottomWidgetType::Proc = self.current_widget.widget_type {
// Toggle on
if let Some(proc_widget_state) = self
@ -295,6 +299,7 @@ impl App {
.search_state
.is_enabled = true;
self.move_widget_selection(&WidgetDirection::Down);
self.is_force_redraw = true;
}
}
}
@ -302,6 +307,7 @@ impl App {
pub fn toggle_sort(&mut self) {
match &self.current_widget.widget_type {
// FIXME: [REFACTOR] Remove these @'s if unneeded, they were an idea but they're ultimately useless for me here...?
widget_type @ BottomWidgetType::Proc | widget_type @ BottomWidgetType::ProcSort => {
let widget_id = self.current_widget.widget_id
- match &widget_type {
@ -326,6 +332,8 @@ impl App {
self.move_widget_selection(&WidgetDirection::Right);
}
}
self.is_force_redraw = true;
}
_ => {}
}
@ -1151,7 +1159,6 @@ impl App {
'L' | 'D' => self.move_widget_selection(&WidgetDirection::Right),
'K' | 'W' => self.move_widget_selection(&WidgetDirection::Up),
'J' | 'S' => self.move_widget_selection(&WidgetDirection::Down),
' ' => self.on_space(),
'+' => self.zoom_in(),
'-' => self.zoom_out(),
'=' => self.reset_zoom(),
@ -1214,7 +1221,16 @@ impl App {
}
pub fn move_widget_selection(&mut self, direction: &WidgetDirection) {
// Since we only want to call reset once, we do it like this to avoid
// redundant calls on recursion.
self.move_widget_selection_logic(direction);
self.reset_multi_tap_keys();
}
fn move_widget_selection_logic(&mut self, direction: &WidgetDirection) {
/*
The actual logic for widget movement.
We follow these following steps:
1. Send a movement signal in `direction`.
2. Check if this new widget we've landed on is hidden. If not, halt.
@ -1234,7 +1250,6 @@ impl App {
match &new_widget.widget_type {
BottomWidgetType::Temp
| BottomWidgetType::Proc
| BottomWidgetType::ProcSearch
| BottomWidgetType::ProcSort
| BottomWidgetType::Disk
| BottomWidgetType::Battery
@ -1272,13 +1287,16 @@ impl App {
basic_table_widget_state.currently_displayed_widget_type =
self.current_widget.widget_type.clone();
}
// And let's not forget:
self.is_determining_widget_boundary = true;
}
BottomWidgetType::BasicTables => {
match &direction {
WidgetDirection::Up => {
// Note this case would fail if it moved up into a hidden
// widget, but it's for basic so whatever, it's all hard-coded
// right now anyways.
// right now anyways...
if let Some(next_new_widget_id) = new_widget.up_neighbour {
if let Some(next_new_widget) =
self.widget_map.get(&next_new_widget_id)
@ -1288,11 +1306,22 @@ impl App {
}
}
WidgetDirection::Down => {
// This means we're in basic mode. As such, then
// we want to move DOWN to the currently shown widget
// Assuming we're in basic mode (BasicTables), then
// we want to move DOWN to the currently shown widget.
if let Some(basic_table_widget_state) =
&self.basic_table_widget_state
&mut self.basic_table_widget_state
{
// We also want to move towards Proc if we had set it to ProcSort.
if let BottomWidgetType::ProcSort =
basic_table_widget_state.currently_displayed_widget_type
{
basic_table_widget_state
.currently_displayed_widget_type =
BottomWidgetType::Proc;
basic_table_widget_state
.currently_displayed_widget_id -= 2;
}
if let Some(next_new_widget) = self.widget_map.get(
&basic_table_widget_state.currently_displayed_widget_id,
) {
@ -1308,13 +1337,13 @@ impl App {
if let Some((parent_direction, offset)) = &new_widget.parent_reflector {
if direction.is_opposite(parent_direction) {
// Keep going in the current direction if hidden...
let next_neighbour_id = match &direction {
// unless we hit a wall of sorts.
let option_next_neighbour_id = match &direction {
WidgetDirection::Left => new_widget.left_neighbour,
WidgetDirection::Right => new_widget.right_neighbour,
WidgetDirection::Up => new_widget.up_neighbour,
WidgetDirection::Down => new_widget.down_neighbour,
}
.unwrap_or(*new_widget_id);
};
match &new_widget.widget_type {
BottomWidgetType::CpuLegend => {
if let Some(cpu_widget_state) = self
@ -1323,11 +1352,15 @@ impl App {
.get(&(new_widget_id - *offset))
{
if cpu_widget_state.is_legend_hidden {
if let Some(next_neighbour_widget) =
self.widget_map.get(&next_neighbour_id)
if let Some(next_neighbour_id) =
option_next_neighbour_id
{
self.current_widget =
next_neighbour_widget.clone();
if let Some(next_neighbour_widget) =
self.widget_map.get(&next_neighbour_id)
{
self.current_widget =
next_neighbour_widget.clone();
}
}
} else {
self.current_widget = new_widget.clone();
@ -1344,12 +1377,17 @@ impl App {
match &new_widget.widget_type {
BottomWidgetType::ProcSearch => {
if !proc_widget_state.is_search_enabled() {
if let Some(next_neighbour_widget) =
self.widget_map
.get(&next_neighbour_id)
if let Some(next_neighbour_id) =
option_next_neighbour_id
{
self.current_widget =
next_neighbour_widget.clone();
if let Some(next_neighbour_widget) =
self.widget_map
.get(&next_neighbour_id)
{
self.current_widget =
next_neighbour_widget
.clone();
}
}
} else {
self.current_widget =
@ -1358,12 +1396,17 @@ impl App {
}
BottomWidgetType::ProcSort => {
if !proc_widget_state.is_sort_open {
if let Some(next_neighbour_widget) =
self.widget_map
.get(&next_neighbour_id)
if let Some(next_neighbour_id) =
option_next_neighbour_id
{
self.current_widget =
next_neighbour_widget.clone();
if let Some(next_neighbour_widget) =
self.widget_map
.get(&next_neighbour_id)
{
self.current_widget =
next_neighbour_widget
.clone();
}
}
} else {
self.current_widget =
@ -1498,7 +1541,7 @@ impl App {
}
if let Some(ref_dir) = &reflection_dir {
self.move_widget_selection(ref_dir);
self.move_widget_selection_logic(ref_dir);
}
}
}
@ -1544,8 +1587,6 @@ impl App {
},
}
}
self.reset_multi_tap_keys();
}
fn handle_left_expanded_movement(&mut self) {
@ -1768,11 +1809,11 @@ impl App {
pub fn decrement_position_count(&mut self) {
if !self.is_in_dialog() {
match self.current_widget.widget_type {
BottomWidgetType::Proc => self.change_process_position(-1),
BottomWidgetType::ProcSort => self.change_process_sort_position(-1),
BottomWidgetType::Temp => self.change_temp_position(-1),
BottomWidgetType::Disk => self.change_disk_position(-1),
BottomWidgetType::CpuLegend => self.change_cpu_table_position(-1),
BottomWidgetType::Proc => self.increment_process_position(-1),
BottomWidgetType::ProcSort => self.increment_process_sort_position(-1),
BottomWidgetType::Temp => self.increment_temp_position(-1),
BottomWidgetType::Disk => self.increment_disk_position(-1),
BottomWidgetType::CpuLegend => self.increment_cpu_legend_position(-1),
_ => {}
}
}
@ -1781,17 +1822,17 @@ impl App {
pub fn increment_position_count(&mut self) {
if !self.is_in_dialog() {
match self.current_widget.widget_type {
BottomWidgetType::Proc => self.change_process_position(1),
BottomWidgetType::ProcSort => self.change_process_sort_position(1),
BottomWidgetType::Temp => self.change_temp_position(1),
BottomWidgetType::Disk => self.change_disk_position(1),
BottomWidgetType::CpuLegend => self.change_cpu_table_position(1),
BottomWidgetType::Proc => self.increment_process_position(1),
BottomWidgetType::ProcSort => self.increment_process_sort_position(1),
BottomWidgetType::Temp => self.increment_temp_position(1),
BottomWidgetType::Disk => self.increment_disk_position(1),
BottomWidgetType::CpuLegend => self.increment_cpu_legend_position(1),
_ => {}
}
}
}
fn change_process_sort_position(&mut self, num_to_change_by: i64) {
fn increment_process_sort_position(&mut self, num_to_change_by: i64) {
if let Some(proc_widget_state) = self
.proc_state
.get_mut_widget_state(self.current_widget.widget_id - 2)
@ -1814,7 +1855,7 @@ impl App {
}
}
fn change_cpu_table_position(&mut self, num_to_change_by: i64) {
fn increment_cpu_legend_position(&mut self, num_to_change_by: i64) {
if let Some(cpu_widget_state) = self
.cpu_state
.widget_states
@ -1838,13 +1879,12 @@ impl App {
}
}
fn change_process_position(&mut self, num_to_change_by: i64) {
fn increment_process_position(&mut self, num_to_change_by: i64) {
if let Some(proc_widget_state) = self
.proc_state
.get_mut_widget_state(self.current_widget.widget_id)
{
let current_posn = proc_widget_state.scroll_state.current_scroll_position;
if let Some(finalized_process_data) = self
.canvas_data
.finalized_process_data_map
@ -1866,7 +1906,7 @@ impl App {
}
}
fn change_temp_position(&mut self, num_to_change_by: i64) {
fn increment_temp_position(&mut self, num_to_change_by: i64) {
if let Some(temp_widget_state) = self
.temp_state
.widget_states
@ -1890,7 +1930,7 @@ impl App {
}
}
fn change_disk_position(&mut self, num_to_change_by: i64) {
fn increment_disk_position(&mut self, num_to_change_by: i64) {
if let Some(disk_widget_state) = self
.disk_state
.widget_states
@ -2168,4 +2208,219 @@ impl App {
_ => {}
}
}
/// Moves the mouse to the widget that was clicked on, then propagates the click down to be
/// handled by the widget specifically.
pub fn left_mouse_click_movement(&mut self, x: u16, y: u16) {
// Pretty dead simple - iterate through the widget map and go to the widget where the click
// is within.
if let Some(bt) = &mut self.basic_table_widget_state {
if let (
Some((left_tlc_x, left_tlc_y)),
Some((left_brc_x, left_brc_y)),
Some((right_tlc_x, right_tlc_y)),
Some((right_brc_x, right_brc_y)),
) = (bt.left_tlc, bt.left_brc, bt.right_tlc, bt.right_brc)
{
if (x >= left_tlc_x && y >= left_tlc_y) && (x <= left_brc_x && y <= left_brc_y) {
if let Some(new_widget) =
self.widget_map.get(&(bt.currently_displayed_widget_id))
{
// We have to move to the current table widget first...
self.current_widget = new_widget.clone();
if let BottomWidgetType::Proc = &new_widget.widget_type {
if let Some(proc_widget_state) =
self.proc_state.get_widget_state(new_widget.widget_id)
{
if proc_widget_state.is_sort_open {
self.move_widget_selection(&WidgetDirection::Left);
}
}
}
self.move_widget_selection(&WidgetDirection::Left);
return;
}
} else if (x >= right_tlc_x && y >= right_tlc_y)
&& (x <= right_brc_x && y <= right_brc_y)
{
if let Some(new_widget) =
self.widget_map.get(&(bt.currently_displayed_widget_id))
{
// We have to move to the current table widget first...
self.current_widget = new_widget.clone();
if let BottomWidgetType::ProcSort = &new_widget.widget_type {
if let Some(proc_widget_state) =
self.proc_state.get_widget_state(new_widget.widget_id - 2)
{
if proc_widget_state.is_sort_open {
self.move_widget_selection(&WidgetDirection::Right);
}
}
}
}
self.move_widget_selection(&WidgetDirection::Right);
// Bit extra logic to ensure you always land on a proc widget, not the sort
if let BottomWidgetType::ProcSort = &self.current_widget.widget_type {
self.move_widget_selection(&WidgetDirection::Right);
}
return;
}
}
}
let mut failed_to_get = true;
// TODO: [MOUSE] We could use a better data structure for this? Currently it's a blind
// traversal through a hashmap, using a 2d binary tree of sorts would be better.
for (new_widget_id, widget) in &self.widget_map {
if let (Some((tlc_x, tlc_y)), Some((brc_x, brc_y))) =
(widget.top_left_corner, widget.bottom_right_corner)
{
if (x >= tlc_x && y >= tlc_y) && (x <= brc_x && y <= brc_y) {
if let Some(new_widget) = self.widget_map.get(&new_widget_id) {
self.current_widget = new_widget.clone();
match &self.current_widget.widget_type {
BottomWidgetType::Temp
| BottomWidgetType::Proc
| BottomWidgetType::ProcSort
| BottomWidgetType::Disk
| BottomWidgetType::Battery => {
if let Some(basic_table_widget_state) =
&mut self.basic_table_widget_state
{
basic_table_widget_state.currently_displayed_widget_id =
self.current_widget.widget_id;
basic_table_widget_state.currently_displayed_widget_type =
self.current_widget.widget_type.clone();
}
}
_ => {}
}
failed_to_get = false;
break;
}
}
}
}
if failed_to_get {
return;
}
// Now handle click propagation down to widget.
if let Some((_tlc_x, tlc_y)) = &self.current_widget.top_left_corner {
match &self.current_widget.widget_type {
BottomWidgetType::Proc
| BottomWidgetType::ProcSort
| BottomWidgetType::CpuLegend
| BottomWidgetType::Temp
| BottomWidgetType::Disk => {
// Get our index...
let clicked_entry = y - *tlc_y;
// + 1 so we start at 0.
let offset = 1
+ if self.is_drawing_border() { 1 } else { 0 }
+ if self.is_drawing_gap(&self.current_widget) {
self.app_config_fields.table_gap
} else {
0
};
if clicked_entry >= offset {
let offset_clicked_entry = clicked_entry - offset;
match &self.current_widget.widget_type {
BottomWidgetType::Proc => {
if let Some(proc_widget_state) = self
.proc_state
.get_widget_state(self.current_widget.widget_id)
{
if let Some(visual_index) =
proc_widget_state.scroll_state.table_state.selected()
{
self.increment_process_position(
offset_clicked_entry as i64 - visual_index as i64,
);
}
}
}
BottomWidgetType::ProcSort => {
if let Some(proc_widget_state) = self
.proc_state
.get_widget_state(self.current_widget.widget_id - 2)
{
if let Some(visual_index) =
proc_widget_state.columns.column_state.selected()
{
self.increment_process_sort_position(
offset_clicked_entry as i64 - visual_index as i64,
);
}
}
}
BottomWidgetType::CpuLegend => {
if let Some(cpu_widget_state) = self
.cpu_state
.get_widget_state(self.current_widget.widget_id - 1)
{
if let Some(visual_index) =
cpu_widget_state.scroll_state.table_state.selected()
{
self.increment_cpu_legend_position(
offset_clicked_entry as i64 - visual_index as i64,
);
}
}
}
BottomWidgetType::Temp => {
if let Some(temp_widget_state) = self
.temp_state
.get_widget_state(self.current_widget.widget_id)
{
if let Some(visual_index) =
temp_widget_state.scroll_state.table_state.selected()
{
self.increment_temp_position(
offset_clicked_entry as i64 - visual_index as i64,
);
}
}
}
BottomWidgetType::Disk => {
if let Some(disk_widget_state) = self
.disk_state
.get_widget_state(self.current_widget.widget_id)
{
if let Some(visual_index) =
disk_widget_state.scroll_state.table_state.selected()
{
self.increment_disk_position(
offset_clicked_entry as i64 - visual_index as i64,
);
}
}
}
_ => {}
}
}
}
_ => {}
}
}
}
fn is_drawing_border(&self) -> bool {
self.is_expanded || !self.app_config_fields.use_basic_mode
}
fn is_drawing_gap(&self, widget: &BottomWidget) -> bool {
if let (Some((_tlc_x, tlc_y)), Some((_brc_x, brc_y))) =
(widget.top_left_corner, widget.bottom_right_corner)
{
brc_y - tlc_y >= constants::TABLE_GAP_HEIGHT_LIMIT
} else {
self.app_config_fields.table_gap == 0
}
}
}

View File

@ -867,6 +867,14 @@ pub struct BottomWidget {
/// The value is the direction to bounce, as well as the parent offset.
#[builder(default = None)]
pub parent_reflector: Option<(WidgetDirection, u64)>,
/// Top left corner when drawn, for mouse click detection
#[builder(default = None)]
pub top_left_corner: Option<(u16, u16)>,
/// Bottom right corner when drawn, for mouse click detection
#[builder(default = None)]
pub bottom_right_corner: Option<(u16, u16)>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
@ -935,7 +943,7 @@ impl std::str::FromStr for BottomWidgetType {
"empty" => Ok(BottomWidgetType::Empty),
"battery" | "batt" => Ok(BottomWidgetType::Battery),
_ => Err(BottomError::ConfigError(format!(
"invalid widget type: {}",
"invalid widget type: {}", // FIXME: Make this more helpful, specify valid widget types (just go through the list)
s
))),
}

View File

@ -730,6 +730,10 @@ pub struct BasicTableWidgetState {
pub currently_displayed_widget_type: BottomWidgetType,
pub currently_displayed_widget_id: u64,
pub widget_id: i64,
pub left_tlc: Option<(u16, u16)>,
pub left_brc: Option<(u16, u16)>,
pub right_tlc: Option<(u16, u16)>,
pub right_brc: Option<(u16, u16)>,
}
#[derive(Default)]

View File

@ -47,7 +47,11 @@ fn main() -> error::Result<()> {
)?;
// Create painter and set colours.
let mut painter = canvas::Painter::init(widget_layout, app.app_config_fields.table_gap);
let mut painter = canvas::Painter::init(
widget_layout,
app.app_config_fields.table_gap,
app.app_config_fields.use_basic_mode,
);
generate_config_colours(&config, &mut painter)?;
painter.colours.generate_remaining_cpu_colours();
painter.complete_painter_init();

View File

@ -68,10 +68,11 @@ pub struct Painter {
widget_layout: BottomLayout,
derived_widget_draw_locs: Vec<Vec<Vec<Vec<Rect>>>>,
table_height_offset: u16,
requires_boundary_recalculation: bool,
}
impl Painter {
pub fn init(widget_layout: BottomLayout, table_gap: u16) -> Self {
pub fn init(widget_layout: BottomLayout, table_gap: u16, is_basic_mode: bool) -> Self {
// Now for modularity; we have to also initialize the base layouts!
// We want to do this ONCE and reuse; after this we can just construct
// based on the console size.
@ -151,7 +152,8 @@ impl Painter {
layout_constraints,
widget_layout,
derived_widget_draw_locs: Vec::default(),
table_height_offset: 4 + table_gap,
table_height_offset: if is_basic_mode { 2 } else { 4 } + table_gap,
requires_boundary_recalculation: true,
}
}
@ -208,6 +210,14 @@ impl Painter {
self.width = current_width;
}
if app_state.should_get_widget_bounds() {
// If we're force drawing, reset ALL mouse boundaries.
for widget in app_state.widget_map.values_mut() {
widget.top_left_corner = None;
widget.bottom_right_corner = None;
}
}
terminal.autoresize()?;
terminal.draw(|mut f| {
if app_state.help_dialog_state.is_showing_help {
@ -398,6 +408,14 @@ impl Painter {
} else {
1
});
// A little hack to force the widget boundary recalculation. This is required here
// as basic mode has a height of 0 initially, which breaks things.
if self.requires_boundary_recalculation {
app_state.is_determining_widget_boundary = true;
}
self.requires_boundary_recalculation = cpu_height == 0;
let vertical_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
@ -419,18 +437,11 @@ impl Painter {
self.draw_basic_cpu(&mut f, app_state, vertical_chunks[0], 1);
self.draw_basic_memory(&mut f, app_state, middle_chunks[0], 2);
self.draw_basic_network(&mut f, app_state, middle_chunks[1], 3);
let mut later_widget_id: Option<u64> = None;
if let Some(basic_table_widget_state) = &app_state.basic_table_widget_state {
let widget_id = basic_table_widget_state.currently_displayed_widget_id;
if let Some(current_table) = app_state.widget_map.get(&widget_id) {
self.draw_basic_table_arrows(
&mut f,
app_state,
vertical_chunks[3],
current_table,
);
}
later_widget_id = Some(widget_id);
match basic_table_widget_state.currently_displayed_widget_type {
Disk => self.draw_disk_table(
&mut f,
@ -442,6 +453,7 @@ impl Painter {
Proc | ProcSort => {
let wid = widget_id
- match basic_table_widget_state.currently_displayed_widget_type {
ProcSearch => 1,
ProcSort => 2,
_ => 0,
};
@ -470,6 +482,10 @@ impl Painter {
_ => {}
}
}
if let Some(widget_id) = later_widget_id {
self.draw_basic_table_arrows(&mut f, app_state, vertical_chunks[3], widget_id);
}
} else {
// Draws using the passed in (or default) layout. NOT basic so far.
if self.derived_widget_draw_locs.is_empty() || app_state.is_force_redraw {
@ -570,6 +586,7 @@ impl Painter {
})?;
app_state.is_force_redraw = false;
app_state.is_determining_widget_boundary = false;
Ok(())
}

View File

@ -36,7 +36,7 @@ impl HelpDialog for Painter {
"".repeat(usize::from(draw_loc.width).saturating_sub(HELP_BASE.chars().count() + 2))
);
if app_state.is_force_redraw {
if app_state.should_get_widget_bounds() {
// We must also recalculate how many lines are wrapping to properly get scrolling to work on
// small terminal sizes... oh joy.

View File

@ -1,116 +1,149 @@
use crate::{
app::{
layout_manager::{BottomWidget, BottomWidgetType},
App,
},
app::{layout_manager::BottomWidgetType, App},
canvas::Painter,
};
use tui::{
backend::Backend,
layout::{Constraint, Layout, Rect},
layout::{Alignment, Constraint, Direction, Layout, Rect},
terminal::Frame,
widgets::{Block, Paragraph, Text},
};
pub trait BasicTableArrows {
fn draw_basic_table_arrows<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &App, draw_loc: Rect, current_table: &BottomWidget,
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
);
}
impl BasicTableArrows for Painter {
fn draw_basic_table_arrows<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &App, draw_loc: Rect, current_table: &BottomWidget,
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) {
let current_table = if let BottomWidgetType::ProcSort = current_table.widget_type {
current_table
.right_neighbour
.map(|id| app_state.widget_map.get(&id).unwrap())
.unwrap()
} else {
current_table
};
// Effectively a paragraph with a ton of spacing
let (left_table, right_table) = (
{
current_table
.left_neighbour
.map(|left_widget_id| {
app_state
.widget_map
.get(&left_widget_id)
.map(|left_widget| {
if left_widget.widget_type == BottomWidgetType::ProcSort {
left_widget
.left_neighbour
.map(|left_left_widget_id| {
app_state.widget_map.get(&left_left_widget_id).map(
|left_left_widget| &left_left_widget.widget_type,
)
})
.unwrap_or_else(|| Some(&BottomWidgetType::Temp))
.unwrap_or_else(|| &BottomWidgetType::Temp)
} else {
&left_widget.widget_type
}
})
.unwrap_or_else(|| &BottomWidgetType::Temp)
})
.unwrap_or_else(|| &BottomWidgetType::Temp)
},
{
if let Some(current_table) = app_state.widget_map.get(&widget_id) {
let current_table = if let BottomWidgetType::ProcSort = current_table.widget_type {
current_table
.right_neighbour
.map(|right_widget_id| {
app_state
.widget_map
.get(&right_widget_id)
.map(|right_widget| {
if right_widget.widget_type == BottomWidgetType::ProcSort {
right_widget
.right_neighbour
.map(|right_right_widget_id| {
app_state.widget_map.get(&right_right_widget_id).map(
|right_right_widget| {
&right_right_widget.widget_type
},
)
})
.unwrap_or_else(|| Some(&BottomWidgetType::Disk))
.unwrap_or_else(|| &BottomWidgetType::Disk)
} else {
&right_widget.widget_type
}
})
.unwrap_or_else(|| &BottomWidgetType::Disk)
})
.unwrap_or_else(|| &BottomWidgetType::Disk)
},
);
.map(|id| app_state.widget_map.get(&id).unwrap())
.unwrap()
} else {
current_table
};
let left_name = left_table.get_pretty_name();
let right_name = right_table.get_pretty_name();
let (left_table, right_table) = (
{
current_table
.left_neighbour
.map(|left_widget_id| {
app_state
.widget_map
.get(&left_widget_id)
.map(|left_widget| {
if left_widget.widget_type == BottomWidgetType::ProcSort {
left_widget
.left_neighbour
.map(|left_left_widget_id| {
app_state.widget_map.get(&left_left_widget_id).map(
|left_left_widget| {
&left_left_widget.widget_type
},
)
})
.unwrap_or_else(|| Some(&BottomWidgetType::Temp))
.unwrap_or_else(|| &BottomWidgetType::Temp)
} else {
&left_widget.widget_type
}
})
.unwrap_or_else(|| &BottomWidgetType::Temp)
})
.unwrap_or_else(|| &BottomWidgetType::Temp)
},
{
current_table
.right_neighbour
.map(|right_widget_id| {
app_state
.widget_map
.get(&right_widget_id)
.map(|right_widget| {
if right_widget.widget_type == BottomWidgetType::ProcSort {
right_widget
.right_neighbour
.map(|right_right_widget_id| {
app_state
.widget_map
.get(&right_right_widget_id)
.map(|right_right_widget| {
&right_right_widget.widget_type
})
})
.unwrap_or_else(|| Some(&BottomWidgetType::Disk))
.unwrap_or_else(|| &BottomWidgetType::Disk)
} else {
&right_widget.widget_type
}
})
.unwrap_or_else(|| &BottomWidgetType::Disk)
})
.unwrap_or_else(|| &BottomWidgetType::Disk)
},
);
let num_spaces =
usize::from(draw_loc.width).saturating_sub(6 + left_name.len() + right_name.len());
let left_name = left_table.get_pretty_name();
let right_name = right_table.get_pretty_name();
let arrow_text = vec![
Text::raw("\n"),
Text::styled(format!("{}", left_name), self.colours.text_style),
Text::raw(" ".repeat(num_spaces)),
Text::styled(format!("{}", right_name), self.colours.text_style),
];
let num_spaces =
usize::from(draw_loc.width).saturating_sub(6 + left_name.len() + right_name.len());
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(1)
.split(draw_loc);
let left_arrow_text = vec![
Text::raw("\n"),
Text::styled(format!("{}", left_name), self.colours.text_style),
];
f.render_widget(
Paragraph::new(arrow_text.iter()).block(Block::default()),
margined_draw_loc[0],
);
let right_arrow_text = vec![
Text::raw("\n"),
Text::styled(format!("{}", right_name), self.colours.text_style),
];
let margined_draw_loc = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Length(2 + left_name.len() as u16),
Constraint::Length(num_spaces as u16),
Constraint::Length(2 + right_name.len() as u16),
]
.as_ref(),
)
.horizontal_margin(1)
.split(draw_loc);
f.render_widget(
Paragraph::new(left_arrow_text.iter()).block(Block::default()),
margined_draw_loc[0],
);
f.render_widget(
Paragraph::new(right_arrow_text.iter())
.block(Block::default())
.alignment(Alignment::Right),
margined_draw_loc[2],
);
if app_state.should_get_widget_bounds() {
if let Some(basic_table) = &mut app_state.basic_table_widget_state {
basic_table.left_tlc = Some((margined_draw_loc[0].x, margined_draw_loc[0].y));
basic_table.left_brc = Some((
margined_draw_loc[0].x + margined_draw_loc[0].width,
margined_draw_loc[0].y + margined_draw_loc[0].height,
));
basic_table.right_tlc = Some((margined_draw_loc[2].x, margined_draw_loc[2].y));
basic_table.right_brc = Some((
margined_draw_loc[2].x + margined_draw_loc[2].width,
margined_draw_loc[2].y + margined_draw_loc[2].height,
));
}
}
}
}
}

View File

@ -6,7 +6,7 @@ use crate::{
use tui::{
backend::Backend,
layout::{Constraint, Rect},
layout::{Constraint, Direction, Layout, Rect},
terminal::Frame,
widgets::{Block, Borders, Paragraph, Row, Table, Tabs, Text},
};
@ -109,6 +109,12 @@ impl BatteryDisplayWidget for Painter {
// draw_loc,
// );
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
.direction(Direction::Horizontal)
.split(draw_loc)[0];
if let Some(battery_details) = app_state
.canvas_data
.battery_data
@ -166,7 +172,7 @@ impl BatteryDisplayWidget for Painter {
.block(battery_block)
.header_style(self.colours.table_header_style)
.widths([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()),
draw_loc,
margined_draw_loc,
);
} else {
f.render_widget(
@ -178,7 +184,7 @@ impl BatteryDisplayWidget for Painter {
.iter(),
)
.block(battery_block),
draw_loc,
margined_draw_loc,
);
}
@ -209,6 +215,17 @@ impl BatteryDisplayWidget for Painter {
.select(battery_widget_state.currently_selected_battery_index),
draw_loc,
);
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
widget.bottom_right_corner = Some((
margined_draw_loc.x + margined_draw_loc.width,
margined_draw_loc.y + margined_draw_loc.height,
));
}
}
}
}
}

View File

@ -127,14 +127,23 @@ impl CpuBasicWidget for Painter {
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(1)
.split(*chunk);
.split(*chunk)[0];
f.render_widget(
Paragraph::new(cpu_column.iter()).block(Block::default()),
margined_loc[0],
margined_loc,
);
}
}
}
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
widget.bottom_right_corner =
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
}
}
}
}

View File

@ -65,6 +65,15 @@ impl CpuGraphWidget for Painter {
if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&widget_id) {
cpu_widget_state.is_legend_hidden = true;
}
// Update draw loc in widget map
if app_state.should_get_widget_bounds() {
if let Some(bottom_widget) = app_state.widget_map.get_mut(&widget_id) {
bottom_widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
bottom_widget.bottom_right_corner =
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
}
}
} else {
let (graph_index, legend_index, constraints) =
if app_state.app_config_fields.left_legend {
@ -94,6 +103,35 @@ impl CpuGraphWidget for Painter {
partitioned_draw_loc[legend_index],
widget_id + 1,
);
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
if let Some(cpu_widget) = app_state.widget_map.get_mut(&widget_id) {
cpu_widget.top_left_corner = Some((
partitioned_draw_loc[graph_index].x,
partitioned_draw_loc[graph_index].y,
));
cpu_widget.bottom_right_corner = Some((
partitioned_draw_loc[graph_index].x
+ partitioned_draw_loc[graph_index].width,
partitioned_draw_loc[graph_index].y
+ partitioned_draw_loc[graph_index].height,
));
}
if let Some(legend_widget) = app_state.widget_map.get_mut(&(widget_id + 1)) {
legend_widget.top_left_corner = Some((
partitioned_draw_loc[legend_index].x,
partitioned_draw_loc[legend_index].y,
));
legend_widget.bottom_right_corner = Some((
partitioned_draw_loc[legend_index].x
+ partitioned_draw_loc[legend_index].width,
partitioned_draw_loc[legend_index].y
+ partitioned_draw_loc[legend_index].height,
));
}
}
}
}
@ -239,20 +277,35 @@ impl CpuGraphWidget for Painter {
{
cpu_widget_state.is_legend_hidden = false;
let cpu_data: &mut [ConvertedCpuData] = &mut app_state.canvas_data.cpu_data;
let cpu_table_state = &mut cpu_widget_state.scroll_state.table_state;
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let start_position = get_start_position(
usize::from(draw_loc.height.saturating_sub(self.table_height_offset)),
usize::from(
(draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
),
&cpu_widget_state.scroll_state.scroll_direction,
&mut cpu_widget_state.scroll_state.previous_scroll_position,
cpu_widget_state.scroll_state.current_scroll_position,
app_state.is_force_redraw,
);
let is_on_widget = widget_id == app_state.current_widget.widget_id;
cpu_table_state.select(Some(
cpu_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position),
));
let sliced_cpu_data = &cpu_data[start_position..];
let mut offset_scroll_index =
cpu_widget_state.scroll_state.current_scroll_position - start_position;
let mut offset_scroll_index = cpu_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position);
let show_avg_cpu = app_state.app_config_fields.show_average_cpu;
let cpu_rows = sliced_cpu_data.iter().enumerate().filter_map(|(itx, cpu)| {
@ -306,7 +359,7 @@ impl CpuGraphWidget for Painter {
};
// Draw
f.render_widget(
f.render_stateful_widget(
Table::new(CPU_LEGEND_HEADER.iter(), cpu_rows)
.block(
Block::default()
@ -321,8 +374,9 @@ impl CpuGraphWidget for Painter {
.map(|calculated_width| Constraint::Length(*calculated_width as u16))
.collect::<Vec<_>>()),
)
.header_gap(app_state.app_config_fields.table_gap),
.header_gap(table_gap),
draw_loc,
cpu_table_state,
);
}
}

View File

@ -39,8 +39,15 @@ impl DiskTableWidget for Painter {
) {
if let Some(disk_widget_state) = app_state.disk_state.widget_states.get_mut(&widget_id) {
let disk_data: &mut [Vec<String>] = &mut app_state.canvas_data.disk_data;
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let start_position = get_start_position(
usize::from(draw_loc.height.saturating_sub(self.table_height_offset)),
usize::from(
(draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
),
&disk_widget_state.scroll_state.scroll_direction,
&mut disk_widget_state.scroll_state.previous_scroll_position,
disk_widget_state.scroll_state.current_scroll_position,
@ -49,15 +56,13 @@ impl DiskTableWidget for Painter {
let is_on_widget = app_state.current_widget.widget_id == widget_id;
let disk_table_state = &mut disk_widget_state.scroll_state.table_state;
disk_table_state.select(Some(
disk_widget_state.scroll_state.current_scroll_position - start_position,
disk_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position),
));
let sliced_vec = &mut disk_data[start_position..];
let disk_rows = sliced_vec.iter().map(|disk| Row::Data(disk.iter()));
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
// Calculate widths
// TODO: [PRETTY] Ellipsis on strings?
@ -133,7 +138,7 @@ impl DiskTableWidget for Painter {
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
.direction(Direction::Horizontal)
.split(draw_loc);
.split(draw_loc)[0];
// Draw!
f.render_stateful_widget(
@ -149,9 +154,20 @@ impl DiskTableWidget for Painter {
.collect::<Vec<_>>()),
)
.header_gap(table_gap),
margined_draw_loc[0],
margined_draw_loc,
disk_table_state,
);
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
widget.bottom_right_corner = Some((
margined_draw_loc.x + margined_draw_loc.width,
margined_draw_loc.y + margined_draw_loc.height,
));
}
}
}
}
}

View File

@ -115,5 +115,14 @@ impl MemBasicWidget for Painter {
Paragraph::new(mem_text.iter()).block(Block::default()),
margined_loc[0],
);
// Update draw loc in widget map
if app_state.should_get_widget_bounds() {
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
widget.bottom_right_corner =
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
}
}
}
}

View File

@ -133,5 +133,14 @@ impl MemGraphWidget for Painter {
draw_loc,
);
}
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
widget.bottom_right_corner =
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
}
}
}
}

View File

@ -67,5 +67,14 @@ impl NetworkBasicWidget for Painter {
Paragraph::new(total_net_text.iter()).block(Block::default()),
total_loc[0],
);
// Update draw loc in widget map
if app_state.should_get_widget_bounds() {
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
widget.bottom_right_corner =
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
}
}
}
}

View File

@ -62,6 +62,17 @@ impl NetworkGraphWidget for Painter {
} else {
self.draw_network_graph(f, app_state, draw_loc, widget_id, false);
}
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
// Note that in both cases, we always go to the same widget id so it's fine to do it like
// this lol.
if let Some(network_widget) = app_state.widget_map.get_mut(&widget_id) {
network_widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
network_widget.bottom_right_corner =
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
}
}
}
fn draw_network_graph<B: Backend>(
@ -93,12 +104,8 @@ impl NetworkGraphWidget for Painter {
}
}
// Main idea is that we have some "limits" --- if we're, say, under a logged kibibyte,
// then we are just gonna set the cap at a kibibyte.
// For gibi/giga and beyond, we instead start going up by 1 rather than jumping to a tera/tebi.
// So, it would look a bit like:
// - < Kibi => Kibi => Mebi => Gibi => 2 Gibi => ... => 999 Gibi => Tebi => 2 Tebi => ...
// FIXME [NETWORKING]: Do ya think it would be possible for a more granular approach?
// Currently we do 32 -> 33... which skips some gigabit values
let true_max_val: f64;
let mut labels = vec![];
if max_val_bytes < LOG_KIBI_LIMIT {

View File

@ -109,6 +109,65 @@ impl ProcessTableWidget for Painter {
widget_id: u64,
) {
if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) {
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
.direction(Direction::Horizontal)
.split(draw_loc)[0];
let (border_and_title_style, highlight_style) = if is_on_widget {
(
self.colours.highlighted_border_style,
self.colours.currently_selected_text_style,
)
} else {
(self.colours.border_style, self.colours.text_style)
};
let title = if draw_border {
if app_state.is_expanded
&& !proc_widget_state
.process_search_state
.search_state
.is_enabled
&& !proc_widget_state.is_sort_open
{
const TITLE_BASE: &str = " Processes ── Esc to go back ";
format!(
" Processes ─{}─ Esc to go back ",
"".repeat(
usize::from(draw_loc.width)
.saturating_sub(TITLE_BASE.chars().count() + 2)
)
)
} else {
" Processes ".to_string()
}
} else {
String::default()
};
let title_style = if app_state.is_expanded {
border_and_title_style
} else {
self.colours.widget_title_style
};
let process_block = if draw_border {
Block::default()
.title(&title)
.title_style(title_style)
.borders(Borders::ALL)
.border_style(border_and_title_style)
} else if is_on_widget {
Block::default()
.borders(*SIDE_BORDERS)
.border_style(self.colours.highlighted_border_style)
} else {
Block::default().borders(Borders::NONE)
};
if let Some(process_data) = &app_state
.canvas_data
.finalized_process_data_map
@ -122,10 +181,17 @@ impl ProcessTableWidget for Painter {
// hit the process we've currently scrolled to.
// We also need to move the list - we can
// do so by hiding some elements!
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let position = get_start_position(
usize::from(draw_loc.height.saturating_sub(self.table_height_offset)),
usize::from(
(draw_loc.height + (1 - table_gap))
.saturating_sub(self.table_height_offset),
),
&proc_widget_state.scroll_state.scroll_direction,
&mut proc_widget_state.scroll_state.previous_scroll_position,
proc_widget_state.scroll_state.current_scroll_position,
@ -142,13 +208,11 @@ impl ProcessTableWidget for Painter {
let sliced_vec = &process_data[start_position..];
let proc_table_state = &mut proc_widget_state.scroll_state.table_state;
proc_table_state.select(Some(
proc_widget_state.scroll_state.current_scroll_position - start_position,
proc_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position),
));
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
// Draw!
let is_proc_widget_grouped = proc_widget_state.is_grouped;
@ -216,64 +280,6 @@ impl ProcessTableWidget for Painter {
let intrinsic_widths =
&(variable_intrinsic_results.0)[0..variable_intrinsic_results.1];
let (border_and_title_style, highlight_style) = if is_on_widget {
(
self.colours.highlighted_border_style,
self.colours.currently_selected_text_style,
)
} else {
(self.colours.border_style, self.colours.text_style)
};
let title = if draw_border {
if app_state.is_expanded
&& !proc_widget_state
.process_search_state
.search_state
.is_enabled
&& !proc_widget_state.is_sort_open
{
const TITLE_BASE: &str = " Processes ── Esc to go back ";
format!(
" Processes ─{}─ Esc to go back ",
"".repeat(
usize::from(draw_loc.width)
.saturating_sub(TITLE_BASE.chars().count() + 2)
)
)
} else {
" Processes ".to_string()
}
} else {
String::default()
};
let title_style = if app_state.is_expanded {
border_and_title_style
} else {
self.colours.widget_title_style
};
let process_block = if draw_border {
Block::default()
.title(&title)
.title_style(title_style)
.borders(Borders::ALL)
.border_style(border_and_title_style)
} else if is_on_widget {
Block::default()
.borders(*SIDE_BORDERS)
.border_style(self.colours.highlighted_border_style)
} else {
Block::default().borders(Borders::NONE)
};
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
.direction(Direction::Horizontal)
.split(draw_loc);
f.render_stateful_widget(
Table::new(process_headers.iter(), process_rows)
.block(process_block)
@ -289,9 +295,22 @@ impl ProcessTableWidget for Painter {
.collect::<Vec<_>>()),
)
.header_gap(table_gap),
margined_draw_loc[0],
margined_draw_loc,
proc_table_state,
);
} else {
f.render_widget(process_block, margined_draw_loc);
}
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
widget.bottom_right_corner = Some((
margined_draw_loc.x + margined_draw_loc.width,
margined_draw_loc.y + margined_draw_loc.height,
));
}
}
}
}
@ -497,15 +516,26 @@ impl ProcessTableWidget for Painter {
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
.direction(Direction::Horizontal)
.split(draw_loc);
.split(draw_loc)[0];
f.render_widget(
Paragraph::new(search_text.iter())
.block(process_search_block)
.style(self.colours.text_style)
.alignment(Alignment::Left),
margined_draw_loc[0],
margined_draw_loc,
);
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
widget.bottom_right_corner = Some((
margined_draw_loc.x + margined_draw_loc.width,
margined_draw_loc.y + margined_draw_loc.height,
));
}
}
}
}
@ -531,21 +561,18 @@ impl ProcessTableWidget for Painter {
.unwrap()
.enabled
})
.enumerate()
.map(|(itx, column_type)| {
if current_scroll_position == itx {
(
column_type.to_string(),
self.colours.currently_selected_text_style,
)
} else {
(column_type.to_string(), self.colours.text_style)
}
})
.map(|column_type| column_type.to_string())
.collect::<Vec<_>>();
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let position = get_start_position(
usize::from(draw_loc.height.saturating_sub(self.table_height_offset)),
usize::from(
(draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
),
&proc_widget_state.columns.scroll_direction,
&mut proc_widget_state.columns.previous_scroll_position,
current_scroll_position,
@ -563,9 +590,15 @@ impl ProcessTableWidget for Painter {
let sort_options = sliced_vec
.iter()
.map(|(column, style)| Row::StyledData(vec![column].into_iter(), *style));
.map(|column| Row::Data(vec![column].into_iter()));
let column_state = &mut proc_widget_state.columns.column_state;
column_state.select(Some(
proc_widget_state
.columns
.current_scroll_position
.saturating_sub(start_position),
));
let current_border_style = if proc_widget_state
.process_search_state
.search_state
@ -590,21 +623,40 @@ impl ProcessTableWidget for Painter {
Block::default().borders(Borders::NONE)
};
let highlight_style = if is_on_widget {
self.colours.currently_selected_text_style
} else {
self.colours.text_style
};
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
.direction(Direction::Horizontal)
.split(draw_loc);
.split(draw_loc)[0];
f.render_stateful_widget(
Table::new(["Sort By"].iter(), sort_options)
.block(process_sort_block)
.highlight_style(highlight_style)
.style(self.colours.text_style)
.header_style(self.colours.table_header_style)
.widths(&[Constraint::Percentage(100)])
.header_gap(1),
margined_draw_loc[0],
.header_gap(table_gap),
margined_draw_loc,
column_state,
);
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
widget.bottom_right_corner = Some((
margined_draw_loc.x + margined_draw_loc.width,
margined_draw_loc.y + margined_draw_loc.height,
));
}
}
}
}
}

View File

@ -40,8 +40,15 @@ impl TempTableWidget for Painter {
if let Some(temp_widget_state) = app_state.temp_state.widget_states.get_mut(&widget_id) {
let temp_sensor_data: &mut [Vec<String>] = &mut app_state.canvas_data.temp_sensor_data;
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let start_position = get_start_position(
usize::from(draw_loc.height.saturating_sub(self.table_height_offset)),
usize::from(
(draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
),
&temp_widget_state.scroll_state.scroll_direction,
&mut temp_widget_state.scroll_state.previous_scroll_position,
temp_widget_state.scroll_state.current_scroll_position,
@ -50,15 +57,13 @@ impl TempTableWidget for Painter {
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let temp_table_state = &mut temp_widget_state.scroll_state.table_state;
temp_table_state.select(Some(
temp_widget_state.scroll_state.current_scroll_position - start_position,
temp_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position),
));
let sliced_vec = &temp_sensor_data[start_position..];
let temperature_rows = sliced_vec.iter().map(|temp_row| Row::Data(temp_row.iter()));
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
// Calculate widths
let width = f64::from(draw_loc.width);
@ -113,7 +118,7 @@ impl TempTableWidget for Painter {
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
.direction(Direction::Horizontal)
.split(draw_loc);
.split(draw_loc)[0];
// Draw
f.render_stateful_widget(
@ -129,9 +134,21 @@ impl TempTableWidget for Painter {
.collect::<Vec<_>>()),
)
.header_gap(table_gap),
margined_draw_loc[0],
margined_draw_loc,
temp_table_state,
);
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
// Note there is no difference between this and using draw_loc, but I'm too lazy to fix it.
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
widget.bottom_right_corner = Some((
margined_draw_loc.x + margined_draw_loc.width,
margined_draw_loc.y + margined_draw_loc.height,
));
}
}
}
}
}

View File

@ -85,6 +85,7 @@ pub fn get_matches() -> clap::ArgMatches<'static> {
(@arg USE_OLD_NETWORK_LEGEND: --use_old_network_legend "Use the older (pre-0.4) network widget legend.")
(@arg HIDE_TABLE_GAP: --hide_table_gap "Hides the spacing between the table headers and entries.")
(@arg BATTERY: --battery "Shows the battery widget in default or basic mode. No effect on custom layouts.")
(@arg DISABLE_CLICK: --disable_click "Disables mouse clicks from interacting with the program.")
)
.get_matches()
}
@ -93,6 +94,20 @@ pub fn handle_mouse_event(event: MouseEvent, app: &mut App) {
match event {
MouseEvent::ScrollUp(_x, _y, _modifiers) => app.handle_scroll_up(),
MouseEvent::ScrollDown(_x, _y, _modifiers) => app.handle_scroll_down(),
MouseEvent::Down(button, x, y, _modifiers) => {
// debug!("Button down: {:?}, x: {}, y: {}", button, x, y);
if !app.app_config_fields.disable_click {
match button {
crossterm::event::MouseButton::Left => {
// Trigger left click widget activity
app.left_mouse_click_movement(x, y);
}
crossterm::event::MouseButton::Right => {}
_ => {}
}
}
}
_ => {}
};
}

View File

@ -42,6 +42,7 @@ pub struct ConfigFlags {
pub use_old_network_legend: Option<bool>,
pub hide_table_gap: Option<bool>,
pub battery: Option<bool>,
pub disable_click: Option<bool>,
}
#[derive(Default, Deserialize)]
@ -192,11 +193,19 @@ pub fn build_app(
currently_displayed_widget_type: initial_widget_type,
currently_displayed_widget_id: initial_widget_id,
widget_id: 100,
left_tlc: None,
left_brc: None,
right_tlc: None,
right_brc: None,
},
_ => BasicTableWidgetState {
currently_displayed_widget_type: Proc,
currently_displayed_widget_id: DEFAULT_WIDGET_ID,
widget_id: 100,
left_tlc: None,
left_brc: None,
right_tlc: None,
right_brc: None,
},
})
} else {
@ -221,6 +230,7 @@ pub fn build_app(
} else {
1
},
disable_click: get_disable_click(matches, config),
};
let used_widgets = UsedWidgets {
@ -259,6 +269,7 @@ pub fn get_widget_layout(
let bottom_layout = if get_use_basic_mode(matches, config) {
default_widget_id = DEFAULT_WIDGET_ID;
BottomLayout::init_basic_default(get_use_battery(matches, config))
} else {
let ref_row: Vec<Row>; // Required to handle reference
@ -297,7 +308,7 @@ pub fn get_widget_layout(
total_row_height_ratio: total_height_ratio,
};
// Confirm that we have at least ONE widget - if not, error out!
// Confirm that we have at least ONE widget left - if not, error out!
if iter_id > 0 {
ret_bottom_layout.get_movement_mappings();
// debug!("Bottom layout: {:#?}", ret_bottom_layout);
@ -489,9 +500,7 @@ pub fn get_app_grouping(matches: &clap::ArgMatches<'static>, config: &Config) ->
return true;
} else if let Some(flags) = &config.flags {
if let Some(grouping) = flags.group_processes {
if grouping {
return true;
}
return grouping;
}
}
false
@ -502,9 +511,7 @@ pub fn get_app_case_sensitive(matches: &clap::ArgMatches<'static>, config: &Conf
return true;
} else if let Some(flags) = &config.flags {
if let Some(case_sensitive) = flags.case_sensitive {
if case_sensitive {
return true;
}
return case_sensitive;
}
}
false
@ -515,9 +522,7 @@ pub fn get_app_match_whole_word(matches: &clap::ArgMatches<'static>, config: &Co
return true;
} else if let Some(flags) = &config.flags {
if let Some(whole_word) = flags.whole_word {
if whole_word {
return true;
}
return whole_word;
}
}
false
@ -528,9 +533,7 @@ pub fn get_app_use_regex(matches: &clap::ArgMatches<'static>, config: &Config) -
return true;
} else if let Some(flags) = &config.flags {
if let Some(regex) = flags.regex {
if regex {
return true;
}
return regex;
}
}
false
@ -541,9 +544,7 @@ fn get_hide_time(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
return true;
} else if let Some(flags) = &config.flags {
if let Some(hide_time) = flags.hide_time {
if hide_time {
return true;
}
return hide_time;
}
}
false
@ -554,9 +555,7 @@ fn get_autohide_time(matches: &clap::ArgMatches<'static>, config: &Config) -> bo
return true;
} else if let Some(flags) = &config.flags {
if let Some(autohide_time) = flags.autohide_time {
if autohide_time {
return true;
}
return autohide_time;
}
}
@ -613,14 +612,23 @@ fn get_default_widget_and_count(
}
}
fn get_disable_click(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
if matches.is_present("DISABLE_CLICK") {
return true;
} else if let Some(flags) = &config.flags {
if let Some(disable_click) = flags.disable_click {
return disable_click;
}
}
false
}
pub fn get_use_old_network_legend(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
if matches.is_present("USE_OLD_NETWORK_LEGEND") {
return true;
} else if let Some(flags) = &config.flags {
if let Some(use_old_network_legend) = flags.use_old_network_legend {
if use_old_network_legend {
return true;
}
return use_old_network_legend;
}
}
false
@ -631,9 +639,7 @@ pub fn get_hide_table_gap(matches: &clap::ArgMatches<'static>, config: &Config)
return true;
} else if let Some(flags) = &config.flags {
if let Some(hide_table_gap) = flags.hide_table_gap {
if hide_table_gap {
return true;
}
return hide_table_gap;
}
}
false
@ -644,9 +650,7 @@ pub fn get_use_battery(matches: &clap::ArgMatches<'static>, config: &Config) ->
return true;
} else if let Some(flags) = &config.flags {
if let Some(battery) = flags.battery {
if battery {
return true;
}
return battery;
}
}
false

View File

@ -0,0 +1,9 @@
// TODO: Test basic mode
// #[test]
// fn test_basic_mode() {
// let ret_bottom_layout = BottomLayout::init_basic_default(false);
// }
// TODO: Test moving around with procs and their hidden children.
// TODO: Test moving around with cpus if they get hidden.