mirror of
https://github.com/subhobhai943/SUB-OS.git
synced 2026-10-25 16:52:29 +00:00
Kernel: the passive-open half of TCP. tcp_listen claims a port, inbound
SYNs to it are completed by the receive path, and the finished
connections queue in a backlog until tcp_accept collects them. Accepted
connections run the same stream state machine as active opens, so
tcp_send/tcp_recv/tcp_close work on them unchanged. sys_listen and
sys_accept expose this through the BSD socket API, with sys_accept
returning a fresh descriptor and leaving the listener available for the
next caller. The built-in sshd and httpd predate this and are still
answered inline from the receive path; the role they use is now named
TCP_ROLE_SERVICE to keep that distinction explicit.
GUI: a Network Monitor app. Interface configuration and link state, a
rolling dual-line chart of receive and transmit rates sampled from the
NIC counters on a fixed 2 Hz cadence (so it reads in packets per half
second no matter how often the compositor repaints), byte and packet
totals, and the TCP engine's connection table with each connection's real
state and direction, scrollable when it outgrows the window.
Two bugs the monitor made visible, both fixed:
- Served HTTP connections were never closed. The response advertises
"Connection: close" but the engine left the connection ESTABLISHED
forever, so the 32-entry table filled up and the server stopped
answering after 32 requests. The service path now sends its FIN after
the response and lingers in TIME_WAIT until the reaper takes it. 40
sequential requests through a 32-slot table now yield 39 successes
where the 33rd onwards previously failed permanently.
- tcp_close gave up in FIN_WAIT2 after 500 ms and freed the slot, so a
peer whose own FIN arrived later got an RST instead of an ACK. It now
falls into TIME_WAIT instead, and TIME_WAIT acknowledges a first FIN
rather than only a retransmitted one. Expired TIME_WAIT entries are
also reaped from tcp_get_connections_count, so a table that sees no
further connections still empties out.
Verified on x86_64 against real peers, checked packet by packet in a
capture: a host client connected to `tcpserve 9000` in the guest and got
its reply -- SYN/SYN-ACK/ACK with a random ISN, 30-byte request, 43-byte
reply, and a clean four-way close whose final ACK is correct even when
the peer's FIN arrives 1.2 s late. Zero bad checksums throughout. The
monitor was screenshotted under live load showing connections moving
through TIME_WAIT and being reclaimed. Builds clean on x86_64, aarch64
and armv8i.
Known limit: 32 connection slots with a 2 s TIME_WAIT linger means
sustained rates much above ~16 requests/second can transiently exhaust
the table; it recovers on its own as entries expire.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fh5z6V7nGPbHvqVG5p4CXX
897 lines
34 KiB
C
897 lines
34 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 },
|
|
{ "Sorry", GUI_ICON_HEART, gui_app_sorry_launch, 260, 60, 660, 470 },
|
|
{ "Network", GUI_ICON_NETWORK, gui_app_netmon_launch, 300, 80, 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, Sorry. (Objects/RegEdit live in the start
|
|
// menu.) Network sits alongside them.
|
|
static const int g_desktop_icons[] = { 0, 1, 2, 4, 5, 9, 10, 14, 15, 16, 17 };
|
|
#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;
|
|
}
|