use std::cmp::min;
use tui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
text::{Line, Span},
widgets::{Cell, Paragraph, Row, Table, Tabs},
};
use unicode_width::UnicodeWidthStr;
use crate::{
app::App,
canvas::{Painter, drawing_utils::widget_block},
collection::batteries::BatteryState,
constants::*,
};
fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize {
min(
(num_bars_available as f64 * use_percentage / 100.0).round() as usize,
num_bars_available,
)
}
impl Painter {
pub fn draw_battery(
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) {
let should_get_widget_bounds = app_state.should_get_widget_bounds();
if let Some(battery_widget_state) = app_state
.states
.battery_state
.widget_states
.get_mut(&widget_id)
{
let is_selected = widget_id == app_state.current_widget.widget_id;
let border_style = if is_selected {
self.styles.highlighted_border_style
} else {
self.styles.border_style
};
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let block = {
let mut block = widget_block(
app_state.app_config_fields.use_basic_mode,
is_selected,
self.styles.border_type,
)
.border_style(border_style)
.title_top(Line::styled(" Battery ", self.styles.widget_title_style));
if app_state.is_expanded {
block = block.title_top(
Line::styled(" Esc to go back ", self.styles.widget_title_style)
.right_aligned(),
)
}
block
};
let battery_harvest = &(app_state.data_store.get_data().battery_harvest);
if battery_harvest.len() > 1 {
let battery_names = battery_harvest
.iter()
.enumerate()
.map(|(itx, _)| format!("Battery {itx}"))
.collect::<Vec<_>>();
let tab_draw_loc = Layout::default()
.constraints([
Constraint::Length(1),
Constraint::Length(2),
Constraint::Min(0),
])
.direction(Direction::Vertical)
.split(draw_loc)[1];
f.render_widget(
Tabs::new(
battery_names
.iter()
.map(|name| Line::from((*name).clone()))
.collect::<Vec<_>>(),
)
.divider(tui::symbols::line::VERTICAL)
.style(self.styles.text_style)
.highlight_style(self.styles.selected_text_style)
.select(battery_widget_state.currently_selected_battery_index),
tab_draw_loc,
);
if should_get_widget_bounds {
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 {
let width = UnicodeWidthStr::width(battery.as_str()) as u16;
tab_click_locs
.push(((current_x, current_y), (current_x + width, current_y)));
current_x += width + 4;
}
battery_widget_state.tab_click_locs = Some(tab_click_locs);
}
}
let is_basic = app_state.app_config_fields.use_basic_mode;
let [margined_draw_loc] = Layout::default()
.constraints([Constraint::Percentage(100)])
.horizontal_margin(u16::from(is_basic && !is_selected))
.direction(Direction::Horizontal)
.areas(draw_loc);
if let Some(battery_details) =
battery_harvest.get(battery_widget_state.currently_selected_battery_index)
{
let full_width = draw_loc.width.saturating_sub(2);
let bar_length = usize::from(full_width.saturating_sub(6));
let charge_percent = battery_details.charge_percent;
let num_bars = calculate_basic_use_bars(charge_percent, bar_length);
let bars = format!(
"[{}{}{:3.0}%]",
"|".repeat(num_bars),
" ".repeat(bar_length - num_bars),
charge_percent,
);
let mut battery_charge_rows = Vec::with_capacity(2);
battery_charge_rows.push(Row::new([
Cell::from("Charge").style(self.styles.text_style)
]));
battery_charge_rows.push(Row::new([Cell::from(bars).style(
if charge_percent < 10.0 {
self.styles.low_battery
} else if charge_percent < 50.0 {
self.styles.medium_battery
} else {
self.styles.high_battery
},
)]));
let mut battery_rows = Vec::with_capacity(3);
let watt_consumption = battery_details.watt_consumption();
let health = battery_details.health();
battery_rows.push(Row::new([""]).bottom_margin(table_gap + 1));
battery_rows
.push(Row::new(["Rate", &watt_consumption]).style(self.styles.text_style));
battery_rows.push(
Row::new(["State", battery_details.state.as_str()])
.style(self.styles.text_style),
);
let mut time: String; {
let style = self.styles.text_style;
let time_width = (full_width / 2) as usize;
match &battery_details.state {
BatteryState::Charging {
time_to_full: Some(secs),
} => {
time = long_time(*secs);
if time_width >= time.len() {
battery_rows.push(Row::new(["Time to full", &time]).style(style));
} else {
time = short_time(*secs);
battery_rows.push(Row::new(["To full", &time]).style(style));
}
}
BatteryState::Discharging {
time_to_empty: Some(secs),
} => {
time = long_time(*secs);
if time_width >= time.len() {
battery_rows.push(Row::new(["Time to empty", &time]).style(style));
} else {
time = short_time(*secs);
battery_rows.push(Row::new(["To empty", &time]).style(style));
}
}
_ => {}
}
}
battery_rows.push(Row::new(["Health", &health]).style(self.styles.text_style));
let header = if battery_harvest.len() > 1 {
Row::new([""]).bottom_margin(table_gap)
} else {
Row::default()
};
f.render_widget(
Table::new(battery_charge_rows, [Constraint::Percentage(100)])
.block(block.clone())
.header(header.clone()),
margined_draw_loc,
);
f.render_widget(
Table::new(
battery_rows,
[Constraint::Percentage(50), Constraint::Percentage(50)],
)
.block(block)
.header(header),
margined_draw_loc,
);
} else {
let mut contents = vec![Line::default(); table_gap.into()];
contents.push(Line::from(Span::styled(
"No data found for this battery",
self.styles.text_style,
)));
f.render_widget(Paragraph::new(contents).block(block), margined_draw_loc);
}
if should_get_widget_bounds {
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,
));
}
}
}
}
}
fn get_hms(secs: u32) -> (u32, u32, u32) {
let hours = secs / (60 * 60);
let minutes = (secs / 60) - hours * 60;
let seconds = secs - minutes * 60 - hours * 60 * 60;
(hours, minutes, seconds)
}
fn long_time(secs: u32) -> String {
let (hours, minutes, seconds) = get_hms(secs);
if hours > 0 {
let h = if hours == 1 { "hour" } else { "hours" };
let m = if minutes == 1 { "minute" } else { "minutes" };
let s = if seconds == 1 { "second" } else { "seconds" };
format!("{hours} {h}, {minutes} {m}, {seconds} {s}")
} else {
let m = if minutes == 1 { "minute" } else { "minutes" };
let s = if seconds == 1 { "second" } else { "seconds" };
format!("{minutes} {m}, {seconds} {s}")
}
}
fn short_time(secs: u32) -> String {
let (hours, minutes, seconds) = get_hms(secs);
if hours > 0 {
format!("{hours}h {minutes}m {seconds}s")
} else {
format!("{minutes}m {seconds}s")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_hms() {
assert_eq!(get_hms(10), (0, 0, 10));
assert_eq!(get_hms(60), (0, 1, 0));
assert_eq!(get_hms(61), (0, 1, 1));
assert_eq!(get_hms(3600), (1, 0, 0));
assert_eq!(get_hms(3601), (1, 0, 1));
assert_eq!(get_hms(3661), (1, 1, 1));
}
#[test]
fn test_long_time() {
assert_eq!(long_time(1), "0 minutes, 1 second".to_string());
assert_eq!(long_time(10), "0 minutes, 10 seconds".to_string());
assert_eq!(long_time(60), "1 minute, 0 seconds".to_string());
assert_eq!(long_time(61), "1 minute, 1 second".to_string());
assert_eq!(long_time(3600), "1 hour, 0 minutes, 0 seconds".to_string());
assert_eq!(long_time(3601), "1 hour, 0 minutes, 1 second".to_string());
assert_eq!(long_time(3661), "1 hour, 1 minute, 1 second".to_string());
}
#[test]
fn test_short_time() {
assert_eq!(short_time(1), "0m 1s".to_string());
assert_eq!(short_time(10), "0m 10s".to_string());
assert_eq!(short_time(60), "1m 0s".to_string());
assert_eq!(short_time(61), "1m 1s".to_string());
assert_eq!(short_time(3600), "1h 0m 0s".to_string());
assert_eq!(short_time(3601), "1h 0m 1s".to_string());
assert_eq!(short_time(3661), "1h 1m 1s".to_string());
}
#[test]
fn test_calculate_basic_use_bars() {
assert_eq!(calculate_basic_use_bars(0.0, 15), 0);
assert_eq!(calculate_basic_use_bars(1.0, 15), 0);
assert_eq!(calculate_basic_use_bars(5.0, 15), 1);
assert_eq!(calculate_basic_use_bars(10.0, 15), 2);
assert_eq!(calculate_basic_use_bars(40.0, 15), 6);
assert_eq!(calculate_basic_use_bars(45.0, 15), 7);
assert_eq!(calculate_basic_use_bars(50.0, 15), 8);
assert_eq!(calculate_basic_use_bars(100.0, 15), 15);
assert_eq!(calculate_basic_use_bars(150.0, 15), 15);
}
}