// SUB-OS Desktop Shell: wallpaper, icon grid, taskbar, menus and compositor loop // // The shell owns the frame loop. Every iteration it samples the input devices, // routes the result through the modal dialog layer, then the desktop chrome, // then the window manager, and finally composites the scene back to front: // wallpaper, icons, windows, taskbar, menus, dialog, cursor. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define START_MENU_W 232 #define START_ITEM_H 26 #define CONTEXT_MENU_W 150 #define CONTEXT_ITEM_H 22 typedef void (*launch_fn_t)(int x, int y, int w, int h); typedef struct { const char* label; gui_icon_id_t icon; launch_fn_t launch; int x, y, w, h; } desktop_app_t; // Launchers used by both the icon grid and the start menu. static const desktop_app_t g_apps[] = { { "Terminal", GUI_ICON_TERMINAL, gui_app_terminal_launch, 30, 40, 560, 340 }, { "Files", GUI_ICON_FOLDER, gui_app_fileman_launch, 80, 70, 380, 240 }, { "Monitor", GUI_ICON_MONITOR, gui_app_sysmon_launch, 420, 40, 340, 240 }, { "Tasks", GUI_ICON_TASKS, gui_app_taskmgr_launch, 180, 60, 380, 300 }, { "Kernel Log", GUI_ICON_LOG, gui_app_logviewer_launch, 60, 90, 640, 330 }, { "KTest", GUI_ICON_FLASK, gui_app_ktest_launch, 140, 80, 470, 250 }, { "Editor", GUI_ICON_FILE, gui_app_editor_launch, 120, 50, 500, 330 }, { "Calculator", GUI_ICON_CALC, gui_app_calc_launch, 260, 100, 220, 240 }, { "Paint", GUI_ICON_PAINT, gui_app_paint_launch, 200, 60, 360, 260 }, { "Life", GUI_ICON_FLASK, gui_app_life_launch, 240, 70, 480, 380 }, { "Rust Lab", GUI_ICON_INFO, gui_app_rustlab_launch, 300, 80, 420, 300 }, { "Objects", GUI_ICON_TASKS, gui_app_objbrowse_launch,280, 70, 460, 340 }, { "RegEdit", GUI_ICON_SETTINGS, gui_app_regedit_launch, 300, 90, 480, 360 }, { "Clock", GUI_ICON_CLOCK, gui_app_clock_launch, 460, 120, 280, 260 }, { "Settings", GUI_ICON_SETTINGS, gui_app_settings_launch, 160, 60, 400, 320 }, { "Analytics", GUI_ICON_MONITOR, gui_app_analytics_launch,320, 70, 560, 400 }, { "Sorry", GUI_ICON_HEART, gui_app_sorry_launch, 260, 60, 660, 470 }, { "About", GUI_ICON_INFO, gui_app_about_launch, 240, 140, 330, 170 }, }; #define APP_COUNT ((int)(sizeof(g_apps) / sizeof(g_apps[0]))) // Which of the above appear on the desktop itself. // Indices into g_apps[]: Terminal, Files, Monitor, Kernel Log, KTest, Life, // Rust Lab, Settings, Analytics, Sorry. (Objects/RegEdit live in the start // menu.) static const int g_desktop_icons[] = { 0, 1, 2, 4, 5, 9, 10, 14, 15, 16 }; #define DESKTOP_ICON_COUNT ((int)(sizeof(g_desktop_icons) / sizeof(g_desktop_icons[0]))) static bool g_start_menu_open = false; static bool g_context_menu_open = false; static int g_context_x = 0, g_context_y = 0; static bool g_desktop_running = false; static bool g_prev_left = false, g_prev_right = false; static bool g_show_grid = true; static bool g_clock_seconds = true; static int g_selected_icon = -1; static int g_hover_menu_item = -1; static uint64_t g_frames = 0; static uint32_t g_fps = 0; // frames per second, sampled from ktime static uint64_t g_fps_last_ms = 0; static uint64_t g_fps_last_frames = 0; static uint64_t g_last_scene_ms = 0; // last full scene recomposite (for heartbeat) static int g_prev_mx = -1, g_prev_my = -1; static bool g_anim_request = false; // an app asked for the next frame back // Target frame budget: 60 FPS == 16666 us. The compositor only repaints the // full scene when it is dirty; between times it just moves the cursor overlay, // so idle and pointer-only frames comfortably hit the cap. #define FRAME_BUDGET_US 16666 #define SCENE_HEARTBEAT_MS 100 // ~10 Hz refresh for live content (clock, graphs) /* * Cached wallpaper. * * The background is a full-screen vertical gradient with an optional blended * grid and three strings on top. None of it changes between frames, but it was * being recomputed pixel by pixel on every pass of the compositor loop. It is * drawn once into this buffer instead and blitted back each frame; the cache * is dropped whenever the resolution or the grid setting changes. */ /* Rolling count of pixels actually written to the framebuffer, so the cost of * the compositor is visible on the console without a profiler attached. */ static uint64_t g_pushed_pixels = 0; static uint32_t* g_wallpaper = NULL; static int g_wallpaper_w = 0; static int g_wallpaper_h = 0; static bool g_wallpaper_grid = false; static uint8_t g_cpu_history[48]; // The launcher geometry below is authored against an 800x600 work area. #define DESIGN_W 800 #define DESIGN_H 568 static void launch_app(int index) { if (index < 0 || index >= APP_COUNT) return; const desktop_app_t* a = &g_apps[index]; if (!a->launch) return; int sw = gui_gfx_get_width(); int wh = gui_desktop_workarea_height(); // Scale the design geometry to the live resolution, but never shrink below // it: a window sized for 800x600 is already at its comfortable minimum. int x = (a->x * sw) / DESIGN_W; int y = (a->y * wh) / DESIGN_H; int w = (a->w * sw) / DESIGN_W; int h = (a->h * wh) / DESIGN_H; if (w < a->w) w = a->w; if (h < a->h) h = a->h; // Leave the desktop breathing room: a window scaled straight from the // design size swallows most of a 1280x720 screen and buries the wallpaper. int max_w = (sw * 62) / 100; int max_h = (wh * 72) / 100; if (w > max_w) w = max_w; if (h > max_h) h = max_h; // Keep clear of the icon column so launcher labels stay readable. int icon_strip = GUI_DESKTOP_ICON_W + 28; if (x < icon_strip) x = icon_strip; if (x + w > sw - 8) x = sw - 8 - w; if (y + h > wh - 8) y = wh - 8 - h; if (y < 8) y = 8; a->launch(x, y, w, h); } int gui_desktop_workarea_height(void) { return gui_gfx_get_height() - GUI_TASKBAR_HEIGHT; } void gui_desktop_init(void) { const fb_info_t* fb = fb_get_info(); uint32_t* fb_addr = fb ? fb->address : NULL; int w = (fb && fb->width > 0) ? fb->width : GUI_DEFAULT_WIDTH; int h = (fb && fb->height > 0) ? fb->height : GUI_DEFAULT_HEIGHT; gui_gfx_init(fb_addr, w, h); /* The console owned the framebuffer until now, so nothing on screen * matches the compositor's idea of it; drop both caches. */ gui_gfx_invalidate(); gui_desktop_invalidate_background(); gui_icons_init(); gui_cursor_init(); gui_dialog_init(); gui_wm_init(); gui_wm_set_workarea(w, h - GUI_TASKBAR_HEIGHT); mouse_set_bounds(w, h); g_start_menu_open = false; g_context_menu_open = false; g_selected_icon = -1; // Seed the taskbar CPU trace with a plausible idle curve. for (int i = 0; i < 48; i++) { g_cpu_history[i] = (uint8_t)(12 + ((i * 7) % 23)); } } // =========================================================================== // Wallpaper and desktop icons // =========================================================================== /* Paint the wallpaper straight into the back buffer. A three-stop vertical * gradient gives the sky depth, a soft edge vignette frames it, and an optional * fine grid adds texture without banding. Painted once and cached. */ static void desktop_paint_background(int sw, int wh) { // Three-stop vertical gradient: deep space blue -> indigo -> a touch lighter. int mid = wh * 55 / 100; gui_gfx_draw_gradient_v(0, 0, sw, mid, 0xFF070B18, 0xFF141C38); gui_gfx_draw_gradient_v(0, mid, sw, wh - mid, 0xFF141C38, 0xFF1D2A52); if (g_show_grid) { // A finer, fainter grid than before reads as texture rather than lines. for (int y = 0; y < wh; y += 48) { gui_gfx_fill_rect_blend(0, y, sw, 1, 0xFF3B4E80, 22); } for (int x = 0; x < sw; x += 48) { gui_gfx_fill_rect_blend(x, 0, 1, wh, 0xFF3B4E80, 22); } } // Soft vignette: concentric 1px frames darkening toward the edges. The // alpha falls off with depth, so the corners recede smoothly. int depth = (wh < sw ? wh : sw) / 6; for (int i = 0; i < depth; i++) { uint8_t a = (uint8_t)(((depth - i) * 42) / depth); if (a == 0) continue; gui_gfx_fill_rect_blend(i, i, sw - 2 * i, 1, GUI_COLOR_BLACK, a); // top gui_gfx_fill_rect_blend(i, wh - 1 - i, sw - 2 * i, 1, GUI_COLOR_BLACK, a); // bottom gui_gfx_fill_rect_blend(i, i, 1, wh - 2 * i, GUI_COLOR_BLACK, a); // left gui_gfx_fill_rect_blend(sw - 1 - i, i, 1, wh - 2 * i, GUI_COLOR_BLACK, a); // right } // Centred brand mark with a subtle glow (drawn as a shadowed 16px title). const char* brand = "SUB-OS"; int glyph_w = 16; // draw_string_16 advances ~16px per glyph int bw = (int)strlen(brand) * glyph_w; int bx = (sw - bw) / 2; int by = wh / 2 - 40; gui_gfx_draw_string_16_shadow(bx, by, brand, 0xFF7DD3FC, 0xFF0C4A6E); const char* sub = "Modular Monolithic Kernel - v0.2.0-LTS"; int sw2 = (int)strlen(sub) * 8; gui_gfx_draw_string((sw - sw2) / 2, by + 24, sub, 0xFF64748B); const char* hint = "F5 cycle F6 cascade F7 tile F8 maximize"; int hw = (int)strlen(hint) * 8; gui_gfx_draw_string((sw - hw) / 2, by + 40, hint, 0xFF3E4C6B); } void gui_desktop_invalidate_background(void) { if (g_wallpaper) { kfree(g_wallpaper); g_wallpaper = NULL; } g_wallpaper_w = 0; g_wallpaper_h = 0; } void gui_desktop_render_background(void) { int sw = gui_gfx_get_width(); int wh = gui_desktop_workarea_height(); uint32_t* back = gui_gfx_get_backbuffer(); if (sw <= 0 || wh <= 0 || !back) return; bool stale = (!g_wallpaper || g_wallpaper_w != sw || g_wallpaper_h != wh || g_wallpaper_grid != g_show_grid); if (stale) { if (g_wallpaper) { kfree(g_wallpaper); g_wallpaper = NULL; } /* Paint it once through the normal primitives, then keep a copy. The * back buffer is the scratch space, which is safe because the caller * redraws everything above the wallpaper straight afterwards. */ desktop_paint_background(sw, wh); g_wallpaper = (uint32_t*)kmalloc((size_t)sw * (size_t)wh * sizeof(uint32_t)); if (g_wallpaper) { for (int y = 0; y < wh; y++) { memcpy(g_wallpaper + (size_t)y * sw, back + (size_t)y * sw, (size_t)sw * sizeof(uint32_t)); } g_wallpaper_w = sw; g_wallpaper_h = wh; g_wallpaper_grid = g_show_grid; } return; /* the back buffer already holds the freshly painted copy */ } for (int y = 0; y < wh; y++) { memcpy(back + (size_t)y * sw, g_wallpaper + (size_t)y * sw, (size_t)sw * sizeof(uint32_t)); } } void gui_desktop_render_icons(void) { for (int i = 0; i < DESKTOP_ICON_COUNT; i++) { const desktop_app_t* a = &g_apps[g_desktop_icons[i]]; int ix = 16; int iy = 16 + i * GUI_DESKTOP_ICON_H; bool selected = (g_selected_icon == i); // A translucent tile behind every icon: several glyphs are dark by // design and would otherwise vanish into the dark wallpaper. if (selected) { gui_gfx_fill_rect_blend(ix - 4, iy - 4, GUI_DESKTOP_ICON_W, GUI_DESKTOP_ICON_H - 4, GUI_THEME_PRIMARY, 70); gui_gfx_draw_rect(ix - 4, iy - 4, GUI_DESKTOP_ICON_W, GUI_DESKTOP_ICON_H - 4, GUI_THEME_PRIMARY); } else { gui_gfx_fill_rect_blend(ix + 10, iy - 4, 40, 40, GUI_THEME_BG_ELEVATED, 150); gui_gfx_draw_rect(ix + 10, iy - 4, 40, 40, 0xFF3D4C63); } gui_icon_draw_scaled(a->icon, ix + 14, iy, 2); int tw = (int)strlen(a->label) * 8; int tx = ix + 30 - tw / 2; if (tx < 0) tx = 0; gui_gfx_draw_string_shadow(tx, iy + 40, a->label, GUI_THEME_TEXT_MAIN, GUI_COLOR_BLACK); } } // Returns the icon index under the point, or -1. static int icon_hit_test(int mx, int my) { for (int i = 0; i < DESKTOP_ICON_COUNT; i++) { int ix = 12; int iy = 12 + i * GUI_DESKTOP_ICON_H; if (mx >= ix && mx < ix + GUI_DESKTOP_ICON_W && my >= iy && my < iy + GUI_DESKTOP_ICON_H - 4) { return i; } } return -1; } // =========================================================================== // Taskbar // =========================================================================== void gui_desktop_render_taskbar(void) { int sw = gui_gfx_get_width(); int ty = gui_desktop_workarea_height(); gui_gfx_draw_gradient_v(0, ty, sw, GUI_TASKBAR_HEIGHT, 0xFF111827, GUI_THEME_TASKBAR_BG); gui_gfx_draw_line(0, ty, sw - 1, ty, GUI_THEME_BORDER); // Start button uint32_t start_bg = g_start_menu_open ? GUI_THEME_PRIMARY_DARK : GUI_THEME_PRIMARY; gui_gfx_fill_rect(6, ty + 4, 92, 24, start_bg); gui_gfx_draw_rect(6, ty + 4, 92, 24, GUI_THEME_PRIMARY_DARK); gui_icon_draw(GUI_ICON_TERMINAL, 11, ty + 8); gui_gfx_draw_string_16_shadow(32, ty + 8, "SUB-OS", GUI_COLOR_WHITE, GUI_COLOR_BLACK); // One button per window, in z-order int tab_x = 106; int tray_x = sw - 260; // widened tray: FPS pill + CPU trace + mem + clock for (int i = 0; i < gui_wm_get_window_count() && tab_x < tray_x - 106; i++) { gui_window_t* win = gui_wm_get_window_at_index(i); if (!win || !win->visible) continue; uint32_t tab_bg = win->active && !win->minimized ? GUI_THEME_BG_ELEVATED : GUI_THEME_BG_SURFACE; gui_gfx_fill_rect(tab_x, ty + 4, 100, 24, tab_bg); gui_gfx_draw_rect(tab_x, ty + 4, 100, 24, win->active && !win->minimized ? GUI_THEME_PRIMARY : GUI_THEME_BORDER); // Active window gets an accent underline; minimized ones are dimmed. if (win->active && !win->minimized) { gui_gfx_fill_rect(tab_x + 1, ty + 25, 98, 2, GUI_THEME_PRIMARY); } char short_title[12]; strncpy(short_title, win->title, 11); short_title[11] = '\0'; gui_gfx_draw_string(tab_x + 8, ty + 12, short_title, win->minimized ? GUI_THEME_TEXT_DIM : (win->active ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED)); tab_x += 106; } // System tray: CPU trace, memory badge, clock for (int i = 0; i < 47; i++) { int x0 = tray_x + i; int y0 = ty + 26 - (g_cpu_history[i] * 18) / 100; int y1 = ty + 26 - (g_cpu_history[i + 1] * 18) / 100; gui_gfx_draw_line(x0, y0, x0 + 1, y1, GUI_THEME_SUCCESS); } uint64_t total = pmm_get_total_pages(); uint64_t used = pmm_get_used_pages(); char buf[24]; snprintf(buf, sizeof(buf), "%llu MB", (unsigned long long)((used * 4096) / (1024 * 1024))); gui_gfx_fill_rect(sw - 145, ty + 6, 52, 20, GUI_THEME_SUCCESS); gui_gfx_draw_rect(sw - 145, ty + 6, 52, 20, 0xFF059669); gui_gfx_draw_string(sw - 140, ty + 12, buf, GUI_COLOR_BLACK); (void)total; // Live FPS pill, tinted by how smooth the compositor is running. char fps[16]; snprintf(fps, sizeof(fps), "%u FPS", g_fps); uint32_t fps_col = (g_fps >= 45) ? GUI_THEME_SUCCESS : (g_fps >= 20) ? GUI_THEME_WARNING : GUI_THEME_DANGER; gui_gfx_draw_string(sw - 205, ty + 12, fps, fps_col); rtc_time_t now; rtc_get_time(&now); if (g_clock_seconds) { snprintf(buf, sizeof(buf), "%02u:%02u:%02u", now.hour, now.minute, now.second); } else { snprintf(buf, sizeof(buf), "%02u:%02u", now.hour, now.minute); } gui_gfx_draw_string_16_shadow(sw - 84, ty + 8, buf, GUI_THEME_PRIMARY, GUI_COLOR_BLACK); } void gui_desktop_set_grid(bool on) { g_show_grid = on; } bool gui_desktop_get_grid(void) { return g_show_grid; } void gui_desktop_set_clock_seconds(bool on) { g_clock_seconds = on; } bool gui_desktop_get_clock_seconds(void) { return g_clock_seconds; } // =========================================================================== // Start menu // =========================================================================== static int start_menu_height(void) { // Header + one row per app + separator + power row. return 32 + APP_COUNT * START_ITEM_H + 10 + START_ITEM_H + 8; } static void start_menu_origin(int* mx, int* my) { *mx = 6; *my = gui_desktop_workarea_height() - start_menu_height() - 6; if (*my < 4) *my = 4; } void gui_desktop_render_start_menu(void) { if (!g_start_menu_open) return; int mx, my; start_menu_origin(&mx, &my); int mh = start_menu_height(); gui_gfx_draw_shadow(mx, my, START_MENU_W, mh, 10); gui_gfx_fill_rect(mx, my, START_MENU_W, mh, GUI_THEME_BG_SURFACE); gui_gfx_draw_rect(mx, my, START_MENU_W, mh, GUI_THEME_PRIMARY); gui_gfx_draw_gradient_v(mx + 1, my + 1, START_MENU_W - 2, 30, GUI_THEME_PRIMARY, GUI_THEME_PRIMARY_DARK); gui_gfx_draw_string_16_shadow(mx + 12, my + 7, "Applications", GUI_COLOR_WHITE, GUI_COLOR_BLACK); for (int i = 0; i < APP_COUNT; i++) { int iy = my + 32 + i * START_ITEM_H; bool hot = (g_hover_menu_item == i); if (hot) { gui_gfx_fill_rect(mx + 2, iy, START_MENU_W - 4, START_ITEM_H, GUI_THEME_BG_ELEVATED); gui_gfx_fill_rect(mx + 2, iy, 3, START_ITEM_H, GUI_THEME_PRIMARY); } gui_icon_draw(g_apps[i].icon, mx + 12, iy + (START_ITEM_H - 16) / 2); gui_gfx_draw_string(mx + 36, iy + (START_ITEM_H - 8) / 2, g_apps[i].label, hot ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED); } int sep_y = my + 32 + APP_COUNT * START_ITEM_H + 4; gui_gfx_draw_line(mx + 8, sep_y, mx + START_MENU_W - 9, sep_y, GUI_THEME_BORDER); int power_y = sep_y + 6; bool power_hot = (g_hover_menu_item == APP_COUNT); if (power_hot) { gui_gfx_fill_rect(mx + 2, power_y, START_MENU_W - 4, START_ITEM_H, 0xFF3B1418); gui_gfx_fill_rect(mx + 2, power_y, 3, START_ITEM_H, GUI_THEME_DANGER); } gui_icon_draw(GUI_ICON_POWER, mx + 12, power_y + (START_ITEM_H - 16) / 2); gui_gfx_draw_string(mx + 36, power_y + (START_ITEM_H - 8) / 2, "Exit to Kernel TTY", GUI_THEME_DANGER); } // =========================================================================== // Desktop context menu // =========================================================================== static const char* const g_context_items[] = { "Tile Windows", "Cascade Windows", "Toggle Grid", "New Terminal", "About SUB-OS" }; #define CONTEXT_ITEM_COUNT 5 void gui_desktop_open_context_menu(int mx, int my) { g_context_menu_open = true; g_start_menu_open = false; int menu_h = CONTEXT_ITEM_COUNT * CONTEXT_ITEM_H + 8; g_context_x = mx; g_context_y = my; // Flip the menu back on screen when opened near an edge. if (g_context_x + CONTEXT_MENU_W > gui_gfx_get_width()) { g_context_x = gui_gfx_get_width() - CONTEXT_MENU_W - 2; } if (g_context_y + menu_h > gui_desktop_workarea_height()) { g_context_y = gui_desktop_workarea_height() - menu_h - 2; } } void gui_desktop_render_context_menu(void) { if (!g_context_menu_open) return; int mh = CONTEXT_ITEM_COUNT * CONTEXT_ITEM_H + 8; gui_gfx_draw_shadow(g_context_x, g_context_y, CONTEXT_MENU_W, mh, 6); gui_gfx_fill_rect(g_context_x, g_context_y, CONTEXT_MENU_W, mh, GUI_THEME_BG_SURFACE); gui_gfx_draw_rect(g_context_x, g_context_y, CONTEXT_MENU_W, mh, GUI_THEME_ACCENT); for (int i = 0; i < CONTEXT_ITEM_COUNT; i++) { int iy = g_context_y + 4 + i * CONTEXT_ITEM_H; bool hot = (g_hover_menu_item == 100 + i); if (hot) { gui_gfx_fill_rect(g_context_x + 2, iy, CONTEXT_MENU_W - 4, CONTEXT_ITEM_H, GUI_THEME_BG_ELEVATED); } gui_gfx_draw_string(g_context_x + 12, iy + (CONTEXT_ITEM_H - 8) / 2, g_context_items[i], hot ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED); } } void gui_desktop_toggle_start_menu(void) { g_start_menu_open = !g_start_menu_open; if (g_start_menu_open) g_context_menu_open = false; } bool gui_desktop_is_start_menu_open(void) { return g_start_menu_open; } void gui_desktop_close_menus(void) { g_start_menu_open = false; g_context_menu_open = false; } void gui_desktop_request_animation_frame(void) { g_anim_request = true; } void gui_desktop_request_exit(void) { g_desktop_running = false; } static void exit_confirm_cb(gui_dialog_result_t result, void* ctx) { (void)ctx; if (result == GUI_DIALOG_RESULT_OK) g_desktop_running = false; } // =========================================================================== // Input routing // =========================================================================== // Track which menu row the pointer is over so rendering can highlight it. static void update_menu_hover(int mx, int my) { g_hover_menu_item = -1; if (g_start_menu_open) { int smx, smy; start_menu_origin(&smx, &smy); if (mx >= smx && mx < smx + START_MENU_W) { int rel = my - (smy + 32); if (rel >= 0) { int idx = rel / START_ITEM_H; if (idx >= 0 && idx < APP_COUNT) { g_hover_menu_item = idx; } else { int power_y = smy + 32 + APP_COUNT * START_ITEM_H + 10; if (my >= power_y && my < power_y + START_ITEM_H) { g_hover_menu_item = APP_COUNT; } } } } return; } if (g_context_menu_open) { if (mx >= g_context_x && mx < g_context_x + CONTEXT_MENU_W) { int rel = my - (g_context_y + 4); int idx = rel / CONTEXT_ITEM_H; if (rel >= 0 && idx < CONTEXT_ITEM_COUNT) g_hover_menu_item = 100 + idx; } } } static bool handle_start_menu_click(int mx, int my) { int smx, smy; start_menu_origin(&smx, &smy); int mh = start_menu_height(); if (mx < smx || mx >= smx + START_MENU_W || my < smy || my >= smy + mh) { g_start_menu_open = false; return true; // The dismissing click is consumed, not passed through. } int rel = my - (smy + 32); int idx = rel / START_ITEM_H; if (rel >= 0 && idx >= 0 && idx < APP_COUNT) { g_start_menu_open = false; launch_app(idx); return true; } int power_y = smy + 32 + APP_COUNT * START_ITEM_H + 10; if (my >= power_y && my < power_y + START_ITEM_H) { g_start_menu_open = false; gui_dialog_confirm("Exit Desktop", "Leave the graphical session and return to the kernel TTY?", exit_confirm_cb, NULL); return true; } return true; } static bool handle_context_menu_click(int mx, int my) { int mh = CONTEXT_ITEM_COUNT * CONTEXT_ITEM_H + 8; if (mx < g_context_x || mx >= g_context_x + CONTEXT_MENU_W || my < g_context_y || my >= g_context_y + mh) { g_context_menu_open = false; return true; } int rel = my - (g_context_y + 4); g_context_menu_open = false; if (rel < 0) return true; int idx = rel / CONTEXT_ITEM_H; switch (idx) { case 0: gui_wm_tile_windows(); break; case 1: gui_wm_cascade_windows(); break; case 2: g_show_grid = !g_show_grid; break; case 3: launch_app(0); break; case 4: launch_app(APP_COUNT - 1); break; default: break; } return true; } static bool handle_taskbar_click(int mx, int my) { int ty = gui_desktop_workarea_height(); if (mx >= 6 && mx < 98 && my >= ty + 4 && my < ty + 28) { gui_desktop_toggle_start_menu(); return true; } int tab_x = 106; int tray_x = gui_gfx_get_width() - 200; for (int i = 0; i < gui_wm_get_window_count() && tab_x < tray_x - 106; i++) { gui_window_t* win = gui_wm_get_window_at_index(i); if (!win || !win->visible) continue; if (mx >= tab_x && mx < tab_x + 100 && my >= ty + 4 && my < ty + 28) { // Clicking the focused window's button minimizes it; otherwise // the window is restored and raised. if (win->active && !win->minimized) { gui_wm_minimize_window(win->id); } else { gui_wm_restore_window(win->id); gui_wm_focus_window(win->id); } return true; } tab_x += 106; } return true; // Bare taskbar clicks are absorbed } bool gui_desktop_handle_click(int mx, int my) { if (g_start_menu_open) return handle_start_menu_click(mx, my); if (g_context_menu_open) return handle_context_menu_click(mx, my); if (my >= gui_desktop_workarea_height()) return handle_taskbar_click(mx, my); // Icons only claim the click when no window covers that spot. for (int i = gui_wm_get_window_count() - 1; i >= 0; i--) { gui_window_t* win = gui_wm_get_window_at_index(i); if (!win || !win->visible || win->minimized) continue; if (mx >= win->x && mx < win->x + win->width && my >= win->y && my < win->y + win->height) { return false; } } int icon = icon_hit_test(mx, my); if (icon >= 0) { if (g_selected_icon == icon) { // Second click on an already-selected icon launches it. launch_app(g_desktop_icons[icon]); g_selected_icon = -1; } else { g_selected_icon = icon; } return true; } g_selected_icon = -1; return false; } // =========================================================================== // Frame loop // =========================================================================== static void sample_cpu_history(void) { // Shift in a fresh sample derived from the current P-state. for (int i = 0; i < 47; i++) g_cpu_history[i] = g_cpu_history[i + 1]; cpufreq_stats_t cf = cpufreq_get_stats(); uint32_t span = (cf.max_khz > cf.min_khz) ? (cf.max_khz - cf.min_khz) : 1; uint32_t rel = (cf.current_khz > cf.min_khz) ? (cf.current_khz - cf.min_khz) : 0; uint32_t pct = (rel * 100) / span; if (pct > 100) pct = 100; g_cpu_history[47] = (uint8_t)pct; } int gui_desktop_run(void) { fbcon_enable(false); gui_desktop_init(); // Open a default session so the desktop is not empty on first boot. Placed // and sized for the HD work area rather than the old 800x600 defaults. int sw = gui_gfx_get_width(); int wh = gui_desktop_workarea_height(); gui_app_sysmon_launch(sw - 620, 60, 560, 360); gui_app_terminal_launch(120, 90, sw - 820, wh - 200); // Discard anything the console left in the keyboard queue; a stale key // arriving on frame 0 would otherwise act as a desktop shortcut. for (int drained = 0; drained < 16 && keyboard_has_key(); drained++) { (void)keyboard_get_key(); } g_desktop_running = true; printk(KERN_INFO "GUI: Desktop session started (ESC returns to the kernel TTY)\n"); while (g_desktop_running) { uint64_t frame_start_us = ktime_us(); // --- 1. Sample input ----------------------------------------------- const mouse_state_t* ms = mouse_get_state(); int mx = ms ? ms->x : gui_gfx_get_width() / 2; int my = ms ? ms->y : gui_gfx_get_height() / 2; bool left = ms ? ms->left_btn : false; bool right = ms ? ms->right_btn : false; gui_cursor_set_pos(mx, my); bool click_edge = left && !g_prev_left; bool right_click_edge = right && !g_prev_right; // --- 2. Route the pointer, modal layer first ------------------------ if (gui_dialog_is_open()) { gui_dialog_handle_mouse(mx, my, click_edge); gui_wm_sync_button_state(left); } else { update_menu_hover(mx, my); if (right_click_edge && my < gui_desktop_workarea_height()) { gui_desktop_open_context_menu(mx, my); gui_wm_sync_button_state(left); } else if (click_edge) { if (gui_desktop_handle_click(mx, my)) { gui_wm_sync_button_state(left); } else { gui_wm_handle_mouse(mx, my, left, right); } } else { gui_wm_handle_mouse(mx, my, left, right); } } g_prev_left = left; g_prev_right = right; // --- 3. Route the keyboard ------------------------------------------ // keyboard_get_key() blocks until a key arrives, which would freeze // the compositor, so only call it once the driver reports one ready. uint16_t key = keyboard_has_key() ? keyboard_get_key() : 0; if (key != 0) { if (gui_dialog_is_open()) { gui_dialog_handle_key(key); } else if ((char)(key & 0xFF) == 27) { if (g_start_menu_open || g_context_menu_open) { gui_desktop_close_menus(); } else { gui_dialog_confirm("Exit Desktop", "Leave the graphical session and return to " "the kernel TTY?", exit_confirm_cb, NULL); } } else { gui_wm_handle_key(key); } } // --- 4. Decide what needs repainting -------------------------------- // Advancing open animations mutates window geometry, so this both drives // the effect and reports whether it is still running. bool wm_animating = gui_wm_update(); uint64_t now_ms = ktime_ms(); bool heartbeat = (now_ms - g_last_scene_ms) >= SCENE_HEARTBEAT_MS; bool interacting = left || right || key || g_start_menu_open || g_context_menu_open || gui_dialog_is_open(); // Claim any animation request left by the previous frame's painters. // Clearing it here means a request only survives while an app keeps // renewing it from its paint routine. bool animating = g_anim_request; g_anim_request = false; // A full recomposite is needed when anything but the bare cursor moved; // otherwise we keep the retained scene and just slide the cursor overlay. bool scene_dirty = wm_animating || interacting || heartbeat || animating || (g_frames == 0); if ((g_frames & 0x1F) == 0) sample_cpu_history(); if (scene_dirty) { gui_desktop_render_background(); gui_desktop_render_icons(); gui_wm_render(); gui_desktop_render_taskbar(); gui_desktop_render_start_menu(); gui_desktop_render_context_menu(); gui_dialog_render(); g_last_scene_ms = now_ms; } else { // Retained scene: erase the previous cursor from it before redrawing. gui_cursor_restore_under(); } // The cursor is an overlay drawn on top of the (retained or fresh) scene, // so pointer motion never forces a full recomposite. gui_cursor_composite(); gui_gfx_present(); g_frames++; g_pushed_pixels += gui_gfx_last_present_pixels(); g_prev_mx = mx; g_prev_my = my; // --- 5. Pace to the 60 FPS budget ----------------------------------- uint64_t elapsed_us = ktime_us() - frame_start_us; if (elapsed_us < FRAME_BUDGET_US) { ktime_delay_us(FRAME_BUDGET_US - elapsed_us); } // Sample the frame rate roughly twice a second from the monotonic clock. uint64_t fps_now_ms = ktime_ms(); uint64_t dt = fps_now_ms - g_fps_last_ms; if (dt >= 500) { g_fps = (uint32_t)(((g_frames - g_fps_last_frames) * 1000ULL) / dt); g_fps_last_ms = fps_now_ms; g_fps_last_frames = g_frames; } if ((g_frames & 0xFF) == 0) { uint64_t full = (uint64_t)gui_gfx_get_width() * (uint64_t)gui_gfx_get_height(); uint64_t avg = g_pushed_pixels / 256; printk(KERN_INFO "GUI: %llu frames, %llu px/frame pushed of %llu (%llu%%)\n", (unsigned long long)g_frames, (unsigned long long)avg, (unsigned long long)full, (unsigned long long)(full ? (avg * 100) / full : 0)); g_pushed_pixels = 0; } } // Hand the screen to the framebuffer console. Without this the shell writes // into the VGA text buffer while the adapter is still scanning out the // framebuffer, so the TTY is visible only on the serial line -- which is // the host terminal, not the emulator window. fbcon_enable(true); /* The console is about to draw over the framebuffer, so the shadow copy * stops describing the screen; release the wallpaper cache with it. */ gui_gfx_invalidate(); gui_desktop_invalidate_background(); printk(KERN_INFO "GUI: Desktop session ended after %llu frames, TTY restored\n", (unsigned long long)g_frames); return 0; }