Files
SUB-OS/gui/gui_desktop.c
T
subhobhai943andClaude Opus 4.8 5bd18c4733 feat(cpp,gui): C++ Analytics Engine + live GUI Analytics app
Kernel (C++17 freestanding):
  - New Analytics Engine (kernel/cpp/cpp_analytics.cpp): templated rolling
    time-series (TimeSeries<N>) sampling four live channels -- CPU load %,
    memory %, kernel-heap %, network pkt/interval -- with windowed statistics
    (min/max/mean/stddev) computed in pure integer math (kernel is -mno-sse, so
    no FP; stddev uses a Newton integer isqrt). Demonstrates virtual dispatch
    via an Observer pattern (IAnalyticsObserver + a ThresholdAlarmObserver that
    counts breaches) and RAII singletons.
  - Extended cpp_containers.hpp with Optional<T>, Pair<A,B>, and the kmin/kmax/
    kclamp/kswap algorithms.
  - Full C fallback (cpp_fallback.c) so the engine is functionally identical on
    aarch64/armv8i, which don't build the .cpp path.
  - Wired cpp_analytics_init() into boot [8/14]; seeds a few samples so graphs
    are populated immediately.

GUI:
  - New "Analytics" desktop app (gui/gui_apps.c): a 2x2 grid of live line-charts
    backed by the C++ engine, each with gridlines, a soft baseline fill, and
    min/avg/max(/stddev) overlays; click a panel to focus one channel full-size,
    click again to return. Throttled to ~2 Hz sampling off the compositor's
    repaint cadence. Registered on the desktop and in the start menu.

Userland: new `cppstat [samples]` applet dumps the engine's per-channel stats
and Observer breach counters.

Verified in QEMU (x86_64): boots clean on x86_64/aarch64/armv8i; `cppstat`
prints live stats (CPU 6%, Mem 7%, Heap 22%, computed min/avg/max/stddev,
Observer breaches); the GUI Analytics window renders all four graphs with the
Kernel Heap curve visibly climbing 22->55% as windows open, compositor steady
at ~53 FPS.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fh5z6V7nGPbHvqVG5p4CXX
2026-08-26 10:42:48 +05:30

882 lines
33 KiB
C

// 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 <gui/gui_desktop.h>
#include <gui/gui_gfx.h>
#include <gui/gui_cursor.h>
#include <gui/gui_wm.h>
#include <gui/gui_apps.h>
#include <gui/gui_terminal.h>
#include <gui/gui_apps_ext.h>
#include <gui/gui_widgets.h>
#include <gui/gui_icons.h>
#include <gui/gui_dialog.h>
#include <gui/gui_theme.h>
#include <drivers/fb.h>
#include <drivers/bochs.h>
#include <drivers/fbcon.h>
#include <drivers/tty.h>
#include <drivers/mouse.h>
#include <drivers/keyboard.h>
#include <drivers/rtc.h>
#include <drivers/cpufreq.h>
#include <kernel/ktime.h>
#include <mm/pmm.h>
#include <mm/kmalloc.h>
#include <arch/arch.h>
#include <lib/string.h>
#include <lib/printf.h>
#include <kernel/printk.h>
#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 },
{ "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. (Objects/RegEdit live in the start menu.)
static const int g_desktop_icons[] = { 0, 1, 2, 4, 5, 9, 10, 14, 15 };
#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;
// 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_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();
// 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 || (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;
}