diff --git a/.vscode/settings.json b/.vscode/settings.json index dacfb4b4..554f5994 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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", diff --git a/CHANGELOG.md b/CHANGELOG.md index 55f8c06f..fa1f2b1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 68e5da17..82a355a8 100644 --- a/README.md +++ b/README.md @@ -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`
`Shift-Left`
`H`
`A` | Move widget selection left | -| `Ctrl-Right`
`Shift-Right`
`L`
`D` | Move widget selection right | -| `Ctrl-Up`
`Shift-Up`
`K`
`W` | Move widget selection up | -| `Ctrl-Down`
`Shift-Down`
`J`
`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
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`
`Shift-Left`
`H`
`A` | Move widget selection left | +| `Ctrl-Right`
`Shift-Right`
`L`
`D` | Move widget selection right | +| `Ctrl-Up`
`Shift-Up`
`K`
`W` | Move widget selection up | +| `Ctrl-Down`
`Shift-Down`
`J`
`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. | -------- | ---------------------------------------------------- | -------------------------- | | `()` | `( AND ) OR ` | Group together a condition | +### Mousebindings + +#### General + +| | | +| ------------ | --------------------------------------------------------------------------------------------------------------------- | +| Mouse scroll | Table: Scroll
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 + ## Thanks diff --git a/src/app.rs b/src/app.rs index 2d3e494f..f84dfadc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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, - pub app_config_fields: AppConfigFields, pub widget_map: HashMap, 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 + } + } } diff --git a/src/app/layout_manager.rs b/src/app/layout_manager.rs index 8b5ba40d..35c6e02f 100644 --- a/src/app/layout_manager.rs +++ b/src/app/layout_manager.rs @@ -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 ))), } diff --git a/src/app/states.rs b/src/app/states.rs index 9bd61e1f..cf9123db 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -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)] diff --git a/src/bin/main.rs b/src/bin/main.rs index c443bb1f..34c8c2b7 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -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(); diff --git a/src/canvas.rs b/src/canvas.rs index e6538001..7036bd49 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -68,10 +68,11 @@ pub struct Painter { widget_layout: BottomLayout, derived_widget_draw_locs: Vec>>>, 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 = 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(()) } diff --git a/src/canvas/dialogs/help_dialog.rs b/src/canvas/dialogs/help_dialog.rs index dbd5c74c..2b148f16 100644 --- a/src/canvas/dialogs/help_dialog.rs +++ b/src/canvas/dialogs/help_dialog.rs @@ -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. diff --git a/src/canvas/widgets/basic_table_arrows.rs b/src/canvas/widgets/basic_table_arrows.rs index 591f71c6..fd64e8dc 100644 --- a/src/canvas/widgets/basic_table_arrows.rs +++ b/src/canvas/widgets/basic_table_arrows.rs @@ -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( - &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( - &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, + )); + } + } + } } } diff --git a/src/canvas/widgets/battery_display.rs b/src/canvas/widgets/battery_display.rs index 7c3b7896..9166d50c 100644 --- a/src/canvas/widgets/battery_display.rs +++ b/src/canvas/widgets/battery_display.rs @@ -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, + )); + } + } } } } diff --git a/src/canvas/widgets/cpu_basic.rs b/src/canvas/widgets/cpu_basic.rs index 4683a677..7f9381f9 100644 --- a/src/canvas/widgets/cpu_basic.rs +++ b/src/canvas/widgets/cpu_basic.rs @@ -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)); + } + } } } diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/widgets/cpu_graph.rs index bd931deb..e000b66f 100644 --- a/src/canvas/widgets/cpu_graph.rs +++ b/src/canvas/widgets/cpu_graph.rs @@ -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::>()), ) - .header_gap(app_state.app_config_fields.table_gap), + .header_gap(table_gap), draw_loc, + cpu_table_state, ); } } diff --git a/src/canvas/widgets/disk_table.rs b/src/canvas/widgets/disk_table.rs index fdb9fc31..f791500b 100644 --- a/src/canvas/widgets/disk_table.rs +++ b/src/canvas/widgets/disk_table.rs @@ -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] = &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::>()), ) .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, + )); + } + } } } } diff --git a/src/canvas/widgets/mem_basic.rs b/src/canvas/widgets/mem_basic.rs index aa069f47..89f96dd5 100644 --- a/src/canvas/widgets/mem_basic.rs +++ b/src/canvas/widgets/mem_basic.rs @@ -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)); + } + } } } diff --git a/src/canvas/widgets/mem_graph.rs b/src/canvas/widgets/mem_graph.rs index b6cb1af1..7accfa55 100644 --- a/src/canvas/widgets/mem_graph.rs +++ b/src/canvas/widgets/mem_graph.rs @@ -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)); + } + } } } diff --git a/src/canvas/widgets/network_basic.rs b/src/canvas/widgets/network_basic.rs index 2d8c6340..126c8ae0 100644 --- a/src/canvas/widgets/network_basic.rs +++ b/src/canvas/widgets/network_basic.rs @@ -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)); + } + } } } diff --git a/src/canvas/widgets/network_graph.rs b/src/canvas/widgets/network_graph.rs index d69f68f3..32887c32 100644 --- a/src/canvas/widgets/network_graph.rs +++ b/src/canvas/widgets/network_graph.rs @@ -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( @@ -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 { diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/widgets/process_table.rs index ca5642ba..8d6ea0bb 100644 --- a/src/canvas/widgets/process_table.rs +++ b/src/canvas/widgets/process_table.rs @@ -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::>()), ) .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::>(); + 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, + )); + } + } } } } diff --git a/src/canvas/widgets/temp_table.rs b/src/canvas/widgets/temp_table.rs index 23822b68..9ab6f985 100644 --- a/src/canvas/widgets/temp_table.rs +++ b/src/canvas/widgets/temp_table.rs @@ -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] = &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::>()), ) .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, + )); + } + } } } } diff --git a/src/lib.rs b/src/lib.rs index a5689747..2022e1b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 => {} + _ => {} + } + } + } _ => {} }; } diff --git a/src/options.rs b/src/options.rs index d772ed9e..41059f9c 100644 --- a/src/options.rs +++ b/src/options.rs @@ -42,6 +42,7 @@ pub struct ConfigFlags { pub use_old_network_legend: Option, pub hide_table_gap: Option, pub battery: Option, + pub disable_click: Option, } #[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; // 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 diff --git a/tests/layout_movement_tests.rs b/tests/layout_movement_tests.rs new file mode 100644 index 00000000..ef73c683 --- /dev/null +++ b/tests/layout_movement_tests.rs @@ -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. \ No newline at end of file