1use super::{Bus, Event, Hub, Id, RenderData, RenderQueue, View, ViewId, ID_FEEDER};
39use super::{BORDER_RADIUS_MEDIUM, SMALL_BAR_HEIGHT, THICKNESS_LARGE};
40use crate::color::{BLACK, TEXT_NORMAL, WHITE};
41use crate::context::Context;
42use crate::device::CURRENT_DEVICE;
43use crate::font::{font_from_style, Fonts, NORMAL_STYLE};
44use crate::framebuffer::{Framebuffer, UpdateMode};
45use crate::geom::{BorderSpec, CornerSpec, Rectangle};
46use crate::gesture::GestureEvent;
47use crate::input::DeviceEvent;
48use crate::unit::scale_by_dpi;
49use std::thread;
50use std::time::Duration;
51
52const NOTIFICATION_CLOSE_DELAY: Duration = Duration::from_secs(4);
53
54#[derive(Debug, Clone)]
56pub enum NotificationEvent {
57 Show(String),
59 ShowPinned(ViewId, String),
61 UpdateText(ViewId, String),
63 UpdateProgress(ViewId, u8),
65}
66
67pub struct Notification {
76 id: Id,
77 rect: Rectangle,
78 children: Vec<Box<dyn View>>,
79 text: String,
80 max_width: i32,
81 index: u8,
82 view_id: ViewId,
83 progress: Option<u8>,
84}
85
86impl Notification {
87 pub fn new(
102 view_id: Option<ViewId>,
103 text: String,
104 pinned: bool,
105 hub: &Hub,
106 rq: &mut RenderQueue,
107 context: &mut Context,
108 ) -> Notification {
109 let id = ID_FEEDER.next();
110 let view_id = view_id.unwrap_or(ViewId::MessageNotif(id));
111 let index = context.notification_index;
112
113 if !pinned {
114 let hub2 = hub.clone();
115 thread::spawn(move || {
116 thread::sleep(NOTIFICATION_CLOSE_DELAY);
117 hub2.send(Event::Close(view_id)).ok();
118 });
119 }
120
121 let dpi = CURRENT_DEVICE.dpi;
122 let (width, _) = context.display.dims;
123 let small_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32;
124
125 let font = font_from_style(&mut context.fonts, &NORMAL_STYLE, dpi);
126 let x_height = font.x_heights.0 as i32;
127 let padding = font.em() as i32;
128
129 let max_message_width = width as i32 - 5 * padding;
130 let plan = font.plan(&text, Some(max_message_width), None);
131
132 let dialog_width = plan.width + 3 * padding;
133 let dialog_height = 7 * x_height;
134
135 let side = (index / 3) % 2;
136 let dx = if side == 0 {
137 width as i32 - dialog_width - padding
138 } else {
139 padding
140 };
141 let dy = small_height + padding + (index % 3) as i32 * (dialog_height + padding);
142
143 let rect = rect![dx, dy, dx + dialog_width, dy + dialog_height];
144
145 rq.add(RenderData::new(id, rect, UpdateMode::Gui));
146 context.notification_index = index.wrapping_add(1);
147
148 Notification {
149 id,
150 rect,
151 children: Vec::new(),
152 text,
153 max_width: max_message_width,
154 index,
155 view_id,
156 progress: None,
157 }
158 }
159
160 pub fn update_text(&mut self, text: String, rq: &mut RenderQueue) {
172 self.text = text;
173 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
174 }
175
176 pub fn update_progress(&mut self, progress: u8, rq: &mut RenderQueue) {
188 self.progress = Some(progress);
189 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
190 }
191}
192
193impl View for Notification {
194 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, _bus, _rq, _context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
195 fn handle_event(
196 &mut self,
197 evt: &Event,
198 _hub: &Hub,
199 _bus: &mut Bus,
200 _rq: &mut RenderQueue,
201 _context: &mut Context,
202 ) -> bool {
203 match *evt {
204 Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(center) => true,
205 Event::Gesture(GestureEvent::Swipe { start, .. }) if self.rect.includes(start) => true,
206 Event::Device(DeviceEvent::Finger { position, .. }) if self.rect.includes(position) => {
207 true
208 }
209 _ => false,
210 }
211 }
212
213 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, fonts, _rect), fields(rect = ?_rect)))]
214 fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) {
215 let dpi = CURRENT_DEVICE.dpi;
216
217 let border_radius = scale_by_dpi(BORDER_RADIUS_MEDIUM, dpi) as i32;
218 let border_thickness = scale_by_dpi(THICKNESS_LARGE, dpi) as u16;
219
220 fb.draw_rounded_rectangle_with_border(
221 &self.rect,
222 &CornerSpec::Uniform(border_radius),
223 &BorderSpec {
224 thickness: border_thickness,
225 color: BLACK,
226 },
227 &WHITE,
228 );
229
230 let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
231 let plan = font.plan(&self.text, Some(self.max_width), None);
232 let x_height = font.x_heights.0 as i32;
233
234 let dx = (self.rect.width() as i32 - plan.width) as i32 / 2;
235 let dy = (self.rect.height() as i32 - x_height) / 2;
236 let pt = pt!(self.rect.min.x + dx, self.rect.max.y - dy);
237
238 font.render(fb, TEXT_NORMAL[1], &plan, pt);
239
240 if let Some(progress) = self.progress {
241 let progress_clamped = progress.min(100);
242 let padding = font.em() as i32;
243 let progress_bar_height = scale_by_dpi(2.0, dpi) as i32;
244 let progress_bar_width = self.rect.width() as i32 - 2 * padding;
245 let progress_bar_y = self.rect.max.y - padding - progress_bar_height;
246
247 let progress_bg_rect = rect![
248 self.rect.min.x + padding,
249 progress_bar_y,
250 self.rect.min.x + padding + progress_bar_width,
251 progress_bar_y + progress_bar_height
252 ];
253 fb.draw_rectangle(&progress_bg_rect, TEXT_NORMAL[0]);
254
255 let filled_width = (progress_bar_width * progress_clamped as i32) / 100;
256 if filled_width > 0 {
257 let progress_fill_rect = rect![
258 self.rect.min.x + padding,
259 progress_bar_y,
260 self.rect.min.x + padding + filled_width,
261 progress_bar_y + progress_bar_height
262 ];
263 fb.draw_rectangle(&progress_fill_rect, BLACK);
264 }
265 }
266 }
267
268 fn resize(
269 &mut self,
270 _rect: Rectangle,
271 _hub: &Hub,
272 _rq: &mut RenderQueue,
273 context: &mut Context,
274 ) {
275 let dpi = CURRENT_DEVICE.dpi;
276 let (width, height) = context.display.dims;
277 let small_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32;
278 let side = (self.index / 3) % 2;
279 let padding = if side == 0 {
280 height as i32 - self.rect.max.x
281 } else {
282 self.rect.min.x
283 };
284 let dialog_width = self.rect.width() as i32;
285 let dialog_height = self.rect.height() as i32;
286 let dx = if side == 0 {
287 width as i32 - dialog_width - padding
288 } else {
289 padding
290 };
291 let dy = small_height + padding + (self.index % 3) as i32 * (dialog_height + padding);
292 let rect = rect![dx, dy, dx + dialog_width, dy + dialog_height];
293 self.rect = rect;
294 }
295
296 fn rect(&self) -> &Rectangle {
297 &self.rect
298 }
299
300 fn rect_mut(&mut self) -> &mut Rectangle {
301 &mut self.rect
302 }
303
304 fn children(&self) -> &Vec<Box<dyn View>> {
305 &self.children
306 }
307
308 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
309 &mut self.children
310 }
311
312 fn id(&self) -> Id {
313 self.id
314 }
315
316 fn view_id(&self) -> Option<ViewId> {
317 Some(self.view_id)
318 }
319}