use presentar_core::{
Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
};
use std::any::Any;
use std::time::Duration;
#[derive(Debug, Clone, Default)]
pub struct BatteryInfo {
pub capacity: u8,
pub status: String,
pub time_remaining_mins: Option<u32>,
pub present: bool,
}
impl BatteryInfo {
pub fn new(capacity: u8, status: impl Into<String>, present: bool) -> Self {
Self {
capacity,
status: status.into(),
time_remaining_mins: None,
present,
}
}
pub fn with_time(mut self, mins: u32) -> Self {
self.time_remaining_mins = Some(mins);
self
}
}
#[derive(Debug, Clone, Default)]
pub struct BatteryPanel {
pub info: Option<BatteryInfo>,
bounds: Rect,
}
impl BatteryPanel {
pub fn new() -> Self {
Self::default()
}
pub fn with_info(mut self, info: Option<BatteryInfo>) -> Self {
self.info = info;
self
}
}
impl Widget for BatteryPanel {
fn type_id(&self) -> TypeId {
TypeId::of::<Self>()
}
fn measure(&self, constraints: Constraints) -> Size {
let height = 2.0;
let width = constraints.max_width.min(40.0);
constraints.constrain(Size::new(width, height))
}
fn layout(&mut self, bounds: Rect) -> LayoutResult {
self.bounds = bounds;
LayoutResult {
size: Size::new(bounds.width, bounds.height),
}
}
fn paint(&self, canvas: &mut dyn Canvas) {
if self.bounds.width < 1.0 || self.bounds.height < 1.0 {
return;
}
if let Some(bat) = &self.info {
if !bat.present {
canvas.draw_text(
"No battery detected",
Point::new(self.bounds.x, self.bounds.y),
&TextStyle {
color: Color::new(0.5, 0.5, 0.5, 1.0),
..Default::default()
},
);
return;
}
let bar_width = (self.bounds.width as usize).min(30);
let filled = ((bat.capacity as f32 / 100.0) * bar_width as f32) as usize;
let bar = "â–ˆ".repeat(filled) + &"â–‘".repeat(bar_width.saturating_sub(filled));
let color = if bat.capacity < 20 {
Color::new(1.0, 0.3, 0.3, 1.0) } else if bat.capacity < 50 {
Color::new(1.0, 0.8, 0.2, 1.0) } else {
Color::new(0.3, 0.9, 0.3, 1.0) };
canvas.draw_text(
&bar,
Point::new(self.bounds.x, self.bounds.y),
&TextStyle {
color,
..Default::default()
},
);
if self.bounds.height >= 2.0 {
let status_icon = match bat.status.as_str() {
"Charging" => "âš¡ Charging",
"Discharging" => "🔋 Discharging",
"Full" => "✓ Full",
"Not charging" => "— Idle",
_ => "? Unknown",
};
let time_str = bat.time_remaining_mins.map(|m| {
if m >= 60 {
format!(" ({}h{}m)", m / 60, m % 60)
} else {
format!(" ({m}m)")
}
}).unwrap_or_default();
let text = format!("{status_icon}{time_str}");
canvas.draw_text(
&text,
Point::new(self.bounds.x, self.bounds.y + 1.0),
&TextStyle {
color: Color::new(0.7, 0.7, 0.7, 1.0),
..Default::default()
},
);
}
} else {
canvas.draw_text(
"No battery detected",
Point::new(self.bounds.x, self.bounds.y),
&TextStyle {
color: Color::new(0.5, 0.5, 0.5, 1.0),
..Default::default()
},
);
}
}
fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
None
}
fn children(&self) -> &[Box<dyn Widget>] {
&[]
}
fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
&mut []
}
}
impl Brick for BatteryPanel {
fn brick_name(&self) -> &'static str {
"battery_panel"
}
fn assertions(&self) -> &[BrickAssertion] {
static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(8)];
ASSERTIONS
}
fn budget(&self) -> BrickBudget {
BrickBudget::uniform(8)
}
fn verify(&self) -> BrickVerification {
BrickVerification {
passed: vec![BrickAssertion::max_latency_ms(8)],
failed: vec![],
verification_time: Duration::from_micros(5),
}
}
fn to_html(&self) -> String {
String::new()
}
fn to_css(&self) -> String {
String::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_battery_info_new() {
let info = BatteryInfo::new(80, "Charging", true);
assert_eq!(info.capacity, 80);
assert_eq!(info.status, "Charging");
assert!(info.present);
}
#[test]
fn test_battery_panel_new() {
let panel = BatteryPanel::new();
assert!(panel.info.is_none());
}
#[test]
fn test_battery_panel_with_info() {
let info = BatteryInfo::new(50, "Discharging", true);
let panel = BatteryPanel::new().with_info(Some(info));
assert!(panel.info.is_some());
}
#[test]
fn test_battery_panel_paint_no_info() {
use crate::{CellBuffer, DirectTerminalCanvas};
let panel = BatteryPanel::new();
let mut buffer = CellBuffer::new(40, 5);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
panel.paint(&mut canvas);
}
}