1use embedded_graphics_core::{draw_target::DrawTarget, pixelcolor::Rgb565};
7use heapless::Vec;
8
9use crate::{
10 geometry::Rect,
11 render::RenderCtx,
12 style::{Border, Style},
13 widget::{PropertyKey, PropertyValue, Widget},
14};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct NotificationError;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum NotificationPriority {
23 Silent,
24 Normal,
25 Important,
26 Critical,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct NotificationAction<'a> {
32 pub label: &'a str,
33 pub action_id: u16,
34}
35
36#[derive(Clone, Debug, PartialEq)]
38pub struct NotificationSheetWidget<'a, const MAX_ACTIONS: usize = 3> {
39 pub title: &'a str,
40 pub message: &'a str,
41 pub priority: NotificationPriority,
42 pub actions: Vec<NotificationAction<'a>, MAX_ACTIONS>,
43 pub selected_action: usize,
44 pub auto_dismiss_progress: f32, pub background_color: Rgb565,
46 pub text_color: Rgb565,
47 pub accent_color: Rgb565,
48}
49
50impl<'a, const MAX_ACTIONS: usize> NotificationSheetWidget<'a, MAX_ACTIONS> {
51 pub const fn new(title: &'a str, message: &'a str, priority: NotificationPriority) -> Self {
52 let accent = match priority {
53 NotificationPriority::Silent | NotificationPriority::Normal => {
54 Rgb565::new(0, 35, 30) }
56 NotificationPriority::Important => Rgb565::new(31, 35, 0), NotificationPriority::Critical => Rgb565::new(31, 0, 0), };
59
60 Self {
61 title,
62 message,
63 priority,
64 actions: Vec::new(),
65 selected_action: 0,
66 auto_dismiss_progress: 1.0,
67 background_color: Rgb565::new(2, 4, 6),
68 text_color: Rgb565::new(31, 63, 31),
69 accent_color: accent,
70 }
71 }
72
73 pub fn add_action(&mut self, label: &'a str, action_id: u16) -> Result<(), NotificationError> {
74 self.actions
75 .push(NotificationAction { label, action_id })
76 .map_err(|_| NotificationError)
77 }
78
79 pub fn render<D, C>(&self, ctx: &mut RenderCtx<'_, D, C>, bounds: Rect) -> Result<(), D::Error>
80 where
81 D: DrawTarget<Color = Rgb565>,
82 C: crate::render::Compositor<D>,
83 {
84 ctx.fill_rounded_rect(bounds, 6, self.background_color)?;
86 ctx.stroke_rounded_rect(bounds, 6, Border::one(self.accent_color))?;
87
88 let header_rect = Rect::new(bounds.x, bounds.y, bounds.w, 18);
90 ctx.fill_rounded_rect(header_rect, 4, self.accent_color)?;
91 ctx.draw_text(bounds.x + 8, bounds.y + 4, self.title, Rgb565::new(0, 0, 0))?;
92
93 ctx.draw_text(bounds.x + 8, bounds.y + 24, self.message, self.text_color)?;
95
96 if self.auto_dismiss_progress > 0.0 && self.auto_dismiss_progress <= 1.0 {
98 let progress_w = ((bounds.w as f32) * self.auto_dismiss_progress) as u32;
99 let bar_rect = Rect::new(bounds.x, bounds.bottom() - 3, progress_w, 2);
100 ctx.fill_rect(bar_rect, self.accent_color)?;
101 }
102
103 if !self.actions.is_empty() {
105 let action_h = 16;
106 let action_y = bounds.bottom() - 22;
107 let btn_w = (bounds.w.saturating_sub(16) / self.actions.len() as u32).max(20);
108
109 for (i, action) in self.actions.iter().enumerate() {
110 let btn_x = bounds.x + 8 + (i as i32 * (btn_w as i32 + 4));
111 let btn_rect = Rect::new(btn_x, action_y, btn_w, action_h);
112 let is_sel = i == self.selected_action;
113
114 let bg = if is_sel {
115 self.accent_color
116 } else {
117 Rgb565::new(6, 12, 18)
118 };
119 let fg = if is_sel {
120 Rgb565::new(0, 0, 0)
121 } else {
122 self.text_color
123 };
124
125 ctx.fill_rounded_rect(btn_rect, 2, bg)?;
126 ctx.draw_text(btn_x + 4, action_y + 3, action.label, fg)?;
127 }
128 }
129
130 Ok(())
131 }
132}
133
134impl<'a, const MAX_ACTIONS: usize> Widget for NotificationSheetWidget<'a, MAX_ACTIONS> {
135 fn render_widget_bounds(&self, _bounds: Rect, _style: &Style) {}
136
137 fn get_property(&self, key: PropertyKey) -> Option<PropertyValue<'_>> {
138 match key {
139 PropertyKey::Text => Some(PropertyValue::Str(self.title)),
140 PropertyKey::Progress => Some(PropertyValue::Float(self.auto_dismiss_progress)),
141 PropertyKey::Selected => Some(PropertyValue::Int(self.selected_action as i32)),
142 _ => None,
143 }
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::framebuffer::Framebuffer;
151
152 #[test]
153 fn test_notification_sheet_render() {
154 let mut notif = NotificationSheetWidget::<2>::new(
155 "BATTERY WARNING",
156 "Battery level 15%",
157 NotificationPriority::Important,
158 );
159 assert!(notif.add_action("DISMISS", 1).is_ok());
160 assert!(notif.add_action("POWER SAVE", 2).is_ok());
161
162 let mut fb = Framebuffer::<24000>::new(200, 100);
163 let mut ctx = RenderCtx::new(&mut fb, Rect::new(0, 0, 200, 100));
164 assert!(notif.render(&mut ctx, Rect::new(0, 0, 200, 100)).is_ok());
165 }
166}