Files
MontaukOS/programs/src/desktop/main.cpp
T

1699 lines
61 KiB
C++

/*
* main.cpp
* ZenithOS Desktop Environment - window manager, compositor, and run loop
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
#include "wallpaper.hpp"
// ============================================================================
// Desktop Implementation
// ============================================================================
static const char* month_names[] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
void gui::desktop_init(DesktopState* ds) {
ds->screen_w = ds->fb.width();
ds->screen_h = ds->fb.height();
// Immediately clear the screen to hide flanterm boot text
ds->fb.clear(colors::DESKTOP_BG);
ds->fb.flip();
// Load TrueType fonts
fonts::init();
ds->window_count = 0;
ds->focused_window = -1;
ds->prev_buttons = 0;
ds->app_menu_open = false;
zenith::memset(&ds->mouse, 0, sizeof(Zenith::MouseState));
zenith::set_mouse_bounds(ds->screen_w - 1, ds->screen_h - 1);
// Load SVG icons — scalable (colorful) for app menu, symbolic for toolbar/panel
Color defColor = colors::ICON_COLOR;
ds->icon_terminal = svg_load("0:/icons/utilities-terminal.svg", 20, 20, defColor);
ds->icon_filemanager = svg_load("0:/icons/system-file-manager.svg", 20, 20, defColor);
ds->icon_sysinfo = svg_load("0:/icons/preferences-desktop-apps.svg", 20, 20, defColor);
ds->icon_appmenu = svg_load("0:/icons/view-app-grid-symbolic.svg", 20, 20, colors::PANEL_TEXT);
ds->icon_folder = svg_load("0:/icons/folder.svg", 16, 16, defColor);
ds->icon_file = svg_load("0:/icons/text-x-generic.svg", 16, 16, defColor);
ds->icon_computer = svg_load("0:/icons/computer.svg", 20, 20, defColor);
ds->icon_network = svg_load("0:/icons/network-wired-symbolic.svg", 16, 16, colors::PANEL_TEXT);
ds->icon_calculator = svg_load("0:/icons/accessories-calculator.svg", 20, 20, defColor);
ds->icon_texteditor = svg_load("0:/icons/accessories-text-editor.svg", 20, 20, defColor);
ds->icon_go_up = svg_load("0:/icons/go-up-symbolic.svg", 16, 16, defColor);
ds->icon_go_back = svg_load("0:/icons/go-previous-symbolic.svg", 16, 16, defColor);
ds->icon_go_forward = svg_load("0:/icons/go-next-symbolic.svg", 16, 16, defColor);
ds->icon_save = svg_load("0:/icons/document-save-symbolic.svg", 16, 16, defColor);
ds->icon_home = svg_load("0:/icons/user-home.svg", 16, 16, defColor);
ds->icon_exec = svg_load("0:/icons/utilities-terminal.svg", 16, 16, defColor);
ds->icon_wikipedia = svg_load("0:/icons/web-browser.svg", 20, 20, defColor);
ds->icon_folder_lg = svg_load("0:/icons/folder.svg", 48, 48, defColor);
ds->icon_file_lg = svg_load("0:/icons/text-x-generic.svg", 48, 48, defColor);
ds->icon_exec_lg = svg_load("0:/icons/utilities-terminal.svg", 48, 48, defColor);
ds->icon_settings = svg_load("0:/icons/help-about.svg", 20, 20, defColor);
ds->icon_reboot = svg_load("0:/icons/system-reboot.svg", 20, 20, defColor);
ds->icon_shutdown = svg_load("0:/icons/system-shutdown.svg", 20, 20, defColor);
ds->icon_weather = svg_load("0:/icons/weather-widget.svg", 20, 20, defColor);
ds->icon_doom = svg_load("0:/icons/doom.svg", 20, 20, defColor);
ds->icon_procmgr = svg_load("0:/icons/system-monitor.svg", 20, 20, defColor);
ds->icon_mandelbrot = svg_load("0:/icons/applications-science.svg", 20, 20, defColor);
ds->icon_devexplorer = svg_load("0:/icons/hardware.svg", 20, 20, defColor);
// Settings defaults
ds->settings.bg_gradient = true;
ds->settings.bg_image = false;
ds->settings.bg_grad_top = Color::from_rgb(0xD0, 0xD8, 0xE8);
ds->settings.bg_grad_bottom = Color::from_rgb(0xA0, 0xA8, 0xB8);
ds->settings.bg_solid = Color::from_rgb(0xD0, 0xD8, 0xE8);
ds->settings.bg_image_path[0] = '\0';
ds->settings.bg_wallpaper = nullptr;
ds->settings.bg_wallpaper_w = 0;
ds->settings.bg_wallpaper_h = 0;
ds->settings.panel_color = Color::from_rgb(0x10, 0x10, 0x10);
ds->settings.accent_color = colors::ACCENT;
ds->settings.show_shadows = true;
ds->settings.clock_24h = true;
ds->settings.ui_scale = 1;
// Try to load default wallpaper
wallpaper_load(&ds->settings, "0:/home/troy-olson-MhYIwOiyZpM-unsplash.jpg",
ds->screen_w, ds->screen_h);
zenith::win_setscale(1);
ds->ctx_menu_open = false;
ds->ctx_menu_x = 0;
ds->ctx_menu_y = 0;
ds->net_popup_open = false;
zenith::get_netcfg(&ds->cached_net_cfg);
ds->net_cfg_last_poll = zenith::get_milliseconds();
ds->net_icon_rect = {0, 0, 0, 0};
}
int gui::desktop_create_window(DesktopState* ds, const char* title, int x, int y, int w, int h) {
if (ds->window_count >= MAX_WINDOWS) return -1;
int idx = ds->window_count;
Window* win = &ds->windows[idx];
zenith::memset(win, 0, sizeof(Window));
zenith::strncpy(win->title, title, MAX_TITLE_LEN);
win->frame = {x, y, w, h};
win->state = WIN_NORMAL;
win->z_order = idx;
win->focused = true;
win->dirty = true;
win->dragging = false;
win->resizing = false;
win->saved_frame = win->frame;
// Allocate content buffer
Rect cr = win->content_rect();
win->content_w = cr.w;
win->content_h = cr.h;
int buf_size = cr.w * cr.h * 4;
win->content = (uint32_t*)zenith::alloc(buf_size);
zenith::memset(win->content, 0xFF, buf_size);
win->on_draw = nullptr;
win->on_mouse = nullptr;
win->on_key = nullptr;
win->on_close = nullptr;
win->on_poll = nullptr;
win->app_data = nullptr;
win->external = false;
win->ext_win_id = -1;
// Unfocus previous window
if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) {
ds->windows[ds->focused_window].focused = false;
}
ds->focused_window = idx;
ds->window_count++;
return idx;
}
void gui::desktop_close_window(DesktopState* ds, int idx) {
if (idx < 0 || idx >= ds->window_count) return;
Window* win = &ds->windows[idx];
// For external windows, send a close event instead of freeing the buffer
if (win->external) {
Zenith::WinEvent ev;
zenith::memset(&ev, 0, sizeof(ev));
ev.type = 3; // close
zenith::win_sendevent(win->ext_win_id, &ev);
}
if (win->on_close) win->on_close(win);
// Free content buffer (skip for external windows — shared memory)
if (win->content && !win->external) {
zenith::free(win->content);
win->content = nullptr;
}
// Shift remaining windows down
for (int i = idx; i < ds->window_count - 1; i++) {
ds->windows[i] = ds->windows[i + 1];
}
ds->window_count--;
// Fix focused window index
if (ds->focused_window == idx) {
ds->focused_window = ds->window_count > 0 ? ds->window_count - 1 : -1;
} else if (ds->focused_window > idx) {
ds->focused_window--;
}
if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) {
ds->windows[ds->focused_window].focused = true;
}
}
void gui::desktop_raise_window(DesktopState* ds, int idx) {
if (idx < 0 || idx >= ds->window_count) return;
if (idx == ds->window_count - 1) {
// Already on top, just focus
if (ds->focused_window >= 0 && ds->focused_window < ds->window_count)
ds->windows[ds->focused_window].focused = false;
ds->focused_window = idx;
ds->windows[idx].focused = true;
return;
}
// Unfocus current
if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) {
ds->windows[ds->focused_window].focused = false;
}
// Move window to end (top of stack)
Window tmp = ds->windows[idx];
for (int i = idx; i < ds->window_count - 1; i++) {
ds->windows[i] = ds->windows[i + 1];
}
ds->windows[ds->window_count - 1] = tmp;
ds->focused_window = ds->window_count - 1;
ds->windows[ds->focused_window].focused = true;
}
void gui::desktop_draw_window(DesktopState* ds, int idx) {
if (idx < 0 || idx >= ds->window_count) return;
Window* win = &ds->windows[idx];
if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) return;
Framebuffer& fb = ds->fb;
int x = win->frame.x;
int y = win->frame.y;
int w = win->frame.w;
int h = win->frame.h;
// Draw shadow
if (ds->settings.show_shadows)
draw_shadow(fb, x, y, w, h, SHADOW_SIZE, colors::SHADOW);
// Draw window body
fb.fill_rect(x, y, w, h, colors::WINDOW_BG);
// Draw titlebar
Color tb_bg = win->focused ? colors::TITLEBAR_BG : Color::from_rgb(0xE8, 0xE8, 0xE8);
fb.fill_rect(x, y, w, TITLEBAR_HEIGHT, tb_bg);
// Draw border
draw_rect(fb, x, y, w, h, colors::BORDER);
// Titlebar bottom separator
draw_hline(fb, x, y + TITLEBAR_HEIGHT - 1, w, colors::BORDER);
// Draw window buttons (macOS style: close, minimize, maximize)
Rect close_r = win->close_btn_rect();
Rect min_r = win->min_btn_rect();
Rect max_r = win->max_btn_rect();
fill_circle(fb, close_r.x + BTN_RADIUS, close_r.y + BTN_RADIUS, BTN_RADIUS, colors::CLOSE_BTN);
fill_circle(fb, min_r.x + BTN_RADIUS, min_r.y + BTN_RADIUS, BTN_RADIUS, colors::MIN_BTN);
fill_circle(fb, max_r.x + BTN_RADIUS, max_r.y + BTN_RADIUS, BTN_RADIUS, colors::MAX_BTN);
// Draw title text centered in titlebar (after buttons)
int title_x = x + 12 + 44 + BTN_RADIUS * 2 + 12; // after buttons
int title_y = y + (TITLEBAR_HEIGHT - system_font_height()) / 2;
int title_w = text_width(win->title);
// Center in remaining space
int remaining_w = w - (title_x - x) - 12;
if (remaining_w > title_w) {
title_x += (remaining_w - title_w) / 2;
}
draw_text(fb, title_x, title_y, win->title, colors::TEXT_COLOR);
// Call app draw callback to render content (skip during resize — buffer is old size)
if (win->on_draw && !win->resizing) {
win->on_draw(win, fb);
}
// Blit content buffer to framebuffer (clip to actual buffer size during resize)
Rect cr = win->content_rect();
if (win->content) {
if (win->external && (cr.w != win->content_w || cr.h != win->content_h)) {
// Nearest-neighbor scale for external windows (fixed-size shared buffer)
int src_w = win->content_w;
int src_h = win->content_h;
int dst_w = cr.w;
int dst_h = cr.h;
uint32_t* buf = fb.buffer();
int pitch = fb.pitch();
for (int y = 0; y < dst_h; y++) {
int dy = cr.y + y;
if (dy < 0 || dy >= fb.height()) continue;
int sy = y * src_h / dst_h;
uint32_t* dst_row = (uint32_t*)((uint8_t*)buf + dy * pitch);
uint32_t* src_row = win->content + sy * src_w;
for (int x = 0; x < dst_w; x++) {
int dx = cr.x + x;
if (dx < 0 || dx >= fb.width()) continue;
dst_row[dx] = src_row[x * src_w / dst_w];
}
}
} else {
int blit_w = cr.w < win->content_w ? cr.w : win->content_w;
int blit_h = cr.h < win->content_h ? cr.h : win->content_h;
fb.blit(cr.x, cr.y, blit_w, blit_h, win->content);
}
}
}
void gui::desktop_draw_panel(DesktopState* ds) {
Framebuffer& fb = ds->fb;
int sw = ds->screen_w;
// Panel gradient background (slightly lighter at top)
Color pc = ds->settings.panel_color;
for (int y = 0; y < PANEL_HEIGHT; y++) {
int t = y * 255 / PANEL_HEIGHT;
uint8_t r = pc.r + (10 - t * 10 / 255);
uint8_t g = pc.g + (10 - t * 10 / 255);
uint8_t b = pc.b + (10 - t * 10 / 255);
fb.fill_rect(0, y, sw, 1, Color::from_rgb(r, g, b));
}
// Bottom highlight line (skip when wallpaper is set — the white bleeds through)
if (!ds->settings.bg_image) {
fb.fill_rect(0, PANEL_HEIGHT - 1, sw, 1, Color::from_rgba(0xFF, 0xFF, 0xFF, 0x10));
}
// App menu button (left side)
int btn_x = 4;
int btn_y = 2;
int btn_w = 28;
int btn_h = 28;
if (ds->icon_appmenu.pixels) {
int ix = btn_x + (btn_w - ds->icon_appmenu.width) / 2;
int iy = btn_y + (btn_h - ds->icon_appmenu.height) / 2;
fb.blit_alpha(ix, iy, ds->icon_appmenu.width, ds->icon_appmenu.height, ds->icon_appmenu.pixels);
} else {
for (int gr = 0; gr < 3; gr++) {
for (int gc = 0; gc < 3; gc++) {
int dx = btn_x + 6 + gc * 6;
int dy = btn_y + 6 + gr * 6;
fb.fill_rect(dx, dy, 3, 3, colors::PANEL_TEXT);
}
}
}
// Window indicator pills (center area)
int indicator_x = 40;
for (int i = 0; i < ds->window_count; i++) {
Window* win = &ds->windows[i];
if (win->state == WIN_CLOSED) continue;
int tw = text_width(win->title);
int pad = 12;
int iw = tw + pad * 2;
if (iw > 150) iw = 150;
// Pre-blended indicator pill colors (opaque, blended against PANEL_BG)
Color pill_bg = (i == ds->focused_window)
? colors::PANEL_INDICATOR_ACTIVE
: colors::PANEL_INDICATOR_INACTIVE;
fill_rounded_rect(fb, indicator_x, 4, iw, 24, 6, pill_bg);
// Active window accent underline bar
if (i == ds->focused_window) {
fb.fill_rect(indicator_x + 4, 26, iw - 8, 2, ds->settings.accent_color);
}
// Truncate title if too long
char short_title[20];
zenith::strncpy(short_title, win->title, 18);
int tx = indicator_x + pad;
int ty = 4 + (24 - system_font_height()) / 2;
draw_text(fb, tx, ty, short_title, colors::PANEL_TEXT);
indicator_x += iw + 4;
}
// Date + Clock (right side)
Zenith::DateTime dt;
zenith::gettime(&dt);
char clock_str[12];
if (ds->settings.clock_24h) {
snprintf(clock_str, sizeof(clock_str), "%02d:%02d", (int)dt.Hour, (int)dt.Minute);
} else {
int h12 = (int)dt.Hour % 12;
if (h12 == 0) h12 = 12;
const char* ampm = dt.Hour < 12 ? "AM" : "PM";
snprintf(clock_str, sizeof(clock_str), "%d:%02d %s", h12, (int)dt.Minute, ampm);
}
int clock_w = text_width(clock_str);
int clock_x = sw - clock_w - 12;
int clock_y = (PANEL_HEIGHT - system_font_height()) / 2;
draw_text(fb, clock_x, clock_y, clock_str, colors::PANEL_TEXT);
// Date before clock
char date_str[12];
int month_idx = dt.Month > 0 && dt.Month <= 12 ? dt.Month - 1 : 0;
snprintf(date_str, sizeof(date_str), "%s %d", month_names[month_idx], (int)dt.Day);
int date_w = text_width(date_str);
int date_x = clock_x - date_w - 10;
draw_text(fb, date_x, clock_y, date_str, colors::PANEL_TEXT);
// Network icon (to the left of the date)
uint64_t now = zenith::get_milliseconds();
if (now - ds->net_cfg_last_poll > 5000) {
zenith::get_netcfg(&ds->cached_net_cfg);
ds->net_cfg_last_poll = now;
}
int net_icon_x = date_x - 16 - 12;
int net_icon_y = (PANEL_HEIGHT - 16) / 2;
ds->net_icon_rect = {net_icon_x, net_icon_y, 16, 16};
if (ds->icon_network.pixels) {
if (ds->cached_net_cfg.ipAddress == 0) {
uint32_t* src = ds->icon_network.pixels;
int npx = 16 * 16;
uint32_t tinted[256];
for (int p = 0; p < npx; p++) {
uint32_t px = src[p];
uint8_t a = (px >> 24) & 0xFF;
tinted[p] = ((uint32_t)a << 24) | 0x004444CC;
}
fb.blit_alpha(net_icon_x, net_icon_y, 16, 16, tinted);
} else {
fb.blit_alpha(net_icon_x, net_icon_y, ds->icon_network.width, ds->icon_network.height, ds->icon_network.pixels);
}
}
}
// ============================================================================
// App Menu (categorized)
// ============================================================================
static constexpr int MENU_W = 220;
static constexpr int MENU_ITEM_H = 36;
static constexpr int MENU_CAT_H = 28;
static constexpr int MENU_DIV_H = 10;
struct MenuRow {
bool is_category;
const char* label; // "" for divider-only rows
int app_id; // -1 for category headers / dividers
};
static constexpr int MENU_ROW_COUNT = 20;
static const MenuRow menu_rows[MENU_ROW_COUNT] = {
{ true, "Applications", -1 },
{ false, "Terminal", 0 },
{ false, "Files", 1 },
{ false, "Text Editor", 4 },
{ false, "Calculator", 3 },
{ true, "Internet", -1 },
{ false, "Wikipedia", 9 },
{ false, "Weather", 13 },
{ true, "System", -1 },
{ false, "System Info", 2 },
{ false, "Kernel Log", 5 },
{ false, "Processes", 6 },
{ false, "Devices", 8 },
{ true, "Games", -1 },
{ false, "Mandelbrot", 7 },
{ false, "DOOM", 10 },
{ true, "", -1 }, // divider
{ false, "Settings", 11 },
{ false, "Reboot", 12 },
{ false, "Shutdown", 14 },
};
static int menu_row_height(const MenuRow& row) {
if (!row.is_category) return MENU_ITEM_H;
return row.label[0] ? MENU_CAT_H : MENU_DIV_H;
}
static int menu_total_height() {
int h = 10; // top + bottom padding
for (int i = 0; i < MENU_ROW_COUNT; i++)
h += menu_row_height(menu_rows[i]);
return h;
}
static void desktop_draw_app_menu(DesktopState* ds) {
Framebuffer& fb = ds->fb;
int menu_x = 4;
int menu_y = PANEL_HEIGHT + 2;
int menu_h = menu_total_height();
// Menu shadow
draw_shadow(fb, menu_x, menu_y, MENU_W, menu_h, 4, colors::SHADOW);
// Menu background with rounded corners
fill_rounded_rect(fb, menu_x, menu_y, MENU_W, menu_h, 8, colors::MENU_BG);
draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER);
// Icon lookup by app_id
SvgIcon* icons[15] = {
&ds->icon_terminal, // 0
&ds->icon_filemanager, // 1
&ds->icon_sysinfo, // 2
&ds->icon_calculator, // 3
&ds->icon_texteditor, // 4
&ds->icon_terminal, // 5 (Kernel Log)
&ds->icon_procmgr, // 6
&ds->icon_mandelbrot, // 7
&ds->icon_devexplorer, // 8
&ds->icon_wikipedia, // 9
&ds->icon_doom, // 10
&ds->icon_settings, // 11
&ds->icon_reboot, // 12
&ds->icon_weather, // 13
&ds->icon_shutdown, // 14
};
int mx = ds->mouse.x;
int my = ds->mouse.y;
int iy = menu_y + 5;
for (int i = 0; i < MENU_ROW_COUNT; i++) {
const MenuRow& row = menu_rows[i];
int row_h = menu_row_height(row);
if (row.is_category) {
// Separator line above (except first category)
if (i > 0) {
for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++)
fb.put_pixel(sx, iy + row_h / 2, colors::BORDER);
}
// Category label (dimmed), skip for divider-only rows
if (row.label[0]) {
int tx = menu_x + 12;
int ty = iy + (row_h - system_font_height()) / 2;
if (i > 0) ty += 2;
draw_text(fb, tx, ty, row.label, Color::from_rgb(0x88, 0x88, 0x88));
}
} else {
Rect item_rect = {menu_x + 4, iy, MENU_W - 8, row_h};
// Hover highlight
if (item_rect.contains(mx, my)) {
fill_rounded_rect(fb, item_rect.x, item_rect.y, item_rect.w, item_rect.h, 4, colors::MENU_HOVER);
}
// Icon
int icon_x = item_rect.x + 8;
int icon_y = item_rect.y + (row_h - 20) / 2;
if (row.app_id >= 0 && row.app_id < 15) {
SvgIcon* icon = icons[row.app_id];
if (icon && icon->pixels) {
fb.blit_alpha(icon_x, icon_y, icon->width, icon->height, icon->pixels);
}
}
// Label
int tx = icon_x + 28;
int ty = item_rect.y + (row_h - system_font_height()) / 2;
draw_text(fb, tx, ty, row.label, colors::TEXT_COLOR);
}
iy += row_h;
}
}
static void desktop_draw_net_popup(DesktopState* ds) {
Framebuffer& fb = ds->fb;
int popup_w = 220;
int popup_h = 130;
int popup_x = ds->net_icon_rect.x + ds->net_icon_rect.w - popup_w;
int popup_y = PANEL_HEIGHT + 2;
if (popup_x < 4) popup_x = 4;
draw_shadow(fb, popup_x, popup_y, popup_w, popup_h, 4, colors::SHADOW);
fb.fill_rect(popup_x, popup_y, popup_w, popup_h, colors::MENU_BG);
draw_rect(fb, popup_x, popup_y, popup_w, popup_h, colors::BORDER);
int tx = popup_x + 12;
int ty = popup_y + 10;
int line_h = system_font_height() + 6;
char line[64];
Zenith::NetCfg& nc = ds->cached_net_cfg;
if (nc.ipAddress != 0) {
char ipbuf[20];
format_ip(ipbuf, nc.ipAddress);
snprintf(line, sizeof(line), "IP: %s", ipbuf);
} else {
snprintf(line, sizeof(line), "IP: Not connected");
}
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
ty += line_h;
char buf[20];
format_ip(buf, nc.subnetMask);
snprintf(line, sizeof(line), "Subnet: %s", buf);
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
ty += line_h;
format_ip(buf, nc.gateway);
snprintf(line, sizeof(line), "Gateway: %s", buf);
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
ty += line_h;
format_ip(buf, nc.dnsServer);
snprintf(line, sizeof(line), "DNS: %s", buf);
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
ty += line_h;
format_mac(buf, nc.macAddress);
snprintf(line, sizeof(line), "MAC: %s", buf);
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
}
// ============================================================================
// Reboot Dialog
// ============================================================================
struct RebootDialogState {
DesktopState* ds;
// Button layout (pixel-buffer-relative coordinates)
int btn_w, btn_h, btn_y, reboot_x, cancel_x;
bool hover_reboot, hover_cancel;
};
static void reboot_dialog_on_draw(Window* win, Framebuffer& fb) {
RebootDialogState* rs = (RebootDialogState*)win->app_data;
if (!rs) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
// "Reboot the system?" centered
const char* msg = "Reboot the system?";
int tw = text_width(msg);
c.text((c.w - tw) / 2, 30, msg, colors::TEXT_COLOR);
// Compute button layout
int btn_w = 100;
int btn_h = 32;
int btn_y = c.h - btn_h - 20;
int gap = 20;
int total_w = btn_w * 2 + gap;
int bx = (c.w - total_w) / 2;
rs->btn_w = btn_w;
rs->btn_h = btn_h;
rs->btn_y = btn_y;
rs->reboot_x = bx;
rs->cancel_x = bx + btn_w + gap;
// Draw Reboot button
Color reboot_bg = rs->hover_reboot
? Color::from_rgb(0xDD, 0x44, 0x44)
: Color::from_rgb(0xCC, 0x33, 0x33);
c.button(rs->reboot_x, btn_y, btn_w, btn_h, "Reboot", reboot_bg, colors::WHITE, 4);
// Draw Cancel button
Color cancel_bg = rs->hover_cancel
? Color::from_rgb(0x99, 0x99, 0x99)
: Color::from_rgb(0x88, 0x88, 0x88);
c.button(rs->cancel_x, btn_y, btn_w, btn_h, "Cancel", cancel_bg, colors::WHITE, 4);
}
static void reboot_dialog_on_mouse(Window* win, MouseEvent& ev) {
RebootDialogState* rs = (RebootDialogState*)win->app_data;
if (!rs) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
Rect rb = {rs->reboot_x, rs->btn_y, rs->btn_w, rs->btn_h};
Rect cb = {rs->cancel_x, rs->btn_y, rs->btn_w, rs->btn_h};
rs->hover_reboot = rb.contains(lx, ly);
rs->hover_cancel = cb.contains(lx, ly);
if (ev.left_pressed()) {
if (rs->hover_reboot) zenith::reset();
if (rs->hover_cancel) {
for (int i = 0; i < rs->ds->window_count; i++) {
if (rs->ds->windows[i].app_data == rs) {
desktop_close_window(rs->ds, i);
return;
}
}
}
}
}
static void reboot_dialog_on_key(Window* win, const Zenith::KeyEvent& key) {
RebootDialogState* rs = (RebootDialogState*)win->app_data;
if (!rs || !key.pressed) return;
if (key.ascii == '\n' || key.ascii == '\r') {
zenith::reset();
}
if (key.scancode == 0x01) { // Escape
for (int i = 0; i < rs->ds->window_count; i++) {
if (rs->ds->windows[i].app_data == rs) {
desktop_close_window(rs->ds, i);
return;
}
}
}
}
static void reboot_dialog_on_close(Window* win) {
if (win->app_data) {
zenith::mfree(win->app_data);
win->app_data = nullptr;
}
}
void open_reboot_dialog(DesktopState* ds) {
int wx = (ds->screen_w - 300) / 2;
int wy = (ds->screen_h - 150) / 2;
int idx = desktop_create_window(ds, "Reboot", wx, wy, 300, 150);
if (idx < 0) return;
Window* win = &ds->windows[idx];
RebootDialogState* rs = (RebootDialogState*)zenith::malloc(sizeof(RebootDialogState));
zenith::memset(rs, 0, sizeof(RebootDialogState));
rs->ds = ds;
win->app_data = rs;
win->on_draw = reboot_dialog_on_draw;
win->on_mouse = reboot_dialog_on_mouse;
win->on_key = reboot_dialog_on_key;
win->on_close = reboot_dialog_on_close;
}
// ============================================================================
// Shutdown Dialog
// ============================================================================
struct ShutdownDialogState {
DesktopState* ds;
int btn_w, btn_h, btn_y, shutdown_x, cancel_x;
bool hover_shutdown, hover_cancel;
};
static void shutdown_dialog_on_draw(Window* win, Framebuffer& fb) {
ShutdownDialogState* ss = (ShutdownDialogState*)win->app_data;
if (!ss) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
const char* msg = "Shut down the system?";
int tw = text_width(msg);
c.text((c.w - tw) / 2, 30, msg, colors::TEXT_COLOR);
int btn_w = 100;
int btn_h = 32;
int btn_y = c.h - btn_h - 20;
int gap = 20;
int total_w = btn_w * 2 + gap;
int bx = (c.w - total_w) / 2;
ss->btn_w = btn_w;
ss->btn_h = btn_h;
ss->btn_y = btn_y;
ss->shutdown_x = bx;
ss->cancel_x = bx + btn_w + gap;
Color shutdown_bg = ss->hover_shutdown
? Color::from_rgb(0xDD, 0x44, 0x44)
: Color::from_rgb(0xCC, 0x33, 0x33);
c.button(ss->shutdown_x, btn_y, btn_w, btn_h, "Shut Down", shutdown_bg, colors::WHITE, 4);
Color cancel_bg = ss->hover_cancel
? Color::from_rgb(0x99, 0x99, 0x99)
: Color::from_rgb(0x88, 0x88, 0x88);
c.button(ss->cancel_x, btn_y, btn_w, btn_h, "Cancel", cancel_bg, colors::WHITE, 4);
}
static void shutdown_dialog_on_mouse(Window* win, MouseEvent& ev) {
ShutdownDialogState* ss = (ShutdownDialogState*)win->app_data;
if (!ss) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
Rect sb = {ss->shutdown_x, ss->btn_y, ss->btn_w, ss->btn_h};
Rect cb = {ss->cancel_x, ss->btn_y, ss->btn_w, ss->btn_h};
ss->hover_shutdown = sb.contains(lx, ly);
ss->hover_cancel = cb.contains(lx, ly);
if (ev.left_pressed()) {
if (ss->hover_shutdown) zenith::shutdown();
if (ss->hover_cancel) {
for (int i = 0; i < ss->ds->window_count; i++) {
if (ss->ds->windows[i].app_data == ss) {
desktop_close_window(ss->ds, i);
return;
}
}
}
}
}
static void shutdown_dialog_on_key(Window* win, const Zenith::KeyEvent& key) {
ShutdownDialogState* ss = (ShutdownDialogState*)win->app_data;
if (!ss || !key.pressed) return;
if (key.ascii == '\n' || key.ascii == '\r') {
zenith::shutdown();
}
if (key.scancode == 0x01) { // Escape
for (int i = 0; i < ss->ds->window_count; i++) {
if (ss->ds->windows[i].app_data == ss) {
desktop_close_window(ss->ds, i);
return;
}
}
}
}
static void shutdown_dialog_on_close(Window* win) {
if (win->app_data) {
zenith::mfree(win->app_data);
win->app_data = nullptr;
}
}
void open_shutdown_dialog(DesktopState* ds) {
int wx = (ds->screen_w - 300) / 2;
int wy = (ds->screen_h - 150) / 2;
int idx = desktop_create_window(ds, "Shut Down", wx, wy, 300, 150);
if (idx < 0) return;
Window* win = &ds->windows[idx];
ShutdownDialogState* ss = (ShutdownDialogState*)zenith::malloc(sizeof(ShutdownDialogState));
zenith::memset(ss, 0, sizeof(ShutdownDialogState));
ss->ds = ds;
win->app_data = ss;
win->on_draw = shutdown_dialog_on_draw;
win->on_mouse = shutdown_dialog_on_mouse;
win->on_key = shutdown_dialog_on_key;
win->on_close = shutdown_dialog_on_close;
}
static gui::ResizeEdge hit_test_resize_edge(const gui::Rect& f, int mx, int my) {
using namespace gui;
int G = RESIZE_GRAB;
if (!f.contains(mx, my)) return RESIZE_NONE;
bool near_left = mx < f.x + G;
bool near_right = mx >= f.x + f.w - G;
bool near_top = my < f.y + G;
bool near_bottom = my >= f.y + f.h - G;
// Corners first
if (near_top && near_left) return RESIZE_TOP_LEFT;
if (near_top && near_right) return RESIZE_TOP_RIGHT;
if (near_bottom && near_left) return RESIZE_BOTTOM_LEFT;
if (near_bottom && near_right) return RESIZE_BOTTOM_RIGHT;
// Edges
if (near_top) return RESIZE_TOP;
if (near_bottom) return RESIZE_BOTTOM;
if (near_left) return RESIZE_LEFT;
if (near_right) return RESIZE_RIGHT;
return RESIZE_NONE;
}
static gui::CursorStyle cursor_for_edge(gui::ResizeEdge edge) {
using namespace gui;
switch (edge) {
case RESIZE_LEFT: case RESIZE_RIGHT:
return CURSOR_RESIZE_H;
case RESIZE_TOP: case RESIZE_BOTTOM:
return CURSOR_RESIZE_V;
case RESIZE_TOP_LEFT: case RESIZE_BOTTOM_RIGHT:
return CURSOR_RESIZE_NWSE;
case RESIZE_TOP_RIGHT: case RESIZE_BOTTOM_LEFT:
return CURSOR_RESIZE_NESW;
default:
return CURSOR_ARROW;
}
}
void gui::desktop_compose(DesktopState* ds) {
Framebuffer& fb = ds->fb;
// Desktop background gradient
int sw = ds->screen_w;
int sh = ds->screen_h;
int grad_start = PANEL_HEIGHT;
int grad_range = sh - grad_start;
if (grad_range < 1) grad_range = 1;
uint32_t* buf = fb.buffer();
int pitch = fb.pitch();
for (int y = 0; y < sh; y++) {
uint32_t* row = (uint32_t*)((uint8_t*)buf + y * pitch);
if (ds->settings.bg_image && ds->settings.bg_wallpaper) {
// Wallpaper covers the entire screen; panel draws on top
uint32_t* wp = ds->settings.bg_wallpaper + y * ds->settings.bg_wallpaper_w;
int cw = sw < ds->settings.bg_wallpaper_w ? sw : ds->settings.bg_wallpaper_w;
for (int x = 0; x < cw; x++) row[x] = wp[x];
} else if (y < grad_start) {
// Panel area - will be overwritten by panel drawing
uint32_t px = ds->settings.panel_color.to_pixel();
for (int x = 0; x < sw; x++) row[x] = px;
} else if (ds->settings.bg_gradient) {
int t = y - grad_start;
Color top = ds->settings.bg_grad_top;
Color bot = ds->settings.bg_grad_bottom;
uint8_t r = top.r - (top.r - bot.r) * t / grad_range;
uint8_t g = top.g - (top.g - bot.g) * t / grad_range;
uint8_t b = top.b - (top.b - bot.b) * t / grad_range;
uint32_t px = 0xFF000000 | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
for (int x = 0; x < sw; x++) row[x] = px;
} else {
uint32_t px = ds->settings.bg_solid.to_pixel();
for (int x = 0; x < sw; x++) row[x] = px;
}
}
// Draw windows from bottom to top
for (int i = 0; i < ds->window_count; i++) {
if (ds->windows[i].state != WIN_MINIMIZED && ds->windows[i].state != WIN_CLOSED) {
desktop_draw_window(ds, i);
}
}
// Draw panel on top
desktop_draw_panel(ds);
// Draw app menu if open
if (ds->app_menu_open) {
desktop_draw_app_menu(ds);
}
// Draw network popup if open
if (ds->net_popup_open) {
desktop_draw_net_popup(ds);
}
// Draw right-click context menu if open
if (ds->ctx_menu_open) {
static constexpr int CTX_MENU_W = 180;
static constexpr int CTX_ITEM_H = 36;
static constexpr int CTX_ITEM_COUNT = 5;
int cmx = ds->ctx_menu_x;
int cmy = ds->ctx_menu_y;
int cmh = CTX_ITEM_H * CTX_ITEM_COUNT + 8;
// Clamp to screen
if (cmx + CTX_MENU_W > sw) cmx = sw - CTX_MENU_W;
if (cmy + cmh > sh) cmy = sh - cmh;
draw_shadow(fb, cmx, cmy, CTX_MENU_W, cmh, 4, colors::SHADOW);
fill_rounded_rect(fb, cmx, cmy, CTX_MENU_W, cmh, 8, colors::MENU_BG);
draw_rect(fb, cmx, cmy, CTX_MENU_W, cmh, colors::BORDER);
struct CtxItem { const char* label; SvgIcon* icon; };
CtxItem ctx_items[CTX_ITEM_COUNT] = {
{ "Terminal", &ds->icon_terminal },
{ "Files", &ds->icon_filemanager },
{ "About", &ds->icon_settings },
{ "Reboot", &ds->icon_reboot },
{ "Shutdown", &ds->icon_shutdown },
};
int mmx = ds->mouse.x;
int mmy = ds->mouse.y;
for (int i = 0; i < CTX_ITEM_COUNT; i++) {
int iy = cmy + 4 + i * CTX_ITEM_H;
Rect item_r = {cmx + 4, iy, CTX_MENU_W - 8, CTX_ITEM_H};
if (item_r.contains(mmx, mmy)) {
fill_rounded_rect(fb, item_r.x, item_r.y, item_r.w, item_r.h, 4, colors::MENU_HOVER);
}
int icon_x = item_r.x + 8;
int icon_y = item_r.y + (CTX_ITEM_H - 20) / 2;
if (ctx_items[i].icon && ctx_items[i].icon->pixels) {
fb.blit_alpha(icon_x, icon_y, ctx_items[i].icon->width, ctx_items[i].icon->height, ctx_items[i].icon->pixels);
}
int tx = icon_x + 28;
int ty = item_r.y + (CTX_ITEM_H - system_font_height()) / 2;
draw_text(fb, tx, ty, ctx_items[i].label, colors::TEXT_COLOR);
}
}
// Draw snap preview overlay while dragging to screen edge
for (int i = 0; i < ds->window_count; i++) {
Window* win = &ds->windows[i];
if (win->dragging) {
int dmx = ds->mouse.x;
if (dmx <= 0) {
fb.fill_rect_alpha(0, PANEL_HEIGHT, sw / 2, sh - PANEL_HEIGHT,
Color::from_rgba(0x33, 0x77, 0xCC, 0x30));
} else if (dmx >= sw - 1) {
fb.fill_rect_alpha(sw / 2, PANEL_HEIGHT, sw / 2, sh - PANEL_HEIGHT,
Color::from_rgba(0x33, 0x77, 0xCC, 0x30));
}
break;
}
}
// Determine cursor style based on resize hover or active resize
CursorStyle cur_style = CURSOR_ARROW;
for (int i = ds->window_count - 1; i >= 0; i--) {
Window* win = &ds->windows[i];
if (win->resizing) {
cur_style = cursor_for_edge(win->resize_edge);
break;
}
if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED || win->state == WIN_MAXIMIZED)
continue;
if (win->frame.contains(ds->mouse.x, ds->mouse.y)) {
ResizeEdge edge = hit_test_resize_edge(win->frame, ds->mouse.x, ds->mouse.y);
if (edge != RESIZE_NONE) {
cur_style = cursor_for_edge(edge);
}
break;
}
}
// Draw cursor last
draw_cursor(fb, ds->mouse.x, ds->mouse.y, cur_style);
}
void gui::desktop_handle_mouse(DesktopState* ds) {
int mx = ds->mouse.x;
int my = ds->mouse.y;
uint8_t buttons = ds->mouse.buttons;
uint8_t prev = ds->prev_buttons;
bool left_pressed = (buttons & 0x01) && !(prev & 0x01);
bool left_held = (buttons & 0x01);
bool left_released = !(buttons & 0x01) && (prev & 0x01);
bool right_pressed = (buttons & 0x02) && !(prev & 0x02);
MouseEvent ev;
ev.x = mx;
ev.y = my;
ev.buttons = buttons;
ev.prev_buttons = prev;
ev.scroll = ds->mouse.scrollDelta;
// Handle context menu clicks
if (ds->ctx_menu_open) {
if (left_pressed) {
static constexpr int CTX_MENU_W = 180;
static constexpr int CTX_ITEM_H = 36;
static constexpr int CTX_ITEM_COUNT = 5;
int cmx = ds->ctx_menu_x;
int cmy = ds->ctx_menu_y;
int cmh = CTX_ITEM_H * CTX_ITEM_COUNT + 8;
if (cmx + CTX_MENU_W > ds->screen_w) cmx = ds->screen_w - CTX_MENU_W;
if (cmy + cmh > ds->screen_h) cmy = ds->screen_h - cmh;
Rect ctx_rect = {cmx, cmy, CTX_MENU_W, cmh};
if (ctx_rect.contains(mx, my)) {
int rel_y = my - cmy - 4;
int item_idx = rel_y / CTX_ITEM_H;
if (item_idx >= 0 && item_idx < CTX_ITEM_COUNT) {
ds->ctx_menu_open = false;
switch (item_idx) {
case 0: open_terminal(ds); break;
case 1: open_filemanager(ds); break;
case 2: open_settings(ds); break;
case 3: open_reboot_dialog(ds); break;
case 4: open_shutdown_dialog(ds); break;
}
return;
}
}
ds->ctx_menu_open = false;
return;
}
if (right_pressed) {
ds->ctx_menu_open = false;
return;
}
}
// Check for ongoing window drags first
for (int i = 0; i < ds->window_count; i++) {
Window* win = &ds->windows[i];
if (win->dragging) {
if (left_held) {
win->frame.x = mx - win->drag_offset_x;
win->frame.y = my - win->drag_offset_y;
if (win->frame.x < -win->frame.w + 50) win->frame.x = -win->frame.w + 50;
if (win->frame.y < 0) win->frame.y = 0;
if (win->frame.x > ds->screen_w - 50) win->frame.x = ds->screen_w - 50;
if (win->frame.y > ds->screen_h - 50) win->frame.y = ds->screen_h - 50;
}
if (left_released) {
win->dragging = false;
// Window edge snapping
if (mx <= 0) {
win->saved_frame = win->frame;
win->frame = {0, PANEL_HEIGHT, ds->screen_w / 2, ds->screen_h - PANEL_HEIGHT};
win->state = WIN_MAXIMIZED;
if (!win->external) {
Rect cr = win->content_rect();
if (cr.w != win->content_w || cr.h != win->content_h) {
if (win->content) zenith::free(win->content);
win->content_w = cr.w;
win->content_h = cr.h;
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
zenith::memset(win->content, 0xFF, cr.w * cr.h * 4);
}
} else {
Rect cr = win->content_rect();
Zenith::WinEvent rev;
zenith::memset(&rev, 0, sizeof(rev));
rev.type = 2;
rev.resize.w = cr.w;
rev.resize.h = cr.h;
zenith::win_sendevent(win->ext_win_id, &rev);
}
} else if (mx >= ds->screen_w - 1) {
win->saved_frame = win->frame;
win->frame = {ds->screen_w / 2, PANEL_HEIGHT, ds->screen_w / 2, ds->screen_h - PANEL_HEIGHT};
win->state = WIN_MAXIMIZED;
if (!win->external) {
Rect cr = win->content_rect();
if (cr.w != win->content_w || cr.h != win->content_h) {
if (win->content) zenith::free(win->content);
win->content_w = cr.w;
win->content_h = cr.h;
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
zenith::memset(win->content, 0xFF, cr.w * cr.h * 4);
}
} else {
Rect cr = win->content_rect();
Zenith::WinEvent rev;
zenith::memset(&rev, 0, sizeof(rev));
rev.type = 2;
rev.resize.w = cr.w;
rev.resize.h = cr.h;
zenith::win_sendevent(win->ext_win_id, &rev);
}
}
}
return;
}
}
// Check for ongoing window resizes
for (int i = 0; i < ds->window_count; i++) {
Window* win = &ds->windows[i];
if (win->resizing) {
if (left_held) {
int dx = mx - win->resize_start_mx;
int dy = my - win->resize_start_my;
Rect sf = win->resize_start_frame;
ResizeEdge edge = win->resize_edge;
int new_x = sf.x, new_y = sf.y, new_w = sf.w, new_h = sf.h;
if (edge == RESIZE_RIGHT || edge == RESIZE_TOP_RIGHT || edge == RESIZE_BOTTOM_RIGHT)
new_w = sf.w + dx;
if (edge == RESIZE_BOTTOM || edge == RESIZE_BOTTOM_LEFT || edge == RESIZE_BOTTOM_RIGHT)
new_h = sf.h + dy;
if (edge == RESIZE_LEFT || edge == RESIZE_TOP_LEFT || edge == RESIZE_BOTTOM_LEFT) {
new_x = sf.x + dx;
new_w = sf.w - dx;
}
if (edge == RESIZE_TOP || edge == RESIZE_TOP_LEFT || edge == RESIZE_TOP_RIGHT) {
new_y = sf.y + dy;
new_h = sf.h - dy;
}
// Enforce minimum size
if (new_w < MIN_WINDOW_W) {
if (edge == RESIZE_LEFT || edge == RESIZE_TOP_LEFT || edge == RESIZE_BOTTOM_LEFT)
new_x = sf.x + sf.w - MIN_WINDOW_W;
new_w = MIN_WINDOW_W;
}
if (new_h < MIN_WINDOW_H) {
if (edge == RESIZE_TOP || edge == RESIZE_TOP_LEFT || edge == RESIZE_TOP_RIGHT)
new_y = sf.y + sf.h - MIN_WINDOW_H;
new_h = MIN_WINDOW_H;
}
win->frame = {new_x, new_y, new_w, new_h};
}
if (left_released) {
win->resizing = false;
// Reallocate content buffer if dimensions changed (skip for external)
if (!win->external) {
Rect cr = win->content_rect();
if (cr.w != win->content_w || cr.h != win->content_h) {
if (win->content) zenith::free(win->content);
win->content_w = cr.w;
win->content_h = cr.h;
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
zenith::memset(win->content, 0xFF, cr.w * cr.h * 4);
}
win->dirty = true;
} else {
Rect cr = win->content_rect();
Zenith::WinEvent rev;
zenith::memset(&rev, 0, sizeof(rev));
rev.type = 2;
rev.resize.w = cr.w;
rev.resize.h = cr.h;
zenith::win_sendevent(win->ext_win_id, &rev);
}
}
return;
}
}
// Handle app menu clicks
if (ds->app_menu_open && left_pressed) {
int menu_x = 4;
int menu_y = PANEL_HEIGHT + 2;
int menu_h = menu_total_height();
Rect menu_rect = {menu_x, menu_y, MENU_W, menu_h};
if (menu_rect.contains(mx, my)) {
// Walk rows to find which one was clicked
int iy = menu_y + 5;
for (int i = 0; i < MENU_ROW_COUNT; i++) {
const MenuRow& row = menu_rows[i];
int row_h = menu_row_height(row);
if (my >= iy && my < iy + row_h) {
if (!row.is_category) {
switch (row.app_id) {
case 0: open_terminal(ds); break;
case 1: open_filemanager(ds); break;
case 2: open_sysinfo(ds); break;
case 3: open_calculator(ds); break;
case 4: open_texteditor(ds); break;
case 5: open_klog(ds); break;
case 6: open_procmgr(ds); break;
case 7: open_mandelbrot(ds); break;
case 8: open_devexplorer(ds); break;
case 9: open_wiki(ds); break;
case 10: open_doom(ds); break;
case 11: open_settings(ds); break;
case 12: open_reboot_dialog(ds); break;
case 13: open_weather(ds); break;
case 14: open_shutdown_dialog(ds); break;
}
ds->app_menu_open = false;
}
break;
}
iy += row_h;
}
return;
} else {
ds->app_menu_open = false;
}
}
// Handle net popup clicks
if (ds->net_popup_open && left_pressed) {
int popup_w = 220;
int popup_h = 130;
int popup_x = ds->net_icon_rect.x + ds->net_icon_rect.w - popup_w;
int popup_y = PANEL_HEIGHT + 2;
if (popup_x < 4) popup_x = 4;
Rect popup_rect = {popup_x, popup_y, popup_w, popup_h};
if (popup_rect.contains(mx, my)) {
return;
} else if (!ds->net_icon_rect.contains(mx, my)) {
ds->net_popup_open = false;
}
}
// Panel click check
if (left_pressed && my < PANEL_HEIGHT) {
// App menu button
if (mx < 36) {
ds->app_menu_open = !ds->app_menu_open;
ds->net_popup_open = false;
ds->ctx_menu_open = false;
return;
}
// Network icon
if (ds->net_icon_rect.w > 0 && ds->net_icon_rect.contains(mx, my)) {
ds->net_popup_open = !ds->net_popup_open;
ds->app_menu_open = false;
ds->ctx_menu_open = false;
return;
}
// Window indicator buttons
int indicator_x = 40;
for (int i = 0; i < ds->window_count; i++) {
Window* win = &ds->windows[i];
if (win->state == WIN_CLOSED) continue;
int tw = text_width(win->title);
int pad = 12;
int iw = tw + pad * 2;
if (iw > 150) iw = 150;
Rect btn_rect = {indicator_x, 4, iw, 24};
if (btn_rect.contains(mx, my)) {
if (win->state == WIN_MINIMIZED) {
win->state = WIN_NORMAL;
}
desktop_raise_window(ds, i);
return;
}
indicator_x += iw + 4;
}
return;
}
// Window interaction: check from top (last) to bottom (first)
if (left_pressed) {
for (int i = ds->window_count - 1; i >= 0; i--) {
Window* win = &ds->windows[i];
if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) continue;
// Check close button
Rect close_r = win->close_btn_rect();
if (close_r.contains(mx, my)) {
desktop_close_window(ds, i);
return;
}
// Check minimize button
Rect min_r = win->min_btn_rect();
if (min_r.contains(mx, my)) {
win->state = WIN_MINIMIZED;
if (ds->focused_window == i) {
ds->focused_window = -1;
for (int j = ds->window_count - 1; j >= 0; j--) {
if (ds->windows[j].state == WIN_NORMAL || ds->windows[j].state == WIN_MAXIMIZED) {
ds->focused_window = j;
ds->windows[j].focused = true;
break;
}
}
}
return;
}
// Check maximize button
Rect max_r = win->max_btn_rect();
if (max_r.contains(mx, my)) {
if (win->state == WIN_MAXIMIZED) {
win->frame = win->saved_frame;
win->state = WIN_NORMAL;
} else {
win->saved_frame = win->frame;
win->frame = {0, PANEL_HEIGHT, ds->screen_w, ds->screen_h - PANEL_HEIGHT};
win->state = WIN_MAXIMIZED;
}
// Reallocate content buffer for local windows only
if (!win->external) {
Rect cr = win->content_rect();
if (cr.w != win->content_w || cr.h != win->content_h) {
if (win->content) zenith::free(win->content);
win->content_w = cr.w;
win->content_h = cr.h;
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
zenith::memset(win->content, 0xFF, cr.w * cr.h * 4);
}
} else {
Rect cr = win->content_rect();
Zenith::WinEvent rev;
zenith::memset(&rev, 0, sizeof(rev));
rev.type = 2;
rev.resize.w = cr.w;
rev.resize.h = cr.h;
zenith::win_sendevent(win->ext_win_id, &rev);
}
desktop_raise_window(ds, i);
return;
}
// Check resize edges (before titlebar drag, so corner grabs work)
if (win->state != WIN_MAXIMIZED) {
ResizeEdge edge = hit_test_resize_edge(win->frame, mx, my);
if (edge != RESIZE_NONE) {
win->resizing = true;
win->resize_edge = edge;
win->resize_start_frame = win->frame;
win->resize_start_mx = mx;
win->resize_start_my = my;
desktop_raise_window(ds, i);
int new_idx = ds->window_count - 1;
ds->windows[new_idx].resizing = true;
ds->windows[new_idx].resize_edge = edge;
ds->windows[new_idx].resize_start_frame = ds->windows[new_idx].frame;
ds->windows[new_idx].resize_start_mx = mx;
ds->windows[new_idx].resize_start_my = my;
return;
}
}
// Check titlebar (start drag)
Rect tb = win->titlebar_rect();
if (tb.contains(mx, my)) {
win->dragging = true;
win->drag_offset_x = mx - win->frame.x;
win->drag_offset_y = my - win->frame.y;
desktop_raise_window(ds, i);
int new_idx = ds->window_count - 1;
ds->windows[new_idx].dragging = true;
ds->windows[new_idx].drag_offset_x = mx - ds->windows[new_idx].frame.x;
ds->windows[new_idx].drag_offset_y = my - ds->windows[new_idx].frame.y;
return;
}
// Check content area
Rect cr = win->content_rect();
if (cr.contains(mx, my)) {
desktop_raise_window(ds, i);
int new_idx = ds->window_count - 1;
Window* raised = &ds->windows[new_idx];
if (raised->external) {
// Forward mouse event to external window
Zenith::WinEvent wev;
zenith::memset(&wev, 0, sizeof(wev));
wev.type = 1; // mouse
wev.mouse.x = mx - cr.x;
wev.mouse.y = my - cr.y;
wev.mouse.scroll = ev.scroll;
wev.mouse.buttons = buttons;
wev.mouse.prev_buttons = prev;
zenith::win_sendevent(raised->ext_win_id, &wev);
} else if (raised->on_mouse) {
ev.x = mx;
ev.y = my;
raised->on_mouse(raised, ev);
}
return;
}
// Check full frame
if (win->frame.contains(mx, my)) {
desktop_raise_window(ds, i);
return;
}
}
ds->app_menu_open = false;
ds->ctx_menu_open = false;
}
// Handle scroll events on focused window
if (ev.scroll != 0 && ds->focused_window >= 0) {
Window* win = &ds->windows[ds->focused_window];
Rect cr = win->content_rect();
if (cr.contains(mx, my)) {
if (win->external) {
Zenith::WinEvent wev;
zenith::memset(&wev, 0, sizeof(wev));
wev.type = 1; // mouse
wev.mouse.x = mx - cr.x;
wev.mouse.y = my - cr.y;
wev.mouse.scroll = ev.scroll;
wev.mouse.buttons = buttons;
wev.mouse.prev_buttons = prev;
zenith::win_sendevent(win->ext_win_id, &wev);
} else if (win->on_mouse) {
win->on_mouse(win, ev);
}
}
}
// Right-click on desktop background opens context menu
if (right_pressed && my >= PANEL_HEIGHT) {
bool on_window = false;
for (int i = ds->window_count - 1; i >= 0; i--) {
Window* win = &ds->windows[i];
if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) continue;
if (win->frame.contains(mx, my)) {
on_window = true;
break;
}
}
if (!on_window) {
ds->ctx_menu_open = true;
ds->ctx_menu_x = mx;
ds->ctx_menu_y = my;
ds->app_menu_open = false;
ds->net_popup_open = false;
}
}
}
void gui::desktop_handle_keyboard(DesktopState* ds, const Zenith::KeyEvent& key) {
// Global shortcuts (only on key press)
if (key.pressed && key.ctrl && key.alt) {
if (key.ascii == 't' || key.ascii == 'T') {
open_terminal(ds);
return;
}
if (key.ascii == 'f' || key.ascii == 'F') {
open_filemanager(ds);
return;
}
if (key.ascii == 'i' || key.ascii == 'I') {
open_sysinfo(ds);
return;
}
if (key.ascii == 'c' || key.ascii == 'C') {
open_calculator(ds);
return;
}
if (key.ascii == 'e' || key.ascii == 'E') {
open_texteditor(ds);
return;
}
if (key.ascii == 'k' || key.ascii == 'K') {
open_klog(ds);
return;
}
if (key.ascii == 'd' || key.ascii == 'D') {
open_doom(ds);
return;
}
}
// Dispatch to focused window
if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) {
Window* win = &ds->windows[ds->focused_window];
if (win->external) {
// Forward key event to external window via syscall
Zenith::WinEvent ev;
zenith::memset(&ev, 0, sizeof(ev));
ev.type = 0; // key
ev.key = key;
zenith::win_sendevent(win->ext_win_id, &ev);
} else if (win->on_key) {
win->on_key(win, key);
}
}
}
// ============================================================================
// External Window Polling
// ============================================================================
void desktop_poll_external_windows(DesktopState* ds) {
Zenith::WinInfo extWins[8];
int extCount = zenith::win_enumerate(extWins, 8);
// Check for new external windows and map them
for (int e = 0; e < extCount; e++) {
int extId = extWins[e].id;
// Check if we already have this window
bool found = false;
for (int i = 0; i < ds->window_count; i++) {
if (ds->windows[i].external && ds->windows[i].ext_win_id == extId) {
found = true;
// Update dirty flag
if (extWins[e].dirty) {
ds->windows[i].dirty = true;
}
// Re-map if external app resized its buffer
if (extWins[e].width != ds->windows[i].content_w ||
extWins[e].height != ds->windows[i].content_h) {
uint64_t va = zenith::win_map(extId);
if (va != 0) {
ds->windows[i].content = (uint32_t*)va;
ds->windows[i].content_w = extWins[e].width;
ds->windows[i].content_h = extWins[e].height;
ds->windows[i].dirty = true;
}
}
break;
}
}
if (!found && ds->window_count < MAX_WINDOWS) {
// Map the pixel buffer into our address space
uint64_t va = zenith::win_map(extId);
if (va == 0) continue;
int idx = ds->window_count;
Window* win = &ds->windows[idx];
zenith::memset(win, 0, sizeof(Window));
zenith::strncpy(win->title, extWins[e].title, MAX_TITLE_LEN);
int w = extWins[e].width;
int h = extWins[e].height;
// Position the window centered-ish
int wx = (ds->screen_w - w - 2 * BORDER_WIDTH) / 2 + idx * 30;
int wy = PANEL_HEIGHT + 20 + idx * 30;
win->frame = {wx, wy, w + 2 * BORDER_WIDTH, h + TITLEBAR_HEIGHT + BORDER_WIDTH};
win->state = WIN_NORMAL;
win->z_order = idx;
win->focused = true;
win->dirty = true;
win->dragging = false;
win->resizing = false;
win->saved_frame = win->frame;
// Point content to the shared pixel buffer
win->content = (uint32_t*)va;
win->content_w = w;
win->content_h = h;
win->on_draw = nullptr;
win->on_mouse = nullptr;
win->on_key = nullptr;
win->on_close = nullptr;
win->on_poll = nullptr;
win->app_data = nullptr;
win->external = true;
win->ext_win_id = extId;
// Unfocus previous window
if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) {
ds->windows[ds->focused_window].focused = false;
}
ds->focused_window = idx;
ds->window_count++;
}
}
// Check for removed external windows (process exited)
for (int i = ds->window_count - 1; i >= 0; i--) {
if (!ds->windows[i].external) continue;
int extId = ds->windows[i].ext_win_id;
bool stillExists = false;
for (int e = 0; e < extCount; e++) {
if (extWins[e].id == extId) {
stillExists = true;
break;
}
}
if (!stillExists) {
// Window gone — remove without freeing content (shared memory)
ds->windows[i].content = nullptr; // prevent free
gui::desktop_close_window(ds, i);
}
}
}
void gui::desktop_run(DesktopState* ds) {
for (;;) {
// Poll mouse state
ds->prev_buttons = ds->mouse.buttons;
zenith::mouse_state(&ds->mouse);
// Poll keyboard events
while (zenith::is_key_available()) {
Zenith::KeyEvent key;
zenith::getkey(&key);
desktop_handle_keyboard(ds, key);
}
// Poll external windows (discover new, remove dead, update dirty)
desktop_poll_external_windows(ds);
// Poll windows that have a poll callback
for (int i = 0; i < ds->window_count; i++) {
Window* win = &ds->windows[i];
if (win->state == WIN_CLOSED) continue;
if (win->on_poll) {
win->on_poll(win);
}
}
// Handle mouse events
desktop_handle_mouse(ds);
// Compose and present
desktop_compose(ds);
ds->fb.flip();
// Target ~60fps
zenith::sleep_ms(16);
}
}
// ============================================================================
// Entry Point
// ============================================================================
static DesktopState* g_desktop;
extern "C" void _start() {
DesktopState* ds = (DesktopState*)zenith::malloc(sizeof(DesktopState));
zenith::memset(ds, 0, sizeof(DesktopState));
// Placement-new the Framebuffer since it has a constructor
new (&ds->fb) Framebuffer();
g_desktop = ds;
desktop_init(ds);
desktop_run(ds);
zenith::exit(0);
}