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

veridian_kernel/desktop/
renderer.rs

1//! Desktop Renderer
2//!
3//! Connects the Wayland compositor's back-buffer to the hardware framebuffer.
4//! Creates the initial desktop scene (background gradient, panel, terminal
5//! placeholder) and runs the compositing loop.
6//!
7//! Includes server-side decoration (SSD) types and rendering functions that
8//! define the complete decoration API surface. Some items (hit-testing, button
9//! rendering) are not yet wired into the event dispatch path.
10#![allow(dead_code)]
11
12use alloc::vec;
13use core::sync::atomic::{AtomicU32, Ordering};
14
15use crate::graphics::{
16    cursor,
17    fbcon::{self, FbPixelFormat},
18};
19
20/// Global surface/pool ID counters for compositor objects.
21static NEXT_SURFACE_ID: AtomicU32 = AtomicU32::new(2000);
22static NEXT_POOL_ID: AtomicU32 = AtomicU32::new(200);
23
24/// Initialize the desktop environment and start the compositor.
25///
26/// This is the main entry point called by the `startgui` shell command.
27/// It:
28/// 1. Gets framebuffer hardware info from fbcon
29/// 2. Initializes the desktop subsystem (Wayland, window manager, etc.)
30/// 3. Creates the initial desktop scene
31/// 4. Enters the render loop
32pub fn start_desktop() {
33    let hw = match fbcon::get_hw_info() {
34        Some(info) => info,
35        None => {
36            crate::println!("[DESKTOP] No framebuffer available -- cannot start GUI");
37            return;
38        }
39    };
40
41    crate::println!(
42        "[DESKTOP] Starting GUI on {}x{} framebuffer (stride={}, bpp={})",
43        hw.width,
44        hw.height,
45        hw.stride,
46        hw.bpp,
47    );
48
49    // Initialize the desktop subsystem (wayland, window manager, fonts, etc.)
50    // If already initialized during bootstrap, that's fine -- proceed to render.
51    match crate::desktop::init() {
52        Ok(()) => {}
53        Err(crate::error::KernelError::InvalidState { .. }) => {
54            crate::println!("[DESKTOP] Desktop subsystem already initialized, proceeding");
55        }
56        Err(e) => {
57            crate::println!("[DESKTOP] Failed to initialize desktop: {:?}", e);
58            return;
59        }
60    }
61
62    // Configure the Wayland compositor with framebuffer dimensions
63    crate::desktop::wayland::with_display(|display| {
64        display
65            .wl_compositor
66            .set_output_size(hw.width as u32, hw.height as u32);
67    });
68
69    crate::println!(
70        "[DESKTOP] Compositor configured for {}x{}",
71        hw.width,
72        hw.height,
73    );
74
75    // Create the initial desktop scene
76    let mut state = create_desktop_scene(hw.width as u32, hw.height as u32);
77
78    // Disable fbcon text output -- the compositor takes over the framebuffer
79    fbcon::disable_output();
80
81    // Clear the framebuffer to black before first composite
82    // SAFETY: hw.fb_ptr is valid for stride * height bytes.
83    unsafe {
84        core::ptr::write_bytes(hw.fb_ptr, 0, hw.stride * hw.height);
85    }
86
87    crate::serial::_serial_print(format_args!("[DESKTOP] Entering compositor render loop\n"));
88
89    // Switch keyboard to GUI mode: arrow keys emit single-byte codes (0x80+)
90    // instead of ANSI escape sequences, preventing the 0x1B ESC prefix from
91    // triggering the GUI exit guard.
92    crate::drivers::keyboard::set_gui_mode(true);
93
94    // Render loop: composite -> blit to framebuffer -> poll input -> repeat
95    render_loop(&hw, &mut state);
96
97    // Restore keyboard to shell mode (ANSI escape sequences for arrows)
98    crate::drivers::keyboard::set_gui_mode(false);
99
100    // Clear the entire framebuffer to remove GUI artifacts before returning
101    // to text console mode.
102    // SAFETY: hw.fb_ptr is valid for stride * height bytes.
103    unsafe {
104        core::ptr::write_bytes(hw.fb_ptr, 0, hw.stride * hw.height);
105    }
106
107    // Re-enable fbcon and force a full repaint of the text console
108    fbcon::mark_all_dirty_and_flush();
109    crate::println!("[DESKTOP] GUI stopped, returning to text console");
110}
111
112/// Info for a single desktop application: WM window ID + compositor surface ID.
113struct AppInfo {
114    wid: u32,
115    surface_id: u32,
116}
117
118/// Type of dynamically-spawned GUI application.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120enum AppKind {
121    ImageViewer,
122    Settings,
123    MediaPlayer,
124    SystemMonitor,
125    Browser,
126    PdfViewer,
127    Calculator,
128}
129
130/// A dynamically spawned application window.
131struct DynamicApp {
132    kind: AppKind,
133    wid: u32,
134    surface_id: u32,
135    pool_id: u32,
136    pool_buf_id: u32,
137    width: u32,
138    height: u32,
139}
140
141/// Calculator application state for integer arithmetic.
142struct CalculatorState {
143    display_value: i64,
144    accumulator: i64,
145    pending_op: Option<u8>,
146    input_started: bool,
147    error: bool,
148}
149
150impl CalculatorState {
151    fn new() -> Self {
152        Self {
153            display_value: 0,
154            accumulator: 0,
155            pending_op: None,
156            input_started: false,
157            error: false,
158        }
159    }
160
161    fn handle_key(&mut self, key: u8) {
162        if self.error && key != b'C' {
163            return;
164        }
165        match key {
166            b'0'..=b'9' => {
167                let digit = (key - b'0') as i64;
168                if !self.input_started {
169                    self.display_value = digit;
170                    self.input_started = true;
171                } else {
172                    self.display_value =
173                        self.display_value.saturating_mul(10).saturating_add(digit);
174                }
175            }
176            b'+' | b'-' | b'*' | b'/' => {
177                self.evaluate_pending();
178                self.accumulator = self.display_value;
179                self.pending_op = Some(key);
180                self.input_started = false;
181            }
182            b'=' => {
183                self.evaluate_pending();
184                self.pending_op = None;
185                self.input_started = false;
186            }
187            b'C' => {
188                *self = Self::new();
189            }
190            b'<' => {
191                self.display_value /= 10;
192            }
193            _ => {}
194        }
195    }
196
197    fn evaluate_pending(&mut self) {
198        if let Some(op) = self.pending_op {
199            let result = match op {
200                b'+' => self.accumulator.checked_add(self.display_value),
201                b'-' => self.accumulator.checked_sub(self.display_value),
202                b'*' => self.accumulator.checked_mul(self.display_value),
203                b'/' => {
204                    if self.display_value == 0 {
205                        self.error = true;
206                        self.display_value = 0;
207                        return;
208                    }
209                    self.accumulator.checked_div(self.display_value)
210                }
211                _ => Some(self.display_value),
212            };
213            match result {
214                Some(v) => self.display_value = v,
215                None => {
216                    self.error = true;
217                    self.display_value = 0;
218                }
219            }
220            self.accumulator = self.display_value;
221        }
222    }
223}
224
225/// Desktop runtime state: application windows + Phase 7 overlay modules.
226struct DesktopState {
227    // Existing app windows
228    terminal: AppInfo,
229    file_manager: AppInfo,
230    text_editor: AppInfo,
231    panel_surface_id: u32,
232    panel_pool_id: u32,
233    panel_pool_buf_id: u32,
234
235    // Phase 7 overlay modules (owned, no global state needed)
236    app_switcher: crate::desktop::app_switcher::AppSwitcher,
237    screen_locker: crate::desktop::screen_lock::ScreenLocker,
238    animation_mgr: crate::desktop::animation::AnimationManager,
239
240    // Settings app instance (owned, renders full UI)
241    settings_app: crate::desktop::settings::SettingsApp,
242
243    // Image viewer instance (owned, renders full UI)
244    image_viewer: crate::desktop::image_viewer::ImageViewer,
245
246    // Calculator state (owned, integer arithmetic)
247    calculator: CalculatorState,
248
249    // Theme engine for color scheme management
250    theme: crate::desktop::desktop_ext::theme::ThemeManager,
251
252    // Clipboard manager for copy/paste
253    clipboard: crate::desktop::desktop_ext::clipboard::ClipboardManager,
254
255    // Drag-and-drop manager
256    dnd: crate::desktop::desktop_ext::dnd::DndManager,
257
258    // Desktop icon grid
259    icon_grid: crate::desktop::desktop_icons::IconGrid,
260
261    // Browser engine instance
262    browser: Option<crate::browser::browser_main::Browser>,
263
264    // PDF viewer page index
265    pdf_page_index: usize,
266
267    // Dynamic apps (spawned from launcher, closeable)
268    dynamic_apps: alloc::vec::Vec<DynamicApp>,
269
270    // Input state
271    frame_count: u64,
272    drag: Option<DragState>,
273    prev_focused: Option<u32>,
274}
275
276impl DesktopState {
277    /// Look up the compositor surface ID for a given WM window ID.
278    fn surface_for_window(&self, wid: u32) -> Option<u32> {
279        if wid > 0 && wid == self.terminal.wid {
280            return Some(self.terminal.surface_id);
281        }
282        if wid > 0 && wid == self.file_manager.wid {
283            return Some(self.file_manager.surface_id);
284        }
285        if wid > 0 && wid == self.text_editor.wid {
286            return Some(self.text_editor.surface_id);
287        }
288        for app in &self.dynamic_apps {
289            if wid > 0 && wid == app.wid {
290                return Some(app.surface_id);
291            }
292        }
293        None
294    }
295}
296
297/// Create the initial desktop scene: background gradient, real apps, and panel.
298fn create_desktop_scene(width: u32, height: u32) -> DesktopState {
299    // --- Background surface ---
300    let bg_surface_id = 1000;
301    crate::desktop::wayland::with_display(|display| {
302        let _ = display.wl_compositor.create_surface(bg_surface_id);
303        display
304            .wl_compositor
305            .set_surface_position(bg_surface_id, 0, 0);
306    });
307
308    let pool_size = (width as usize) * (height as usize) * 4;
309    let mut bg_pixels = vec![0u8; pool_size];
310    paint_gradient_background(&mut bg_pixels, width as usize, height as usize);
311
312    // Render desktop icons into the background surface so they appear behind
313    // all windows naturally via compositor z-order.
314    let panel_h = crate::desktop::panel::PANEL_HEIGHT;
315    let icon_grid = create_default_icon_grid(width, height - panel_h);
316    render_icons_into_bgra(&icon_grid, &mut bg_pixels, width as usize, height as usize);
317
318    let pool_id = 100;
319    let mut pool = crate::desktop::wayland::buffer::WlShmPool::new(pool_id, 0, pool_size);
320    pool.write_data(0, &bg_pixels);
321    let buf_id = pool
322        .create_buffer(
323            0,
324            width,
325            height,
326            width * 4,
327            crate::graphics::PixelFormat::Xrgb8888,
328        )
329        .unwrap_or(0);
330    crate::desktop::wayland::buffer::register_pool(pool);
331
332    let bg_buffer = crate::desktop::wayland::buffer::Buffer::from_pool(
333        1,
334        pool_id,
335        buf_id,
336        width,
337        height,
338        width * 4,
339        crate::graphics::PixelFormat::Xrgb8888,
340    );
341    crate::desktop::wayland::with_display(|display| {
342        display
343            .wl_compositor
344            .with_surface_mut(bg_surface_id, |surface| {
345                surface.attach_buffer(bg_buffer.clone());
346                surface.damage_full();
347                let _ = surface.commit();
348            });
349        display.wl_compositor.request_composite();
350    });
351
352    // --- Initialize panel ---
353    let _ = crate::desktop::panel::init(width, height);
354    let (panel_surface_id, panel_pool_id, panel_pool_buf_id) =
355        create_app_surface(0, (height - panel_h) as i32, width, panel_h);
356
357    // --- Create real applications ---
358    // Terminal: 640x384 (80cols x 24rows x 16px)
359    let (terminal_wid, terminal_sid) =
360        crate::desktop::terminal::with_terminal_manager(|tm| match tm.create_terminal(640, 384) {
361            Ok(idx) => (
362                tm.get_window_id(idx).unwrap_or(0),
363                tm.get_surface_id(idx).unwrap_or(0),
364            ),
365            Err(_) => (0, 0),
366        })
367        .unwrap_or((0, 0));
368
369    // File manager: 640x480
370    let (file_manager_wid, file_manager_sid) =
371        if crate::desktop::file_manager::create_file_manager().is_ok() {
372            crate::desktop::file_manager::with_file_manager(|fm| {
373                let r = fm.read();
374                (r.window_id(), r.surface_id())
375            })
376            .unwrap_or((0, 0))
377        } else {
378            (0, 0)
379        };
380
381    // Text editor: 800x600
382    let (text_editor_wid, text_editor_sid) =
383        if crate::desktop::text_editor::create_text_editor(None).is_ok() {
384            crate::desktop::text_editor::with_text_editor(|te| {
385                let r = te.read();
386                (r.window_id(), r.surface_id())
387            })
388            .unwrap_or((0, 0))
389        } else {
390            (0, 0)
391        };
392
393    // Set window titles so the panel taskbar shows meaningful labels
394    crate::desktop::window_manager::with_window_manager(|wm| {
395        wm.set_window_title(terminal_wid, "Terminal");
396        wm.set_window_title(file_manager_wid, "Files");
397        wm.set_window_title(text_editor_wid, "Text Editor");
398    });
399
400    // Write welcome message to terminal so it's not a blank black window
401    crate::desktop::terminal::with_terminal_manager(|tm| {
402        tm.write_welcome(0);
403    });
404
405    // Focus the terminal by default and sync compositor z_order
406    if terminal_wid > 0 {
407        crate::desktop::window_manager::with_window_manager(|wm| {
408            let _ = wm.focus_window(terminal_wid);
409        });
410        // Raise the terminal surface in the compositor, then ensure panel stays on top
411        crate::desktop::wayland::with_display(|display| {
412            display.wl_compositor.raise_surface(terminal_sid);
413            display.wl_compositor.raise_surface(panel_surface_id);
414        });
415    }
416
417    crate::serial::_serial_print(format_args!(
418        "[DESKTOP] Desktop scene created: bg + terminal({}/{}) + files({}/{}) + editor({}/{}) + \
419         panel\n",
420        terminal_wid,
421        terminal_sid,
422        file_manager_wid,
423        file_manager_sid,
424        text_editor_wid,
425        text_editor_sid
426    ));
427
428    // Send a welcome notification to demonstrate the notification system
429    crate::desktop::notification::notify(
430        "VeridianOS Desktop",
431        "Welcome to VeridianOS v0.25.2",
432        crate::desktop::notification::NotificationUrgency::Normal,
433        "desktop",
434    );
435
436    DesktopState {
437        terminal: AppInfo {
438            wid: terminal_wid,
439            surface_id: terminal_sid,
440        },
441        file_manager: AppInfo {
442            wid: file_manager_wid,
443            surface_id: file_manager_sid,
444        },
445        text_editor: AppInfo {
446            wid: text_editor_wid,
447            surface_id: text_editor_sid,
448        },
449        panel_surface_id,
450        panel_pool_id,
451        panel_pool_buf_id,
452        app_switcher: crate::desktop::app_switcher::AppSwitcher::new(),
453        screen_locker: crate::desktop::screen_lock::ScreenLocker::new(
454            width as usize,
455            height as usize,
456        ),
457        animation_mgr: crate::desktop::animation::AnimationManager::new(),
458        settings_app: crate::desktop::settings::SettingsApp::new(),
459        image_viewer: crate::desktop::image_viewer::ImageViewer::new(),
460        calculator: CalculatorState::new(),
461        theme: crate::desktop::desktop_ext::theme::ThemeManager::new(),
462        clipboard: crate::desktop::desktop_ext::clipboard::ClipboardManager::new(),
463        dnd: crate::desktop::desktop_ext::dnd::DndManager::new(),
464        icon_grid,
465        browser: None,
466        pdf_page_index: 0,
467        dynamic_apps: alloc::vec::Vec::new(),
468        frame_count: 0,
469        drag: None,
470        prev_focused: None,
471    }
472}
473
474/// Create a default icon grid with common application shortcuts.
475fn create_default_icon_grid(
476    desktop_width: u32,
477    desktop_height: u32,
478) -> crate::desktop::desktop_icons::IconGrid {
479    use crate::desktop::desktop_icons::{DesktopIcon, IconGrid};
480
481    let mut grid = IconGrid::new(desktop_width, desktop_height);
482
483    let apps = [
484        ("Terminal", "terminal", 0xFF2ECC71u32),
485        ("Files", "files", 0xFF3498DB),
486        ("Editor", "editor", 0xFFE67E22),
487        ("Settings", "settings", 0xFF9B59B6),
488        ("Browser", "browser", 0xFF1ABC9C),
489    ];
490
491    for (name, _exec, color) in &apps {
492        let mut icon = DesktopIcon::new(name, 0, 0);
493        // Set a solid colour for the icon
494        let pixel_count = (crate::desktop::desktop_icons::ICON_SIZE
495            * crate::desktop::desktop_icons::ICON_SIZE) as usize;
496        let data = alloc::vec![*color; pixel_count];
497        icon.set_icon_data(&data);
498        grid.add_icon(icon);
499    }
500
501    grid.arrange();
502    grid
503}
504
505/// Render desktop icons (bitmap + label) into a BGRA u8 pixel buffer.
506///
507/// Each icon's 16x16 ARGB bitmap is converted to BGRA on the fly, and the
508/// icon name is drawn below using the 8x16 VGA font.
509fn render_icons_into_bgra(
510    grid: &crate::desktop::desktop_icons::IconGrid,
511    buf: &mut [u8],
512    buf_width: usize,
513    buf_height: usize,
514) {
515    let icon_sz = crate::desktop::desktop_icons::ICON_SIZE as i32;
516    let bw = buf_width as i32;
517    let bh = buf_height as i32;
518
519    for icon in &grid.icons {
520        // Draw icon bitmap (ARGB -> BGRA)
521        for row in 0..icon_sz {
522            let dy = icon.y + row;
523            if dy < 0 || dy >= bh {
524                continue;
525            }
526            for col in 0..icon_sz {
527                let dx = icon.x + col;
528                if dx < 0 || dx >= bw {
529                    continue;
530                }
531                let src = icon.icon_data[(row * icon_sz + col) as usize];
532                let off = ((dy * bw + dx) as usize) * 4;
533                if off + 3 < buf.len() {
534                    buf[off] = (src & 0xFF) as u8; // B
535                    buf[off + 1] = ((src >> 8) & 0xFF) as u8; // G
536                    buf[off + 2] = ((src >> 16) & 0xFF) as u8; // R
537                    buf[off + 3] = 0xFF; // A
538                }
539            }
540        }
541
542        // Draw label below icon
543        let label_x = icon.x.max(0) as usize;
544        let label_y = (icon.y + icon_sz + 2).max(0) as usize;
545        let name_bytes = icon.name.as_bytes();
546        let label_len = name_bytes.len().min(8);
547        for (ci, &ch) in name_bytes[..label_len].iter().enumerate() {
548            draw_char_into_buffer(buf, buf_width, ch, label_x + ci * 8, label_y, 0xCCCCCC);
549        }
550    }
551}
552
553/// Draw a string into a BGRA pixel buffer at (px, py) with the given color.
554///
555/// Uses the 8x16 VGA font. Characters are spaced 8 pixels apart horizontally.
556/// CJK wide characters (detected via `cjk::char_width`) advance 16px instead of
557/// 8px.
558pub fn draw_string_into_buffer(
559    buf: &mut [u8],
560    buf_width: usize,
561    text: &[u8],
562    px: usize,
563    py: usize,
564    color: u32,
565) {
566    let mut cursor_x = px;
567    for &ch in text.iter() {
568        let w = crate::desktop::desktop_ext::cjk::char_width(ch as char);
569        if w == 0 {
570            continue; // skip zero-width / combining marks
571        }
572        draw_char_into_buffer(buf, buf_width, ch, cursor_x, py, color);
573        cursor_x += (w as usize) * 8;
574    }
575}
576
577/// Draw a single 8x16 character into a BGRA pixel buffer.
578pub fn draw_char_into_buffer(
579    buf: &mut [u8],
580    buf_width: usize,
581    ch: u8,
582    px: usize,
583    py: usize,
584    color: u32,
585) {
586    let glyph = crate::graphics::font8x16::glyph(ch);
587    let r = ((color >> 16) & 0xFF) as u8;
588    let g = ((color >> 8) & 0xFF) as u8;
589    let b = (color & 0xFF) as u8;
590
591    for (row, &bits) in glyph.iter().enumerate() {
592        for col in 0..8 {
593            if (bits >> (7 - col)) & 1 != 0 {
594                let x = px + col;
595                let y = py + row;
596                let offset = (y * buf_width + x) * 4;
597                if offset + 3 < buf.len() {
598                    buf[offset] = b;
599                    buf[offset + 1] = g;
600                    buf[offset + 2] = r;
601                    buf[offset + 3] = 0xFF;
602                }
603            }
604        }
605    }
606}
607
608/// Create a compositor surface backed by a SHM pool + buffer.
609///
610/// Returns `(surface_id, pool_id, pool_buf_id)` for later use with
611/// `update_surface_pixels`.
612pub fn create_app_surface(x: i32, y: i32, w: u32, h: u32) -> (u32, u32, u32) {
613    let surface_id = NEXT_SURFACE_ID.fetch_add(1, Ordering::Relaxed);
614    let pool_id = NEXT_POOL_ID.fetch_add(1, Ordering::Relaxed);
615
616    // Create compositor surface
617    crate::desktop::wayland::with_display(|display| {
618        let _ = display.wl_compositor.create_surface(surface_id);
619        display.wl_compositor.set_surface_position(surface_id, x, y);
620    });
621
622    // Create SHM pool
623    let pool_size = (w as usize) * (h as usize) * 4;
624    let mut pool = crate::desktop::wayland::buffer::WlShmPool::new(pool_id, 0, pool_size);
625
626    // Create buffer in pool
627    let pool_buf_id = pool
628        .create_buffer(0, w, h, w * 4, crate::graphics::PixelFormat::Xrgb8888)
629        .unwrap_or(0);
630
631    crate::desktop::wayland::buffer::register_pool(pool);
632
633    // Attach an initial buffer to the surface
634    let buffer = crate::desktop::wayland::buffer::Buffer::from_pool(
635        surface_id + 1000,
636        pool_id,
637        pool_buf_id,
638        w,
639        h,
640        w * 4,
641        crate::graphics::PixelFormat::Xrgb8888,
642    );
643
644    crate::desktop::wayland::with_display(|display| {
645        display
646            .wl_compositor
647            .with_surface_mut(surface_id, |surface| {
648                surface.attach_buffer(buffer.clone());
649                surface.damage_full();
650                let _ = surface.commit();
651            });
652    });
653
654    (surface_id, pool_id, pool_buf_id)
655}
656
657/// Update the pixel data for an app surface and request recomposite.
658///
659/// `pixels` must be exactly `w * h * 4` bytes (BGRA).
660pub fn update_surface_pixels(surface_id: u32, pool_id: u32, pool_buf_id: u32, pixels: &[u8]) {
661    // Write pixel data into pool backing memory
662    crate::desktop::wayland::buffer::with_pool_mut(pool_id, |pool| {
663        pool.write_buffer_pixels(pool_buf_id, pixels);
664    });
665
666    // Mark surface as damaged and request recomposite
667    crate::desktop::wayland::with_display(|display| {
668        display
669            .wl_compositor
670            .with_surface_mut(surface_id, |surface| {
671                surface.damage_full();
672                let _ = surface.commit();
673            });
674        display.wl_compositor.request_composite();
675    });
676}
677
678/// Paint a gradient background into a BGRA pixel buffer.
679fn paint_gradient_background(buf: &mut [u8], width: usize, height: usize) {
680    for y in 0..height {
681        // Vertical gradient from dark blue-grey (#2D3436) to darker (#1a1a2e)
682        // Using integer math (fixed-point 8.8) to avoid soft-float overhead
683        let t256 = (y * 256) / height; // 0..255
684        let inv_t = 256 - t256;
685        let r = ((0x2D * inv_t + 0x1a * t256) / 256) as u8;
686        let g = ((0x34 * inv_t + 0x1a * t256) / 256) as u8;
687        let b = ((0x36 * inv_t + 0x2e * t256) / 256) as u8;
688
689        for x in 0..width {
690            let offset = (y * width + x) * 4;
691            buf[offset] = b; // B
692            buf[offset + 1] = g; // G
693            buf[offset + 2] = r; // R
694            buf[offset + 3] = 0xFF; // A
695        }
696    }
697
698    // Draw a centered "VeridianOS" title
699    let title = b"VeridianOS Desktop Environment";
700    let title_x = (width - title.len() * 8) / 2;
701    let title_y = height / 2 - 20;
702    for (i, &ch) in title.iter().enumerate() {
703        draw_char_into_buffer(buf, width, ch, title_x + i * 8, title_y, 0xAAAAAA);
704    }
705
706    // Draw subtitle
707    let sub = b"Wayland Compositor Active";
708    let sub_x = (width - sub.len() * 8) / 2;
709    let sub_y = height / 2 + 4;
710    for (i, &ch) in sub.iter().enumerate() {
711        draw_char_into_buffer(buf, width, ch, sub_x + i * 8, sub_y, 0x666666);
712    }
713}
714
715/// Translate a raw input_event::InputEvent to a window_manager::InputEvent.
716fn translate_input_event(
717    raw: &crate::drivers::input_event::InputEvent,
718    mouse_x: i32,
719    mouse_y: i32,
720) -> Option<crate::desktop::window_manager::InputEvent> {
721    use crate::{desktop::window_manager::InputEvent as WmEvent, drivers::input_event::*};
722
723    match raw.event_type {
724        EV_KEY => {
725            let code = raw.code;
726            let pressed = raw.value != 0;
727
728            // Mouse buttons (BTN_LEFT=0x110, BTN_RIGHT=0x111, BTN_MIDDLE=0x112)
729            if (BTN_LEFT..=BTN_MIDDLE).contains(&code) {
730                let button = (code - BTN_LEFT) as u8;
731                return Some(WmEvent::MouseButton {
732                    button,
733                    pressed,
734                    x: mouse_x,
735                    y: mouse_y,
736                });
737            }
738
739            // Keyboard: code is the decoded ASCII byte from the PS/2 driver
740            if pressed {
741                // Map some codes to characters
742                let ch = if code < 0x80 {
743                    code as u8 as char
744                } else {
745                    '\0'
746                };
747                Some(WmEvent::KeyPress {
748                    scancode: code as u8,
749                    character: ch,
750                })
751            } else {
752                Some(WmEvent::KeyRelease {
753                    scancode: code as u8,
754                })
755            }
756        }
757        EV_REL => {
758            // Mouse movement: we use absolute cursor position, not relative deltas,
759            // for the WM events. Movement updates happen in cursor_position().
760            // Only emit MouseMove if we see REL_X (to avoid double events for X+Y).
761            if raw.code == REL_X {
762                Some(WmEvent::MouseMove {
763                    x: mouse_x,
764                    y: mouse_y,
765                })
766            } else {
767                None
768            }
769        }
770        _ => None,
771    }
772}
773
774/// Title bar height in pixels. Clicks in this region initiate window drag.
775const TITLE_BAR_HEIGHT: i32 = 28;
776
777/// Drag state for window movement.
778struct DragState {
779    /// WM window ID being dragged
780    wid: u32,
781    /// Compositor surface ID of the dragged window
782    surface_id: u32,
783    /// Offset from window top-left to mouse grab point
784    offset_x: i32,
785    offset_y: i32,
786}
787
788/// Raise a focused window's compositor surface (and keep panel on top).
789fn sync_compositor_focus(state: &DesktopState, focused_wid: u32) {
790    if let Some(surface_id) = state.surface_for_window(focused_wid) {
791        crate::desktop::wayland::with_display(|display| {
792            display.wl_compositor.raise_surface(surface_id);
793            // Panel must always be the topmost surface
794            display.wl_compositor.raise_surface(state.panel_surface_id);
795        });
796    }
797}
798
799/// Spawn a dynamic app: create WM window + compositor surface, return index in
800/// dynamic_apps.
801fn spawn_dynamic_app(
802    state: &mut DesktopState,
803    kind: AppKind,
804    title: &str,
805    w: u32,
806    h: u32,
807) -> Option<usize> {
808    // Check if one of this kind already exists
809    if state.dynamic_apps.iter().any(|a| a.kind == kind) {
810        return None;
811    }
812
813    let title_bar_h = 28u32;
814    let wid = crate::desktop::window_manager::with_window_manager(|wm| {
815        wm.create_window(100, 80, w, h + title_bar_h, 0)
816    })
817    .and_then(|r| r.ok())?;
818
819    // Surface covers full window area (title bar + content)
820    let (surface_id, pool_id, pool_buf_id) = create_app_surface(100, 80, w, h + title_bar_h);
821
822    crate::desktop::window_manager::with_window_manager(|wm| {
823        wm.set_window_title(wid, title);
824        let _ = wm.focus_window(wid);
825    });
826
827    // Raise in compositor
828    crate::desktop::wayland::with_display(|display| {
829        display.wl_compositor.raise_surface(surface_id);
830        display.wl_compositor.raise_surface(state.panel_surface_id);
831    });
832
833    state.dynamic_apps.push(DynamicApp {
834        kind,
835        wid,
836        surface_id,
837        pool_id,
838        pool_buf_id,
839        width: w,
840        height: h,
841    });
842
843    Some(state.dynamic_apps.len() - 1)
844}
845
846/// Close a dynamic app by WM window ID.
847fn close_dynamic_app(state: &mut DesktopState, wid: u32) {
848    if let Some(pos) = state.dynamic_apps.iter().position(|a| a.wid == wid) {
849        let app = state.dynamic_apps.remove(pos);
850        // Unmap and destroy surface
851        crate::desktop::wayland::with_display(|display| {
852            display
853                .wl_compositor
854                .set_surface_mapped(app.surface_id, false);
855        });
856        crate::desktop::window_manager::with_window_manager(|wm| {
857            let _ = wm.destroy_window(app.wid);
858        });
859    }
860}
861
862/// Close any window (dynamic or static) by WM window ID.
863fn close_any_window(state: &mut DesktopState, wid: u32) {
864    // Dynamic apps: use existing close path
865    if state.dynamic_apps.iter().any(|a| a.wid == wid) {
866        close_dynamic_app(state, wid);
867        return;
868    }
869    // Static apps: unmap surface, destroy WM window, zero the wid
870    if wid == state.terminal.wid {
871        crate::desktop::wayland::with_display(|display| {
872            display
873                .wl_compositor
874                .set_surface_mapped(state.terminal.surface_id, false);
875        });
876        crate::desktop::window_manager::with_window_manager(|wm| {
877            let _ = wm.destroy_window(wid);
878        });
879        state.terminal.wid = 0;
880    } else if wid == state.file_manager.wid {
881        crate::desktop::wayland::with_display(|display| {
882            display
883                .wl_compositor
884                .set_surface_mapped(state.file_manager.surface_id, false);
885        });
886        crate::desktop::window_manager::with_window_manager(|wm| {
887            let _ = wm.destroy_window(wid);
888        });
889        state.file_manager.wid = 0;
890    } else if wid == state.text_editor.wid {
891        crate::desktop::wayland::with_display(|display| {
892            display
893                .wl_compositor
894                .set_surface_mapped(state.text_editor.surface_id, false);
895        });
896        crate::desktop::window_manager::with_window_manager(|wm| {
897            let _ = wm.destroy_window(wid);
898        });
899        state.text_editor.wid = 0;
900    }
901}
902
903/// Draw a title bar (background + title text + close button) into a BGRA
904/// surface pixel buffer.  The title bar occupies the top 28 rows of the
905/// buffer.  The window's title and focus state are read from the WM.
906pub fn draw_title_bar_into_surface(pixels: &mut [u8], width: usize, _total_h: usize, wid: u32) {
907    let cfg = DecorationConfig::default_config();
908    let tbh = cfg.title_bar_height as usize;
909
910    // Look up window title and focus state
911    let (title_buf, title_len, focused) =
912        crate::desktop::window_manager::with_window_manager(|wm| {
913            wm.get_window(wid)
914                .map(|w| (w.title, w.title_len, w.focused))
915        })
916        .flatten()
917        .unwrap_or(([0u8; 64], 0, false));
918
919    let bg_argb = if focused {
920        cfg.title_bg_focused
921    } else {
922        cfg.title_bg_unfocused
923    };
924    // Convert ARGB -> BGRA components
925    let bg_r = ((bg_argb >> 16) & 0xFF) as u8;
926    let bg_g = ((bg_argb >> 8) & 0xFF) as u8;
927    let bg_b = (bg_argb & 0xFF) as u8;
928
929    // Fill title bar background
930    for y in 0..tbh {
931        for x in 0..width {
932            let off = (y * width + x) * 4;
933            if off + 3 < pixels.len() {
934                pixels[off] = bg_b;
935                pixels[off + 1] = bg_g;
936                pixels[off + 2] = bg_r;
937                pixels[off + 3] = 0xFF;
938            }
939        }
940    }
941
942    // Draw title text (vertically centered in title bar)
943    let text_color = cfg.title_text_color & 0x00FF_FFFF; // strip alpha for draw helper
944    let text_y = (tbh.saturating_sub(16)) / 2;
945    for (ci, &ch) in title_buf[..title_len].iter().enumerate() {
946        draw_char_into_buffer(pixels, width, ch, 8 + ci * 8, text_y, text_color);
947    }
948
949    // Draw close button (red square with white X) at top-right
950    let btn_sz = 16usize;
951    let btn_x = width.saturating_sub(22);
952    let btn_y = (tbh.saturating_sub(btn_sz)) / 2;
953    let close_r: u8 = 0xE7;
954    let close_g: u8 = 0x4C;
955    let close_b: u8 = 0x3C;
956    for dy in 0..btn_sz {
957        for dx in 0..btn_sz {
958            let off = ((btn_y + dy) * width + btn_x + dx) * 4;
959            if off + 3 < pixels.len() {
960                pixels[off] = close_b;
961                pixels[off + 1] = close_g;
962                pixels[off + 2] = close_r;
963                pixels[off + 3] = 0xFF;
964            }
965        }
966    }
967    // White X (2px thick diagonals)
968    for i in 0..14usize {
969        let coords = [
970            (btn_x + 1 + i, btn_y + 1 + i),
971            (btn_x + 2 + i, btn_y + 1 + i),
972            (btn_x + 14 - i, btn_y + 1 + i),
973            (btn_x + 13 - i, btn_y + 1 + i),
974        ];
975        for (px, py) in coords {
976            let off = (py * width + px) * 4;
977            if off + 3 < pixels.len() {
978                pixels[off] = 0xFF;
979                pixels[off + 1] = 0xFF;
980                pixels[off + 2] = 0xFF;
981                pixels[off + 3] = 0xFF;
982            }
983        }
984    }
985}
986
987/// Focus an existing dynamic app of the given kind, or return false if not
988/// found.
989fn focus_dynamic_app(state: &DesktopState, kind: AppKind) -> bool {
990    if let Some(app) = state.dynamic_apps.iter().find(|a| a.kind == kind) {
991        let wid = app.wid;
992        crate::desktop::window_manager::with_window_manager(|wm| {
993            let _ = wm.focus_window(wid);
994        });
995        sync_compositor_focus(state, wid);
996        true
997    } else {
998        false
999    }
1000}
1001
1002/// Handle a launcher launch action by spawning or focusing the appropriate app.
1003fn handle_launcher_launch(state: &mut DesktopState, exec_path: &str) {
1004    match exec_path {
1005        "terminal" | "/usr/bin/terminal" => {
1006            if state.terminal.wid > 0 {
1007                crate::desktop::window_manager::with_window_manager(|wm| {
1008                    let _ = wm.focus_window(state.terminal.wid);
1009                });
1010                sync_compositor_focus(state, state.terminal.wid);
1011            }
1012        }
1013        "files" | "/usr/bin/files" => {
1014            if state.file_manager.wid > 0 {
1015                crate::desktop::window_manager::with_window_manager(|wm| {
1016                    let _ = wm.focus_window(state.file_manager.wid);
1017                });
1018                sync_compositor_focus(state, state.file_manager.wid);
1019            }
1020        }
1021        "editor" | "/usr/bin/editor" => {
1022            if state.text_editor.wid > 0 {
1023                crate::desktop::window_manager::with_window_manager(|wm| {
1024                    let _ = wm.focus_window(state.text_editor.wid);
1025                });
1026                sync_compositor_focus(state, state.text_editor.wid);
1027            }
1028        }
1029        "settings" | "/usr/bin/settings" => {
1030            if !focus_dynamic_app(state, AppKind::Settings) {
1031                spawn_dynamic_app(state, AppKind::Settings, "Settings", 600, 450);
1032            }
1033        }
1034        "sysmonitor" | "/usr/bin/sysmonitor" => {
1035            if !focus_dynamic_app(state, AppKind::SystemMonitor) {
1036                spawn_dynamic_app(state, AppKind::SystemMonitor, "System Monitor", 640, 400);
1037            }
1038        }
1039        "imageviewer" | "/usr/bin/image-viewer" => {
1040            if !focus_dynamic_app(state, AppKind::ImageViewer) {
1041                spawn_dynamic_app(state, AppKind::ImageViewer, "Image Viewer", 800, 600);
1042                // Try to load a sample image from VFS on first launch
1043                if state.image_viewer.image.is_none() {
1044                    // Attempt well-known paths for sample images
1045                    let sample_paths = [
1046                        "/usr/share/images/wallpaper.ppm",
1047                        "/usr/share/images/sample.bmp",
1048                        "/usr/share/images/logo.qoi",
1049                    ];
1050                    for path in &sample_paths {
1051                        if let Ok(data) = crate::fs::read_file(path) {
1052                            state.image_viewer.load_file(path, &data);
1053                            break;
1054                        }
1055                    }
1056                }
1057            }
1058        }
1059        "mediaplayer" | "/usr/bin/mediaplayer" => {
1060            if !focus_dynamic_app(state, AppKind::MediaPlayer) {
1061                spawn_dynamic_app(state, AppKind::MediaPlayer, "Media Player", 640, 300);
1062            }
1063        }
1064        "browser" | "/usr/bin/browser" => {
1065            if !focus_dynamic_app(state, AppKind::Browser) {
1066                spawn_dynamic_app(state, AppKind::Browser, "Web Browser", 900, 600);
1067            }
1068        }
1069        "pdfviewer" | "/usr/bin/pdfviewer" => {
1070            if !focus_dynamic_app(state, AppKind::PdfViewer) {
1071                spawn_dynamic_app(state, AppKind::PdfViewer, "PDF Viewer", 800, 600);
1072            }
1073        }
1074        "calculator" | "/usr/bin/calculator" => {
1075            if !focus_dynamic_app(state, AppKind::Calculator) {
1076                spawn_dynamic_app(state, AppKind::Calculator, "Calculator", 320, 400);
1077            }
1078        }
1079        _ => {}
1080    }
1081}
1082
1083/// Framebuffer layout info passed to render sub-functions.
1084struct FrameLayout {
1085    fb_ptr: *mut u8,
1086    fb_width: usize,
1087    fb_height: usize,
1088    fb_stride: usize,
1089    is_bgr: bool,
1090    panel_y: i32,
1091}
1092
1093/// Main compositor render loop.
1094///
1095/// Composites all Wayland surfaces, blits to the hardware framebuffer,
1096/// routes input events through the window manager to applications.
1097/// Integrates Phase 7 desktop features: app switcher, screen lock,
1098/// launcher, notifications, system tray, snap-to-edge, virtual
1099/// workspaces, window decorations, and animations.
1100fn render_loop(hw: &fbcon::FbHwInfo, state: &mut DesktopState) {
1101    let layout = FrameLayout {
1102        fb_ptr: hw.fb_ptr,
1103        fb_width: hw.width,
1104        fb_height: hw.height,
1105        fb_stride: hw.stride,
1106        is_bgr: matches!(hw.pixel_format, FbPixelFormat::Bgr),
1107        panel_y: (hw.height as u32 - crate::desktop::panel::PANEL_HEIGHT) as i32,
1108    };
1109
1110    // Initial composite
1111    let _drawn = crate::desktop::wayland::with_display(|display| display.wl_compositor.composite());
1112
1113    // Do initial render of all app surfaces
1114    render_all_apps(state);
1115
1116    // Set screen size for window manager placement heuristics
1117    crate::desktop::window_manager::with_window_manager(|wm| {
1118        wm.set_screen_size(layout.fb_width as u32, layout.fb_height as u32);
1119    });
1120
1121    loop {
1122        state.frame_count += 1;
1123        let tick = crate::arch::timer::get_ticks();
1124
1125        // Screen lock takes over all input and rendering
1126        if state.screen_locker.is_locked() {
1127            handle_screen_lock(state, &layout, tick);
1128            continue;
1129        }
1130
1131        // Poll, translate, and dispatch input events; returns true if ESC exit
1132        if handle_input_events(state, &layout) {
1133            return;
1134        }
1135
1136        // Update focus, idle timeout, animations, notifications
1137        update_ui_state(state, tick);
1138
1139        // Render apps, composite overlays, blit to framebuffer
1140        render_and_composite(state, &layout, tick);
1141    }
1142}
1143
1144/// Handle the screen lock: consume input, tick lock state, render lock screen.
1145fn handle_screen_lock(state: &mut DesktopState, layout: &FrameLayout, tick: u64) {
1146    crate::drivers::input_event::poll_all();
1147    while let Some(raw_event) = crate::drivers::input_event::read_event() {
1148        if raw_event.event_type == crate::drivers::input_event::EV_KEY && raw_event.value == 1 {
1149            let action = state.screen_locker.handle_key(raw_event.code as u8, tick);
1150            if matches!(action, crate::desktop::screen_lock::LockAction::Unlocked) {
1151                break;
1152            }
1153        }
1154    }
1155    state.screen_locker.tick(tick);
1156    crate::desktop::wayland::with_display(|display| {
1157        display.wl_compositor.with_back_buffer_mut(|bb| {
1158            state
1159                .screen_locker
1160                .render_to_buffer(bb, layout.fb_width, layout.fb_height, tick);
1161        });
1162        blit_back_buffer(
1163            &display.wl_compositor,
1164            layout.fb_ptr,
1165            layout.fb_width,
1166            layout.fb_height,
1167            layout.fb_stride,
1168            layout.is_bgr,
1169        );
1170    });
1171    for _ in 0..50_000 {
1172        core::hint::spin_loop();
1173    }
1174}
1175
1176/// Poll hardware input, translate events, and dispatch hotkeys/mouse/keyboard.
1177///
1178/// Returns `true` if the GUI should exit (ESC pressed without overlays).
1179fn handle_input_events(state: &mut DesktopState, layout: &FrameLayout) -> bool {
1180    crate::drivers::input_event::poll_all();
1181    let (mouse_x, mouse_y) = crate::drivers::mouse::cursor_position();
1182    let mods = crate::drivers::keyboard::get_modifiers();
1183    let tick = crate::arch::timer::get_ticks();
1184
1185    // Record activity for idle timeout
1186    state.screen_locker.record_activity(tick);
1187
1188    while let Some(raw_event) = crate::drivers::input_event::read_event() {
1189        let is_key_press =
1190            raw_event.event_type == crate::drivers::input_event::EV_KEY && raw_event.value == 1;
1191
1192        // ESC without modifiers exits the GUI
1193        if is_key_press && raw_event.code == 0x1B && mods == 0 {
1194            // Only exit if no overlay is open
1195            if !state.app_switcher.is_visible() {
1196                crate::serial::_serial_print(format_args!("[DESKTOP] ESC pressed, exiting GUI\n"));
1197                return true;
1198            }
1199        }
1200
1201        // --- Hotkey detection (before normal event dispatch) ---
1202
1203        // Alt+Tab: show/cycle app switcher
1204        if is_key_press && raw_event.code == 0x09 && mods & crate::drivers::keyboard::MOD_ALT != 0 {
1205            if !state.app_switcher.is_visible() {
1206                let windows = get_window_list_for_switcher();
1207                state.app_switcher.show(windows);
1208            } else {
1209                state.app_switcher.next();
1210            }
1211            continue;
1212        }
1213
1214        // Alt released while switcher visible: commit selection
1215        if state.app_switcher.is_visible()
1216            && mods & crate::drivers::keyboard::MOD_ALT == 0
1217            && raw_event.event_type == crate::drivers::input_event::EV_KEY
1218        {
1219            if let Some(wid) = state.app_switcher.hide() {
1220                crate::desktop::window_manager::with_window_manager(|wm| {
1221                    let _ = wm.focus_window(wid);
1222                });
1223                sync_compositor_focus(state, wid);
1224            }
1225            continue;
1226        }
1227
1228        // Ctrl+Alt+L: lock screen
1229        if is_key_press
1230            && raw_event.code == b'l' as u16
1231            && mods & (crate::drivers::keyboard::MOD_CTRL | crate::drivers::keyboard::MOD_ALT)
1232                == (crate::drivers::keyboard::MOD_CTRL | crate::drivers::keyboard::MOD_ALT)
1233        {
1234            state.screen_locker.lock();
1235            continue;
1236        }
1237
1238        // Ctrl+Alt+Arrow: switch workspace
1239        if is_key_press
1240            && mods & (crate::drivers::keyboard::MOD_CTRL | crate::drivers::keyboard::MOD_ALT)
1241                == (crate::drivers::keyboard::MOD_CTRL | crate::drivers::keyboard::MOD_ALT)
1242        {
1243            if raw_event.code == crate::drivers::keyboard::KEY_LEFT as u16 {
1244                switch_workspace_prev(state);
1245                continue;
1246            }
1247            if raw_event.code == crate::drivers::keyboard::KEY_RIGHT as u16 {
1248                switch_workspace_next(state);
1249                continue;
1250            }
1251        }
1252
1253        // Super key: toggle launcher
1254        if is_key_press && mods & crate::drivers::keyboard::MOD_SUPER != 0 {
1255            crate::desktop::launcher::with_launcher(|l| l.toggle());
1256            continue;
1257        }
1258
1259        // ESC closes launcher or app switcher overlays
1260        if is_key_press && raw_event.code == 0x1B {
1261            if state.app_switcher.is_visible() {
1262                let _ = state.app_switcher.hide();
1263                continue;
1264            }
1265            let launcher_visible =
1266                crate::desktop::launcher::with_launcher_ref(|l| l.is_visible()).unwrap_or(false);
1267            if launcher_visible {
1268                crate::desktop::launcher::with_launcher(|l| l.hide());
1269                continue;
1270            }
1271        }
1272
1273        // Forward keyboard to launcher when visible
1274        let launcher_visible =
1275            crate::desktop::launcher::with_launcher_ref(|l| l.is_visible()).unwrap_or(false);
1276        if launcher_visible && is_key_press && raw_event.code < 0x80 {
1277            let action =
1278                crate::desktop::launcher::with_launcher(|l| l.handle_key(raw_event.code as u8));
1279            if let Some(Some(action)) = action {
1280                match action {
1281                    crate::desktop::launcher::LauncherAction::Launch(exec) => {
1282                        handle_launcher_launch(state, &exec);
1283                    }
1284                    crate::desktop::launcher::LauncherAction::Hide => {}
1285                }
1286            }
1287            continue;
1288        }
1289
1290        // Ctrl+C: copy from focused app to clipboard
1291        if is_key_press
1292            && raw_event.code == b'c' as u16
1293            && mods & crate::drivers::keyboard::MOD_CTRL != 0
1294            && mods & crate::drivers::keyboard::MOD_ALT == 0
1295        {
1296            let _ = state.clipboard.copy(
1297                crate::desktop::desktop_ext::clipboard::SelectionType::Clipboard,
1298                0,
1299                crate::desktop::desktop_ext::clipboard::ClipboardMime::TextPlainUtf8,
1300                alloc::vec![],
1301            );
1302            continue;
1303        }
1304
1305        // Ctrl+V: paste from clipboard to focused app
1306        if is_key_press
1307            && raw_event.code == b'v' as u16
1308            && mods & crate::drivers::keyboard::MOD_CTRL != 0
1309            && mods & crate::drivers::keyboard::MOD_ALT == 0
1310        {
1311            if let Ok(data) = state.clipboard.paste(
1312                crate::desktop::desktop_ext::clipboard::SelectionType::Clipboard,
1313                crate::desktop::desktop_ext::clipboard::ClipboardMime::TextPlainUtf8,
1314            ) {
1315                if state.terminal.wid > 0 {
1316                    crate::desktop::terminal::with_terminal_manager(|tm| {
1317                        for &byte in data {
1318                            let event = crate::desktop::window_manager::InputEvent::KeyPress {
1319                                scancode: 0,
1320                                character: byte as char,
1321                            };
1322                            let _ = tm.process_input(0, event);
1323                        }
1324                    });
1325                }
1326            }
1327            continue;
1328        }
1329
1330        // --- Normal event dispatch ---
1331        if let Some(wm_event) = translate_input_event(&raw_event, mouse_x, mouse_y) {
1332            dispatch_mouse_and_keyboard(state, layout, wm_event, launcher_visible);
1333        }
1334    }
1335
1336    false
1337}
1338
1339/// Dispatch a translated WM event: mouse clicks, drags, releases, and keyboard.
1340fn dispatch_mouse_and_keyboard(
1341    state: &mut DesktopState,
1342    layout: &FrameLayout,
1343    wm_event: crate::desktop::window_manager::InputEvent,
1344    launcher_visible: bool,
1345) {
1346    // --- Handle left mouse button press ---
1347    if let crate::desktop::window_manager::InputEvent::MouseButton {
1348        button: 0,
1349        pressed: true,
1350        x,
1351        y,
1352    } = wm_event
1353    {
1354        // Dismiss launcher on click outside
1355        if launcher_visible {
1356            crate::desktop::launcher::with_launcher(|l| l.hide());
1357        }
1358
1359        // Panel click
1360        if y >= layout.panel_y {
1361            crate::desktop::panel::with_panel(|p| p.update_buttons());
1362            if let Some(focus_wid) =
1363                crate::desktop::panel::with_panel(|p| p.handle_click(x, y - layout.panel_y))
1364                    .flatten()
1365            {
1366                crate::desktop::window_manager::with_window_manager(|wm| {
1367                    let _ = wm.focus_window(focus_wid);
1368                });
1369                sync_compositor_focus(state, focus_wid);
1370            }
1371            return;
1372        }
1373
1374        // Window hit test: find which window was clicked
1375        let hit =
1376            crate::desktop::window_manager::with_window_manager(|wm| wm.window_at_position(x, y))
1377                .flatten();
1378
1379        if let Some(wid) = hit {
1380            crate::desktop::window_manager::with_window_manager(|wm| {
1381                let _ = wm.focus_window(wid);
1382            });
1383            sync_compositor_focus(state, wid);
1384
1385            let in_title_bar = crate::desktop::window_manager::with_window_manager(|wm| {
1386                wm.get_window(wid).map(|w| y < w.y + TITLE_BAR_HEIGHT)
1387            })
1388            .flatten()
1389            .unwrap_or(false);
1390
1391            if in_title_bar {
1392                let is_close = crate::desktop::window_manager::with_window_manager(|wm| {
1393                    wm.get_window(wid).map(|w| x >= w.x + w.width as i32 - 28)
1394                })
1395                .flatten()
1396                .unwrap_or(false);
1397
1398                if is_close {
1399                    close_any_window(state, wid);
1400                    return;
1401                }
1402
1403                // Start drag
1404                if let Some(surface_id) = state.surface_for_window(wid) {
1405                    let win_pos = crate::desktop::window_manager::with_window_manager(|wm| {
1406                        wm.get_window(wid).map(|w| (w.x, w.y))
1407                    })
1408                    .flatten();
1409                    if let Some((wx, wy)) = win_pos {
1410                        state.drag = Some(DragState {
1411                            wid,
1412                            surface_id,
1413                            offset_x: x - wx,
1414                            offset_y: y - wy,
1415                        });
1416                    }
1417                }
1418            } else {
1419                // Not title bar -- forward click to the app
1420                crate::desktop::window_manager::with_window_manager(|wm| {
1421                    wm.queue_event(crate::desktop::window_manager::WindowEvent {
1422                        window_id: wid,
1423                        event: wm_event,
1424                    });
1425                });
1426            }
1427        }
1428        return;
1429    }
1430
1431    // --- Handle right-click ---
1432    if let crate::desktop::window_manager::InputEvent::MouseButton {
1433        button: 1,
1434        pressed: true,
1435        x,
1436        y,
1437    } = wm_event
1438    {
1439        let launcher_visible =
1440            crate::desktop::launcher::with_launcher_ref(|l| l.is_visible()).unwrap_or(false);
1441        if launcher_visible {
1442            crate::desktop::launcher::with_launcher(|l| l.hide());
1443        }
1444
1445        if y < layout.panel_y {
1446            let hit = crate::desktop::window_manager::with_window_manager(|wm| {
1447                wm.window_at_position(x, y)
1448            })
1449            .flatten();
1450
1451            if let Some(wid) = hit {
1452                let in_title_bar = crate::desktop::window_manager::with_window_manager(|wm| {
1453                    wm.get_window(wid).map(|w| y < w.y + TITLE_BAR_HEIGHT)
1454                })
1455                .flatten()
1456                .unwrap_or(false);
1457                if in_title_bar {
1458                    close_any_window(state, wid);
1459                }
1460            } else {
1461                crate::desktop::launcher::with_launcher(|l| l.toggle());
1462            }
1463        }
1464        return;
1465    }
1466
1467    // --- Handle mouse button release (end drag + snap-to-edge) ---
1468    if let crate::desktop::window_manager::InputEvent::MouseButton {
1469        button: 0,
1470        pressed: false,
1471        x,
1472        y,
1473    } = wm_event
1474    {
1475        if let Some(ref d) = state.drag {
1476            let zone = crate::desktop::window_manager::WindowManager::detect_snap_zone(
1477                x,
1478                y,
1479                layout.fb_width as u32,
1480                layout.fb_height as u32,
1481            );
1482            if zone != crate::desktop::window_manager::SnapZone::None {
1483                let drag_wid = d.wid;
1484                let drag_sid = d.surface_id;
1485                crate::desktop::window_manager::with_window_manager(|wm| {
1486                    wm.snap_window(drag_wid, zone);
1487                    if let Some(w) = wm.get_window(drag_wid) {
1488                        crate::desktop::wayland::with_display(|display| {
1489                            display
1490                                .wl_compositor
1491                                .set_surface_position(drag_sid, w.x, w.y);
1492                        });
1493                    }
1494                });
1495            }
1496        }
1497        state.drag = None;
1498        crate::desktop::window_manager::with_window_manager(|wm| {
1499            wm.process_input(wm_event);
1500        });
1501        return;
1502    }
1503
1504    // --- Handle mouse move (drag window or forward) ---
1505    if let crate::desktop::window_manager::InputEvent::MouseMove { x, y } = wm_event {
1506        if let Some(ref d) = state.drag {
1507            let new_x = x - d.offset_x;
1508            let new_y = y - d.offset_y;
1509            crate::desktop::window_manager::with_window_manager(|wm| {
1510                let _ = wm.move_window(d.wid, new_x, new_y);
1511            });
1512            crate::desktop::wayland::with_display(|display| {
1513                display
1514                    .wl_compositor
1515                    .set_surface_position(d.surface_id, new_x, new_y);
1516            });
1517            return;
1518        }
1519    }
1520
1521    // All other events: dispatch to window manager
1522    crate::desktop::window_manager::with_window_manager(|wm| {
1523        wm.process_input(wm_event);
1524    });
1525}
1526
1527/// Update focus tracking, idle timeout, animations, and notifications.
1528fn update_ui_state(state: &mut DesktopState, tick: u64) {
1529    // Detect focus changes and sync compositor z_order
1530    let current_focused =
1531        crate::desktop::window_manager::with_window_manager(|wm| wm.get_focused_window_id())
1532            .flatten();
1533    if current_focused != state.prev_focused {
1534        if let Some(fwid) = current_focused {
1535            sync_compositor_focus(state, fwid);
1536        }
1537        state.prev_focused = current_focused;
1538    }
1539
1540    // Check idle timeout for screen lock
1541    if state.screen_locker.check_idle_timeout(tick) {
1542        state.screen_locker.lock();
1543    }
1544
1545    // Forward queued WM events to apps
1546    forward_events_to_apps(state);
1547
1548    // Terminal: read PTY output
1549    crate::desktop::terminal::with_terminal_manager(|tm| {
1550        let _ = tm.update_all();
1551    });
1552
1553    // Tick animation manager
1554    state.animation_mgr.tick(16); // ~16ms per frame at 60fps
1555    state.animation_mgr.remove_completed();
1556
1557    // Tick notification expiry (every 30th frame to avoid overhead)
1558    if state.frame_count.is_multiple_of(30) {
1559        crate::desktop::notification::with_notification_manager(|mgr| {
1560            mgr.tick(tick);
1561        });
1562    }
1563}
1564
1565/// Render all apps, composite overlays, and blit to the hardware framebuffer.
1566fn render_and_composite(state: &mut DesktopState, layout: &FrameLayout, tick: u64) {
1567    render_all_apps(state);
1568
1569    // Panel: update clock + buttons + systray periodically
1570    if state.frame_count.is_multiple_of(10) {
1571        render_panel(state, layout.fb_width as u32);
1572    }
1573
1574    // Composite, render overlays, and blit
1575    crate::desktop::wayland::with_display(|display| {
1576        display.wl_compositor.request_composite();
1577        let composited = display.wl_compositor.composite().unwrap_or(false);
1578
1579        if composited {
1580            display.wl_compositor.with_back_buffer_mut(|bb| {
1581                render_overlays(state, bb, layout.fb_width, layout.fb_height, tick);
1582            });
1583
1584            blit_back_buffer(
1585                &display.wl_compositor,
1586                layout.fb_ptr,
1587                layout.fb_width,
1588                layout.fb_height,
1589                layout.fb_stride,
1590                layout.is_bgr,
1591            );
1592        }
1593    });
1594
1595    // Always draw cursor (cheap: 16x16 pixels) so it stays responsive
1596    let (mouse_x, mouse_y) = crate::drivers::mouse::cursor_position();
1597    // SAFETY: fb_ptr is valid for stride * height bytes.
1598    let fb_slice = unsafe {
1599        core::slice::from_raw_parts_mut(layout.fb_ptr, layout.fb_stride * layout.fb_height)
1600    };
1601    cursor::draw_cursor(
1602        fb_slice,
1603        layout.fb_stride,
1604        layout.fb_width,
1605        layout.fb_height,
1606        mouse_x,
1607        mouse_y,
1608    );
1609
1610    // Yield CPU -- short spin for frame pacing
1611    for _ in 0..5_000 {
1612        core::hint::spin_loop();
1613    }
1614}
1615
1616/// Render modal overlays into the composited back-buffer (post-composite,
1617/// pre-blit).
1618///
1619/// Only true overlays (app switcher, launcher, notifications) are drawn here.
1620/// Title bars, close buttons, and desktop icons are now part of their
1621/// respective surfaces.
1622fn render_overlays(
1623    state: &DesktopState,
1624    bb: &mut [u32],
1625    fb_width: usize,
1626    fb_height: usize,
1627    tick: u64,
1628) {
1629    // Title bars and close buttons are rendered into each surface's pixel
1630    // buffer (see draw_title_bar_into_surface), and desktop icons are baked
1631    // into the background surface (see render_icons_into_bgra).  The
1632    // compositor's z-order compositing handles occlusion naturally.
1633
1634    // App switcher overlay (Alt+Tab)
1635    if state.app_switcher.is_visible() {
1636        state
1637            .app_switcher
1638            .render(bb, fb_width as u32, fb_height as u32);
1639    }
1640
1641    // Launcher overlay (Super key)
1642    crate::desktop::launcher::with_launcher_ref(|l| {
1643        if l.is_visible() {
1644            l.render_to_buffer(bb, fb_width, fb_height);
1645        }
1646    });
1647
1648    // Notification toasts (top-right)
1649    crate::desktop::notification::with_notification_manager(|mgr| {
1650        if mgr.active_count() > 0 {
1651            mgr.render_to_buffer(bb, fb_width, fb_height, tick);
1652        }
1653    });
1654}
1655
1656/// Try to blit the compositor back-buffer via VirtIO GPU (DMA path).
1657///
1658/// Returns `true` if the GPU handled the blit, `false` to fall back to
1659/// direct MMIO writes.
1660#[cfg(target_arch = "x86_64")]
1661fn try_gpu_blit(compositor: &crate::desktop::wayland::compositor::Compositor) -> bool {
1662    if !crate::drivers::virtio_gpu::is_available() {
1663        return false;
1664    }
1665    crate::drivers::virtio_gpu::with_driver(|gpu| {
1666        compositor.with_back_buffer(|bb| {
1667            if let Some(backing) = gpu.get_framebuffer_mut() {
1668                let copy_len = bb.len().min(backing.len());
1669                backing[..copy_len].copy_from_slice(&bb[..copy_len]);
1670            }
1671        });
1672        let _ = gpu.flush_framebuffer();
1673    });
1674    true
1675}
1676
1677/// Blit the compositor's back-buffer to the hardware framebuffer.
1678fn blit_back_buffer(
1679    compositor: &crate::desktop::wayland::compositor::Compositor,
1680    fb_ptr: *mut u8,
1681    fb_width: usize,
1682    fb_height: usize,
1683    fb_stride: usize,
1684    is_bgr: bool,
1685) {
1686    // Try VirtIO GPU first (hardware-accelerated DMA path)
1687    #[cfg(target_arch = "x86_64")]
1688    if try_gpu_blit(compositor) {
1689        return;
1690    }
1691
1692    compositor.with_back_buffer(|bb| {
1693        if is_bgr {
1694            // Direct row-based memcpy -- format already matches
1695            for y in 0..fb_height {
1696                let src_start = y * fb_width;
1697                let src_end = src_start + fb_width;
1698                if src_end > bb.len() {
1699                    break;
1700                }
1701                let dst_offset = y * fb_stride;
1702                // SAFETY: fb_ptr valid for stride*height bytes; src slice
1703                // is fb_width u32s = fb_width*4 bytes.
1704                unsafe {
1705                    core::ptr::copy_nonoverlapping(
1706                        bb[src_start..src_end].as_ptr() as *const u8,
1707                        fb_ptr.add(dst_offset),
1708                        fb_width * 4,
1709                    );
1710                }
1711            }
1712        } else {
1713            // RGB format: swap R and B channels
1714            for y in 0..fb_height {
1715                for x in 0..fb_width {
1716                    let src_idx = y * fb_width + x;
1717                    if src_idx >= bb.len() {
1718                        break;
1719                    }
1720                    let pixel = bb[src_idx];
1721                    let r = (pixel >> 16) & 0xFF;
1722                    let g = (pixel >> 8) & 0xFF;
1723                    let b = pixel & 0xFF;
1724                    let swapped = 0xFF00_0000 | (b << 16) | (g << 8) | r;
1725                    let dst_offset = y * fb_stride + x * 4;
1726                    unsafe {
1727                        (fb_ptr.add(dst_offset) as *mut u32).write(swapped);
1728                    }
1729                }
1730            }
1731        }
1732    });
1733}
1734
1735/// Get the list of windows for the app switcher overlay.
1736fn get_window_list_for_switcher() -> alloc::vec::Vec<(u32, alloc::string::String)> {
1737    use alloc::string::ToString;
1738    crate::desktop::window_manager::with_window_manager(|wm| {
1739        wm.get_visible_windows()
1740            .iter()
1741            .map(|w| (w.id, w.title_str().to_string()))
1742            .collect()
1743    })
1744    .unwrap_or_default()
1745}
1746
1747/// Switch to the previous workspace.
1748fn switch_workspace_prev(state: &DesktopState) {
1749    crate::desktop::window_manager::with_window_manager(|wm| {
1750        let current = wm.get_active_workspace();
1751        if current > 0 {
1752            wm.switch_workspace(current - 1);
1753            update_surface_visibility(state, current - 1);
1754        }
1755    });
1756}
1757
1758/// Switch to the next workspace.
1759fn switch_workspace_next(state: &DesktopState) {
1760    crate::desktop::window_manager::with_window_manager(|wm| {
1761        let current = wm.get_active_workspace();
1762        if current < (crate::desktop::window_manager::MAX_WORKSPACES as u8 - 1) {
1763            wm.switch_workspace(current + 1);
1764            update_surface_visibility(state, current + 1);
1765        }
1766    });
1767}
1768
1769/// Update compositor surface mapped state for a workspace switch.
1770///
1771/// Surfaces belonging to windows on the target workspace are mapped;
1772/// all others are unmapped. Panel and background are always visible.
1773fn update_surface_visibility(state: &DesktopState, target_ws: u8) {
1774    crate::desktop::window_manager::with_window_manager(|wm| {
1775        let all_windows = wm.get_all_windows();
1776        for w in &all_windows {
1777            if let Some(sid) = state.surface_for_window(w.id) {
1778                let should_show = w.workspace == target_ws;
1779                crate::desktop::wayland::with_display(|display| {
1780                    display.wl_compositor.set_surface_mapped(sid, should_show);
1781                });
1782            }
1783        }
1784    });
1785    // Update panel workspace indicator
1786    crate::desktop::panel::with_panel(|p| {
1787        p.set_active_workspace(target_ws as usize);
1788    });
1789}
1790
1791/// Render all desktop app surfaces.
1792fn render_all_apps(state: &mut DesktopState) {
1793    // Terminal
1794    if state.terminal.wid > 0 {
1795        crate::desktop::terminal::with_terminal_manager(|tm| {
1796            tm.render_all_surfaces();
1797        });
1798    }
1799
1800    // File manager
1801    crate::desktop::file_manager::with_file_manager(|fm| {
1802        fm.read().render_to_surface();
1803    });
1804
1805    // Text editor
1806    crate::desktop::text_editor::with_text_editor(|te| {
1807        te.read().render_to_surface();
1808    });
1809
1810    // Compute themed background color for dynamic apps (strip alpha byte)
1811    let app_bg = state.theme.colors().window_background.0 & 0x00FFFFFF;
1812    let app_error_color = state.theme.colors().error.0 & 0x00FFFFFF;
1813    let app_accent_color = state.theme.colors().accent.0 & 0x00FFFFFF;
1814
1815    // Dynamic apps -- render content then prepend title bar into full surface
1816    let title_bar_h: usize = 28;
1817    for app in &state.dynamic_apps {
1818        let w = app.width as usize;
1819        let content_h = app.height as usize;
1820        let total_h = content_h + title_bar_h;
1821        let content_size = w * content_h * 4;
1822        let mut content = alloc::vec![0u8; content_size];
1823
1824        match app.kind {
1825            AppKind::SystemMonitor => {
1826                render_system_monitor(&mut content, w, content_h, state.frame_count, app_bg);
1827            }
1828            AppKind::MediaPlayer => {
1829                render_media_player(&mut content, w, content_h, app_bg);
1830            }
1831            AppKind::ImageViewer => {
1832                state
1833                    .image_viewer
1834                    .render_to_u8_buffer(&mut content, w, content_h);
1835            }
1836            AppKind::Settings => {
1837                state
1838                    .settings_app
1839                    .render_to_u8_buffer(&mut content, w, content_h);
1840            }
1841            AppKind::Browser => {
1842                render_browser(&mut content, w, content_h, app_bg, &mut state.browser);
1843            }
1844            AppKind::PdfViewer => {
1845                render_pdf_viewer(&mut content, w, content_h, app_bg, state.pdf_page_index);
1846            }
1847            AppKind::Calculator => {
1848                render_calculator(
1849                    &mut content,
1850                    w,
1851                    content_h,
1852                    &state.calculator,
1853                    app_bg,
1854                    app_accent_color,
1855                    app_error_color,
1856                );
1857            }
1858        }
1859
1860        // Build full surface: title bar (28 rows) + content
1861        let mut pixels = alloc::vec![0u8; w * total_h * 4];
1862        for y in 0..content_h {
1863            let src_off = y * w * 4;
1864            let dst_off = (y + title_bar_h) * w * 4;
1865            pixels[dst_off..dst_off + w * 4].copy_from_slice(&content[src_off..src_off + w * 4]);
1866        }
1867        draw_title_bar_into_surface(&mut pixels, w, total_h, app.wid);
1868
1869        update_surface_pixels(app.surface_id, app.pool_id, app.pool_buf_id, &pixels);
1870    }
1871}
1872
1873/// Render a placeholder app with title text on a solid background.
1874fn render_placeholder_app(buf: &mut [u8], w: usize, h: usize, title: &str, bg_color: u32) {
1875    let r = ((bg_color >> 16) & 0xFF) as u8;
1876    let g = ((bg_color >> 8) & 0xFF) as u8;
1877    let b = (bg_color & 0xFF) as u8;
1878    // Fill background
1879    for y in 0..h {
1880        for x in 0..w {
1881            let off = (y * w + x) * 4;
1882            if off + 3 < buf.len() {
1883                buf[off] = b;
1884                buf[off + 1] = g;
1885                buf[off + 2] = r;
1886                buf[off + 3] = 0xFF;
1887            }
1888        }
1889    }
1890    // Draw title centered
1891    let title_bytes = title.as_bytes();
1892    let tx = w.saturating_sub(title_bytes.len() * 8) / 2;
1893    draw_string_into_buffer(buf, w, title_bytes, tx, 20, 0xFFFFFF);
1894}
1895
1896/// Render system monitor showing memory and CPU stats.
1897fn render_system_monitor(buf: &mut [u8], w: usize, h: usize, frame_count: u64, bg_color: u32) {
1898    render_placeholder_app(buf, w, h, "System Monitor", bg_color);
1899    let mem = crate::mm::get_memory_stats();
1900    let total_mb = (mem.total_frames * 4096) / (1024 * 1024);
1901    let used_frames = mem.total_frames.saturating_sub(mem.free_frames);
1902    let used_mb = (used_frames * 4096) / (1024 * 1024);
1903    let pct = if mem.total_frames > 0 {
1904        (used_frames * 100) / mem.total_frames
1905    } else {
1906        0
1907    };
1908
1909    let perf = crate::perf::get_stats();
1910
1911    // Memory line
1912    let mut line_buf = [0u8; 64];
1913    let line = format_stat_line(&mut line_buf, b"Memory: ", used_mb, b"/", total_mb, b" MB");
1914    draw_string_into_buffer(buf, w, line, 20, 50, 0x55EFC4);
1915
1916    // Usage bar
1917    let bar_w = w.saturating_sub(60);
1918    let filled = (bar_w * pct) / 100;
1919    for x in 20..(20 + bar_w).min(w) {
1920        let color = if x < 20 + filled { 0x55EFC4 } else { 0x333333 };
1921        for dy in 0..12 {
1922            let y = 70 + dy;
1923            if y < h {
1924                draw_pixel(buf, w, x, y, color);
1925            }
1926        }
1927    }
1928
1929    // Stats
1930    let stats_y = 95;
1931    let line2 = format_simple(&mut line_buf, b"Ctx switches: ", perf.context_switches);
1932    draw_string_into_buffer(buf, w, line2, 20, stats_y, 0xD4D4D4);
1933    let line3 = format_simple(&mut line_buf, b"Syscalls:     ", perf.syscalls);
1934    draw_string_into_buffer(buf, w, line3, 20, stats_y + 20, 0xD4D4D4);
1935    let line4 = format_simple(&mut line_buf, b"Interrupts:   ", perf.interrupts);
1936    draw_string_into_buffer(buf, w, line4, 20, stats_y + 40, 0xD4D4D4);
1937    let line5 = format_simple(&mut line_buf, b"Page faults:  ", perf.page_faults);
1938    draw_string_into_buffer(buf, w, line5, 20, stats_y + 60, 0xD4D4D4);
1939    let line6 = format_simple(&mut line_buf, b"GUI frames:   ", frame_count);
1940    draw_string_into_buffer(buf, w, line6, 20, stats_y + 80, 0xD4D4D4);
1941
1942    // Process list header
1943    let proc_y = stats_y + 110;
1944    draw_string_into_buffer(buf, w, b"PID   NAME            STATE", 20, proc_y, 0x55EFC4);
1945
1946    // Draw separator line
1947    for x in 20..(w.saturating_sub(20)) {
1948        if proc_y + 16 < h {
1949            draw_pixel(buf, w, x, proc_y + 16, 0x444444);
1950        }
1951    }
1952
1953    // Get process list from process server
1954    let process_server = crate::services::process_server::get_process_server();
1955    let processes = process_server.list_processes();
1956    let max_display = ((h.saturating_sub(proc_y + 20)) / 16).min(processes.len());
1957    for (i, proc_info) in processes.iter().take(max_display).enumerate() {
1958        let py = proc_y + 20 + i * 16;
1959        if py + 16 > h {
1960            break;
1961        }
1962        // Format: PID (6 chars), NAME (16 chars), STATE
1963        let pid_line = format_simple(&mut line_buf, b"", proc_info.pid.0);
1964        draw_string_into_buffer(buf, w, pid_line, 20, py, 0xD4D4D4);
1965
1966        let name_bytes = proc_info.name.as_bytes();
1967        let name_len = name_bytes.len().min(15);
1968        draw_string_into_buffer(buf, w, &name_bytes[..name_len], 68, py, 0xD4D4D4);
1969
1970        let state_str = match proc_info.state {
1971            crate::services::process_server::ProcessState::Running => b"Running" as &[u8],
1972            crate::services::process_server::ProcessState::Sleeping => b"Sleeping",
1973            crate::services::process_server::ProcessState::Waiting => b"Waiting",
1974            crate::services::process_server::ProcessState::Stopped => b"Stopped",
1975            crate::services::process_server::ProcessState::Zombie => b"Zombie",
1976            crate::services::process_server::ProcessState::Dead => b"Dead",
1977        };
1978        draw_string_into_buffer(buf, w, state_str, 196, py, 0xD4D4D4);
1979    }
1980}
1981
1982/// Render media player with playback info and real audio stream data.
1983fn render_media_player(buf: &mut [u8], w: usize, h: usize, bg_color: u32) {
1984    render_placeholder_app(buf, w, h, "Media Player", bg_color);
1985
1986    // Query real audio subsystem for stream info
1987    let (stream_count, sample_rate, channels) = crate::audio::client::with_client(|client| {
1988        (
1989            client.stream_count(),
1990            client.default_sample_rate(),
1991            client.default_channels(),
1992        )
1993    })
1994    .unwrap_or((0, 48000, 2));
1995
1996    let mut line_buf = [0u8; 64];
1997
1998    // Audio engine status
1999    draw_string_into_buffer(buf, w, b"Audio Engine: Active", 20, 50, 0x55EFC4);
2000
2001    // Stream count
2002    let line = format_simple(&mut line_buf, b"Active streams: ", stream_count as u64);
2003    draw_string_into_buffer(buf, w, line, 20, 70, 0xD4D4D4);
2004
2005    // Sample rate
2006    let line2 = format_simple(&mut line_buf, b"Sample rate:    ", sample_rate as u64);
2007    draw_string_into_buffer(buf, w, line2, 20, 90, 0xD4D4D4);
2008
2009    // Channels
2010    let line3 = format_simple(&mut line_buf, b"Channels:       ", channels as u64);
2011    draw_string_into_buffer(buf, w, line3, 20, 110, 0xD4D4D4);
2012
2013    if stream_count == 0 {
2014        draw_string_into_buffer(buf, w, b"No media loaded", 20, 145, 0x95A5A6);
2015        draw_string_into_buffer(
2016            buf,
2017            w,
2018            b"Use file manager to open audio files",
2019            20,
2020            165,
2021            0x7F8C8D,
2022        );
2023    }
2024
2025    draw_string_into_buffer(
2026        buf,
2027        w,
2028        b"Controls: Space=play/pause  S=stop  +/-=volume",
2029        20,
2030        h.saturating_sub(30),
2031        0x7F8C8D,
2032    );
2033}
2034
2035/// Render web browser with live browser engine rendering.
2036fn render_browser(
2037    buf: &mut [u8],
2038    w: usize,
2039    h: usize,
2040    bg_color: u32,
2041    browser: &mut Option<crate::browser::browser_main::Browser>,
2042) {
2043    // Lazily initialize the browser engine on first render
2044    if browser.is_none() {
2045        let config = crate::browser::browser_main::BrowserConfig {
2046            viewport_width: w as u32,
2047            viewport_height: h as u32,
2048            ..crate::browser::browser_main::BrowserConfig::default()
2049        };
2050        let mut b = crate::browser::browser_main::Browser::new(config);
2051        b.init();
2052        *browser = Some(b);
2053    }
2054
2055    if let Some(ref mut b) = browser {
2056        // Tick animations and re-render
2057        b.tick();
2058        b.render();
2059
2060        // Copy browser's u32 BGRA framebuffer into the u8 BGRA buffer
2061        let fb = b.framebuffer();
2062        let (bw, bh) = b.dimensions();
2063        let bw = bw as usize;
2064        let bh = bh as usize;
2065
2066        for y in 0..h.min(bh) {
2067            for x in 0..w.min(bw) {
2068                let src_idx = y * bw + x;
2069                let dst_off = (y * w + x) * 4;
2070                if src_idx < fb.len() && dst_off + 3 < buf.len() {
2071                    let pixel = fb[src_idx];
2072                    buf[dst_off] = (pixel & 0xFF) as u8; // B
2073                    buf[dst_off + 1] = ((pixel >> 8) & 0xFF) as u8; // G
2074                    buf[dst_off + 2] = ((pixel >> 16) & 0xFF) as u8; // R
2075                    buf[dst_off + 3] = ((pixel >> 24) & 0xFF) as u8; // A
2076                }
2077            }
2078        }
2079    } else {
2080        // Fallback: placeholder if browser init failed
2081        render_placeholder_app(buf, w, h, "Web Browser", bg_color);
2082        draw_string_into_buffer(buf, w, b"Browser engine unavailable", 20, 75, 0xFF6666);
2083    }
2084}
2085
2086/// Render PDF viewer with document area and real PDF engine rendering.
2087fn render_pdf_viewer(buf: &mut [u8], w: usize, h: usize, bg_color: u32, page_index: usize) {
2088    render_placeholder_app(buf, w, h, "PDF Viewer", bg_color);
2089
2090    // Try loading a sample PDF from VFS to show engine capability
2091    let pdf_status = crate::fs::read_file("/usr/share/doc/sample.pdf").ok();
2092    let doc_opt = pdf_status.and_then(crate::desktop::pdf::PdfDocument::open);
2093
2094    let (has_pdf, page_count) = if let Some(ref d) = doc_opt {
2095        (true, d.page_count())
2096    } else {
2097        (false, 0)
2098    };
2099
2100    // Clamp page index to valid range
2101    let display_page = if page_count > 0 {
2102        page_index.min(page_count.saturating_sub(1))
2103    } else {
2104        0
2105    };
2106
2107    // Toolbar showing current page
2108    let mut line_buf = [0u8; 64];
2109    if has_pdf {
2110        let toolbar = format_stat_line(
2111            &mut line_buf,
2112            b"[Open] [<] Page ",
2113            display_page.saturating_add(1),
2114            b" of ",
2115            page_count,
2116            b" [>] [Zoom: 100%]",
2117        );
2118        draw_string_into_buffer(buf, w, toolbar, 10, 40, 0xBBBBBB);
2119    } else {
2120        draw_string_into_buffer(
2121            buf,
2122            w,
2123            b"[Open] [<] Page 0 of 0 [>] [Zoom: 100%]",
2124            10,
2125            40,
2126            0xBBBBBB,
2127        );
2128    }
2129
2130    // Document area
2131    let page_x = 40;
2132    let page_y = 65;
2133    let page_w = w.saturating_sub(80);
2134    let page_h = h.saturating_sub(100);
2135
2136    // Render actual PDF page content if available
2137    if has_pdf {
2138        if let Some(ref doc) = doc_opt {
2139            if let Some(page) = doc.get_page(display_page) {
2140                // Render PDF page into u32 buffer via PdfRenderer
2141                let mut render_buf = vec![0xFFF0F0F0u32; page_w * page_h];
2142                let mut renderer =
2143                    crate::desktop::pdf::PdfRenderer::new(page_w as u32, page_h as u32);
2144                renderer.render_page(page, &mut render_buf);
2145
2146                // Blit rendered page into the u8 output buffer at page_x, page_y
2147                for py in 0..page_h.min(h.saturating_sub(page_y)) {
2148                    for px in 0..page_w.min(w.saturating_sub(page_x)) {
2149                        let src_idx = py * page_w + px;
2150                        let dst_off = ((page_y + py) * w + (page_x + px)) * 4;
2151                        if src_idx < render_buf.len() && dst_off + 3 < buf.len() {
2152                            let pixel = render_buf[src_idx];
2153                            buf[dst_off] = (pixel & 0xFF) as u8; // B
2154                            buf[dst_off + 1] = ((pixel >> 8) & 0xFF) as u8; // G
2155                            buf[dst_off + 2] = ((pixel >> 16) & 0xFF) as u8; // R
2156                            buf[dst_off + 3] = 0xFF; // A
2157                        }
2158                    }
2159                }
2160
2161                // Overlay page info
2162                let page_line = format_simple(&mut line_buf, b"Pages: ", page_count as u64);
2163                draw_string_into_buffer(
2164                    buf,
2165                    w,
2166                    page_line,
2167                    page_x + 20,
2168                    page_y + page_h.saturating_sub(25),
2169                    0x888888,
2170                );
2171            } else {
2172                // Page index out of range -- draw white area with message
2173                render_pdf_page_background(buf, w, page_x, page_y, page_w, page_h);
2174                draw_string_into_buffer(
2175                    buf,
2176                    w,
2177                    b"Page not available",
2178                    page_x + 20,
2179                    page_y + 20,
2180                    0x555555,
2181                );
2182            }
2183        }
2184    } else {
2185        // No PDF loaded -- white page area with instructions
2186        render_pdf_page_background(buf, w, page_x, page_y, page_w, page_h);
2187        draw_string_into_buffer(
2188            buf,
2189            w,
2190            b"PDF Engine: VeridianPDF v0.25.2",
2191            page_x + 20,
2192            page_y + 20,
2193            0x333333,
2194        );
2195        draw_string_into_buffer(
2196            buf,
2197            w,
2198            b"No document loaded",
2199            page_x + 20,
2200            page_y + 40,
2201            0x555555,
2202        );
2203        draw_string_into_buffer(
2204            buf,
2205            w,
2206            b"Use File Manager to open a PDF",
2207            page_x + 20,
2208            page_y + 60,
2209            0x777777,
2210        );
2211    }
2212
2213    // Supported features footer
2214    draw_string_into_buffer(
2215        buf,
2216        w,
2217        b"Supports: PDF 1.0-2.0, xref, text extraction",
2218        page_x + 20,
2219        page_y + page_h.saturating_sub(10),
2220        0x888888,
2221    );
2222}
2223
2224/// Draw a light-grey page background for the PDF viewer.
2225fn render_pdf_page_background(
2226    buf: &mut [u8],
2227    w: usize,
2228    page_x: usize,
2229    page_y: usize,
2230    page_w: usize,
2231    page_h: usize,
2232) {
2233    for y in page_y..(page_y + page_h).min(buf.len() / (w * 4)) {
2234        for x in page_x..(page_x + page_w).min(w) {
2235            let off = (y * w + x) * 4;
2236            if off + 3 < buf.len() {
2237                buf[off] = 0xF0; // B
2238                buf[off + 1] = 0xF0; // G
2239                buf[off + 2] = 0xF0; // R
2240                buf[off + 3] = 0xFF;
2241            }
2242        }
2243    }
2244}
2245
2246/// Format an i64 into a decimal byte string (no allocator needed).
2247fn format_i64(fmtbuf: &mut [u8; 24], value: i64) -> &[u8] {
2248    if value == 0 {
2249        fmtbuf[0] = b'0';
2250        return &fmtbuf[..1];
2251    }
2252    let mut pos = 24;
2253    let (mut n, negative) = if value < 0 {
2254        (-(value as i128) as u64, true)
2255    } else {
2256        (value as u64, false)
2257    };
2258    while n > 0 {
2259        pos -= 1;
2260        fmtbuf[pos] = b'0' + (n % 10) as u8;
2261        n /= 10;
2262    }
2263    if negative {
2264        pos -= 1;
2265        fmtbuf[pos] = b'-';
2266    }
2267    &fmtbuf[pos..]
2268}
2269
2270/// Render a basic integer calculator.
2271fn render_calculator(
2272    buf: &mut [u8],
2273    w: usize,
2274    h: usize,
2275    calc: &CalculatorState,
2276    bg_color: u32,
2277    accent_color: u32,
2278    error_color: u32,
2279) {
2280    render_placeholder_app(buf, w, h, "Calculator", bg_color);
2281
2282    // Display area (dark input field)
2283    let display_x = 16;
2284    let display_y = 44;
2285    let display_w = w.saturating_sub(32);
2286    let display_h = 32;
2287    for y in display_y..(display_y + display_h).min(h) {
2288        for x in display_x..(display_x + display_w).min(w) {
2289            let off = (y * w + x) * 4;
2290            if off + 3 < buf.len() {
2291                buf[off] = 0x18;
2292                buf[off + 1] = 0x18;
2293                buf[off + 2] = 0x18;
2294                buf[off + 3] = 0xFF;
2295            }
2296        }
2297    }
2298    // Show calculator display value (right-aligned)
2299    let mut fmtbuf = [0u8; 24];
2300    let display_text: &[u8] = if calc.error {
2301        b"Error"
2302    } else {
2303        format_i64(&mut fmtbuf, calc.display_value)
2304    };
2305    let text_pixel_width = display_text.len() * 8;
2306    let text_x = display_x + display_w.saturating_sub(text_pixel_width + 8);
2307    draw_string_into_buffer(buf, w, display_text, text_x, display_y + 8, 0xFFFFFF);
2308
2309    // Button grid: 4 columns x 5 rows
2310    let buttons: [&[u8]; 20] = [
2311        b"C", b"(", b")", b"/", b"7", b"8", b"9", b"*", b"4", b"5", b"6", b"-", b"1", b"2", b"3",
2312        b"+", b"0", b".", b"<", b"=",
2313    ];
2314    let grid_x = 16;
2315    let grid_y = display_y + display_h + 12;
2316    let btn_w = (w.saturating_sub(48)) / 4;
2317    let btn_h = 36;
2318    let gap = 4;
2319
2320    for (i, label) in buttons.iter().enumerate() {
2321        let col = i % 4;
2322        let row = i / 4;
2323        let bx = grid_x + col * (btn_w + gap);
2324        let by = grid_y + row * (btn_h + gap);
2325
2326        // Button color: operators are accent, clear is error, numbers are neutral
2327        let btn_color: u32 = match label[0] {
2328            b'/' | b'*' | b'-' | b'+' | b'=' => accent_color,
2329            b'C' => error_color,
2330            _ => 0x3A3A4A,
2331        };
2332        let cr = ((btn_color >> 16) & 0xFF) as u8;
2333        let cg = ((btn_color >> 8) & 0xFF) as u8;
2334        let cb = (btn_color & 0xFF) as u8;
2335
2336        for dy in 0..btn_h {
2337            for dx in 0..btn_w {
2338                let px = bx + dx;
2339                let py = by + dy;
2340                if px < w && py < h {
2341                    let off = (py * w + px) * 4;
2342                    if off + 3 < buf.len() {
2343                        buf[off] = cb;
2344                        buf[off + 1] = cg;
2345                        buf[off + 2] = cr;
2346                        buf[off + 3] = 0xFF;
2347                    }
2348                }
2349            }
2350        }
2351
2352        // Center label on button
2353        let lx = bx + (btn_w.saturating_sub(label.len() * 8)) / 2;
2354        let ly = by + (btn_h.saturating_sub(8)) / 2;
2355        if ly < h {
2356            draw_string_into_buffer(buf, w, label, lx, ly, 0xFFFFFF);
2357        }
2358    }
2359}
2360
2361/// Draw a single pixel in BGRA format.
2362fn draw_pixel(buf: &mut [u8], w: usize, x: usize, y: usize, color: u32) {
2363    let off = (y * w + x) * 4;
2364    if off + 3 < buf.len() {
2365        buf[off] = (color & 0xFF) as u8;
2366        buf[off + 1] = ((color >> 8) & 0xFF) as u8;
2367        buf[off + 2] = ((color >> 16) & 0xFF) as u8;
2368        buf[off + 3] = 0xFF;
2369    }
2370}
2371
2372/// Format a stat line: "prefix VALUE suffix" into a fixed buffer. Returns the
2373/// used slice.
2374fn format_stat_line<'a>(
2375    buf: &'a mut [u8; 64],
2376    prefix: &[u8],
2377    value: usize,
2378    sep: &[u8],
2379    value2: usize,
2380    suffix: &[u8],
2381) -> &'a [u8] {
2382    let mut pos = 0;
2383    for &b in prefix {
2384        if pos < 64 {
2385            buf[pos] = b;
2386            pos += 1;
2387        }
2388    }
2389    pos = write_usize_to_buf(buf, pos, value);
2390    for &b in sep {
2391        if pos < 64 {
2392            buf[pos] = b;
2393            pos += 1;
2394        }
2395    }
2396    pos = write_usize_to_buf(buf, pos, value2);
2397    for &b in suffix {
2398        if pos < 64 {
2399            buf[pos] = b;
2400            pos += 1;
2401        }
2402    }
2403    &buf[..pos]
2404}
2405
2406/// Format "prefix VALUE" into a fixed buffer.
2407fn format_simple<'a>(buf: &'a mut [u8; 64], prefix: &[u8], value: u64) -> &'a [u8] {
2408    let mut pos = 0;
2409    for &b in prefix {
2410        if pos < 64 {
2411            buf[pos] = b;
2412            pos += 1;
2413        }
2414    }
2415    pos = write_usize_to_buf(buf, pos, value as usize);
2416    &buf[..pos]
2417}
2418
2419/// Write a usize as decimal digits into a byte buffer at position `pos`.
2420fn write_usize_to_buf(buf: &mut [u8; 64], mut pos: usize, value: usize) -> usize {
2421    if value == 0 {
2422        if pos < 64 {
2423            buf[pos] = b'0';
2424            pos += 1;
2425        }
2426        return pos;
2427    }
2428    let mut digits = [0u8; 20];
2429    let mut n = value;
2430    let mut count = 0;
2431    while n > 0 {
2432        digits[count] = b'0' + (n % 10) as u8;
2433        n /= 10;
2434        count += 1;
2435    }
2436    for i in (0..count).rev() {
2437        if pos < 64 {
2438            buf[pos] = digits[i];
2439            pos += 1;
2440        }
2441    }
2442    pos
2443}
2444
2445/// Render the panel to its compositor surface, including system tray.
2446fn render_panel(state: &DesktopState, screen_width: u32) {
2447    // Update system tray data from kernel stats
2448    crate::desktop::systray::with_system_tray(|tray| {
2449        let mem_stats = crate::mm::get_memory_stats();
2450        let total_mb = (mem_stats.total_frames * 4096 / (1024 * 1024)) as u32;
2451        let used_frames = mem_stats.total_frames.saturating_sub(mem_stats.free_frames);
2452        let used_mb = (used_frames * 4096 / (1024 * 1024)) as u32;
2453        tray.update_memory(used_mb, total_mb);
2454
2455        // CPU: approximate from scheduler context switches
2456        let ctx_switches = crate::sched::metrics::SCHEDULER_METRICS
2457            .context_switches
2458            .load(core::sync::atomic::Ordering::Relaxed);
2459        // Use low bits as a rough utilization proxy (modular, wraps at 100)
2460        let cpu_pct = (ctx_switches % 100) as u8;
2461        tray.update_cpu(cpu_pct);
2462
2463        // Network: check interface state
2464        let iface = crate::net::ip::get_interface_config();
2465        let is_up = iface.ip_addr.0 != [0, 0, 0, 0];
2466        tray.update_network(is_up);
2467
2468        // Volume: read mixer
2469        if let Ok(vol) =
2470            crate::audio::mixer::with_mixer(|m: &mut crate::audio::mixer::AudioMixer| {
2471                m.get_master_volume()
2472            })
2473        {
2474            let vol_pct = ((vol as u32) * 100 / 65535) as u8;
2475            tray.update_volume(vol_pct);
2476        }
2477
2478        // Battery: check power state
2479        let battery_label = if crate::power::is_initialized() {
2480            "AC"
2481        } else {
2482            "--"
2483        };
2484        tray.update_battery(battery_label);
2485    });
2486
2487    let panel_h = crate::desktop::panel::PANEL_HEIGHT;
2488    crate::desktop::panel::with_panel(|panel| {
2489        panel.update_buttons();
2490        panel.update_clock();
2491        let mut buf = vec![0u8; (screen_width as usize) * (panel_h as usize) * 4];
2492        panel.render(&mut buf);
2493        update_surface_pixels(
2494            state.panel_surface_id,
2495            state.panel_pool_id,
2496            state.panel_pool_buf_id,
2497            &buf,
2498        );
2499    });
2500}
2501
2502/// Forward pending window manager events to the appropriate apps.
2503fn forward_events_to_apps(state: &mut DesktopState) {
2504    // Get events for terminal window
2505    if state.terminal.wid > 0 {
2506        let events = crate::desktop::window_manager::with_window_manager(|wm| {
2507            wm.get_events(state.terminal.wid)
2508        })
2509        .unwrap_or_default();
2510        if !events.is_empty() {
2511            crate::desktop::terminal::with_terminal_manager(|tm| {
2512                for event in &events {
2513                    let _ = tm.process_input(0, *event);
2514                }
2515            });
2516        }
2517    }
2518
2519    // Get events for file manager window
2520    if state.file_manager.wid > 0 {
2521        let events = crate::desktop::window_manager::with_window_manager(|wm| {
2522            wm.get_events(state.file_manager.wid)
2523        })
2524        .unwrap_or_default();
2525        if !events.is_empty() {
2526            crate::desktop::file_manager::with_file_manager(|fm| {
2527                let mut manager = fm.write();
2528                for event in &events {
2529                    let _ = manager.process_input(*event);
2530                }
2531            });
2532        }
2533    }
2534
2535    // Get events for text editor window
2536    if state.text_editor.wid > 0 {
2537        let events = crate::desktop::window_manager::with_window_manager(|wm| {
2538            wm.get_events(state.text_editor.wid)
2539        })
2540        .unwrap_or_default();
2541        if !events.is_empty() {
2542            crate::desktop::text_editor::with_text_editor(|te| {
2543                let mut editor = te.write();
2544                for event in &events {
2545                    let _ = editor.process_input(*event);
2546                }
2547            });
2548        }
2549    }
2550
2551    // Dynamic apps: collect (wid, kind, events), then dispatch
2552    let dynamic_info: alloc::vec::Vec<(u32, AppKind)> =
2553        state.dynamic_apps.iter().map(|a| (a.wid, a.kind)).collect();
2554    for (wid, kind) in dynamic_info {
2555        let events = crate::desktop::window_manager::with_window_manager(|wm| wm.get_events(wid))
2556            .unwrap_or_default();
2557        if events.is_empty() {
2558            continue;
2559        }
2560        // Get window position for coordinate transform
2561        let win_pos = crate::desktop::window_manager::with_window_manager(|wm| {
2562            wm.get_window(wid).map(|w| (w.x, w.y))
2563        })
2564        .flatten()
2565        .unwrap_or((0, 0));
2566
2567        let mut should_close = false;
2568        for event in &events {
2569            // ESC closes any dynamic app
2570            if let crate::desktop::window_manager::InputEvent::KeyPress { scancode: 0x1B, .. } =
2571                event
2572            {
2573                should_close = true;
2574                break;
2575            }
2576            match kind {
2577                AppKind::Settings => match event {
2578                    crate::desktop::window_manager::InputEvent::KeyPress { scancode, .. } => {
2579                        state.settings_app.handle_key(*scancode);
2580                    }
2581                    crate::desktop::window_manager::InputEvent::MouseButton {
2582                        button: 0,
2583                        pressed: true,
2584                        x,
2585                        y,
2586                    } => {
2587                        let local_x = (*x - win_pos.0).max(0) as usize;
2588                        let local_y = (*y - win_pos.1).max(0) as usize;
2589                        state.settings_app.handle_click(local_x, local_y);
2590                    }
2591                    _ => {}
2592                },
2593                AppKind::ImageViewer => {
2594                    if let crate::desktop::window_manager::InputEvent::KeyPress {
2595                        scancode, ..
2596                    } = event
2597                    {
2598                        state.image_viewer.handle_key(*scancode);
2599                    }
2600                }
2601                AppKind::Calculator => {
2602                    if let crate::desktop::window_manager::InputEvent::KeyPress {
2603                        scancode, ..
2604                    } = event
2605                    {
2606                        state.calculator.handle_key(*scancode);
2607                    }
2608                }
2609                AppKind::MediaPlayer => {
2610                    if let crate::desktop::window_manager::InputEvent::KeyPress {
2611                        scancode, ..
2612                    } = event
2613                    {
2614                        match scancode {
2615                            b'+' => {
2616                                let _ = crate::audio::mixer::with_mixer(|m| {
2617                                    let vol = m.get_master_volume();
2618                                    m.set_master_volume(vol.saturating_add(3276));
2619                                });
2620                            }
2621                            b'-' => {
2622                                let _ = crate::audio::mixer::with_mixer(|m| {
2623                                    let vol = m.get_master_volume();
2624                                    m.set_master_volume(vol.saturating_sub(3276));
2625                                });
2626                            }
2627                            _ => {}
2628                        }
2629                    }
2630                }
2631                AppKind::PdfViewer => {
2632                    if let crate::desktop::window_manager::InputEvent::KeyPress {
2633                        scancode, ..
2634                    } = event
2635                    {
2636                        match scancode {
2637                            0x81 | b'n' => {
2638                                // Down arrow or 'n' = next page
2639                                state.pdf_page_index = state.pdf_page_index.saturating_add(1);
2640                            }
2641                            0x80 | b'p' => {
2642                                // Up arrow or 'p' = previous page
2643                                state.pdf_page_index = state.pdf_page_index.saturating_sub(1);
2644                            }
2645                            _ => {}
2646                        }
2647                    }
2648                }
2649                AppKind::Browser => match event {
2650                    crate::desktop::window_manager::InputEvent::KeyPress { scancode, .. } => {
2651                        let mods = crate::drivers::keyboard::get_modifiers();
2652                        let ctrl = mods & crate::drivers::keyboard::MOD_CTRL != 0;
2653                        let shift = mods & crate::drivers::keyboard::MOD_SHIFT != 0;
2654                        let alt = mods & crate::drivers::keyboard::MOD_ALT != 0;
2655                        if let Some(ref mut b) = state.browser {
2656                            b.handle_key(*scancode, ctrl, shift, alt);
2657                        }
2658                    }
2659                    crate::desktop::window_manager::InputEvent::MouseButton {
2660                        button: 0,
2661                        pressed: true,
2662                        x,
2663                        y,
2664                    } => {
2665                        let local_x = (*x - win_pos.0).max(0);
2666                        let local_y = (*y - win_pos.1).max(0);
2667                        if let Some(ref mut b) = state.browser {
2668                            b.handle_click(local_x, local_y);
2669                        }
2670                    }
2671                    _ => {}
2672                },
2673                AppKind::SystemMonitor => {
2674                    // System monitor updates driven by polling, no key dispatch
2675                    // needed
2676                }
2677            }
2678        }
2679        if should_close {
2680            close_dynamic_app(state, wid);
2681        }
2682    }
2683}
2684
2685/// Blit the compositor's XRGB8888 back-buffer to the hardware framebuffer.
2686///
2687/// Handles BGR vs RGB pixel format conversion.
2688/// Note: The render loop now blits inline via `with_back_buffer()` to avoid
2689/// a 4MB clone. This standalone version is kept for potential future use.
2690fn blit_to_framebuffer(
2691    back_buffer: &[u32],
2692    fb_ptr: *mut u8,
2693    fb_width: usize,
2694    fb_height: usize,
2695    fb_stride: usize,
2696    is_bgr: bool,
2697) {
2698    for y in 0..fb_height {
2699        for x in 0..fb_width {
2700            let src_idx = y * fb_width + x;
2701            if src_idx >= back_buffer.len() {
2702                break;
2703            }
2704            let pixel = back_buffer[src_idx];
2705            let r = ((pixel >> 16) & 0xFF) as u8;
2706            let g = ((pixel >> 8) & 0xFF) as u8;
2707            let b = (pixel & 0xFF) as u8;
2708
2709            let dst_offset = y * fb_stride + x * 4;
2710            // SAFETY: fb_ptr is valid for stride * height bytes,
2711            // and dst_offset + 3 < stride * height.
2712            unsafe {
2713                let ptr = fb_ptr.add(dst_offset);
2714                if is_bgr {
2715                    // BGRA format (UEFI default)
2716                    ptr.write(b);
2717                    ptr.add(1).write(g);
2718                    ptr.add(2).write(r);
2719                    ptr.add(3).write(0xFF);
2720                } else {
2721                    // RGBA format
2722                    ptr.write(r);
2723                    ptr.add(1).write(g);
2724                    ptr.add(2).write(b);
2725                    ptr.add(3).write(0xFF);
2726                }
2727            }
2728        }
2729    }
2730}
2731
2732// ---------------------------------------------------------------------------
2733// WM-2: Server-side window decorations
2734// ---------------------------------------------------------------------------
2735
2736/// Window decoration rendering configuration.
2737#[derive(Debug, Clone, Copy)]
2738pub struct DecorationConfig {
2739    /// Title bar height in pixels.
2740    pub title_bar_height: u32,
2741    /// Border width in pixels.
2742    pub border_width: u32,
2743    /// Title bar background color (focused): ARGB packed as 0xAARRGGBB.
2744    pub title_bg_focused: u32,
2745    /// Title bar background color (unfocused): ARGB packed as 0xAARRGGBB.
2746    pub title_bg_unfocused: u32,
2747    /// Title text color: ARGB packed as 0xAARRGGBB.
2748    pub title_text_color: u32,
2749    /// Border color (focused): ARGB packed as 0xAARRGGBB.
2750    pub border_focused: u32,
2751    /// Border color (unfocused): ARGB packed as 0xAARRGGBB.
2752    pub border_unfocused: u32,
2753    /// Button size in pixels (close/maximize/minimize).
2754    pub button_size: u32,
2755    /// Padding between title text and buttons.
2756    pub button_padding: u32,
2757}
2758
2759impl DecorationConfig {
2760    /// Default decoration configuration matching the existing desktop style.
2761    pub fn default_config() -> Self {
2762        Self {
2763            title_bar_height: 28,
2764            border_width: 1,
2765            title_bg_focused: 0xFF34_495E,
2766            title_bg_unfocused: 0xFF57_6574,
2767            title_text_color: 0xFFEC_F0F1,
2768            border_focused: 0xFF2C_3E50,
2769            border_unfocused: 0xFF7F_8C8D,
2770            button_size: 16,
2771            button_padding: 6,
2772        }
2773    }
2774}
2775
2776impl Default for DecorationConfig {
2777    fn default() -> Self {
2778        Self::default_config()
2779    }
2780}
2781
2782/// Buttons that can appear in the title bar.
2783#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2784pub enum DecorationButton {
2785    Close,
2786    Maximize,
2787    Minimize,
2788}
2789
2790/// Result of hit-testing a point against window decorations.
2791#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2792pub enum DecorationHitTest {
2793    /// Point is not in any decoration area.
2794    None,
2795    /// Title bar (drag region).
2796    TitleBar,
2797    /// Close button.
2798    CloseButton,
2799    /// Maximize button.
2800    MaximizeButton,
2801    /// Minimize button.
2802    MinimizeButton,
2803    /// Border edge for resize.
2804    Border(BorderEdge),
2805}
2806
2807/// Which border edge was hit for resize operations.
2808#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2809pub enum BorderEdge {
2810    Top,
2811    Bottom,
2812    Left,
2813    Right,
2814    TopLeft,
2815    TopRight,
2816    BottomLeft,
2817    BottomRight,
2818}
2819
2820/// Render a complete window decoration frame into a pixel buffer.
2821///
2822/// Draws the title bar (with title text), border outline, and close/maximize/
2823/// minimize buttons. The buffer is expected to include space for decorations:
2824/// total width = `width`, total height = `height` (title bar at top, border
2825/// around all edges).
2826///
2827/// Pixels are written as 0xAARRGGBB (ARGB8888).
2828pub fn render_window_decoration(
2829    buffer: &mut [u32],
2830    buf_w: u32,
2831    config: &DecorationConfig,
2832    title: &str,
2833    focused: bool,
2834    width: u32,
2835    height: u32,
2836) {
2837    let bw = config.border_width;
2838    let tbh = config.title_bar_height;
2839    let border_color = if focused {
2840        config.border_focused
2841    } else {
2842        config.border_unfocused
2843    };
2844    let title_bg = if focused {
2845        config.title_bg_focused
2846    } else {
2847        config.title_bg_unfocused
2848    };
2849
2850    // Draw border (top, bottom, left, right strips)
2851    for y in 0..height {
2852        for x in 0..width {
2853            let idx = (y * buf_w + x) as usize;
2854            if idx >= buffer.len() {
2855                continue;
2856            }
2857            let in_top_border = y < bw;
2858            let in_bottom_border = y >= height.saturating_sub(bw);
2859            let in_left_border = x < bw;
2860            let in_right_border = x >= width.saturating_sub(bw);
2861
2862            if in_top_border || in_bottom_border || in_left_border || in_right_border {
2863                buffer[idx] = border_color;
2864            }
2865        }
2866    }
2867
2868    // Draw title bar background
2869    for y in bw..bw.saturating_add(tbh).min(height) {
2870        for x in bw..width.saturating_sub(bw) {
2871            let idx = (y * buf_w + x) as usize;
2872            if idx < buffer.len() {
2873                buffer[idx] = title_bg;
2874            }
2875        }
2876    }
2877
2878    // Draw title text (8x16 font)
2879    let text_x = bw + 8;
2880    let text_y = bw + (tbh.saturating_sub(16)) / 2;
2881    let mut cx = text_x;
2882    for &ch in title.as_bytes() {
2883        if cx + 8 >= width.saturating_sub(bw + 3 * (config.button_size + config.button_padding)) {
2884            break;
2885        }
2886        let glyph = crate::graphics::font8x16::glyph(ch);
2887        for row in 0..16u32 {
2888            let bits = glyph[row as usize];
2889            for col in 0..8u32 {
2890                if bits & (0x80 >> col) != 0 {
2891                    let px = cx + col;
2892                    let py = text_y + row;
2893                    let idx = (py * buf_w + px) as usize;
2894                    if idx < buffer.len() {
2895                        buffer[idx] = config.title_text_color;
2896                    }
2897                }
2898            }
2899        }
2900        cx += 8;
2901    }
2902
2903    // Draw buttons (right-aligned in title bar)
2904    let btn_y = bw + (tbh.saturating_sub(config.button_size)) / 2;
2905    let btn_sz = config.button_size;
2906    let btn_pad = config.button_padding;
2907
2908    // Close button (rightmost)
2909    let close_x = width.saturating_sub(bw + btn_pad + btn_sz);
2910    render_decoration_button(
2911        buffer,
2912        buf_w,
2913        close_x,
2914        btn_y,
2915        btn_sz,
2916        DecorationButton::Close,
2917        false,
2918    );
2919
2920    // Maximize button
2921    let max_x = close_x.saturating_sub(btn_pad + btn_sz);
2922    render_decoration_button(
2923        buffer,
2924        buf_w,
2925        max_x,
2926        btn_y,
2927        btn_sz,
2928        DecorationButton::Maximize,
2929        false,
2930    );
2931
2932    // Minimize button
2933    let min_x = max_x.saturating_sub(btn_pad + btn_sz);
2934    render_decoration_button(
2935        buffer,
2936        buf_w,
2937        min_x,
2938        btn_y,
2939        btn_sz,
2940        DecorationButton::Minimize,
2941        false,
2942    );
2943}
2944
2945/// Hit-test a point (relative to the window's top-left including decorations)
2946/// against the decoration regions.
2947pub fn hit_test_decoration(
2948    x: i32,
2949    y: i32,
2950    config: &DecorationConfig,
2951    width: u32,
2952    height: u32,
2953) -> DecorationHitTest {
2954    let bw = config.border_width as i32;
2955    let tbh = config.title_bar_height as i32;
2956    let w = width as i32;
2957    let h = height as i32;
2958    let btn_sz = config.button_size as i32;
2959    let btn_pad = config.button_padding as i32;
2960
2961    // Outside the window entirely
2962    if x < 0 || y < 0 || x >= w || y >= h {
2963        return DecorationHitTest::None;
2964    }
2965
2966    // Border corners (8x8 corner zones)
2967    let corner = bw.max(8);
2968    if x < corner && y < corner {
2969        return DecorationHitTest::Border(BorderEdge::TopLeft);
2970    }
2971    if x >= w - corner && y < corner {
2972        return DecorationHitTest::Border(BorderEdge::TopRight);
2973    }
2974    if x < corner && y >= h - corner {
2975        return DecorationHitTest::Border(BorderEdge::BottomLeft);
2976    }
2977    if x >= w - corner && y >= h - corner {
2978        return DecorationHitTest::Border(BorderEdge::BottomRight);
2979    }
2980
2981    // Border edges
2982    if y < bw {
2983        return DecorationHitTest::Border(BorderEdge::Top);
2984    }
2985    if y >= h - bw {
2986        return DecorationHitTest::Border(BorderEdge::Bottom);
2987    }
2988    if x < bw {
2989        return DecorationHitTest::Border(BorderEdge::Left);
2990    }
2991    if x >= w - bw {
2992        return DecorationHitTest::Border(BorderEdge::Right);
2993    }
2994
2995    // Title bar region
2996    if y >= bw && y < bw + tbh {
2997        // Check buttons (right-aligned)
2998        let close_x = w - bw - btn_pad - btn_sz;
2999        if x >= close_x && x < close_x + btn_sz {
3000            return DecorationHitTest::CloseButton;
3001        }
3002
3003        let max_x = close_x - btn_pad - btn_sz;
3004        if x >= max_x && x < max_x + btn_sz {
3005            return DecorationHitTest::MaximizeButton;
3006        }
3007
3008        let min_x = max_x - btn_pad - btn_sz;
3009        if x >= min_x && x < min_x + btn_sz {
3010            return DecorationHitTest::MinimizeButton;
3011        }
3012
3013        return DecorationHitTest::TitleBar;
3014    }
3015
3016    DecorationHitTest::None
3017}
3018
3019/// Render a single decoration button (close, maximize, or minimize).
3020///
3021/// Draws a small icon inside a square region starting at (`x`, `y`) with
3022/// the given `size`. If `hovered`, the background is slightly highlighted.
3023pub fn render_decoration_button(
3024    buffer: &mut [u32],
3025    buf_w: u32,
3026    x: u32,
3027    y: u32,
3028    size: u32,
3029    button_type: DecorationButton,
3030    hovered: bool,
3031) {
3032    // Button background
3033    let bg = if hovered {
3034        match button_type {
3035            DecorationButton::Close => 0xFFE7_4C3C,    // Red highlight
3036            DecorationButton::Maximize => 0xFF2E_CC71, // Green highlight
3037            DecorationButton::Minimize => 0xFFF3_9C12, // Yellow highlight
3038        }
3039    } else {
3040        match button_type {
3041            DecorationButton::Close => 0xFFC0_392B,
3042            DecorationButton::Maximize => 0xFF27_AE60,
3043            DecorationButton::Minimize => 0xFFF1_C40F,
3044        }
3045    };
3046
3047    // Fill button background (circle approximation: filled square with inset)
3048    let inset = size / 6;
3049    for dy in inset..size.saturating_sub(inset) {
3050        for dx in inset..size.saturating_sub(inset) {
3051            let px = x + dx;
3052            let py = y + dy;
3053            let idx = (py * buf_w + px) as usize;
3054            if idx < buffer.len() {
3055                buffer[idx] = bg;
3056            }
3057        }
3058    }
3059
3060    // Draw icon glyph
3061    let icon_color: u32 = 0xFFFF_FFFF;
3062    let cx = x + size / 2;
3063    let cy = y + size / 2;
3064    let half = (size / 4).max(2);
3065
3066    match button_type {
3067        DecorationButton::Close => {
3068            // X shape: two diagonal lines
3069            for i in 0..half {
3070                let coords = [
3071                    (cx - half + i, cy - half + i),
3072                    (cx + half - i - 1, cy - half + i),
3073                    (cx - half + i, cy + half - i - 1),
3074                    (cx + half - i - 1, cy + half - i - 1),
3075                ];
3076                for &(px, py) in &coords {
3077                    let idx = (py * buf_w + px) as usize;
3078                    if idx < buffer.len() {
3079                        buffer[idx] = icon_color;
3080                    }
3081                }
3082            }
3083        }
3084        DecorationButton::Maximize => {
3085            // Rectangle outline
3086            for i in 0..(half * 2) {
3087                let coords = [
3088                    (cx - half + i, cy - half),     // top
3089                    (cx - half + i, cy + half - 1), // bottom
3090                    (cx - half, cy - half + i),     // left
3091                    (cx + half - 1, cy - half + i), // right
3092                ];
3093                for &(px, py) in &coords {
3094                    let idx = (py * buf_w + px) as usize;
3095                    if idx < buffer.len() {
3096                        buffer[idx] = icon_color;
3097                    }
3098                }
3099            }
3100        }
3101        DecorationButton::Minimize => {
3102            // Horizontal line at bottom
3103            for i in 0..(half * 2) {
3104                let px = cx - half + i;
3105                let py = cy + half / 2;
3106                let idx = (py * buf_w + px) as usize;
3107                if idx < buffer.len() {
3108                    buffer[idx] = icon_color;
3109                }
3110            }
3111        }
3112    }
3113}