From fa00dec1460b8bc6b0c43a01c1e240536575ea5a Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Sun, 5 Sep 2021 18:49:33 -0400 Subject: [PATCH] refactor: move over battery widget --- src/app/widgets/bottom_widgets/battery.rs | 241 ++++++++++++++++++++- src/app/widgets/bottom_widgets/carousel.rs | 1 - src/app/widgets/bottom_widgets/cpu.rs | 11 +- src/app/widgets/bottom_widgets/temp.rs | 2 +- src/app/widgets/tui_widgets/pipe_gauge.rs | 11 +- src/canvas/drawing/battery_display.rs | 18 +- src/data_conversion.rs | 67 +++--- 7 files changed, 293 insertions(+), 58 deletions(-) diff --git a/src/app/widgets/bottom_widgets/battery.rs b/src/app/widgets/bottom_widgets/battery.rs index 2680a8d7..b535d80b 100644 --- a/src/app/widgets/bottom_widgets/battery.rs +++ b/src/app/widgets/bottom_widgets/battery.rs @@ -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, battery_data: Vec, width: LayoutRule, height: LayoutRule, block_border: Borders, + tab_bounds: Vec, } 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( + &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::>(); + 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); + } } diff --git a/src/app/widgets/bottom_widgets/carousel.rs b/src/app/widgets/bottom_widgets/carousel.rs index 0f91c212..64acd9a1 100644 --- a/src/app/widgets/bottom_widgets/carousel.rs +++ b/src/app/widgets/bottom_widgets/carousel.rs @@ -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 diff --git a/src/app/widgets/bottom_widgets/cpu.rs b/src/app/widgets/bottom_widgets/cpu.rs index 753142f5..8999f038 100644 --- a/src/app/widgets/bottom_widgets/cpu.rs +++ b/src/app/widgets/bottom_widgets/cpu.rs @@ -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, + legend: TextTable, 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 { diff --git a/src/app/widgets/bottom_widgets/temp.rs b/src/app/widgets/bottom_widgets/temp.rs index dee5c0d7..51df31db 100644 --- a/src/app/widgets/bottom_widgets/temp.rs +++ b/src/app/widgets/bottom_widgets/temp.rs @@ -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); diff --git a/src/app/widgets/tui_widgets/pipe_gauge.rs b/src/app/widgets/tui_widgets/pipe_gauge.rs index eca6a3ed..57abf812 100644 --- a/src/app/widgets/tui_widgets/pipe_gauge.rs +++ b/src/app/widgets/tui_widgets/pipe_gauge.rs @@ -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(" "); + // } } } diff --git a/src/canvas/drawing/battery_display.rs b/src/canvas/drawing/battery_display.rs index 2b9fe5f5..73cbd213 100644 --- a/src/canvas/drawing/battery_display.rs +++ b/src/canvas/drawing/battery_display.rs @@ -131,15 +131,15 @@ pub fn draw_battery_display( ]), 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), ]; diff --git a/src/data_conversion.rs b/src/data_conversion.rs index c4fbbc69..6a310f59 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -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, - pub duration_until_empty: Option, + pub charge_times: BatteryDuration, pub health: String, } @@ -1362,37 +1374,40 @@ pub fn convert_battery_harvest(current_data: &DataCollection) -> Vec