feature: mouse support for tabs and dd dialog (#230)

This commit is contained in:
Clement Tsang 2020-09-11 04:20:14 -04:00 committed by GitHub
parent 0c21cba189
commit 86c8b474ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 192 additions and 37 deletions

View file

@ -35,6 +35,7 @@
"armhf",
"armv",
"atim",
"autohide",
"choco",
"cmdline",
"commandline",
@ -65,6 +66,7 @@
"nuget",
"nvme",
"paren",
"pids",
"pmem",
"ppid",
"prepush",

View file

@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#208](https://github.com/ClementTsang/bottom/pull/208): Mouse support for tables and moving to widgets.
- [#217](https://github.com/ClementTsang/bottom/pull/217): Unofficial ARM support.
- [#217](https://github.com/ClementTsang/bottom/pull/217): (Kinda) ARM support.
- [#220](https://github.com/ClementTsang/bottom/pull/220): Add ability to hide specific temperature and disk entries via config.

View file

@ -371,9 +371,9 @@ Note that the `and` operator takes precedence over the `or` operator.
#### General
| | |
| ------ | ---------------------------------------------------------------------------------------------------------------- |
| ------ | ----------------------------------------------------------------------------------------------------- |
| Scroll | Table: Scroll<br>Chart: Zooms in or out by scrolling up or down respectively |
| Click | Selects the clicked widget. For tables, clicking can also select an entry.<br>Can be disabled via options/flags. |
| Click | Selects the clicked widget, table entry, dialog option, or tab.<br>Can be disabled via options/flags. |
#### CPU bindings

View file

@ -529,6 +529,7 @@ impl App {
} else {
self.delete_dialog_state.is_showing_dd = false;
}
self.is_force_redraw = true;
} else if let BottomWidgetType::ProcSort = self.current_widget.widget_type {
if let Some(proc_widget_state) = self
.proc_state
@ -2274,6 +2275,14 @@ impl App {
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.
// TODO: [MOUSE] double click functionality...?
// TODO: [REFACTOR] might want to refactor this, it's ugly as sin.
// TODO: [REFACTOR] Might wanna refactor ALL state things in general, currently everything
// is grouped up as an app state. We should separate stuff like event state and gui state and etc.
// Short circuit if we're in basic table... we might have to handle the basic table arrow
// case here...
if let Some(bt) = &mut self.basic_table_widget_state {
if let (
Some((left_tlc_x, left_tlc_y)),
@ -2330,9 +2339,33 @@ impl App {
}
}
// Second short circuit --- are we in the dd dialog state? If so, only check yes/no and
// bail after.
if self.is_in_dialog() {
if let (
Some((yes_tlc_x, yes_tlc_y)),
Some((yes_brc_x, yes_brc_y)),
Some((no_tlc_x, no_tlc_y)),
Some((no_brc_x, no_brc_y)),
) = (
self.delete_dialog_state.yes_tlc,
self.delete_dialog_state.yes_brc,
self.delete_dialog_state.no_tlc,
self.delete_dialog_state.no_brc,
) {
if (x >= yes_tlc_x && y >= yes_tlc_y) && (x <= yes_brc_x && y <= yes_brc_y) {
self.delete_dialog_state.is_on_yes = true;
} else if (x >= no_tlc_x && y >= no_tlc_y) && (x <= no_brc_x && y <= no_brc_y) {
self.delete_dialog_state.is_on_yes = false;
}
}
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.
// See: https://docs.rs/kdtree/0.6.0/kdtree/
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)
@ -2465,6 +2498,23 @@ impl App {
}
}
}
BottomWidgetType::Battery => {
if let Some(battery_widget_state) = self
.battery_state
.get_mut_widget_state(self.current_widget.widget_id)
{
if let Some(tab_spacing) = &battery_widget_state.tab_click_locs {
for (itx, ((tlc_x, tlc_y), (brc_x, brc_y))) in
tab_spacing.iter().enumerate()
{
if (x >= *tlc_x && y >= *tlc_y) && (x <= *brc_x && y <= *brc_y) {
battery_widget_state.currently_selected_battery_index = itx;
break;
}
}
}
}
}
_ => {}
}
}

View file

@ -44,6 +44,10 @@ pub struct AppScrollWidgetState {
pub struct AppDeleteDialogState {
pub is_showing_dd: bool,
pub is_on_yes: bool, // Defaults to "No"
pub yes_tlc: Option<(u16, u16)>,
pub yes_brc: Option<(u16, u16)>,
pub no_tlc: Option<(u16, u16)>,
pub no_brc: Option<(u16, u16)>,
}
pub struct AppHelpDialogState {
@ -768,6 +772,7 @@ pub struct BasicTableWidgetState {
#[derive(Default)]
pub struct BatteryWidgetState {
pub currently_selected_battery_index: usize,
pub tab_click_locs: Option<Vec<((u16, u16), (u16, u16))>>,
}
pub struct BatteryState {

View file

@ -214,6 +214,17 @@ impl Painter {
widget.top_left_corner = None;
widget.bottom_right_corner = None;
}
// And reset dd_dialog...
app_state.delete_dialog_state.yes_tlc = None;
app_state.delete_dialog_state.yes_brc = None;
app_state.delete_dialog_state.no_tlc = None;
app_state.delete_dialog_state.no_brc = None;
// And battery dialog...
for battery_widget in app_state.battery_state.widget_states.values_mut() {
battery_widget.tab_click_locs = None;
}
}
terminal.autoresize()?;

View file

@ -1,6 +1,6 @@
use tui::{
backend::Backend,
layout::{Alignment, Rect},
layout::{Alignment, Constraint, Direction, Layout, Rect},
terminal::Frame,
widgets::{Block, Borders, Paragraph, Text},
};
@ -14,7 +14,7 @@ pub trait KillDialog {
fn get_dd_spans(&self, app_state: &App) -> Option<Vec<Text<'_>>>;
fn draw_dd_dialog<B: Backend>(
&self, f: &mut Frame<'_, B>, dd_text: Option<Vec<Text<'_>>>, app_state: &App,
&self, f: &mut Frame<'_, B>, dd_text: Option<Vec<Text<'_>>>, app_state: &mut App,
draw_loc: Rect,
) -> bool;
}
@ -34,35 +34,22 @@ impl KillDialog for Painter {
if app_state.is_grouped(app_state.current_widget.widget_id) {
if to_kill_processes.1.len() != 1 {
Text::raw(format!(
"Kill {} processes with the name \"{}\"?",
"Kill {} processes with the name \"{}\"? Press ENTER to confirm.",
to_kill_processes.1.len(),
to_kill_processes.0
))
} else {
Text::raw(format!(
"Kill 1 process with the name \"{}\"?",
"Kill 1 process with the name \"{}\"? Press ENTER to confirm.",
to_kill_processes.0
))
}
} else {
Text::raw(format!(
"Kill process \"{}\" with PID {}?",
"Kill process \"{}\" with PID {}? Press ENTER to confirm.",
to_kill_processes.0, first_pid
))
},
Text::raw("\n\n"),
if app_state.delete_dialog_state.is_on_yes {
Text::styled("Yes", self.colours.currently_selected_text_style)
} else {
Text::raw("Yes")
},
Text::raw(" "),
if app_state.delete_dialog_state.is_on_yes {
Text::raw("No")
} else {
Text::styled("No", self.colours.currently_selected_text_style)
},
Text::raw("\n"),
]);
}
}
@ -71,7 +58,7 @@ impl KillDialog for Painter {
}
fn draw_dd_dialog<B: Backend>(
&self, f: &mut Frame<'_, B>, dd_text: Option<Vec<Text<'_>>>, app_state: &App,
&self, f: &mut Frame<'_, B>, dd_text: Option<Vec<Text<'_>>>, app_state: &mut App,
draw_loc: Rect,
) -> bool {
if let Some(dd_text) = dd_text {
@ -131,6 +118,75 @@ impl KillDialog for Painter {
draw_loc,
);
// Now draw buttons if needed...
let split_draw_loc = Layout::default()
.direction(Direction::Vertical)
.constraints(
if app_state.dd_err.is_some() {
vec![Constraint::Percentage(100)]
} else {
vec![Constraint::Min(0), Constraint::Length(3)]
}
.as_ref(),
)
.split(draw_loc);
// This being true implies that dd_err is none.
if let Some(button_draw_loc) = split_draw_loc.get(1) {
let (yes_button, no_button) = if app_state.delete_dialog_state.is_on_yes {
(
Text::styled("Yes", self.colours.currently_selected_text_style),
Text::raw("No"),
)
} else {
(
Text::raw("Yes"),
Text::styled("No", self.colours.currently_selected_text_style),
)
};
let button_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage(35),
Constraint::Percentage(30),
Constraint::Percentage(35),
]
.as_ref(),
)
.split(*button_draw_loc);
f.render_widget(
Paragraph::new([yes_button].iter())
.block(Block::default())
.alignment(Alignment::Right),
button_layout[0],
);
f.render_widget(
Paragraph::new([no_button].iter())
.block(Block::default())
.alignment(Alignment::Left),
button_layout[2],
);
if app_state.should_get_widget_bounds() {
app_state.delete_dialog_state.yes_tlc =
Some((button_layout[0].x, button_layout[0].y));
app_state.delete_dialog_state.yes_brc = Some((
button_layout[0].x + button_layout[0].width,
button_layout[0].y + button_layout[0].height,
));
app_state.delete_dialog_state.no_tlc =
Some((button_layout[2].x, button_layout[2].y));
app_state.delete_dialog_state.no_brc = Some((
button_layout[2].x + button_layout[2].width,
button_layout[2].y + button_layout[2].height,
));
}
}
if app_state.dd_err.is_some() {
return app_state.delete_dialog_state.is_showing_dd;
} else {

View file

@ -23,6 +23,7 @@ impl BatteryDisplayWidget for Painter {
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
) {
let should_get_widget_bounds = app_state.should_get_widget_bounds();
if let Some(battery_widget_state) =
app_state.battery_state.widget_states.get_mut(&widget_id)
{
@ -115,6 +116,18 @@ impl BatteryDisplayWidget for Painter {
.direction(Direction::Horizontal)
.split(draw_loc)[0];
let tab_draw_loc = Layout::default()
.constraints(
[
Constraint::Length(1),
Constraint::Length(2),
Constraint::Min(0),
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(draw_loc)[1];
if let Some(battery_details) = app_state
.canvas_data
.battery_data
@ -188,6 +201,13 @@ impl BatteryDisplayWidget for Painter {
);
}
let battery_names = app_state
.canvas_data
.battery_data
.iter()
.map(|battery| &battery.battery_name)
.collect::<Vec<_>>();
// Has to be placed AFTER for tui 0.9, place BEFORE for 0.10.
f.render_widget(
// Tabs::new(
@ -198,26 +218,36 @@ impl BatteryDisplayWidget for Painter {
// .map(|battery| Spans::from(battery.battery_name.clone())))
// .collect::<Vec<_>>(),
// )
// FIXME: [MOUSE] Support mouse for the tabs?
Tabs::default()
.titles(
app_state
.canvas_data
.battery_data
.iter()
.map(|battery| &battery.battery_name)
.collect::<Vec<_>>()
.as_ref(),
)
.block(battery_block)
.titles(battery_names.as_ref())
.block(Block::default())
.divider(tui::symbols::line::VERTICAL)
.style(self.colours.text_style)
.highlight_style(self.colours.currently_selected_text_style)
.select(battery_widget_state.currently_selected_battery_index),
draw_loc,
tab_draw_loc,
);
if app_state.should_get_widget_bounds() {
if should_get_widget_bounds {
// Tab wizardry
if !battery_names.is_empty() {
let mut current_x = tab_draw_loc.x;
let current_y = tab_draw_loc.y;
let mut tab_click_locs: Vec<((u16, u16), (u16, u16))> = vec![];
for battery in battery_names {
// +1 because there's a space after the tab label.
let width =
unicode_width::UnicodeWidthStr::width(battery.as_str()) as u16 + 1;
tab_click_locs
.push(((current_x, current_y), (current_x + width, current_y)));
// +2 because we want to go one space past to get to the '|', then one MORE
// to start at the blank space before the tab label.
current_x = current_x + width + 2;
}
battery_widget_state.tab_click_locs = Some(tab_click_locs);
}
// 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));

View file

@ -37,6 +37,7 @@ lazy_static! {
tui::style::Style::default().fg(tui::style::Color::LightBlue);
}
// FIXME: [HELP] I wanna update this before release... it's missing mouse too.
// Help text
pub const HELP_CONTENTS_TEXT: [&str; 8] = [
"Press the corresponding numbers to jump to the section, or scroll:\n",