⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

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}