veridian_kernel/desktop/notification.rs
1//! Desktop Notification System
2//!
3//! Provides toast-style notification popups displayed as overlay surfaces.
4//! Notifications stack from the top-right corner of the screen and auto-expire
5//! based on urgency level. Supports programmatic dismiss and tick-based expiry.
6
7#![allow(dead_code)]
8
9use alloc::{string::String, vec::Vec};
10
11use spin::Mutex;
12
13use crate::sync::once_lock::GlobalState;
14
15// ---------------------------------------------------------------------------
16// Constants
17// ---------------------------------------------------------------------------
18
19/// Default expiry in ticks for Low urgency notifications (~3 seconds at
20/// 1000Hz).
21const EXPIRE_TICKS_LOW: u64 = 3_000;
22
23/// Default expiry in ticks for Normal urgency notifications (~5 seconds).
24const EXPIRE_TICKS_NORMAL: u64 = 5_000;
25
26/// Default expiry in ticks for Critical urgency notifications (~10 seconds).
27const EXPIRE_TICKS_CRITICAL: u64 = 10_000;
28
29/// Toast background color (dark semi-transparent: 0xE0303030 ARGB -> BGRA u32).
30const TOAST_BG_COLOR: u32 = 0xE0303030;
31
32/// Toast border color for Normal urgency.
33const TOAST_BORDER_NORMAL: u32 = 0xFF5588CC;
34
35/// Toast border color for Critical urgency (red accent).
36const TOAST_BORDER_CRITICAL: u32 = 0xFFCC4444;
37
38/// Toast border color for Low urgency (subtle gray).
39const TOAST_BORDER_LOW: u32 = 0xFF606060;
40
41/// Summary text color (white).
42const SUMMARY_COLOR: u32 = 0xFFEEEEEE;
43
44/// Body text color (light gray).
45const BODY_COLOR: u32 = 0xFFAAAAAA;
46
47/// App name text color (dim).
48const APP_NAME_COLOR: u32 = 0xFF777777;
49
50/// Font dimensions (8x16 bitmap font).
51const CHAR_W: usize = 8;
52const CHAR_H: usize = 16;
53
54// ---------------------------------------------------------------------------
55// Types
56// ---------------------------------------------------------------------------
57
58/// Urgency level for a notification.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum NotificationUrgency {
61 /// Low priority -- short display time, subtle styling.
62 Low,
63 /// Normal priority -- standard display time.
64 Normal,
65 /// Critical priority -- longer display time, red accent.
66 Critical,
67}
68
69impl NotificationUrgency {
70 /// Convert a raw u8 to an urgency level.
71 pub fn from_u8(v: u8) -> Self {
72 match v {
73 0 => Self::Low,
74 2 => Self::Critical,
75 _ => Self::Normal,
76 }
77 }
78
79 /// Default expiry ticks for this urgency level.
80 fn default_expire_ticks(self) -> u64 {
81 match self {
82 Self::Low => EXPIRE_TICKS_LOW,
83 Self::Normal => EXPIRE_TICKS_NORMAL,
84 Self::Critical => EXPIRE_TICKS_CRITICAL,
85 }
86 }
87
88 /// Border color for this urgency level.
89 fn border_color(self) -> u32 {
90 match self {
91 Self::Low => TOAST_BORDER_LOW,
92 Self::Normal => TOAST_BORDER_NORMAL,
93 Self::Critical => TOAST_BORDER_CRITICAL,
94 }
95 }
96}
97
98/// A single desktop notification.
99#[derive(Debug, Clone)]
100pub struct Notification {
101 /// Unique notification ID.
102 pub id: u32,
103 /// Short summary / title line.
104 pub summary: String,
105 /// Longer body text (may be empty).
106 pub body: String,
107 /// Urgency level.
108 pub urgency: NotificationUrgency,
109 /// Name of the application that sent this notification.
110 pub app_name: String,
111 /// Tick count when the notification was created.
112 pub created_tick: u64,
113 /// Number of ticks after creation when the notification expires.
114 pub expire_ticks: u64,
115 /// Whether the notification has been dismissed by the user.
116 pub dismissed: bool,
117}
118
119impl Notification {
120 /// Returns `true` if the notification has expired at `current_tick`.
121 pub fn is_expired(&self, current_tick: u64) -> bool {
122 current_tick >= self.created_tick.saturating_add(self.expire_ticks)
123 }
124}
125
126/// Manages the set of active desktop notifications.
127pub struct NotificationManager {
128 /// All tracked notifications (active + recently expired).
129 notifications: Vec<Notification>,
130 /// Next notification ID to assign.
131 next_id: u32,
132 /// Maximum number of toasts visible simultaneously.
133 max_visible: usize,
134 /// Width of each toast popup in pixels.
135 toast_width: usize,
136 /// Height of each toast popup in pixels.
137 toast_height: usize,
138 /// Vertical margin between stacked toasts.
139 toast_margin: usize,
140 /// X position (left edge) for the toast column (top-right aligned).
141 position_x: usize,
142 /// Y position (top edge) for the first toast.
143 position_y: usize,
144}
145
146impl NotificationManager {
147 /// Create a new notification manager positioned in the top-right corner.
148 pub fn new(screen_width: usize, screen_height: usize) -> Self {
149 let toast_width = 300;
150 let toast_margin = 10;
151 let _ = screen_height; // available for future use
152 Self {
153 notifications: Vec::new(),
154 next_id: 1,
155 max_visible: 3,
156 toast_width,
157 toast_height: 80,
158 toast_margin,
159 position_x: screen_width.saturating_sub(toast_width + toast_margin),
160 position_y: toast_margin,
161 }
162 }
163
164 /// Post a new notification. Returns the assigned notification ID.
165 pub fn notify(
166 &mut self,
167 summary: String,
168 body: String,
169 urgency: NotificationUrgency,
170 app_name: String,
171 ) -> u32 {
172 let id = self.next_id;
173 self.next_id = self.next_id.wrapping_add(1);
174
175 let current_tick = read_tick();
176 let expire_ticks = urgency.default_expire_ticks();
177
178 self.notifications.push(Notification {
179 id,
180 summary,
181 body,
182 urgency,
183 app_name,
184 created_tick: current_tick,
185 expire_ticks,
186 dismissed: false,
187 });
188
189 id
190 }
191
192 /// Dismiss a specific notification by ID.
193 pub fn dismiss(&mut self, id: u32) {
194 for n in &mut self.notifications {
195 if n.id == id {
196 n.dismissed = true;
197 return;
198 }
199 }
200 }
201
202 /// Dismiss all active notifications.
203 pub fn dismiss_all(&mut self) {
204 for n in &mut self.notifications {
205 n.dismissed = true;
206 }
207 }
208
209 /// Tick the notification manager: remove expired and dismissed entries.
210 pub fn tick(&mut self, current_tick: u64) {
211 self.notifications
212 .retain(|n| !n.dismissed && !n.is_expired(current_tick));
213 }
214
215 /// Return references to the currently visible (non-dismissed, non-expired)
216 /// notifications, up to `max_visible`.
217 pub fn visible_notifications(&self) -> Vec<&Notification> {
218 let current_tick = read_tick();
219 self.notifications
220 .iter()
221 .filter(|n| !n.dismissed && !n.is_expired(current_tick))
222 .take(self.max_visible)
223 .collect()
224 }
225
226 /// Return total count of active (non-dismissed, non-expired) notifications.
227 pub fn active_count(&self) -> usize {
228 let current_tick = read_tick();
229 self.notifications
230 .iter()
231 .filter(|n| !n.dismissed && !n.is_expired(current_tick))
232 .count()
233 }
234
235 /// Render all visible toast notifications into a u32 pixel buffer.
236 ///
237 /// The buffer is `buf_width * buf_height` pixels in BGRA format (one u32
238 /// per pixel). Toasts are rendered from the top-right corner, stacking
239 /// downward. Only pixels belonging to toast rectangles are written; the
240 /// caller is responsible for compositing this buffer as an overlay.
241 pub fn render_to_buffer(
242 &self,
243 buffer: &mut [u32],
244 buf_width: usize,
245 buf_height: usize,
246 current_tick: u64,
247 ) {
248 let visible = self.visible_notifications_at(current_tick);
249 if visible.is_empty() {
250 return;
251 }
252
253 let tw = self.toast_width;
254 let th = self.toast_height;
255
256 for (idx, notif) in visible.iter().enumerate() {
257 let tx = self.position_x;
258 let ty = self.position_y + idx * (th + self.toast_margin);
259
260 // Skip if toast would be off-screen
261 if ty + th > buf_height || tx + tw > buf_width {
262 continue;
263 }
264
265 let border_color = notif.urgency.border_color();
266
267 // Draw toast rectangle (background + 1px border)
268 for row in 0..th {
269 for col in 0..tw {
270 let px = tx + col;
271 let py = ty + row;
272 let offset = py * buf_width + px;
273 if offset >= buffer.len() {
274 continue;
275 }
276
277 // 1-pixel border
278 if row == 0 || row == th - 1 || col == 0 || col == tw - 1 {
279 buffer[offset] = border_color;
280 } else {
281 buffer[offset] = TOAST_BG_COLOR;
282 }
283 }
284 }
285
286 // Render app name (top-left, small and dim)
287 let app_bytes = notif.app_name.as_bytes();
288 let app_max = (tw - 16) / CHAR_W;
289 let app_y = ty + 4;
290 for (i, &ch) in app_bytes.iter().take(app_max).enumerate() {
291 render_glyph_u32(
292 buffer,
293 buf_width,
294 tx + 8 + i * CHAR_W,
295 app_y,
296 ch,
297 APP_NAME_COLOR,
298 );
299 }
300
301 // Render summary (bold-ish white, below app name)
302 let sum_bytes = notif.summary.as_bytes();
303 let sum_max = (tw - 16) / CHAR_W;
304 let sum_y = ty + 4 + CHAR_H + 2;
305 for (i, &ch) in sum_bytes.iter().take(sum_max).enumerate() {
306 render_glyph_u32(
307 buffer,
308 buf_width,
309 tx + 8 + i * CHAR_W,
310 sum_y,
311 ch,
312 SUMMARY_COLOR,
313 );
314 }
315
316 // Render body (gray, below summary, may truncate)
317 let body_bytes = notif.body.as_bytes();
318 let body_max = (tw - 16) / CHAR_W;
319 let body_y = ty + 4 + (CHAR_H + 2) * 2;
320 if body_y + CHAR_H <= ty + th {
321 for (i, &ch) in body_bytes.iter().take(body_max).enumerate() {
322 render_glyph_u32(
323 buffer,
324 buf_width,
325 tx + 8 + i * CHAR_W,
326 body_y,
327 ch,
328 BODY_COLOR,
329 );
330 }
331 }
332 }
333 }
334
335 /// Internal: get visible notifications at a specific tick (avoids
336 /// calling `read_tick()` again when the caller already has it).
337 fn visible_notifications_at(&self, current_tick: u64) -> Vec<&Notification> {
338 self.notifications
339 .iter()
340 .filter(|n| !n.dismissed && !n.is_expired(current_tick))
341 .take(self.max_visible)
342 .collect()
343 }
344}
345
346// ---------------------------------------------------------------------------
347// Glyph rendering helper (u32 pixel buffer)
348// ---------------------------------------------------------------------------
349
350/// Render a single 8x16 glyph into a u32 (BGRA packed) pixel buffer.
351///
352/// Only foreground pixels are written; background pixels are left untouched
353/// so the toast background shows through.
354fn render_glyph_u32(buf: &mut [u32], buf_width: usize, px: usize, py: usize, ch: u8, color: u32) {
355 use crate::graphics::font8x16;
356
357 let glyph = font8x16::glyph(ch);
358 for (row, &bits) in glyph.iter().enumerate() {
359 for col in 0..8 {
360 if (bits >> (7 - col)) & 1 != 0 {
361 let x = px + col;
362 let y = py + row;
363 let offset = y * buf_width + x;
364 if offset < buf.len() {
365 buf[offset] = color;
366 }
367 }
368 }
369 }
370}
371
372// ---------------------------------------------------------------------------
373// Tick source helper
374// ---------------------------------------------------------------------------
375
376/// Read the current hardware tick counter (architecture-independent).
377fn read_tick() -> u64 {
378 crate::arch::timer::read_hw_timestamp() / 1_000_000 // approximate ms-scale
379}
380
381// ---------------------------------------------------------------------------
382// Global instance
383// ---------------------------------------------------------------------------
384
385static NOTIFICATION_MANAGER: GlobalState<Mutex<NotificationManager>> = GlobalState::new();
386
387/// Initialize the global notification manager.
388pub fn init(screen_width: usize, screen_height: usize) {
389 let _ = NOTIFICATION_MANAGER.init(Mutex::new(NotificationManager::new(
390 screen_width,
391 screen_height,
392 )));
393 crate::println!(
394 "[NOTIFY] Notification manager initialized (toast area {}x{})",
395 300,
396 screen_height
397 );
398}
399
400/// Execute a closure with a mutable reference to the notification manager.
401pub fn with_notification_manager<R, F: FnOnce(&mut NotificationManager) -> R>(f: F) -> Option<R> {
402 NOTIFICATION_MANAGER.with(|lock| {
403 let mut mgr = lock.lock();
404 f(&mut mgr)
405 })
406}
407
408/// Convenience: post a notification from anywhere in the kernel.
409pub fn notify(summary: &str, body: &str, urgency: NotificationUrgency, app_name: &str) -> u32 {
410 with_notification_manager(|mgr| {
411 mgr.notify(
412 String::from(summary),
413 String::from(body),
414 urgency,
415 String::from(app_name),
416 )
417 })
418 .unwrap_or(0)
419}
420
421/// Convenience: dismiss a notification by ID.
422pub fn dismiss(id: u32) {
423 with_notification_manager(|mgr| mgr.dismiss(id));
424}
425
426/// Convenience: dismiss all notifications.
427pub fn dismiss_all() {
428 with_notification_manager(|mgr| mgr.dismiss_all());
429}
430
431/// Convenience: tick the notification manager (call from render loop).
432pub fn tick() {
433 let current = read_tick();
434 with_notification_manager(|mgr| mgr.tick(current));
435}