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

veridian_kernel/desktop/
systray.rs

1//! System Tray
2//!
3//! Provides a system tray area in the desktop panel for status indicators
4//! such as clock, CPU usage, memory usage, network status, and battery.
5//! Items are rendered right-aligned in the panel and can be clicked.
6
7#![allow(dead_code)]
8
9use alloc::{string::String, vec::Vec};
10
11use crate::sync::once_lock::GlobalState;
12
13// ---------------------------------------------------------------------------
14// Constants
15// ---------------------------------------------------------------------------
16
17/// Font dimensions (8x16 bitmap font).
18const CHAR_W: usize = 8;
19const CHAR_H: usize = 16;
20
21/// Default tray item padding (pixels between items).
22const TRAY_PADDING: usize = 4;
23
24/// Default tray height (matches panel inner area).
25const TRAY_HEIGHT: usize = 24;
26
27/// Color for normal tray text (light gray).
28const COLOR_NORMAL: u32 = 0xFFBBBBBB;
29
30/// Color for CPU usage below 50% (green).
31const COLOR_CPU_LOW: u32 = 0xFF44CC44;
32
33/// Color for CPU usage 50-79% (yellow).
34const COLOR_CPU_MED: u32 = 0xFFCCCC44;
35
36/// Color for CPU usage >= 80% (red).
37const COLOR_CPU_HIGH: u32 = 0xFFCC4444;
38
39/// Separator color (dim).
40const COLOR_SEPARATOR: u32 = 0xFF555555;
41
42// ---------------------------------------------------------------------------
43// Types
44// ---------------------------------------------------------------------------
45
46/// Type of system tray item.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum SysTrayItemType {
49    /// System clock showing time and date.
50    Clock,
51    /// CPU usage monitor.
52    CpuMonitor,
53    /// Memory usage monitor.
54    MemoryMonitor,
55    /// Network connection status.
56    NetworkStatus,
57    /// Battery charge level.
58    BatteryStatus,
59    /// Audio volume level.
60    Volume,
61    /// Custom/user-defined tray item.
62    Custom,
63}
64
65/// A single item in the system tray.
66#[derive(Debug, Clone)]
67pub struct SysTrayItem {
68    /// What kind of tray item this is.
69    pub item_type: SysTrayItemType,
70    /// Display label (rendered text).
71    pub label: String,
72    /// Tooltip text (for future hover support).
73    pub tooltip: String,
74    /// Whether this item is currently visible.
75    pub visible: bool,
76    /// Width of this item in pixels (based on label length).
77    pub width: usize,
78}
79
80impl SysTrayItem {
81    /// Create a new tray item with auto-calculated width.
82    pub fn new(item_type: SysTrayItemType, label: &str, tooltip: &str) -> Self {
83        let width = label.len() * CHAR_W + TRAY_PADDING * 2;
84        Self {
85            item_type,
86            label: String::from(label),
87            tooltip: String::from(tooltip),
88            visible: true,
89            width,
90        }
91    }
92
93    /// Update the label and recalculate width.
94    fn set_label(&mut self, new_label: &str) {
95        self.label.clear();
96        self.label.push_str(new_label);
97        self.width = self.label.len() * CHAR_W + TRAY_PADDING * 2;
98    }
99}
100
101/// System tray managing a collection of status indicator items.
102pub struct SystemTray {
103    /// Ordered list of tray items (rendered left-to-right within the tray
104    /// area).
105    items: Vec<SysTrayItem>,
106    /// Height of the tray area in pixels.
107    tray_height: usize,
108    /// Padding between items.
109    padding: usize,
110}
111
112impl SystemTray {
113    /// Create a new system tray with default status items.
114    pub fn new() -> Self {
115        let mut tray = Self {
116            items: Vec::new(),
117            tray_height: TRAY_HEIGHT,
118            padding: TRAY_PADDING,
119        };
120
121        // Default items (left-to-right order)
122        tray.items.push(SysTrayItem::new(
123            SysTrayItemType::NetworkStatus,
124            "Net: up",
125            "Network status",
126        ));
127        tray.items.push(SysTrayItem::new(
128            SysTrayItemType::CpuMonitor,
129            "CPU: 0%",
130            "CPU usage",
131        ));
132        tray.items.push(SysTrayItem::new(
133            SysTrayItemType::MemoryMonitor,
134            "MEM: 0/0 MB",
135            "Memory usage",
136        ));
137        tray.items.push(SysTrayItem::new(
138            SysTrayItemType::Volume,
139            "Vol: 75%",
140            "Audio volume",
141        ));
142        tray.items.push(SysTrayItem::new(
143            SysTrayItemType::BatteryStatus,
144            "AC",
145            "Power source",
146        ));
147        tray.items.push(SysTrayItem::new(
148            SysTrayItemType::Clock,
149            "00:00 Jan 01",
150            "System clock",
151        ));
152
153        tray
154    }
155
156    /// Add a new item to the tray.
157    pub fn add_item(&mut self, item: SysTrayItem) {
158        // Avoid duplicate types (except Custom)
159        if item.item_type != SysTrayItemType::Custom
160            && self.items.iter().any(|i| i.item_type == item.item_type)
161        {
162            return;
163        }
164        self.items.push(item);
165    }
166
167    /// Remove the first item matching the given type.
168    pub fn remove_item(&mut self, item_type: SysTrayItemType) {
169        if let Some(pos) = self.items.iter().position(|i| i.item_type == item_type) {
170            self.items.remove(pos);
171        }
172    }
173
174    /// Update the clock display with the given time and date.
175    pub fn update_clock(&mut self, hours: u8, minutes: u8, _seconds: u8, month: u8, day: u8) {
176        let label = format_clock(hours, minutes, month, day);
177        for item in &mut self.items {
178            if item.item_type == SysTrayItemType::Clock {
179                item.set_label(&label);
180                return;
181            }
182        }
183    }
184
185    /// Update the CPU usage display.
186    pub fn update_cpu(&mut self, usage_percent: u8) {
187        let label = format_cpu(usage_percent);
188        for item in &mut self.items {
189            if item.item_type == SysTrayItemType::CpuMonitor {
190                item.set_label(&label);
191                return;
192            }
193        }
194    }
195
196    /// Update the memory usage display.
197    pub fn update_memory(&mut self, used_mb: u32, total_mb: u32) {
198        let label = format_memory(used_mb, total_mb);
199        for item in &mut self.items {
200            if item.item_type == SysTrayItemType::MemoryMonitor {
201                item.set_label(&label);
202                return;
203            }
204        }
205    }
206
207    /// Update the network status display.
208    pub fn update_network(&mut self, is_up: bool) {
209        let label = if is_up { "Net: up" } else { "Net: --" };
210        for item in &mut self.items {
211            if item.item_type == SysTrayItemType::NetworkStatus {
212                item.set_label(label);
213                return;
214            }
215        }
216    }
217
218    /// Update the volume display (0-100).
219    pub fn update_volume(&mut self, volume_percent: u8) {
220        let label = format_volume(volume_percent);
221        for item in &mut self.items {
222            if item.item_type == SysTrayItemType::Volume {
223                item.set_label(&label);
224                return;
225            }
226        }
227    }
228
229    /// Update the battery/power status display.
230    pub fn update_battery(&mut self, label_str: &str) {
231        for item in &mut self.items {
232            if item.item_type == SysTrayItemType::BatteryStatus {
233                item.set_label(label_str);
234                return;
235            }
236        }
237    }
238
239    /// Total pixel width of all visible tray items plus inter-item padding.
240    pub fn total_width(&self) -> usize {
241        let visible: Vec<&SysTrayItem> = self.items.iter().filter(|i| i.visible).collect();
242        if visible.is_empty() {
243            return 0;
244        }
245        let items_width: usize = visible.iter().map(|i| i.width).sum();
246        let separators = if visible.len() > 1 {
247            // pad + 1px separator + pad
248            (visible.len() - 1) * (self.padding + 1 + self.padding)
249        } else {
250            0
251        };
252        items_width + separators
253    }
254
255    /// Render the system tray into a u32 (BGRA) pixel buffer.
256    ///
257    /// Items are drawn starting at `(x_start, y_start)` and proceed
258    /// left-to-right. Separators are drawn between items.
259    pub fn render_to_buffer(
260        &self,
261        buffer: &mut [u32],
262        buf_width: usize,
263        x_start: usize,
264        y_start: usize,
265    ) {
266        let mut cx = x_start;
267
268        for (idx, item) in self.items.iter().filter(|i| i.visible).enumerate() {
269            // Draw separator before each item except the first
270            if idx > 0 {
271                cx += self.padding;
272                // 1-pixel vertical separator line
273                let sep_top = y_start + 2;
274                let sep_bot = y_start + self.tray_height.saturating_sub(2);
275                for sy in sep_top..sep_bot {
276                    let offset = sy * buf_width + cx;
277                    if offset < buffer.len() {
278                        buffer[offset] = COLOR_SEPARATOR;
279                    }
280                }
281                cx += 1 + self.padding;
282            }
283
284            // Determine text color based on item type and content
285            let color = item_color(item);
286
287            // Center text vertically within tray height
288            let text_y = y_start + (self.tray_height.saturating_sub(CHAR_H)) / 2;
289
290            // Render label characters
291            let text_x = cx + self.padding;
292            for (ci, &ch) in item.label.as_bytes().iter().enumerate() {
293                render_glyph_u32(buffer, buf_width, text_x + ci * CHAR_W, text_y, ch, color);
294            }
295
296            cx += item.width;
297        }
298    }
299
300    /// Determine which tray item (if any) was clicked at position `(x, y)`.
301    ///
302    /// Coordinates are relative to the tray area origin (the same coordinate
303    /// system used for `render_to_buffer`). Returns `None` if the click is
304    /// outside any item.
305    pub fn handle_click(&self, x: usize, _y: usize) -> Option<SysTrayItemType> {
306        let mut cx: usize = 0;
307
308        for (idx, item) in self.items.iter().filter(|i| i.visible).enumerate() {
309            if idx > 0 {
310                cx += self.padding + 1 + self.padding; // separator
311            }
312
313            if x >= cx && x < cx + item.width {
314                return Some(item.item_type);
315            }
316            cx += item.width;
317        }
318
319        None
320    }
321}
322
323// ---------------------------------------------------------------------------
324// Formatting helpers (no floating-point, no format! macro for small strings)
325// ---------------------------------------------------------------------------
326
327/// Format clock label: "HH:MM Mon DD"
328fn format_clock(hours: u8, minutes: u8, month: u8, day: u8) -> String {
329    let mut s = String::with_capacity(14);
330    push_2digit(&mut s, hours);
331    s.push(':');
332    push_2digit(&mut s, minutes);
333    s.push(' ');
334    s.push_str(month_abbr(month));
335    s.push(' ');
336    push_2digit(&mut s, day);
337    s
338}
339
340/// Format CPU label: "CPU: XX%"
341fn format_cpu(usage: u8) -> String {
342    let mut s = String::with_capacity(10);
343    s.push_str("CPU:");
344    push_u8_decimal(&mut s, usage);
345    s.push('%');
346    s
347}
348
349/// Format volume label: "Vol: XX%"
350fn format_volume(volume: u8) -> String {
351    let mut s = String::with_capacity(10);
352    s.push_str("Vol:");
353    push_u8_decimal(&mut s, volume);
354    s.push('%');
355    s
356}
357
358/// Format memory label: "MEM: XXX/YYY MB"
359fn format_memory(used_mb: u32, total_mb: u32) -> String {
360    let mut s = String::with_capacity(18);
361    s.push_str("MEM:");
362    push_u32_decimal(&mut s, used_mb);
363    s.push('/');
364    push_u32_decimal(&mut s, total_mb);
365    s.push_str("MB");
366    s
367}
368
369/// Push a zero-padded 2-digit number (0-99).
370fn push_2digit(s: &mut String, v: u8) {
371    let v = v % 100;
372    s.push((b'0' + v / 10) as char);
373    s.push((b'0' + v % 10) as char);
374}
375
376/// Push a u8 as a decimal string (no leading zeros).
377fn push_u8_decimal(s: &mut String, v: u8) {
378    if v >= 100 {
379        s.push((b'0' + v / 100) as char);
380    }
381    if v >= 10 {
382        s.push((b'0' + (v / 10) % 10) as char);
383    }
384    s.push((b'0' + v % 10) as char);
385}
386
387/// Push a u32 as a decimal string (no leading zeros).
388fn push_u32_decimal(s: &mut String, v: u32) {
389    if v == 0 {
390        s.push('0');
391        return;
392    }
393    // Find highest power of 10
394    let mut div = 1u32;
395    let mut tmp = v;
396    while tmp >= 10 {
397        tmp /= 10;
398        div *= 10;
399    }
400    // Emit digits
401    let mut remaining = v;
402    while div > 0 {
403        let digit = remaining / div;
404        s.push((b'0' + digit as u8) as char);
405        remaining %= div;
406        div /= 10;
407    }
408}
409
410/// 3-letter month abbreviation (1-indexed).
411fn month_abbr(month: u8) -> &'static str {
412    match month {
413        1 => "Jan",
414        2 => "Feb",
415        3 => "Mar",
416        4 => "Apr",
417        5 => "May",
418        6 => "Jun",
419        7 => "Jul",
420        8 => "Aug",
421        9 => "Sep",
422        10 => "Oct",
423        11 => "Nov",
424        12 => "Dec",
425        _ => "???",
426    }
427}
428
429/// Determine the display color for a tray item based on its type and content.
430fn item_color(item: &SysTrayItem) -> u32 {
431    match item.item_type {
432        SysTrayItemType::CpuMonitor => {
433            // Parse usage from label "CPU:XX%"
434            let usage = parse_cpu_usage(&item.label);
435            if usage >= 80 {
436                COLOR_CPU_HIGH
437            } else if usage >= 50 {
438                COLOR_CPU_MED
439            } else {
440                COLOR_CPU_LOW
441            }
442        }
443        _ => COLOR_NORMAL,
444    }
445}
446
447/// Extract the numeric CPU usage from a label like "CPU:42%".
448fn parse_cpu_usage(label: &str) -> u8 {
449    // Find digits between ':' and '%'
450    let bytes = label.as_bytes();
451    let mut start = 0;
452    let mut end = bytes.len();
453
454    for (i, &b) in bytes.iter().enumerate() {
455        if b == b':' {
456            start = i + 1;
457        } else if b == b'%' {
458            end = i;
459            break;
460        }
461    }
462
463    let mut result: u8 = 0;
464    for &b in &bytes[start..end] {
465        if b.is_ascii_digit() {
466            result = result.saturating_mul(10).saturating_add(b - b'0');
467        }
468    }
469    result
470}
471
472// ---------------------------------------------------------------------------
473// Glyph rendering helper (u32 pixel buffer)
474// ---------------------------------------------------------------------------
475
476/// Render a single 8x16 glyph into a u32 (BGRA packed) pixel buffer.
477fn render_glyph_u32(buf: &mut [u32], buf_width: usize, px: usize, py: usize, ch: u8, color: u32) {
478    use crate::graphics::font8x16;
479
480    let glyph = font8x16::glyph(ch);
481    for (row, &bits) in glyph.iter().enumerate() {
482        for col in 0..8 {
483            if (bits >> (7 - col)) & 1 != 0 {
484                let x = px + col;
485                let y = py + row;
486                let offset = y * buf_width + x;
487                if offset < buf.len() {
488                    buf[offset] = color;
489                }
490            }
491        }
492    }
493}
494
495// ---------------------------------------------------------------------------
496// Global instance
497// ---------------------------------------------------------------------------
498
499static SYSTEM_TRAY: GlobalState<spin::Mutex<SystemTray>> = GlobalState::new();
500
501/// Initialize the global system tray.
502pub fn init() {
503    let _ = SYSTEM_TRAY.init(spin::Mutex::new(SystemTray::new()));
504    crate::println!("[SYSTRAY] System tray initialized (6 default items)");
505}
506
507/// Execute a closure with a mutable reference to the system tray.
508pub fn with_system_tray<R, F: FnOnce(&mut SystemTray) -> R>(f: F) -> Option<R> {
509    SYSTEM_TRAY.with(|lock| {
510        let mut tray = lock.lock();
511        f(&mut tray)
512    })
513}
514
515/// Execute a closure with a shared reference to the system tray.
516pub fn with_system_tray_ref<R, F: FnOnce(&SystemTray) -> R>(f: F) -> Option<R> {
517    SYSTEM_TRAY.with(|lock| {
518        let tray = lock.lock();
519        f(&tray)
520    })
521}
522
523impl Default for SystemTray {
524    fn default() -> Self {
525        Self::new()
526    }
527}