refactor: move over battery widget

This commit is contained in:
ClementTsang 2021-09-05 18:49:33 -04:00 committed by ClementTsang
parent eddc9a16c7
commit fa00dec146
7 changed files with 293 additions and 58 deletions

View File

@ -1,9 +1,24 @@
use std::collections::HashMap;
use std::{
cmp::{max, min},
collections::HashMap,
};
use tui::{layout::Rect, widgets::Borders};
use crossterm::event::{KeyCode, KeyEvent, MouseEvent};
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
text::{Span, Spans},
widgets::{Block, Borders, Paragraph, Tabs},
Frame,
};
use crate::{
app::{data_farmer::DataCollection, Component, Widget},
app::{
data_farmer::DataCollection, does_bound_intersect_coordinate, event::WidgetEventResult,
widgets::tui_widgets::PipeGauge, Component, Widget,
},
canvas::Painter,
constants::TABLE_GAP_HEIGHT_LIMIT,
data_conversion::{convert_battery_harvest, ConvertedBatteryData},
options::layout_options::LayoutRule,
};
@ -33,28 +48,27 @@ impl BatteryState {
}
}
// TODO: Implement battery widget.
/// A table displaying battery information on a per-battery basis.
pub struct BatteryTable {
bounds: Rect,
selected_index: usize,
batteries: Vec<String>,
battery_data: Vec<ConvertedBatteryData>,
width: LayoutRule,
height: LayoutRule,
block_border: Borders,
tab_bounds: Vec<Rect>,
}
impl Default for BatteryTable {
fn default() -> Self {
Self {
batteries: vec![],
bounds: Default::default(),
selected_index: 0,
battery_data: Default::default(),
width: LayoutRule::default(),
height: LayoutRule::default(),
block_border: Borders::ALL,
tab_bounds: Default::default(),
}
}
}
@ -77,9 +91,16 @@ impl BatteryTable {
self.selected_index
}
/// Returns a reference to the battery names.
pub fn batteries(&self) -> &[String] {
&self.batteries
fn increment_index(&mut self) {
if self.selected_index + 1 < self.battery_data.len() {
self.selected_index += 1;
}
}
fn decrement_index(&mut self) {
if self.selected_index > 0 {
self.selected_index -= 1;
}
}
/// Sets the block border style.
@ -100,6 +121,46 @@ impl Component for BatteryTable {
fn set_bounds(&mut self, new_bounds: tui::layout::Rect) {
self.bounds = new_bounds;
}
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
if event.modifiers.is_empty() {
match event.code {
KeyCode::Left => {
let current_index = self.selected_index;
self.decrement_index();
if current_index == self.selected_index {
WidgetEventResult::NoRedraw
} else {
WidgetEventResult::Redraw
}
}
KeyCode::Right => {
let current_index = self.selected_index;
self.increment_index();
if current_index == self.selected_index {
WidgetEventResult::NoRedraw
} else {
WidgetEventResult::Redraw
}
}
_ => WidgetEventResult::NoRedraw,
}
} else {
WidgetEventResult::NoRedraw
}
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
for (itx, bound) in self.tab_bounds.iter().enumerate() {
if does_bound_intersect_coordinate(event.column, event.row, *bound) {
if itx < self.battery_data.len() {
self.selected_index = itx;
return WidgetEventResult::Redraw;
}
}
}
WidgetEventResult::NoRedraw
}
}
impl Widget for BatteryTable {
@ -109,6 +170,9 @@ impl Widget for BatteryTable {
fn update_data(&mut self, data_collection: &DataCollection) {
self.battery_data = convert_battery_harvest(data_collection);
if self.battery_data.len() <= self.selected_index {
self.selected_index = self.battery_data.len().saturating_sub(1);
}
}
fn width(&self) -> LayoutRule {
@ -118,4 +182,163 @@ impl Widget for BatteryTable {
fn height(&self) -> LayoutRule {
self.height
}
fn draw<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
) {
let block = Block::default()
.border_style(if selected {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
})
.borders(self.block_border.clone());
let inner_area = block.inner(area);
const CONSTRAINTS: [Constraint; 2] = [Constraint::Length(1), Constraint::Min(0)];
let split_area = Layout::default()
.direction(Direction::Vertical)
.constraints(CONSTRAINTS)
.split(inner_area);
let tab_area = Rect::new(
split_area[0].x.saturating_sub(1),
split_area[0].y,
split_area[0].width,
split_area[0].height,
);
let data_area = if inner_area.height >= TABLE_GAP_HEIGHT_LIMIT && split_area[1].height > 0 {
Rect::new(
split_area[1].x,
split_area[1].y + 1,
split_area[1].width,
split_area[1].height - 1,
)
} else {
split_area[1]
};
if self.battery_data.is_empty() {
f.render_widget(
Paragraph::new("No batteries found").style(painter.colours.text_style),
tab_area,
);
} else {
let battery_tab_names = self
.battery_data
.iter()
.map(|d| Spans::from(d.battery_name.as_str()))
.collect::<Vec<_>>();
let mut start_x_offset = tab_area.x + 1;
self.tab_bounds = battery_tab_names
.iter()
.map(|name| {
let length = name.width() as u16;
let start = start_x_offset;
start_x_offset += length;
start_x_offset += 3;
Rect::new(start, tab_area.y, length, 1)
})
.collect();
f.render_widget(
Tabs::new(battery_tab_names)
.divider(tui::symbols::line::VERTICAL)
.style(painter.colours.text_style)
.highlight_style(painter.colours.currently_selected_text_style)
.select(self.selected_index),
tab_area,
);
if let Some(battery_details) = self.battery_data.get(self.selected_index) {
let labels = vec![
Spans::from(Span::styled("Charge %", painter.colours.text_style)),
Spans::from(Span::styled("Consumption", painter.colours.text_style)),
match &battery_details.charge_times {
crate::data_conversion::BatteryDuration::Charging { .. } => {
Spans::from(Span::styled("Time to full", painter.colours.text_style))
}
crate::data_conversion::BatteryDuration::Discharging { .. } => {
Spans::from(Span::styled("Time to empty", painter.colours.text_style))
}
crate::data_conversion::BatteryDuration::Neither => Spans::from(
Span::styled("Time to full/empty", painter.colours.text_style),
),
},
Spans::from(Span::styled("Health %", painter.colours.text_style)),
];
let data_constraints = if let Some(len) = labels.iter().map(|s| s.width()).max() {
[
Constraint::Length(min(
max(len as u16 + 2, data_area.width / 2),
data_area.width,
)),
Constraint::Min(0),
]
} else {
[Constraint::Ratio(1, 2); 2]
};
const VALUE_CONSTRAINTS: [Constraint; 2] =
[Constraint::Length(1), Constraint::Min(0)];
let details_split_area = Layout::default()
.direction(Direction::Horizontal)
.constraints(data_constraints)
.split(data_area);
let per_detail_area = Layout::default()
.direction(Direction::Vertical)
.constraints(VALUE_CONSTRAINTS)
.split(details_split_area[1]);
f.render_widget(Paragraph::new(labels), details_split_area[0]);
f.render_widget(
PipeGauge::default()
.end_label(format!(
"{:3.0}%",
battery_details.charge_percentage.round()
))
.ratio(battery_details.charge_percentage / 100.0)
.style(if battery_details.charge_percentage < 10.0 {
painter.colours.low_battery_colour
} else if battery_details.charge_percentage < 50.0 {
painter.colours.medium_battery_colour
} else {
painter.colours.high_battery_colour
}),
per_detail_area[0],
);
f.render_widget(
Paragraph::new(vec![
Spans::from(Span::styled(
battery_details.watt_consumption.clone(),
painter.colours.text_style,
)),
match &battery_details.charge_times {
crate::data_conversion::BatteryDuration::Charging { short, long }
| crate::data_conversion::BatteryDuration::Discharging {
short,
long,
} => Spans::from(Span::styled(
if (per_detail_area[1].width as usize) >= long.len() {
long
} else {
short
},
painter.colours.text_style,
)),
crate::data_conversion::BatteryDuration::Neither => {
Spans::from(Span::styled("N/A", painter.colours.text_style))
}
},
Spans::from(Span::styled(
battery_details.health.clone(),
painter.colours.text_style,
)),
]),
per_detail_area[1],
);
}
}
// Note the block must be rendered last, to cover up the tabs!
f.render_widget(block, area);
}
}

View File

@ -200,7 +200,6 @@ impl Widget for Carousel {
fn selectable_type(&self) -> SelectableType {
if let Some(node) = self.get_currently_selected() {
debug!("node: {:?}", node);
SelectableType::Redirect(node)
} else {
SelectableType::Unselectable

View File

@ -10,7 +10,7 @@ use tui::{
use crate::{
app::{
event::WidgetEventResult, sort_text_table::SimpleSortableColumn, time_graph::TimeGraphData,
event::WidgetEventResult, text_table::SimpleColumn, time_graph::TimeGraphData,
AppConfigFields, AppScrollWidgetState, CanvasTableWidthState, Component, DataCollection,
TextTable, TimeGraph, Widget,
},
@ -79,7 +79,7 @@ pub enum CpuGraphLegendPosition {
/// A widget designed to show CPU usage via a graph, along with a side legend in a table.
pub struct CpuGraph {
graph: TimeGraph,
legend: TextTable<SimpleSortableColumn>,
legend: TextTable<SimpleColumn>,
legend_position: CpuGraphLegendPosition,
showing_avg: bool,
@ -98,9 +98,10 @@ impl CpuGraph {
pub fn from_config(app_config_fields: &AppConfigFields) -> Self {
let graph = TimeGraph::from_config(app_config_fields);
let legend = TextTable::new(vec![
SimpleSortableColumn::new_flex("CPU".into(), None, false, 0.5),
SimpleSortableColumn::new_flex("Use%".into(), None, false, 0.5),
]);
SimpleColumn::new_flex("CPU".into(), 0.5),
SimpleColumn::new_hard("Use%".into(), None),
])
.default_ltr(false);
let legend_position = if app_config_fields.left_legend {
CpuGraphLegendPosition::Left
} else {

View File

@ -144,7 +144,7 @@ impl Widget for TempTable {
} else {
painter.colours.border_style
})
.borders(self.block_border.clone()); // TODO: Also do the scrolling indicator!
.borders(self.block_border.clone());
self.table
.draw_tui_table(painter, f, &self.display_data, block, area, selected);

View File

@ -86,10 +86,7 @@ impl<'a> Widget for PipeGauge<'a> {
return;
}
let ratio = self.ratio;
let start_label = self
.start_label
.unwrap_or_else(move || Spans::from(format!("{:.0}%", ratio * 100.0)));
let start_label = self.start_label.unwrap_or_else(move || Spans::from(""));
let (col, row) = buf.set_spans(
gauge_area.left(),
@ -131,8 +128,8 @@ impl<'a> Widget for PipeGauge<'a> {
sub_modifier: self.gauge_style.sub_modifier,
});
}
for col in end..gauge_end {
buf.get_mut(col, row).set_symbol(" ");
}
// for col in end..gauge_end {
// buf.get_mut(col, row).set_symbol(" ");
// }
}
}

View File

@ -131,15 +131,15 @@ pub fn draw_battery_display<B: Backend>(
]),
Row::new(vec!["Consumption", &battery_details.watt_consumption])
.style(painter.colours.text_style),
if let Some(duration_until_full) = &battery_details.duration_until_full {
Row::new(vec!["Time to full", duration_until_full])
.style(painter.colours.text_style)
} else if let Some(duration_until_empty) = &battery_details.duration_until_empty {
Row::new(vec!["Time to empty", duration_until_empty])
.style(painter.colours.text_style)
} else {
Row::new(vec!["Time to full/empty", "N/A"]).style(painter.colours.text_style)
},
// if let Some(duration_until_full) = &battery_details.duration_until_full {
// Row::new(vec!["Time to full", duration_until_full])
// .style(painter.colours.text_style)
// } else if let Some(duration_until_empty) = &battery_details.duration_until_empty {
// Row::new(vec!["Time to empty", duration_until_empty])
// .style(painter.colours.text_style)
// } else {
// Row::new(vec!["Time to full/empty", "N/A"]).style(painter.colours.text_style)
// },
Row::new(vec!["Health %", &battery_details.health])
.style(painter.colours.text_style),
];

View File

@ -17,13 +17,25 @@ use std::collections::{HashMap, VecDeque};
/// Point is of time, data
type Point = (f64, f64);
#[derive(Debug)]
pub enum BatteryDuration {
Charging { short: String, long: String },
Discharging { short: String, long: String },
Neither,
}
impl Default for BatteryDuration {
fn default() -> Self {
Self::Neither
}
}
#[derive(Default, Debug)]
pub struct ConvertedBatteryData {
pub battery_name: String,
pub charge_percentage: f64,
pub watt_consumption: String,
pub duration_until_full: Option<String>,
pub duration_until_empty: Option<String>,
pub charge_times: BatteryDuration,
pub health: String,
}
@ -1362,37 +1374,40 @@ pub fn convert_battery_harvest(current_data: &DataCollection) -> Vec<ConvertedBa
battery_name: format!("Battery {}", itx),
charge_percentage: battery_harvest.charge_percent,
watt_consumption: format!("{:.2}W", battery_harvest.power_consumption_rate_watts),
duration_until_empty: if let Some(secs_till_empty) = battery_harvest.secs_until_empty {
charge_times: if let Some(secs_till_empty) = battery_harvest.secs_until_empty {
let time = chrono::Duration::seconds(secs_till_empty);
let num_minutes = time.num_minutes() - time.num_hours() * 60;
let num_seconds = time.num_seconds() - time.num_minutes() * 60;
Some(format!(
"{} hour{}, {} minute{}, {} second{}",
time.num_hours(),
if time.num_hours() == 1 { "" } else { "s" },
num_minutes,
if num_minutes == 1 { "" } else { "s" },
num_seconds,
if num_seconds == 1 { "" } else { "s" },
))
} else {
None
},
duration_until_full: if let Some(secs_till_full) = battery_harvest.secs_until_full {
BatteryDuration::Discharging {
long: format!(
"{} hour{}, {} minute{}, {} second{}",
time.num_hours(),
if time.num_hours() == 1 { "" } else { "s" },
num_minutes,
if num_minutes == 1 { "" } else { "s" },
num_seconds,
if num_seconds == 1 { "" } else { "s" },
),
short: format!("{}:{:02}:{:02}", time.num_hours(), num_minutes, num_seconds),
}
} else if let Some(secs_till_full) = battery_harvest.secs_until_full {
let time = chrono::Duration::seconds(secs_till_full); // FIXME [DEP]: Can I get rid of chrono?
let num_minutes = time.num_minutes() - time.num_hours() * 60;
let num_seconds = time.num_seconds() - time.num_minutes() * 60;
Some(format!(
"{} hour{}, {} minute{}, {} second{}",
time.num_hours(),
if time.num_hours() == 1 { "" } else { "s" },
num_minutes,
if num_minutes == 1 { "" } else { "s" },
num_seconds,
if num_seconds == 1 { "" } else { "s" },
))
BatteryDuration::Charging {
long: format!(
"{} hour{}, {} minute{}, {} second{}",
time.num_hours(),
if time.num_hours() == 1 { "" } else { "s" },
num_minutes,
if num_minutes == 1 { "" } else { "s" },
num_seconds,
if num_seconds == 1 { "" } else { "s" },
),
short: format!("{}:{:02}:{:02}", time.num_hours(), num_minutes, num_seconds),
}
} else {
None
BatteryDuration::Neither
},
health: format!("{:.2}%", battery_harvest.health_percent),
})