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

1209 lines
40 KiB
C++

/*
* main.cpp
* ZenithOS Desktop Environment - window manager, compositor, and applications
* Copyright (c) 2026 Daniel Hammer
*/
#include <zenith/syscall.h>
#include <zenith/string.h>
#include <zenith/heap.h>
#include <gui/gui.hpp>
#include <gui/framebuffer.hpp>
#include <gui/font.hpp>
#include <gui/draw.hpp>
#include <gui/svg.hpp>
#include <gui/widgets.hpp>
#include <gui/window.hpp>
#include <gui/terminal.hpp>
#include <gui/desktop.hpp>
// Placement new for freestanding environment
inline void* operator new(unsigned long, void* p) { return p; }
using namespace gui;
// ============================================================================
// Minimal snprintf (copied from init/main.cpp pattern)
// ============================================================================
using va_list = __builtin_va_list;
#define va_start __builtin_va_start
#define va_end __builtin_va_end
#define va_arg __builtin_va_arg
struct PfState { char* buf; int pos; int max; };
static void pf_putc(PfState* st, char c) {
if (st->pos < st->max) st->buf[st->pos] = c;
st->pos++;
}
static void pf_putnum(PfState* st, unsigned long val, int base, int width, char pad, int neg) {
char tmp[24]; int i = 0;
const char* digits = "0123456789abcdef";
if (val == 0) { tmp[i++] = '0'; }
else { while (val > 0) { tmp[i++] = digits[val % base]; val /= base; } }
int total = (neg ? 1 : 0) + i;
if (neg && pad == '0') pf_putc(st, '-');
for (int w = total; w < width; w++) pf_putc(st, pad);
if (neg && pad != '0') pf_putc(st, '-');
while (i > 0) pf_putc(st, tmp[--i]);
}
static int snprintf(char* buf, int size, const char* fmt, ...) {
va_list ap; va_start(ap, fmt);
PfState st; st.buf = buf; st.pos = 0; st.max = size > 0 ? size - 1 : 0;
while (*fmt) {
if (*fmt != '%') { pf_putc(&st, *fmt++); continue; }
fmt++;
char pad = ' ';
if (*fmt == '0') { pad = '0'; fmt++; }
int width = 0;
while (*fmt >= '0' && *fmt <= '9') { width = width * 10 + (*fmt - '0'); fmt++; }
if (*fmt == 'l') fmt++;
switch (*fmt) {
case 'd': case 'i': {
long val = va_arg(ap, int);
int neg = 0; unsigned long uval;
if (val < 0) { neg = 1; uval = (unsigned long)(-val); } else uval = (unsigned long)val;
pf_putnum(&st, uval, 10, width, pad, neg); break;
}
case 'u': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 10, width, pad, 0); break; }
case 'x': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 16, width, pad, 0); break; }
case 's': {
const char* s = va_arg(ap, const char*); if (!s) s = "(null)";
int slen = 0; while (s[slen]) slen++;
for (int w = slen; w < width; w++) pf_putc(&st, ' ');
for (int j = 0; j < slen; j++) pf_putc(&st, s[j]);
break;
}
case 'c': { char c = (char)va_arg(ap, int); pf_putc(&st, c); break; }
case '%': pf_putc(&st, '%'); break;
default: pf_putc(&st, '%'); pf_putc(&st, *fmt); break;
}
if (*fmt) fmt++;
}
if (size > 0) { if (st.pos < size) st.buf[st.pos] = '\0'; else st.buf[size - 1] = '\0'; }
va_end(ap); return st.pos;
}
// ============================================================================
// String helpers
// ============================================================================
static void str_append(char* dst, const char* src, int max) {
int len = zenith::slen(dst);
int i = 0;
while (src[i] && len < max - 1) {
dst[len++] = src[i++];
}
dst[len] = '\0';
}
// ============================================================================
// File Manager Application
// ============================================================================
struct FileManagerState {
char current_path[256];
char entry_names[64][64];
int entry_count;
int selected;
int scroll_offset;
bool is_dir[64];
};
static void filemanager_read_dir(FileManagerState* fm) {
const char* names[64];
fm->entry_count = zenith::readdir(fm->current_path, names, 64);
if (fm->entry_count < 0) fm->entry_count = 0;
for (int i = 0; i < fm->entry_count; i++) {
zenith::strncpy(fm->entry_names[i], names[i], 63);
// Heuristic: entries ending with '/' or without '.' are directories
int len = zenith::slen(fm->entry_names[i]);
if (len > 0 && fm->entry_names[i][len - 1] == '/') {
fm->is_dir[i] = true;
fm->entry_names[i][len - 1] = '\0'; // strip trailing slash
} else {
// Check if entry has an extension (has a dot)
bool has_dot = false;
for (int j = 0; j < len; j++) {
if (fm->entry_names[i][j] == '.') { has_dot = true; break; }
}
fm->is_dir[i] = !has_dot;
}
}
fm->selected = -1;
fm->scroll_offset = 0;
}
static void filemanager_navigate(FileManagerState* fm, const char* name) {
// Build new path
int path_len = zenith::slen(fm->current_path);
if (path_len > 0 && fm->current_path[path_len - 1] != '/') {
str_append(fm->current_path, "/", 256);
}
str_append(fm->current_path, name, 256);
filemanager_read_dir(fm);
}
static void filemanager_go_up(FileManagerState* fm) {
int len = zenith::slen(fm->current_path);
if (len <= 3) return; // "0:/" is root
// Remove trailing slash if present
if (len > 0 && fm->current_path[len - 1] == '/') {
fm->current_path[len - 1] = '\0';
len--;
}
// Find last slash
int last_slash = -1;
for (int i = len - 1; i >= 0; i--) {
if (fm->current_path[i] == '/') { last_slash = i; break; }
}
if (last_slash >= 0) {
fm->current_path[last_slash + 1] = '\0';
}
filemanager_read_dir(fm);
}
static void filemanager_on_draw(Window* win, Framebuffer& fb) {
FileManagerState* fm = (FileManagerState*)win->app_data;
if (!fm) return;
Rect cr = win->content_rect();
int cw = cr.w;
int ch = cr.h;
// Render to content buffer
uint32_t* pixels = win->content;
uint32_t bg_px = colors::WINDOW_BG.to_pixel();
for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px;
// Draw path bar at top (24px tall, light gray background)
uint32_t pathbar_px = Color::from_rgb(0xF0, 0xF0, 0xF0).to_pixel();
for (int y = 0; y < 24 && y < ch; y++)
for (int x = 0; x < cw; x++)
pixels[y * cw + x] = pathbar_px;
// Draw path text
{
int tx = 8;
int ty = 4;
uint32_t fg_px = colors::TEXT_COLOR.to_pixel();
for (int ci = 0; fm->current_path[ci] && tx + FONT_WIDTH <= cw; ci++) {
const uint8_t* glyph = &font_data[(unsigned char)fm->current_path[ci] * FONT_HEIGHT];
for (int fy = 0; fy < FONT_HEIGHT && ty + fy < ch; fy++) {
uint8_t bits = glyph[fy];
for (int fx = 0; fx < FONT_WIDTH; fx++) {
if (bits & (0x80 >> fx)) {
int dx = tx + fx;
int dy = ty + fy;
if (dx < cw && dy < ch)
pixels[dy * cw + dx] = fg_px;
}
}
}
tx += FONT_WIDTH;
}
}
// Draw separator line
uint32_t sep_px = colors::BORDER.to_pixel();
if (24 < ch) {
for (int x = 0; x < cw; x++)
pixels[24 * cw + x] = sep_px;
}
// Draw file entries
int item_height = 24;
int start_y = 26;
int visible_items = (ch - start_y) / item_height;
for (int i = fm->scroll_offset; i < fm->entry_count && (i - fm->scroll_offset) < visible_items; i++) {
int iy = start_y + (i - fm->scroll_offset) * item_height;
if (iy + item_height > ch) break;
// Highlight selected
if (i == fm->selected) {
uint32_t sel_px = colors::MENU_HOVER.to_pixel();
for (int y = iy; y < iy + item_height && y < ch; y++)
for (int x = 0; x < cw; x++)
pixels[y * cw + x] = sel_px;
}
// Draw icon placeholder (small colored square)
uint32_t icon_px;
if (fm->is_dir[i]) {
icon_px = Color::from_rgb(0xFF, 0xBD, 0x2E).to_pixel(); // folder yellow
} else {
icon_px = Color::from_rgb(0x90, 0x90, 0x90).to_pixel(); // file gray
}
int icon_x = 8;
int icon_y = iy + 4;
for (int dy = 0; dy < 16 && icon_y + dy < ch; dy++)
for (int dx = 0; dx < 16 && icon_x + dx < cw; dx++)
pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px;
// Draw entry name
int tx = 30;
int ty = iy + 4;
uint32_t text_px = colors::TEXT_COLOR.to_pixel();
for (int ci = 0; fm->entry_names[i][ci] && tx + FONT_WIDTH <= cw; ci++) {
const uint8_t* glyph = &font_data[(unsigned char)fm->entry_names[i][ci] * FONT_HEIGHT];
for (int fy = 0; fy < FONT_HEIGHT && ty + fy < ch; fy++) {
uint8_t bits = glyph[fy];
for (int fx = 0; fx < FONT_WIDTH; fx++) {
if (bits & (0x80 >> fx)) {
int dx = tx + fx;
int dy = ty + fy;
if (dx < cw && dy < ch)
pixels[dy * cw + dx] = text_px;
}
}
}
tx += FONT_WIDTH;
}
}
}
static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
FileManagerState* fm = (FileManagerState*)win->app_data;
if (!fm) return;
Rect cr = win->content_rect();
int local_y = ev.y - cr.y;
if (ev.left_pressed()) {
int item_height = 24;
int start_y = 26;
int clicked_idx = fm->scroll_offset + (local_y - start_y) / item_height;
if (local_y >= start_y && clicked_idx >= 0 && clicked_idx < fm->entry_count) {
if (fm->selected == clicked_idx) {
// Double-click: navigate into directory
if (fm->is_dir[clicked_idx]) {
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
}
} else {
fm->selected = clicked_idx;
}
}
}
// Scroll handling
if (ev.scroll != 0) {
fm->scroll_offset -= ev.scroll;
if (fm->scroll_offset < 0) fm->scroll_offset = 0;
int max_off = fm->entry_count - ((cr.h - 26) / 24);
if (max_off < 0) max_off = 0;
if (fm->scroll_offset > max_off) fm->scroll_offset = max_off;
}
}
static void filemanager_on_key(Window* win, const Zenith::KeyEvent& key) {
FileManagerState* fm = (FileManagerState*)win->app_data;
if (!fm || !key.pressed) return;
if (key.ascii == '\b' || key.scancode == 0x0E) {
filemanager_go_up(fm);
} else if (key.scancode == 0x48) {
// Up arrow
if (fm->selected > 0) fm->selected--;
} else if (key.scancode == 0x50) {
// Down arrow
if (fm->selected < fm->entry_count - 1) fm->selected++;
} else if (key.ascii == '\n' || key.ascii == '\r') {
if (fm->selected >= 0 && fm->selected < fm->entry_count) {
if (fm->is_dir[fm->selected]) {
filemanager_navigate(fm, fm->entry_names[fm->selected]);
}
}
}
}
static void filemanager_on_close(Window* win) {
if (win->app_data) {
zenith::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// System Info Application
// ============================================================================
struct SysInfoState {
Zenith::SysInfo sys_info;
Zenith::NetCfg net_cfg;
uint64_t uptime_ms;
};
static void draw_text_to_pixels(uint32_t* pixels, int pw, int ph,
int tx, int ty, const char* text, uint32_t color_px) {
for (int i = 0; text[i] && tx + (i + 1) * FONT_WIDTH <= pw; i++) {
const uint8_t* glyph = &font_data[(unsigned char)text[i] * FONT_HEIGHT];
int cx = tx + i * FONT_WIDTH;
for (int fy = 0; fy < FONT_HEIGHT && ty + fy < ph; fy++) {
uint8_t bits = glyph[fy];
for (int fx = 0; fx < FONT_WIDTH; fx++) {
if (bits & (0x80 >> fx)) {
int dx = cx + fx;
int dy = ty + fy;
if (dx >= 0 && dx < pw && dy >= 0 && dy < ph)
pixels[dy * pw + dx] = color_px;
}
}
}
}
}
static void sysinfo_on_draw(Window* win, Framebuffer& fb) {
SysInfoState* si = (SysInfoState*)win->app_data;
if (!si) return;
// Refresh uptime
si->uptime_ms = zenith::get_milliseconds();
Rect cr = win->content_rect();
int cw = cr.w;
int ch = cr.h;
uint32_t* pixels = win->content;
// Fill background
uint32_t bg_px = colors::WINDOW_BG.to_pixel();
for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px;
uint32_t text_px = colors::TEXT_COLOR.to_pixel();
uint32_t accent_px = colors::ACCENT.to_pixel();
int y = 16;
int x = 16;
char line[128];
// Title
draw_text_to_pixels(pixels, cw, ch, x, y, "System Information", accent_px);
y += FONT_HEIGHT + 12;
// Separator
uint32_t sep_px = colors::BORDER.to_pixel();
for (int sx = x; sx < cw - x && y < ch; sx++)
pixels[y * cw + sx] = sep_px;
y += 8;
// OS Name
snprintf(line, sizeof(line), "OS: %s", si->sys_info.osName);
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
y += FONT_HEIGHT + 6;
// OS Version
snprintf(line, sizeof(line), "Version: %s", si->sys_info.osVersion);
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
y += FONT_HEIGHT + 6;
// API Version
snprintf(line, sizeof(line), "API: %d", (int)si->sys_info.apiVersion);
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
y += FONT_HEIGHT + 6;
// Max Processes
snprintf(line, sizeof(line), "Max PIDs: %d", (int)si->sys_info.maxProcesses);
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
y += FONT_HEIGHT + 12;
// Uptime
int up_sec = (int)(si->uptime_ms / 1000);
int up_min = up_sec / 60;
int up_hr = up_min / 60;
snprintf(line, sizeof(line), "Uptime: %d:%02d:%02d", up_hr, up_min % 60, up_sec % 60);
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
y += FONT_HEIGHT + 12;
// Network section
draw_text_to_pixels(pixels, cw, ch, x, y, "Network", accent_px);
y += FONT_HEIGHT + 8;
for (int sx = x; sx < cw - x && y < ch; sx++)
pixels[y * cw + sx] = sep_px;
y += 8;
// IP Address
uint32_t ip = si->net_cfg.ipAddress;
snprintf(line, sizeof(line), "IP: %d.%d.%d.%d",
(int)(ip & 0xFF), (int)((ip >> 8) & 0xFF),
(int)((ip >> 16) & 0xFF), (int)((ip >> 24) & 0xFF));
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
y += FONT_HEIGHT + 6;
// Subnet
uint32_t mask = si->net_cfg.subnetMask;
snprintf(line, sizeof(line), "Subnet: %d.%d.%d.%d",
(int)(mask & 0xFF), (int)((mask >> 8) & 0xFF),
(int)((mask >> 16) & 0xFF), (int)((mask >> 24) & 0xFF));
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
y += FONT_HEIGHT + 6;
// Gateway
uint32_t gw = si->net_cfg.gateway;
snprintf(line, sizeof(line), "Gateway: %d.%d.%d.%d",
(int)(gw & 0xFF), (int)((gw >> 8) & 0xFF),
(int)((gw >> 16) & 0xFF), (int)((gw >> 24) & 0xFF));
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
y += FONT_HEIGHT + 6;
// DNS
uint32_t dns = si->net_cfg.dnsServer;
snprintf(line, sizeof(line), "DNS: %d.%d.%d.%d",
(int)(dns & 0xFF), (int)((dns >> 8) & 0xFF),
(int)((dns >> 16) & 0xFF), (int)((dns >> 24) & 0xFF));
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
y += FONT_HEIGHT + 6;
// MAC Address
snprintf(line, sizeof(line), "MAC: %02x:%02x:%02x:%02x:%02x:%02x",
(unsigned)si->net_cfg.macAddress[0], (unsigned)si->net_cfg.macAddress[1],
(unsigned)si->net_cfg.macAddress[2], (unsigned)si->net_cfg.macAddress[3],
(unsigned)si->net_cfg.macAddress[4], (unsigned)si->net_cfg.macAddress[5]);
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
}
static void sysinfo_on_close(Window* win) {
if (win->app_data) {
zenith::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// Terminal Application
// ============================================================================
static void terminal_on_draw(Window* win, Framebuffer& fb) {
TerminalState* ts = (TerminalState*)win->app_data;
if (!ts) return;
Rect cr = win->content_rect();
terminal_render(ts, win->content, cr.w, cr.h);
}
static void terminal_on_mouse(Window* win, MouseEvent& ev) {
// Terminal doesn't need mouse handling for now
}
static void terminal_on_key(Window* win, const Zenith::KeyEvent& key) {
TerminalState* ts = (TerminalState*)win->app_data;
if (!ts) return;
terminal_handle_key(ts, key);
}
static void terminal_on_close(Window* win) {
TerminalState* ts = (TerminalState*)win->app_data;
if (ts) {
if (ts->cells) zenith::mfree(ts->cells);
zenith::mfree(ts);
win->app_data = nullptr;
}
}
// ============================================================================
// Application Launchers
// ============================================================================
static DesktopState* g_desktop;
static void open_terminal(DesktopState* ds) {
int idx = desktop_create_window(ds, "Terminal", 200, 80, 648, 480);
if (idx < 0) return;
Window* win = &ds->windows[idx];
Rect cr = win->content_rect();
int cols = cr.w / FONT_WIDTH;
int rows = cr.h / FONT_HEIGHT;
TerminalState* ts = (TerminalState*)zenith::malloc(sizeof(TerminalState));
zenith::memset(ts, 0, sizeof(TerminalState));
terminal_init(ts, cols, rows);
win->app_data = ts;
win->on_draw = terminal_on_draw;
win->on_mouse = terminal_on_mouse;
win->on_key = terminal_on_key;
win->on_close = terminal_on_close;
}
static void open_filemanager(DesktopState* ds) {
int idx = desktop_create_window(ds, "Files", 150, 120, 500, 400);
if (idx < 0) return;
Window* win = &ds->windows[idx];
FileManagerState* fm = (FileManagerState*)zenith::malloc(sizeof(FileManagerState));
zenith::memset(fm, 0, sizeof(FileManagerState));
zenith::strcpy(fm->current_path, "0:/");
fm->selected = -1;
filemanager_read_dir(fm);
win->app_data = fm;
win->on_draw = filemanager_on_draw;
win->on_mouse = filemanager_on_mouse;
win->on_key = filemanager_on_key;
win->on_close = filemanager_on_close;
}
static void open_sysinfo(DesktopState* ds) {
int idx = desktop_create_window(ds, "System Info", 300, 100, 400, 380);
if (idx < 0) return;
Window* win = &ds->windows[idx];
SysInfoState* si = (SysInfoState*)zenith::malloc(sizeof(SysInfoState));
zenith::memset(si, 0, sizeof(SysInfoState));
zenith::get_info(&si->sys_info);
zenith::get_netcfg(&si->net_cfg);
si->uptime_ms = zenith::get_milliseconds();
win->app_data = si;
win->on_draw = sysinfo_on_draw;
win->on_mouse = nullptr;
win->on_key = nullptr;
win->on_close = sysinfo_on_close;
}
// App menu click callbacks
static void on_click_terminal(void* ud) {
DesktopState* ds = (DesktopState*)ud;
open_terminal(ds);
ds->app_menu_open = false;
}
static void on_click_filemanager(void* ud) {
DesktopState* ds = (DesktopState*)ud;
open_filemanager(ds);
ds->app_menu_open = false;
}
static void on_click_sysinfo(void* ud) {
DesktopState* ds = (DesktopState*)ud;
open_sysinfo(ds);
ds->app_menu_open = false;
}
// ============================================================================
// Desktop Implementation
// ============================================================================
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();
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 (filenames match those copied by scripts/copy_icons.sh)
ds->icon_terminal = svg_load("0:/icons/utilities-terminal-symbolic.svg", 20, 20, colors::ICON_COLOR);
ds->icon_filemanager = svg_load("0:/icons/system-file-manager-symbolic.svg", 20, 20, colors::ICON_COLOR);
ds->icon_sysinfo = svg_load("0:/icons/preferences-desktop-apps-symbolic.svg", 20, 20, colors::ICON_COLOR);
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-symbolic.svg", 16, 16, Color::from_rgb(0xFF, 0xBD, 0x2E));
ds->icon_file = svg_load("0:/icons/text-x-generic-symbolic.svg", 16, 16, colors::ICON_COLOR);
ds->icon_computer = svg_load("0:/icons/computer-symbolic.svg", 20, 20, colors::ICON_COLOR);
// Open initial terminal window
open_terminal(ds);
}
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->app_data = nullptr;
// 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];
if (win->on_close) win->on_close(win);
// Free content buffer
if (win->content) {
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
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 - 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
if (win->on_draw) {
win->on_draw(win, fb);
}
// Blit content buffer to framebuffer
Rect cr = win->content_rect();
if (win->content) {
fb.blit(cr.x, cr.y, cr.w, cr.h, win->content);
}
}
void gui::desktop_draw_panel(DesktopState* ds) {
Framebuffer& fb = ds->fb;
int sw = ds->screen_w;
// Panel background
fb.fill_rect(0, 0, sw, PANEL_HEIGHT, colors::PANEL_BG);
// App menu button (left side)
int btn_x = 4;
int btn_y = 2;
int btn_w = 28;
int btn_h = 28;
// Draw grid icon for app menu (3x3 dots)
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 {
// Fallback: draw 3x3 grid of small squares
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 buttons (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;
Color btn_bg = (i == ds->focused_window)
? Color::from_rgba(0xFF, 0xFF, 0xFF, 0x30)
: Color::from_rgba(0xFF, 0xFF, 0xFF, 0x10);
fb.fill_rect_alpha(indicator_x, 4, iw, 24, btn_bg);
// 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 - FONT_HEIGHT) / 2;
draw_text(fb, tx, ty, short_title, colors::PANEL_TEXT);
indicator_x += iw + 4;
}
// Clock (right side)
Zenith::DateTime dt;
zenith::gettime(&dt);
char clock_str[8];
snprintf(clock_str, sizeof(clock_str), "%02d:%02d", (int)dt.Hour, (int)dt.Minute);
int clock_w = text_width(clock_str);
int clock_x = sw - clock_w - 12;
int clock_y = (PANEL_HEIGHT - FONT_HEIGHT) / 2;
draw_text(fb, clock_x, clock_y, clock_str, colors::PANEL_TEXT);
}
static void desktop_draw_app_menu(DesktopState* ds) {
Framebuffer& fb = ds->fb;
int menu_x = 4;
int menu_y = PANEL_HEIGHT + 2;
int menu_w = 200;
int item_h = 36;
int menu_h = item_h * 3 + 8;
// Menu shadow
draw_shadow(fb, menu_x, menu_y, menu_w, menu_h, 4, colors::SHADOW);
// Menu background
fb.fill_rect(menu_x, menu_y, menu_w, menu_h, colors::MENU_BG);
draw_rect(fb, menu_x, menu_y, menu_w, menu_h, colors::BORDER);
// Menu items
struct MenuItem {
const char* label;
SvgIcon* icon;
};
MenuItem items[3] = {
{ "Terminal", &ds->icon_terminal },
{ "Files", &ds->icon_filemanager },
{ "System Info", &ds->icon_sysinfo }
};
int mx = ds->mouse.x;
int my = ds->mouse.y;
for (int i = 0; i < 3; i++) {
int iy = menu_y + 4 + i * item_h;
Rect item_rect = {menu_x + 4, iy, menu_w - 8, item_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 + (item_h - 20) / 2;
if (items[i].icon && items[i].icon->pixels) {
fb.blit_alpha(icon_x, icon_y, items[i].icon->width, items[i].icon->height, items[i].icon->pixels);
}
// Label
int tx = icon_x + 28;
int ty = item_rect.y + (item_h - FONT_HEIGHT) / 2;
draw_text(fb, tx, ty, items[i].label, colors::TEXT_COLOR);
}
}
void gui::desktop_compose(DesktopState* ds) {
Framebuffer& fb = ds->fb;
// Desktop background
fb.clear(colors::DESKTOP_BG);
// Draw windows from bottom to top (index 0 = bottom)
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 of everything
desktop_draw_panel(ds);
// Draw app menu if open
if (ds->app_menu_open) {
desktop_draw_app_menu(ds);
}
// Draw cursor last (on top of everything)
draw_cursor(fb, ds->mouse.x, ds->mouse.y);
}
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);
// Build mouse event
MouseEvent ev;
ev.x = mx;
ev.y = my;
ev.buttons = buttons;
ev.prev_buttons = prev;
ev.scroll = ds->mouse.scrollDelta;
// 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;
// Clamp to screen
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;
}
return;
}
}
// Handle app menu clicks
if (ds->app_menu_open && left_pressed) {
int menu_x = 4;
int menu_y = PANEL_HEIGHT + 2;
int menu_w = 200;
int item_h = 36;
int menu_h = item_h * 3 + 8;
Rect menu_rect = {menu_x, menu_y, menu_w, menu_h};
if (menu_rect.contains(mx, my)) {
// Which item was clicked?
int rel_y = my - menu_y - 4;
int item_idx = rel_y / item_h;
if (item_idx >= 0 && item_idx < 3) {
switch (item_idx) {
case 0: on_click_terminal(ds); break;
case 1: on_click_filemanager(ds); break;
case 2: on_click_sysinfo(ds); break;
}
}
return;
} else {
ds->app_menu_open = false;
// Fall through to handle click on other things
}
}
// Panel click check
if (left_pressed && my < PANEL_HEIGHT) {
// App menu button
if (mx < 36) {
ds->app_menu_open = !ds->app_menu_open;
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;
// Focus next visible window
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) {
// Restore
win->frame = win->saved_frame;
win->state = WIN_NORMAL;
} else {
// Maximize
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 new size
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);
}
desktop_raise_window(ds, i);
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, ds->window_count - 1 == i ? i : i);
// After raise, this window is at the end
// Need to update the reference since raise moved it
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);
// Dispatch mouse event to app
int new_idx = ds->window_count - 1;
if (ds->windows[new_idx].on_mouse) {
ev.x = mx;
ev.y = my;
ds->windows[new_idx].on_mouse(&ds->windows[new_idx], ev);
}
return;
}
// Check full frame (catches border clicks)
if (win->frame.contains(mx, my)) {
desktop_raise_window(ds, i);
return;
}
}
// Clicked on desktop background - close app menu
ds->app_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) && win->on_mouse) {
win->on_mouse(win, ev);
}
}
}
void gui::desktop_handle_keyboard(DesktopState* ds, const Zenith::KeyEvent& key) {
if (!key.pressed) return;
// Global shortcuts
if (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;
}
}
// Dispatch to focused window
if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) {
Window* win = &ds->windows[ds->focused_window];
if (win->on_key) {
win->on_key(win, key);
}
}
}
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 terminal I/O for all terminal windows
for (int i = 0; i < ds->window_count; i++) {
Window* win = &ds->windows[i];
if (win->state == WIN_CLOSED) continue;
if (win->app_data && win->on_draw == terminal_on_draw) {
TerminalState* ts = (TerminalState*)win->app_data;
terminal_poll(ts);
}
}
// Handle mouse events
desktop_handle_mouse(ds);
// Compose and present
desktop_compose(ds);
ds->fb.flip();
// Target ~60fps
zenith::sleep_ms(16);
}
}
// ============================================================================
// Entry Point
// ============================================================================
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);
}