use core::fmt::Debug;
use embedded_graphics_core::{
draw_target::DrawTarget,
pixelcolor::{Rgb565, WebColors},
};
use heapless::String;
use crate::{
geometry::Rect,
render::{Compositor, RenderCtx},
round::UnobstructedArea,
style::Border,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StatusBarError {
RenderError,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum StatusBarMode {
#[default]
ClockAndIcons,
ClockOnly,
IconsOnly,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum BatteryState {
#[default]
Discharging,
Charging,
Full,
}
#[derive(Clone, Debug)]
pub struct StatusBarWidget {
pub mode: StatusBarMode,
pub time_text: String<12>,
pub battery_percent: u8,
pub battery_state: BatteryState,
pub bluetooth_connected: bool,
pub dnd_active: bool,
pub background_color: Rgb565,
pub foreground_color: Rgb565,
pub accent_color: Rgb565,
pub separator_color: Option<Rgb565>,
pub height: u16,
pub is_visible: bool,
}
impl Default for StatusBarWidget {
fn default() -> Self {
let mut time_text = String::new();
let _ = time_text.push_str("12:00");
Self {
mode: StatusBarMode::ClockAndIcons,
time_text,
battery_percent: 85,
battery_state: BatteryState::Discharging,
bluetooth_connected: true,
dnd_active: false,
background_color: Rgb565::new(2, 4, 8),
foreground_color: Rgb565::CSS_WHITE,
accent_color: Rgb565::CSS_CYAN,
separator_color: Some(Rgb565::new(6, 12, 18)),
height: 20,
is_visible: true,
}
}
}
impl StatusBarWidget {
pub fn new(time_text: &str) -> Self {
let mut widget = Self::default();
widget.set_time(time_text);
widget
}
pub fn set_time(&mut self, time_str: &str) {
self.time_text.clear();
let _ = self.time_text.push_str(time_str);
}
pub fn set_battery(&mut self, percent: u8, state: BatteryState) {
self.battery_percent = percent.min(100);
self.battery_state = state;
}
pub fn apply_to_unobstructed_area(&self, area: &mut UnobstructedArea) {
if self.is_visible && self.height > 0 {
area.set_insets(self.height, 0, 0, 0);
}
}
pub fn render<D, C>(
&self,
ctx: &mut RenderCtx<'_, D, C>,
bounds: Rect,
) -> Result<(), StatusBarError>
where
D: DrawTarget<Color = Rgb565>,
C: Compositor<D>,
{
if !self.is_visible || bounds.is_empty() {
return Ok(());
}
ctx.fill_rect(bounds, self.background_color)
.map_err(|_| StatusBarError::RenderError)?;
if let Some(sep_color) = self.separator_color {
ctx.fill_rect(
Rect::new(bounds.x, bounds.bottom() - 1, bounds.w, 1),
sep_color,
)
.map_err(|_| StatusBarError::RenderError)?;
}
let center_y = bounds.y + (bounds.h as i32 / 2);
if self.mode == StatusBarMode::ClockAndIcons || self.mode == StatusBarMode::ClockOnly {
let char_width = 4;
let text_w = self.time_text.len() as i32 * char_width;
let time_x = bounds.x + (bounds.w as i32 - text_w) / 2;
let time_y = center_y - 3;
ctx.draw_text(time_x, time_y, &self.time_text, self.foreground_color)
.map_err(|_| StatusBarError::RenderError)?;
}
if self.mode == StatusBarMode::ClockAndIcons || self.mode == StatusBarMode::IconsOnly {
let mut left_cursor = bounds.x + 6;
if self.bluetooth_connected {
let bt_color = self.accent_color;
let bx = left_cursor;
let by = center_y - 4;
let _ = ctx.stroke_rect(Rect::new(bx, by, 6, 8), Border::one(bt_color));
let _ = ctx.draw_text(bx + 1, by + 1, "B", bt_color);
left_cursor += 10;
}
if self.dnd_active {
let mx = left_cursor;
let my = center_y - 3;
let _ = ctx.fill_circle(mx + 3, my + 3, 3, Rgb565::CSS_GOLD);
let _ = ctx.fill_circle(mx + 4, my + 2, 2, self.background_color);
}
let right_cursor = bounds.right() - 6;
let batt_w = 16u32;
let batt_h = 8u32;
let batt_x = right_cursor - (batt_w as i32);
let batt_y = center_y - 4;
let shell_rect = Rect::new(batt_x, batt_y, batt_w, batt_h);
ctx.stroke_rect(shell_rect, Border::one(self.foreground_color))
.map_err(|_| StatusBarError::RenderError)?;
ctx.fill_rect(
Rect::new(batt_x + batt_w as i32, batt_y + 2, 2, 4),
self.foreground_color,
)
.map_err(|_| StatusBarError::RenderError)?;
let max_fill = batt_w.saturating_sub(4);
let fill_w = ((self.battery_percent as u32 * max_fill) / 100).max(1);
let fill_color = if self.battery_percent <= 15 {
Rgb565::CSS_RED
} else if self.battery_state == BatteryState::Charging {
Rgb565::CSS_GREEN
} else {
self.foreground_color
};
ctx.fill_rect(
Rect::new(batt_x + 2, batt_y + 2, fill_w, batt_h - 4),
fill_color,
)
.map_err(|_| StatusBarError::RenderError)?;
if self.battery_state == BatteryState::Charging {
ctx.draw_text(batt_x - 8, batt_y + 1, "~", Rgb565::CSS_YELLOW)
.map_err(|_| StatusBarError::RenderError)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::framebuffer::Framebuffer;
#[test]
fn test_status_bar_render_and_insets() {
let screen = Rect::new(0, 0, 240, 240);
let mut fb = Framebuffer::<{ 240 * 240 }>::new(240, 240);
let mut ctx = RenderCtx::new(&mut fb, screen);
let mut status_bar = StatusBarWidget::new("09:41");
status_bar.set_battery(72, BatteryState::Charging);
status_bar.dnd_active = true;
let bar_bounds = Rect::new(0, 0, 240, 20);
let res = status_bar.render(&mut ctx, bar_bounds);
assert!(res.is_ok());
let mut area = UnobstructedArea::new(screen);
status_bar.apply_to_unobstructed_area(&mut area);
assert_eq!(area.visible_rect(), Rect::new(0, 20, 240, 220));
}
}