1use core::fmt::Debug;
11use embedded_graphics_core::{
12 draw_target::DrawTarget,
13 pixelcolor::{Rgb565, WebColors},
14};
15use heapless::String;
16
17use crate::{
18 geometry::Rect,
19 render::{Compositor, RenderCtx},
20 round::UnobstructedArea,
21 style::Border,
22};
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum StatusBarError {
27 RenderError,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
33pub enum StatusBarMode {
34 #[default]
36 ClockAndIcons,
37 ClockOnly,
39 IconsOnly,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
45pub enum BatteryState {
46 #[default]
48 Discharging,
49 Charging,
51 Full,
53}
54
55#[derive(Clone, Debug)]
57pub struct StatusBarWidget {
58 pub mode: StatusBarMode,
60 pub time_text: String<12>,
62 pub battery_percent: u8,
64 pub battery_state: BatteryState,
66 pub bluetooth_connected: bool,
68 pub dnd_active: bool,
70 pub background_color: Rgb565,
72 pub foreground_color: Rgb565,
74 pub accent_color: Rgb565,
76 pub separator_color: Option<Rgb565>,
78 pub height: u16,
80 pub is_visible: bool,
82}
83
84impl Default for StatusBarWidget {
85 fn default() -> Self {
86 let mut time_text = String::new();
87 let _ = time_text.push_str("12:00");
88
89 Self {
90 mode: StatusBarMode::ClockAndIcons,
91 time_text,
92 battery_percent: 85,
93 battery_state: BatteryState::Discharging,
94 bluetooth_connected: true,
95 dnd_active: false,
96 background_color: Rgb565::new(2, 4, 8),
97 foreground_color: Rgb565::CSS_WHITE,
98 accent_color: Rgb565::CSS_CYAN,
99 separator_color: Some(Rgb565::new(6, 12, 18)),
100 height: 20,
101 is_visible: true,
102 }
103 }
104}
105
106impl StatusBarWidget {
107 pub fn new(time_text: &str) -> Self {
109 let mut widget = Self::default();
110 widget.set_time(time_text);
111 widget
112 }
113
114 pub fn set_time(&mut self, time_str: &str) {
116 self.time_text.clear();
117 let _ = self.time_text.push_str(time_str);
118 }
119
120 pub fn set_battery(&mut self, percent: u8, state: BatteryState) {
122 self.battery_percent = percent.min(100);
123 self.battery_state = state;
124 }
125
126 pub fn apply_to_unobstructed_area(&self, area: &mut UnobstructedArea) {
128 if self.is_visible && self.height > 0 {
129 area.set_insets(self.height, 0, 0, 0);
130 }
131 }
132
133 pub fn render<D, C>(
135 &self,
136 ctx: &mut RenderCtx<'_, D, C>,
137 bounds: Rect,
138 ) -> Result<(), StatusBarError>
139 where
140 D: DrawTarget<Color = Rgb565>,
141 C: Compositor<D>,
142 {
143 if !self.is_visible || bounds.is_empty() {
144 return Ok(());
145 }
146
147 ctx.fill_rect(bounds, self.background_color)
149 .map_err(|_| StatusBarError::RenderError)?;
150
151 if let Some(sep_color) = self.separator_color {
153 ctx.fill_rect(
154 Rect::new(bounds.x, bounds.bottom() - 1, bounds.w, 1),
155 sep_color,
156 )
157 .map_err(|_| StatusBarError::RenderError)?;
158 }
159
160 let center_y = bounds.y + (bounds.h as i32 / 2);
161
162 if self.mode == StatusBarMode::ClockAndIcons || self.mode == StatusBarMode::ClockOnly {
164 let char_width = 4;
165 let text_w = self.time_text.len() as i32 * char_width;
166 let time_x = bounds.x + (bounds.w as i32 - text_w) / 2;
167 let time_y = center_y - 3;
168 ctx.draw_text(time_x, time_y, &self.time_text, self.foreground_color)
169 .map_err(|_| StatusBarError::RenderError)?;
170 }
171
172 if self.mode == StatusBarMode::ClockAndIcons || self.mode == StatusBarMode::IconsOnly {
174 let mut left_cursor = bounds.x + 6;
175
176 if self.bluetooth_connected {
177 let bt_color = self.accent_color;
179 let bx = left_cursor;
180 let by = center_y - 4;
181
182 let _ = ctx.stroke_rect(Rect::new(bx, by, 6, 8), Border::one(bt_color));
183 let _ = ctx.draw_text(bx + 1, by + 1, "B", bt_color);
184 left_cursor += 10;
185 }
186
187 if self.dnd_active {
188 let mx = left_cursor;
190 let my = center_y - 3;
191 let _ = ctx.fill_circle(mx + 3, my + 3, 3, Rgb565::CSS_GOLD);
192 let _ = ctx.fill_circle(mx + 4, my + 2, 2, self.background_color);
193 }
194
195 let right_cursor = bounds.right() - 6;
197
198 let batt_w = 16u32;
200 let batt_h = 8u32;
201 let batt_x = right_cursor - (batt_w as i32);
202 let batt_y = center_y - 4;
203
204 let shell_rect = Rect::new(batt_x, batt_y, batt_w, batt_h);
205 ctx.stroke_rect(shell_rect, Border::one(self.foreground_color))
206 .map_err(|_| StatusBarError::RenderError)?;
207
208 ctx.fill_rect(
210 Rect::new(batt_x + batt_w as i32, batt_y + 2, 2, 4),
211 self.foreground_color,
212 )
213 .map_err(|_| StatusBarError::RenderError)?;
214
215 let max_fill = batt_w.saturating_sub(4);
217 let fill_w = ((self.battery_percent as u32 * max_fill) / 100).max(1);
218 let fill_color = if self.battery_percent <= 15 {
219 Rgb565::CSS_RED
220 } else if self.battery_state == BatteryState::Charging {
221 Rgb565::CSS_GREEN
222 } else {
223 self.foreground_color
224 };
225
226 ctx.fill_rect(
227 Rect::new(batt_x + 2, batt_y + 2, fill_w, batt_h - 4),
228 fill_color,
229 )
230 .map_err(|_| StatusBarError::RenderError)?;
231
232 if self.battery_state == BatteryState::Charging {
234 ctx.draw_text(batt_x - 8, batt_y + 1, "~", Rgb565::CSS_YELLOW)
235 .map_err(|_| StatusBarError::RenderError)?;
236 }
237 }
238
239 Ok(())
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use crate::framebuffer::Framebuffer;
247
248 #[test]
249 fn test_status_bar_render_and_insets() {
250 let screen = Rect::new(0, 0, 240, 240);
251 let mut fb = Framebuffer::<{ 240 * 240 }>::new(240, 240);
252 let mut ctx = RenderCtx::new(&mut fb, screen);
253
254 let mut status_bar = StatusBarWidget::new("09:41");
255 status_bar.set_battery(72, BatteryState::Charging);
256 status_bar.dnd_active = true;
257
258 let bar_bounds = Rect::new(0, 0, 240, 20);
259 let res = status_bar.render(&mut ctx, bar_bounds);
260 assert!(res.is_ok());
261
262 let mut area = UnobstructedArea::new(screen);
263 status_bar.apply_to_unobstructed_area(&mut area);
264 assert_eq!(area.visible_rect(), Rect::new(0, 20, 240, 220));
265 }
266}