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

veridian_kernel/desktop/
panel.rs

1//! Desktop Panel (Taskbar) with Layer-Shell Integration
2//!
3//! Renders a taskbar at the bottom of the screen showing workspace
4//! indicators, open windows, system clock with date, and system tray area.
5//!
6//! ## Layer-Shell Protocol
7//!
8//! The panel uses the wlr-layer-shell protocol to anchor itself to the
9//! bottom edge of the output. Layer-shell surfaces are rendered above
10//! normal windows and below overlay surfaces (e.g., notifications).
11//!
12//! Layer ordering (bottom to top):
13//! - Background: desktop wallpaper
14//! - Bottom: desktop widgets
15//! - Top: panels, docks (this panel)
16//! - Overlay: lock screen, notifications
17//!
18//! The panel requests exclusive zone equal to its height, so the
19//! compositor reserves that space and prevents normal windows from
20//! overlapping the panel area.
21//!
22//! Layer-shell types, enum variants, and panel configuration fields define
23//! the complete panel API. Unused items are retained for protocol completeness.
24#![allow(dead_code)]
25
26use alloc::{string::String, vec::Vec};
27
28use spin::RwLock;
29
30use super::window_manager::{with_window_manager, WindowId};
31use crate::{error::KernelError, sync::once_lock::GlobalState};
32
33/// Panel height in pixels.
34pub const PANEL_HEIGHT: u32 = 32;
35
36/// Number of workspaces.
37const NUM_WORKSPACES: usize = 4;
38
39/// Width of each workspace button in pixels.
40const WORKSPACE_BUTTON_WIDTH: u32 = 24;
41
42/// Gap between workspace buttons in pixels.
43const WORKSPACE_BUTTON_GAP: u32 = 2;
44
45/// Width of the workspace indicator area (buttons + padding).
46const WORKSPACE_AREA_WIDTH: u32 =
47    NUM_WORKSPACES as u32 * (WORKSPACE_BUTTON_WIDTH + WORKSPACE_BUTTON_GAP) + 8;
48
49// ---------------------------------------------------------------------------
50// Layer-shell types
51// ---------------------------------------------------------------------------
52
53/// Layer-shell layer (from wlr-layer-shell protocol).
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum LayerShellLayer {
56    /// Below all windows
57    Background = 0,
58    /// Below normal windows
59    Bottom = 1,
60    /// Above normal windows (panels, docks)
61    Top = 2,
62    /// Above everything (lock screen, notifications)
63    Overlay = 3,
64}
65
66/// Layer-shell anchor edges (bitmask).
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum LayerShellAnchor {
69    /// No anchor (centered)
70    None = 0,
71    /// Anchored to top edge
72    Top = 1,
73    /// Anchored to bottom edge
74    Bottom = 2,
75    /// Anchored to left edge
76    Left = 4,
77    /// Anchored to right edge
78    Right = 8,
79}
80
81/// Layer-shell surface configuration.
82#[derive(Debug, Clone)]
83pub struct LayerSurfaceConfig {
84    /// The Wayland surface layer
85    pub layer: LayerShellLayer,
86    /// Anchor edges (bitmask of LayerShellAnchor values)
87    pub anchor: u32,
88    /// Exclusive zone: positive = reserve space, 0 = no reservation,
89    /// -1 = extend to edge
90    pub exclusive_zone: i32,
91    /// Margin from anchored edges (top, right, bottom, left)
92    pub margin: (i32, i32, i32, i32),
93    /// Desired width (0 = use anchor width)
94    pub width: u32,
95    /// Desired height (0 = use anchor height)
96    pub height: u32,
97    /// Keyboard interactivity mode
98    pub keyboard_interactivity: bool,
99}
100
101impl LayerSurfaceConfig {
102    /// Create a configuration for a bottom-anchored panel.
103    pub fn bottom_panel(screen_width: u32, height: u32) -> Self {
104        Self {
105            layer: LayerShellLayer::Top,
106            anchor: LayerShellAnchor::Bottom as u32
107                | LayerShellAnchor::Left as u32
108                | LayerShellAnchor::Right as u32,
109            exclusive_zone: height as i32,
110            margin: (0, 0, 0, 0),
111            width: screen_width,
112            height,
113            keyboard_interactivity: false,
114        }
115    }
116}
117
118// ---------------------------------------------------------------------------
119// Panel button
120// ---------------------------------------------------------------------------
121
122/// Panel button representing a window in the taskbar.
123#[derive(Debug, Clone)]
124struct PanelButton {
125    window_id: WindowId,
126    title: String,
127    x: i32,
128    width: u32,
129    focused: bool,
130}
131
132// ---------------------------------------------------------------------------
133// Workspace state
134// ---------------------------------------------------------------------------
135
136/// Workspace indicator state.
137struct WorkspaceState {
138    /// Currently active workspace (0-indexed)
139    active: usize,
140    /// Number of windows on each workspace
141    window_counts: [u32; NUM_WORKSPACES],
142}
143
144impl WorkspaceState {
145    fn new() -> Self {
146        Self {
147            active: 0,
148            window_counts: [0; NUM_WORKSPACES],
149        }
150    }
151}
152
153// ---------------------------------------------------------------------------
154// Panel
155// ---------------------------------------------------------------------------
156
157/// Desktop panel state.
158pub struct Panel {
159    /// Screen width.
160    screen_width: u32,
161    /// Screen height.
162    screen_height: u32,
163    /// Window buttons in the taskbar.
164    buttons: RwLock<Vec<PanelButton>>,
165    /// Clock string (updated periodically).
166    clock_text: RwLock<String>,
167    /// Layer-shell surface ID (if initialized via layer-shell protocol).
168    layer_surface_id: Option<u32>,
169    /// Layer-shell configuration.
170    layer_config: Option<LayerSurfaceConfig>,
171    /// Workspace indicator state.
172    workspaces: RwLock<WorkspaceState>,
173}
174
175impl Panel {
176    /// Create a new panel for the given screen dimensions.
177    pub fn new(screen_width: u32, screen_height: u32) -> Self {
178        Self {
179            screen_width,
180            screen_height,
181            buttons: RwLock::new(Vec::new()),
182            clock_text: RwLock::new(String::from("00:00")),
183            layer_surface_id: None,
184            layer_config: None,
185            workspaces: RwLock::new(WorkspaceState::new()),
186        }
187    }
188
189    /// Get the Y coordinate of the panel (bottom of screen).
190    pub fn y(&self) -> i32 {
191        (self.screen_height - PANEL_HEIGHT) as i32
192    }
193
194    /// Initialize the layer-shell surface for the panel.
195    ///
196    /// This creates a layer-shell surface anchored to the bottom edge of
197    /// the output with an exclusive zone equal to the panel height. The
198    /// compositor will reserve this space and prevent normal windows from
199    /// overlapping.
200    ///
201    /// Returns the layer surface ID, or None if already initialized.
202    pub fn init_layer_surface(&mut self) -> Option<u32> {
203        if self.layer_surface_id.is_some() {
204            return self.layer_surface_id;
205        }
206
207        let config = LayerSurfaceConfig::bottom_panel(self.screen_width, PANEL_HEIGHT);
208
209        // Create the compositor surface via the desktop renderer's helper
210        let (surface_id, _pool_id, _pool_buf_id) =
211            super::renderer::create_app_surface(0, self.y(), self.screen_width, PANEL_HEIGHT);
212
213        // Position the surface at the bottom of the screen
214        crate::desktop::wayland::with_display(|display| {
215            display
216                .wl_compositor
217                .set_surface_position(surface_id, 0, self.y());
218            // Raise to top of z-order (panels above windows)
219            display.wl_compositor.raise_surface(surface_id);
220        });
221
222        crate::println!(
223            "[PANEL] Layer-shell surface initialized: id={}, layer=Top, anchor=Bottom|Left|Right, \
224             exclusive_zone={}",
225            surface_id,
226            PANEL_HEIGHT
227        );
228
229        self.layer_surface_id = Some(surface_id);
230        self.layer_config = Some(config);
231        self.layer_surface_id
232    }
233
234    /// Get the layer-shell surface ID.
235    pub fn layer_surface_id(&self) -> Option<u32> {
236        self.layer_surface_id
237    }
238
239    /// Set the active workspace.
240    pub fn set_active_workspace(&self, index: usize) {
241        if index < NUM_WORKSPACES {
242            self.workspaces.write().active = index;
243        }
244    }
245
246    /// Get the active workspace index.
247    pub fn active_workspace(&self) -> usize {
248        self.workspaces.read().active
249    }
250
251    /// Update workspace window counts from the window manager.
252    pub fn update_workspace_counts(&self) {
253        let windows = with_window_manager(|wm| wm.get_all_windows()).unwrap_or_default();
254        let mut ws = self.workspaces.write();
255
256        // Reset counts
257        for count in ws.window_counts.iter_mut() {
258            *count = 0;
259        }
260
261        // Count visible windows. For now, all windows are on workspace 0
262        // since we don't have multi-workspace support yet.
263        for window in &windows {
264            if window.visible {
265                ws.window_counts[0] += 1;
266            }
267        }
268    }
269
270    /// Handle a click on a workspace button.
271    ///
272    /// Returns the workspace index if a workspace button was clicked.
273    fn handle_workspace_click(&self, x: i32) -> Option<usize> {
274        let start_x = 4i32;
275        for i in 0..NUM_WORKSPACES {
276            let btn_x =
277                start_x + i as i32 * (WORKSPACE_BUTTON_WIDTH as i32 + WORKSPACE_BUTTON_GAP as i32);
278            let btn_end = btn_x + WORKSPACE_BUTTON_WIDTH as i32;
279            if x >= btn_x && x < btn_end {
280                return Some(i);
281            }
282        }
283        None
284    }
285
286    /// Update the panel button list from the window manager.
287    pub fn update_buttons(&self) {
288        let windows = with_window_manager(|wm| wm.get_all_windows()).unwrap_or_default();
289
290        let mut buttons = self.buttons.write();
291        buttons.clear();
292
293        let button_width = 120u32;
294        // Start window buttons after the workspace area
295        let mut x = WORKSPACE_AREA_WIDTH as i32 + 4;
296
297        for window in &windows {
298            if !window.visible {
299                continue;
300            }
301            buttons.push(PanelButton {
302                window_id: window.id,
303                title: String::from(window.title_str()),
304                x,
305                width: button_width,
306                focused: window.focused,
307            });
308            x += button_width as i32 + 4;
309        }
310    }
311
312    /// Update the clock display with real wall-clock date and time.
313    pub fn update_clock(&self) {
314        // Use CMOS RTC on x86_64 for real wall-clock time; fall back to
315        // uptime-based approximation on other architectures.
316        #[cfg(target_arch = "x86_64")]
317        let epoch_secs = crate::arch::x86_64::rtc::current_epoch_secs();
318        #[cfg(not(target_arch = "x86_64"))]
319        let epoch_secs = {
320            let ticks = crate::arch::timer::read_hw_timestamp();
321            ticks / 1_000_000_000
322        };
323
324        // Apply America/New_York timezone (EST = UTC-5)
325        let tz_offset_secs: u64 = 5 * 3600;
326        let local_epoch = epoch_secs.saturating_sub(tz_offset_secs);
327
328        // Convert local epoch seconds to date components
329        let secs_of_day = local_epoch % 86400;
330        let hours = (secs_of_day / 3600) % 24;
331        let minutes = (secs_of_day / 60) % 60;
332
333        // Days since Unix epoch (1970-01-01, a Thursday)
334        let mut remaining_days = (local_epoch / 86400) as u32;
335
336        // Day of week: 1970-01-01 was Thursday (index 4)
337        let day_of_week = (remaining_days + 4) % 7; // 0=Sun..6=Sat
338        let day_name = match day_of_week {
339            0 => "Sun",
340            1 => "Mon",
341            2 => "Tue",
342            3 => "Wed",
343            4 => "Thu",
344            5 => "Fri",
345            6 => "Sat",
346            _ => "???",
347        };
348
349        // Year/month/day from days since epoch
350        let mut year: u32 = 1970;
351        loop {
352            let days_in_year = if is_leap_year(year) { 366 } else { 365 };
353            if remaining_days < days_in_year {
354                break;
355            }
356            remaining_days -= days_in_year;
357            year += 1;
358        }
359
360        let month_days: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
361        let mut month_idx: usize = 0;
362        for (i, &md) in month_days.iter().enumerate() {
363            let days = if i == 1 && is_leap_year(year) { 29 } else { md };
364            if remaining_days < days {
365                month_idx = i;
366                break;
367            }
368            remaining_days -= days;
369            if i == 11 {
370                month_idx = 11;
371            }
372        }
373        let month_day = remaining_days + 1;
374
375        let month = match month_idx {
376            0 => "Jan",
377            1 => "Feb",
378            2 => "Mar",
379            3 => "Apr",
380            4 => "May",
381            5 => "Jun",
382            6 => "Jul",
383            7 => "Aug",
384            8 => "Sep",
385            9 => "Oct",
386            10 => "Nov",
387            11 => "Dec",
388            _ => "???",
389        };
390
391        let mut clock = self.clock_text.write();
392        clock.clear();
393
394        // Format: "Fri Feb 28 14:30"
395        for ch in day_name.chars() {
396            clock.push(ch);
397        }
398        clock.push(' ');
399        for ch in month.chars() {
400            clock.push(ch);
401        }
402        clock.push(' ');
403        if month_day < 10 {
404            clock.push(' ');
405        }
406        for ch in fmt_u64(month_day as u64).chars() {
407            clock.push(ch);
408        }
409        clock.push(' ');
410        if hours < 10 {
411            clock.push('0');
412        }
413        for ch in fmt_u64(hours).chars() {
414            clock.push(ch);
415        }
416        clock.push(':');
417        if minutes < 10 {
418            clock.push('0');
419        }
420        for ch in fmt_u64(minutes).chars() {
421            clock.push(ch);
422        }
423    }
424
425    /// Handle a click on the panel.
426    ///
427    /// Returns the window ID that should be focused, if any.
428    pub fn handle_click(&self, x: i32, _y: i32) -> Option<WindowId> {
429        // Check workspace buttons first
430        if let Some(ws_idx) = self.handle_workspace_click(x) {
431            self.set_active_workspace(ws_idx);
432            return None;
433        }
434
435        // Check window buttons
436        let buttons = self.buttons.read();
437        for button in buttons.iter() {
438            if x >= button.x && x < button.x + button.width as i32 {
439                return Some(button.window_id);
440            }
441        }
442        None
443    }
444
445    /// Render the panel into a pixel buffer.
446    ///
447    /// The buffer is assumed to be `screen_width * PANEL_HEIGHT * 4` bytes
448    /// (BGRA format). Renders workspace indicators, window buttons, and
449    /// the date/time clock.
450    pub fn render(&self, buf: &mut [u8]) {
451        let w = self.screen_width as usize;
452        let h = PANEL_HEIGHT as usize;
453        let stride = w * 4;
454
455        // Dark background (0x2D2D2D)
456        for y in 0..h {
457            for x in 0..w {
458                let offset = y * stride + x * 4;
459                if offset + 3 < buf.len() {
460                    buf[offset] = 0x2D; // B
461                    buf[offset + 1] = 0x2D; // G
462                    buf[offset + 2] = 0x2D; // R
463                    buf[offset + 3] = 0xFF; // A
464                }
465            }
466        }
467
468        // Top border line (subtle highlight)
469        for x in 0..w {
470            let offset = x * 4;
471            if offset + 3 < buf.len() {
472                buf[offset] = 0x44; // B
473                buf[offset + 1] = 0x44; // G
474                buf[offset + 2] = 0x44; // R
475                buf[offset + 3] = 0xFF;
476            }
477        }
478
479        // --- Render workspace indicators ---
480        self.render_workspaces(buf, stride, w, h);
481
482        // --- Render separator after workspace area ---
483        let sep_x = WORKSPACE_AREA_WIDTH as usize;
484        for y in 4..h - 4 {
485            let offset = y * stride + sep_x * 4;
486            if offset + 3 < buf.len() {
487                buf[offset] = 0x55; // B
488                buf[offset + 1] = 0x55; // G
489                buf[offset + 2] = 0x55; // R
490                buf[offset + 3] = 0xFF;
491            }
492        }
493
494        // --- Render window buttons ---
495        let buttons = self.buttons.read();
496        for button in buttons.iter() {
497            let btn_x = button.x as usize;
498            let btn_w = button.width as usize;
499
500            // Button background (lighter if focused)
501            let (br, bg, bb) = if button.focused {
502                (0x50, 0x50, 0x70)
503            } else {
504                (0x40, 0x40, 0x40)
505            };
506
507            for y in 4..h - 4 {
508                for x in btn_x..(btn_x + btn_w).min(w) {
509                    let offset = y * stride + x * 4;
510                    if offset + 3 < buf.len() {
511                        buf[offset] = bb;
512                        buf[offset + 1] = bg;
513                        buf[offset + 2] = br;
514                        buf[offset + 3] = 0xFF;
515                    }
516                }
517            }
518
519            // Focused button underline indicator
520            if button.focused {
521                for x in btn_x..(btn_x + btn_w).min(w) {
522                    let offset = (h - 3) * stride + x * 4;
523                    if offset + 3 < buf.len() {
524                        buf[offset] = 0xDD; // B (accent blue)
525                        buf[offset + 1] = 0x88; // G
526                        buf[offset + 2] = 0x44; // R
527                        buf[offset + 3] = 0xFF;
528                    }
529                }
530            }
531
532            // Render button title (first 14 chars, using 8px font)
533            let title_bytes = button.title.as_bytes();
534            let max_chars = (btn_w / 8).min(14);
535            for (i, &ch) in title_bytes.iter().take(max_chars).enumerate() {
536                render_char_to_buf(buf, stride, btn_x + 4 + i * 8, 10, ch, (0xCC, 0xCC, 0xCC));
537            }
538        }
539
540        // --- Render clock with date on the right side ---
541        let clock = self.clock_text.read();
542        let clock_x = w.saturating_sub(clock.len() * 8 + 12);
543        for (i, &ch) in clock.as_bytes().iter().enumerate() {
544            render_char_to_buf(buf, stride, clock_x + i * 8, 10, ch, (0xBB, 0xBB, 0xBB));
545        }
546    }
547
548    /// Render workspace indicator buttons into the panel buffer.
549    fn render_workspaces(&self, buf: &mut [u8], stride: usize, max_w: usize, h: usize) {
550        let ws = self.workspaces.read();
551        let start_x = 4usize;
552
553        for i in 0..NUM_WORKSPACES {
554            let btn_x =
555                start_x + i * (WORKSPACE_BUTTON_WIDTH as usize + WORKSPACE_BUTTON_GAP as usize);
556            let btn_w = WORKSPACE_BUTTON_WIDTH as usize;
557
558            // Button color: active workspace is highlighted, occupied is dimmer
559            let (br, bg, bb) = if i == ws.active {
560                (0x55, 0x66, 0x99) // Active: blue-ish
561            } else if ws.window_counts[i] > 0 {
562                (0x48, 0x48, 0x48) // Occupied: slightly lighter
563            } else {
564                (0x38, 0x38, 0x38) // Empty: slightly lighter than panel bg
565            };
566
567            // Draw workspace button background
568            for y in 6..h - 6 {
569                for x in btn_x..(btn_x + btn_w).min(max_w) {
570                    let offset = y * stride + x * 4;
571                    if offset + 3 < buf.len() {
572                        buf[offset] = bb;
573                        buf[offset + 1] = bg;
574                        buf[offset + 2] = br;
575                        buf[offset + 3] = 0xFF;
576                    }
577                }
578            }
579
580            // Active workspace underline
581            if i == ws.active {
582                for x in btn_x..(btn_x + btn_w).min(max_w) {
583                    let offset = (h - 4) * stride + x * 4;
584                    if offset + 3 < buf.len() {
585                        buf[offset] = 0xDD; // B (accent blue)
586                        buf[offset + 1] = 0x88; // G
587                        buf[offset + 2] = 0x44; // R
588                        buf[offset + 3] = 0xFF;
589                    }
590                }
591            }
592
593            // Render workspace number (1-4) centered in the button
594            let digit = b'1' + i as u8;
595            let char_x = btn_x + (btn_w / 2).saturating_sub(4);
596            let text_color = if i == ws.active {
597                (0xFF, 0xFF, 0xFF) // White for active
598            } else {
599                (0x99, 0x99, 0x99) // Grey for inactive
600            };
601            render_char_to_buf(buf, stride, char_x, 10, digit, text_color);
602        }
603    }
604}
605
606/// Render a single 8x16 character into a pixel buffer at (px, py).
607fn render_char_to_buf(
608    buf: &mut [u8],
609    stride: usize,
610    px: usize,
611    py: usize,
612    ch: u8,
613    color: (u8, u8, u8),
614) {
615    use crate::graphics::font8x16;
616
617    let glyph = font8x16::glyph(ch);
618    for (row, &bits) in glyph.iter().enumerate() {
619        for col in 0..8 {
620            if (bits >> (7 - col)) & 1 != 0 {
621                let x = px + col;
622                let y = py + row;
623                let offset = y * stride + x * 4;
624                if offset + 3 < buf.len() {
625                    buf[offset] = color.2; // B
626                    buf[offset + 1] = color.1; // G
627                    buf[offset + 2] = color.0; // R
628                    buf[offset + 3] = 0xFF;
629                }
630            }
631        }
632    }
633}
634
635/// Check if a year is a leap year.
636fn is_leap_year(y: u32) -> bool {
637    (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
638}
639
640/// Format a u64 (0-59) as a decimal string (no heap allocation).
641fn fmt_u64(n: u64) -> &'static str {
642    match n {
643        0 => "0",
644        1 => "1",
645        2 => "2",
646        3 => "3",
647        4 => "4",
648        5 => "5",
649        6 => "6",
650        7 => "7",
651        8 => "8",
652        9 => "9",
653        10 => "10",
654        11 => "11",
655        12 => "12",
656        13 => "13",
657        14 => "14",
658        15 => "15",
659        16 => "16",
660        17 => "17",
661        18 => "18",
662        19 => "19",
663        20 => "20",
664        21 => "21",
665        22 => "22",
666        23 => "23",
667        24 => "24",
668        25 => "25",
669        26 => "26",
670        27 => "27",
671        28 => "28",
672        29 => "29",
673        30 => "30",
674        31 => "31",
675        32 => "32",
676        33 => "33",
677        34 => "34",
678        35 => "35",
679        36 => "36",
680        37 => "37",
681        38 => "38",
682        39 => "39",
683        40 => "40",
684        41 => "41",
685        42 => "42",
686        43 => "43",
687        44 => "44",
688        45 => "45",
689        46 => "46",
690        47 => "47",
691        48 => "48",
692        49 => "49",
693        50 => "50",
694        51 => "51",
695        52 => "52",
696        53 => "53",
697        54 => "54",
698        55 => "55",
699        56 => "56",
700        57 => "57",
701        58 => "58",
702        59 => "59",
703        _ => "??",
704    }
705}
706
707/// Global panel instance
708static PANEL: GlobalState<Panel> = GlobalState::new();
709
710/// Initialize the desktop panel.
711pub fn init(screen_width: u32, screen_height: u32) -> Result<(), KernelError> {
712    PANEL
713        .init(Panel::new(screen_width, screen_height))
714        .map_err(|_| KernelError::InvalidState {
715            expected: "uninitialized",
716            actual: "initialized",
717        })?;
718
719    crate::println!(
720        "[PANEL] Desktop panel initialized ({}x{}, layer-shell ready)",
721        screen_width,
722        PANEL_HEIGHT
723    );
724    Ok(())
725}
726
727/// Execute a function with the panel.
728pub fn with_panel<R, F: FnOnce(&Panel) -> R>(f: F) -> Option<R> {
729    PANEL.with(f)
730}