From 1aee7c7a0ded840a0c166d42b1473defb628b2e5 Mon Sep 17 00:00:00 2001 From: subhobhai943 Date: Tue, 25 Aug 2026 00:18:55 +0530 Subject: [PATCH] feat(gui): Add SUB-WT widget toolkit, icon set, modal dialogs, six apps and a reworked window manager Turns the desktop from a fixed set of hand-drawn panels into a real environment applications can be written against. SUB-WT immediate-mode toolkit (gui/gui_widgets.c): Buttons, labels, checkboxes, radios, sliders, list boxes, text fields, tab bars, scrollbars, progress bars, sparklines and badges. An app feeds the WM event into the toolkit and then calls widgets from its paint routine, so each call both draws the control and reports what the user did with it -- no retained widget tree to keep in sync with window state. Icons and dialogs: gui_icons.c holds 14 16x16 palette-indexed glyphs stored as hex text, so the art stays hand-editable and picks up theme changes for free; they scale by integer factors and can be drawn as tint stencils. gui_dialog.c adds modal info/warning/error/confirm dialogs that dim the desktop, take exclusive input, word-wrap on spaces and report results through a callback. Applications (gui/gui_apps_ext.c): Settings (tabbed appearance/input/power), Task Manager (live process table over task_find_by_pid with memory and heap gauges), Kernel Log viewer (reads the dmesg ring, strips ANSI, colours by severity, scrolls), KTest Runner (drives the in-kernel suites and reports 21/21 with timing), Clock & Calendar (Zeller's congruence month grid) and a Text Editor. Window manager (gui/gui_wm.c): Minimize/maximize/restore buttons, a corner resize grip, edge snapping, tile and cascade layouts, F5-F9 shortcuts, title truncation, and index-based enumeration so the taskbar no longer assumes window ids stay dense. Desktop shell (gui/gui_desktop.c): Launcher icon grid, richer start menu, right-click context menu, taskbar buttons that minimize and restore, and a live CPU trace in the tray. Two interaction bugs fixed along the way: - keyboard_get_key() blocks until a key arrives, so calling it every frame froze the compositor; it is now gated on keyboard_has_key(), and stale console input is drained before the first frame. - When the desktop consumed a click, the WM's press-edge detector kept the pre-press state and read the still-held button as a fresh press on the next frame, stealing focus from whatever the click had just opened. gui_wm_sync_button_state() keeps the two layers in step. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L5yKJT4Wo2cV3rfPrwY5MP --- README.md | 18 +- gui/gui.c | 9 +- gui/gui_apps_ext.c | 813 +++++++++++++++++++++++++++++++++++++ gui/gui_desktop.c | 660 +++++++++++++++++++++++------- gui/gui_dialog.c | 211 ++++++++++ gui/gui_gfx.c | 2 +- gui/gui_icons.c | 365 +++++++++++++++++ gui/gui_widgets.c | 499 +++++++++++++++++++++++ gui/gui_wm.c | 726 +++++++++++++++++++++++++-------- include/gui/gui.h | 4 + include/gui/gui_apps_ext.h | 16 + include/gui/gui_desktop.h | 21 +- include/gui/gui_dialog.h | 42 ++ include/gui/gui_icons.h | 40 ++ include/gui/gui_widgets.h | 86 ++++ include/gui/gui_wm.h | 71 +++- 16 files changed, 3248 insertions(+), 335 deletions(-) create mode 100644 gui/gui_apps_ext.c create mode 100644 gui/gui_dialog.c create mode 100644 gui/gui_icons.c create mode 100644 gui/gui_widgets.c create mode 100644 include/gui/gui_apps_ext.h create mode 100644 include/gui/gui_dialog.h create mode 100644 include/gui/gui_icons.h create mode 100644 include/gui/gui_widgets.h diff --git a/README.md b/README.md index 9810b19..159c025 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,14 @@ - 🧪 **In-Kernel Test Harness (`kernel/ktest.c`)**: - KUnit-style suites with assertion macros, per-case pass/fail reporting and timing. - **6 built-in suites, 21 cases, 735 assertions** covering rbtree, kfifo, hashtable, RCU, futex, wait queues, page cache and libcore — runnable from the shell (`ktest`) or the desktop KTest Runner. +- 🖥️ **SUB-OS Graphical Desktop Environment (`gui/`)**: + - **SUB-WM Compositor**: 800x600 TrueColor double-buffered window manager with drag, corner resize, minimize/maximize/restore, edge snapping (left/right/maximize), tile and cascade layouts, and z-ordered painter's-algorithm compositing. + - **SUB-WT Widget Toolkit (`gui/gui_widgets.c`)**: Immediate-mode controls -- buttons, labels, checkboxes, radios, sliders, list boxes, text fields, tab bars, scrollbars, progress bars, sparklines and badges -- so an app rebuilds its interface from state each frame instead of maintaining a retained widget tree. + - **Icon Set (`gui/gui_icons.c`)**: 14 hand-drawn 16x16 palette-indexed glyphs, integer-scalable and tintable, driving the desktop grid, start menu and window chrome. + - **Modal Dialog Layer (`gui/gui_dialog.c`)**: Info, warning, error and confirm dialogs that dim the desktop and take exclusive input, with word-wrapped messages and callback results. + - **Desktop Shell (`gui/gui_desktop.c`)**: Gradient wallpaper with grid overlay, launcher icon grid, start menu, right-click context menu (tile / cascade / toggle grid), and a taskbar with per-window buttons, live CPU trace, memory badge and RTC clock. + - **Applications**: Terminal, File Explorer, System Monitor, Task Manager, Kernel Log viewer (live dmesg with severity colouring and scrollback), KTest Runner (drives the in-kernel suites and reports pass/fail), Text Editor, Calculator, Paint Studio, Clock & Calendar, Settings and About. + - **Keyboard Shortcuts**: `F5` cycle focus, `F6` cascade, `F7` tile, `F8` maximize, `F9` minimize, `Esc` exit to the kernel TTY. - ⚡ **Freestanding C++17 OOP Kernel Engine (`kernel/cpp/`)**: - **Bare-Metal C++ Runtime**: `operator new`/`delete`, sized deallocation, placement `new`, pure virtual handlers (`__cxa_pure_virtual`), and `.init_array` global constructor dispatcher. - **Modern Generic Containers**: Pure freestanding `Vector`, `UniquePtr` (with polymorphic converting constructors and custom deleters), dynamic `String`, and move semantics (`kernel::move`, `kernel::forward`). @@ -210,9 +218,15 @@ make ARCH=armv8i make run ARCH=armv8i ``` -#### Graphical Window Emulation (Optional SDL/GTK GUI) +#### Graphical Desktop Environment +SUB-OS boots straight into the desktop. Pass `nogui`, `text`, `emergency` or +`single` on the kernel command line to go directly to the kernel TTY instead; +from inside the desktop, `Esc` (or Start -> Exit to Kernel TTY) drops to the +same shell. + ```bash -make run-gui +make run-gui # Local SDL/GTK window +make run-vnc # Headless: VNC server on localhost:5900 ``` --- diff --git a/gui/gui.c b/gui/gui.c index ce3db4e..5f2574b 100644 --- a/gui/gui.c +++ b/gui/gui.c @@ -1,9 +1,16 @@ // SUB-OS Graphical User Interface Subsystem #include +#include +#include +#include #include void gui_init(void) { - printk(KERN_INFO "GUI: SUB-OS Graphical Desktop Environment initialized (Ready)\n"); + gui_icons_init(); + gui_dialog_init(); + printk(KERN_INFO "GUI: SUB-OS Desktop Environment ready " + "(SUB-WM compositor, SUB-WT widgets, %d icons)\n", + GUI_ICON_COUNT); } int gui_start_desktop(void) { diff --git a/gui/gui_apps_ext.c b/gui/gui_apps_ext.c new file mode 100644 index 0000000..89bc6c1 --- /dev/null +++ b/gui/gui_apps_ext.c @@ -0,0 +1,813 @@ +// SUB-OS Desktop Applications, second generation +// +// These apps are written entirely against the SUB-WT immediate-mode toolkit: +// each one keeps a small state struct in win->user_data, forwards WM events +// into the toolkit, and rebuilds its interface every frame from that state. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Shared plumbing: every app forwards WM events into the toolkit, and frees +// its state struct when the window closes. +static void app_forward_event(gui_window_t* win, const gui_event_t* ev) { + gui_widget_feed_event(win, ev); + if (ev->type == GUI_EVENT_CLOSE && win->user_data) { + kfree(win->user_data); + win->user_data = NULL; + } +} + +// =========================================================================== +// 1. Settings +// =========================================================================== +typedef struct { + int active_tab; + bool show_grid; + bool window_shadows; + bool sound_enabled; + bool show_clock_seconds; + int theme_choice; // 0 = Nord Dark, 1 = Midnight, 2 = Slate + int cursor_speed; + int brightness; + int governor_choice; // 0 = powersave, 1 = ondemand, 2 = performance +} settings_data_t; + +static const char* const settings_tabs[] = { "Appearance", "Input", "Power" }; + +static void settings_paint(gui_window_t* win) { + settings_data_t* sd = (settings_data_t*)win->user_data; + if (!sd) return; + + gui_widget_begin(win); + + int w = gui_widget_client_width(); + gui_tabbar(1, 6, 6, w - 12, settings_tabs, 3, &sd->active_tab); + + int y = 40; + char buf[64]; + + if (sd->active_tab == 0) { + gui_label_bold(10, y, "Desktop Appearance", GUI_THEME_PRIMARY); + y += 24; + gui_checkbox(10, 10, y, "Show wallpaper grid overlay", &sd->show_grid); + y += 22; + gui_checkbox(11, 10, y, "Drop shadows under windows", &sd->window_shadows); + y += 26; + + gui_label(10, y, "Theme", GUI_THEME_TEXT_MUTED); + y += 14; + gui_radio(12, 14, y, "Nord Dark", &sd->theme_choice, 0); y += 18; + gui_radio(13, 14, y, "Midnight", &sd->theme_choice, 1); y += 18; + gui_radio(14, 14, y, "Slate Grey", &sd->theme_choice, 2); y += 24; + + snprintf(buf, sizeof(buf), "Brightness: %d%%", sd->brightness); + gui_label(10, y, buf, GUI_THEME_TEXT_MUTED); + gui_slider(15, 150, y + 4, w - 170, &sd->brightness, 20, 100); + + } else if (sd->active_tab == 1) { + gui_label_bold(10, y, "Input Devices", GUI_THEME_PRIMARY); + y += 24; + + snprintf(buf, sizeof(buf), "Pointer speed: %d", sd->cursor_speed); + gui_label(10, y, buf, GUI_THEME_TEXT_MUTED); + gui_slider(20, 150, y + 4, w - 170, &sd->cursor_speed, 1, 10); + y += 30; + + gui_checkbox(21, 10, y, "Enable UI sound effects", &sd->sound_enabled); + y += 22; + gui_checkbox(22, 10, y, "Show seconds in taskbar clock", &sd->show_clock_seconds); + y += 30; + + const mouse_state_t* ms = mouse_get_state(); + snprintf(buf, sizeof(buf), "PS/2 mouse at (%d, %d) L:%s R:%s", + ms ? ms->x : 0, ms ? ms->y : 0, + (ms && ms->left_btn) ? "down" : "up", + (ms && ms->right_btn) ? "down" : "up"); + gui_label(10, y, buf, GUI_THEME_TEXT_DIM); + + } else { + gui_label_bold(10, y, "Power & Frequency", GUI_THEME_PRIMARY); + y += 24; + + gui_label(10, y, "CPUFreq governor", GUI_THEME_TEXT_MUTED); + y += 16; + gui_radio(30, 14, y, "powersave", &sd->governor_choice, 0); y += 18; + gui_radio(31, 14, y, "ondemand", &sd->governor_choice, 1); y += 18; + gui_radio(32, 14, y, "performance", &sd->governor_choice, 2); y += 26; + + cpufreq_stats_t cf = cpufreq_get_stats(); + snprintf(buf, sizeof(buf), "Current clock: %u.%03u GHz", + cf.current_khz / 1000000, (cf.current_khz % 1000000) / 1000); + gui_label(10, y, buf, GUI_THEME_TEXT_MAIN); + y += 16; + + int load = (int)(cf.current_khz / 30000); + if (load > 100) load = 100; + gui_progress_bar(10, y, w - 24, 12, load, GUI_THEME_SUCCESS); + } + + int btn_y = gui_widget_client_height() - 32; + if (gui_button(90, 10, btn_y, 96, 24, "Apply")) { + gui_dialog_show(GUI_DIALOG_INFO, "Settings", + "Preferences applied to the current desktop session."); + } + if (gui_button_colored(91, 114, btn_y, 96, 24, "Defaults", GUI_THEME_WARNING)) { + sd->show_grid = true; + sd->window_shadows = true; + sd->sound_enabled = true; + sd->show_clock_seconds = true; + sd->theme_choice = 0; + sd->cursor_speed = 5; + sd->brightness = 100; + sd->governor_choice = 1; + } + + gui_widget_end(); +} + +void gui_app_settings_launch(int x, int y, int w, int h) { + gui_window_t* win = gui_wm_create_window("Settings", x, y, + (w > 0) ? w : 380, (h > 0) ? h : 300); + if (!win) return; + + settings_data_t* sd = (settings_data_t*)kzalloc(sizeof(settings_data_t)); + if (!sd) return; + + sd->show_grid = true; + sd->window_shadows = true; + sd->sound_enabled = true; + sd->show_clock_seconds = true; + sd->cursor_speed = 5; + sd->brightness = 100; + sd->governor_choice = 1; + + win->user_data = sd; + win->paint = settings_paint; + win->handle_event = app_forward_event; +} + +// =========================================================================== +// 2. Task Manager +// =========================================================================== +#define TASKMGR_MAX_ROWS 24 + +typedef struct { + int selected; + int row_count; + char rows[TASKMGR_MAX_ROWS][40]; + const char* row_ptrs[TASKMGR_MAX_ROWS]; + pid_t pids[TASKMGR_MAX_ROWS]; + uint64_t last_refresh; +} taskmgr_data_t; + +static const char* task_state_name(task_state_t s) { + switch (s) { + case TASK_STATE_READY: return "READY"; + case TASK_STATE_RUNNING: return "RUN "; + case TASK_STATE_BLOCKED: return "BLOCK"; + case TASK_STATE_SLEEPING: return "SLEEP"; + case TASK_STATE_ZOMBIE: return "ZOMBI"; + default: return "DEAD "; + } +} + +// Walk the low PID space; the kernel hands out small, dense pids. +static void taskmgr_refresh(taskmgr_data_t* td) { + td->row_count = 0; + + for (pid_t pid = 0; pid < 64 && td->row_count < TASKMGR_MAX_ROWS; pid++) { + task_t* t = task_find_by_pid(pid); + if (!t) continue; + + snprintf(td->rows[td->row_count], sizeof(td->rows[0]), + "%4d %-16s %s p%d", t->pid, t->name, + task_state_name(t->state), t->priority); + td->row_ptrs[td->row_count] = td->rows[td->row_count]; + td->pids[td->row_count] = t->pid; + td->row_count++; + } + + if (td->row_count == 0) { + strcpy(td->rows[0], " 0 kernel_idle RUN p0"); + td->row_ptrs[0] = td->rows[0]; + td->pids[0] = 0; + td->row_count = 1; + } + + if (td->selected >= td->row_count) td->selected = td->row_count - 1; + td->last_refresh = pit_get_ticks(); +} + +static void taskmgr_paint(gui_window_t* win) { + taskmgr_data_t* td = (taskmgr_data_t*)win->user_data; + if (!td) return; + + // Re-scan roughly twice a second rather than on every composited frame. + if (pit_get_ticks() - td->last_refresh > 50) taskmgr_refresh(td); + + gui_widget_begin(win); + + int w = gui_widget_client_width(); + int h = gui_widget_client_height(); + char buf[64]; + + gui_icon_draw(GUI_ICON_TASKS, win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + 7); + gui_label_bold(30, 6, "Process Table", GUI_THEME_PRIMARY); + + gui_label(10, 28, " PID NAME STATE PRIO", GUI_THEME_TEXT_DIM); + gui_listbox(1, 10, 40, w - 20, h - 108, td->row_ptrs, td->row_count, &td->selected); + + int info_y = h - 62; + gui_separator(10, info_y - 6, w - 20); + + uint64_t total = pmm_get_total_pages(); + uint64_t used = pmm_get_used_pages(); + int mem_pct = total ? (int)((used * 100) / total) : 0; + + snprintf(buf, sizeof(buf), "Memory %llu / %llu MB", + (unsigned long long)((used * 4096) / (1024 * 1024)), + (unsigned long long)((total * 4096) / (1024 * 1024))); + gui_label(10, info_y, buf, GUI_THEME_TEXT_MUTED); + gui_progress_bar(160, info_y - 2, w - 176, 12, mem_pct, + mem_pct > 80 ? GUI_THEME_DANGER : GUI_THEME_SUCCESS); + + snprintf(buf, sizeof(buf), "Heap %llu KB used, %d task(s) live", + (unsigned long long)(heap_get_used_bytes() / 1024), td->row_count); + gui_label(10, info_y + 16, buf, GUI_THEME_TEXT_DIM); + + int btn_y = h - 30; + if (gui_button(2, 10, btn_y, 84, 24, "Refresh")) { + taskmgr_refresh(td); + } + if (gui_button_colored(3, 102, btn_y, 84, 24, "End Task", GUI_THEME_DANGER)) { + pid_t victim = (td->selected < td->row_count) ? td->pids[td->selected] : -1; + if (victim <= 0) { + gui_dialog_show(GUI_DIALOG_WARNING, "Task Manager", + "PID 0 is the idle task and cannot be terminated."); + } else { + gui_dialog_show(GUI_DIALOG_WARNING, "Task Manager", + "Terminating kernel tasks from the desktop is disabled " + "in this build."); + } + } + + gui_widget_end(); +} + +void gui_app_taskmgr_launch(int x, int y, int w, int h) { + gui_window_t* win = gui_wm_create_window("Task Manager", x, y, + (w > 0) ? w : 360, (h > 0) ? h : 300); + if (!win) return; + + taskmgr_data_t* td = (taskmgr_data_t*)kzalloc(sizeof(taskmgr_data_t)); + if (!td) return; + + taskmgr_refresh(td); + win->user_data = td; + win->paint = taskmgr_paint; + win->handle_event = app_forward_event; +} + +// =========================================================================== +// 3. Kernel Log Viewer +// =========================================================================== +#define LOG_MAX_LINES 200 +#define LOG_LINE_CHARS 72 + +typedef struct { + int scroll; + int total_lines; + bool wrap_long_lines; + char lines[LOG_MAX_LINES][LOG_LINE_CHARS + 1]; +} logview_data_t; + +// The dmesg ring holds raw bytes with embedded log-level markers; split it +// into printable rows and drop the control prefixes. +static void logview_reload(logview_data_t* lv) { + size_t size = 0; + const char* buf = dmesg_get_buffer(&size); + + lv->total_lines = 0; + if (!buf || size == 0) { + strcpy(lv->lines[0], "(kernel ring buffer is empty)"); + lv->total_lines = 1; + return; + } + + size_t i = 0; + while (i < size && lv->total_lines < LOG_MAX_LINES) { + int col = 0; + char* out = lv->lines[lv->total_lines]; + + while (i < size && buf[i] != '\n') { + char c = buf[i++]; + + if (c == '\033') { + // Skip an ANSI CSI sequence entirely. + while (i < size && buf[i] != 'm' && buf[i] != '\n') i++; + if (i < size && buf[i] == 'm') i++; + continue; + } + if ((unsigned char)c < 32) continue; // Log-level markers and tabs + + if (col < LOG_LINE_CHARS) out[col++] = c; + } + if (i < size) i++; // Consume the newline + + out[col] = '\0'; + if (col > 0) lv->total_lines++; + } + + if (lv->total_lines == 0) { + strcpy(lv->lines[0], "(no printable log records)"); + lv->total_lines = 1; + } +} + +static void logview_paint(gui_window_t* win) { + logview_data_t* lv = (logview_data_t*)win->user_data; + if (!lv) return; + + gui_widget_begin(win); + + int w = gui_widget_client_width(); + int h = gui_widget_client_height(); + char buf[64]; + + gui_icon_draw(GUI_ICON_LOG, win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + 7); + gui_label_bold(30, 6, "Kernel Ring Buffer", GUI_THEME_PRIMARY); + + int view_y = 30; + int view_h = h - 66; + int row_h = 11; + int visible = view_h / row_h; + + gui_gfx_fill_rect(win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + view_y, + w - 32, view_h, 0xFF080B14); + gui_gfx_draw_rect(win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + view_y, + w - 32, view_h, GUI_THEME_BORDER); + + if (lv->scroll > lv->total_lines - visible) lv->scroll = lv->total_lines - visible; + if (lv->scroll < 0) lv->scroll = 0; + + for (int i = 0; i < visible && lv->scroll + i < lv->total_lines; i++) { + const char* line = lv->lines[lv->scroll + i]; + + // Colour-code by the severity words the kernel prints. + uint32_t fg = GUI_THEME_TEXT_MUTED; + if (strstr(line, "PANIC") || strstr(line, "ERROR")) fg = GUI_THEME_DANGER; + else if (strstr(line, "WARN")) fg = GUI_THEME_WARNING; + else if (strstr(line, "OK") || strstr(line, "online") || strstr(line, "initialized")) + fg = GUI_THEME_SUCCESS; + + gui_label(14, view_y + 3 + i * row_h, line, fg); + } + + gui_scrollbar(1, w - 21, view_y, view_h, &lv->scroll, lv->total_lines, visible); + + snprintf(buf, sizeof(buf), "%d lines, showing %d-%d", + lv->total_lines, lv->scroll + 1, + (lv->scroll + visible < lv->total_lines) ? lv->scroll + visible : lv->total_lines); + gui_label(10, h - 30, buf, GUI_THEME_TEXT_DIM); + + if (gui_button(2, w - 190, h - 34, 84, 24, "Reload")) { + logview_reload(lv); + lv->scroll = 0; + } + if (gui_button(3, w - 100, h - 34, 84, 24, "End")) { + lv->scroll = lv->total_lines - visible; + if (lv->scroll < 0) lv->scroll = 0; + } + + gui_widget_end(); +} + +void gui_app_logviewer_launch(int x, int y, int w, int h) { + gui_window_t* win = gui_wm_create_window("Kernel Log", x, y, + (w > 0) ? w : 620, (h > 0) ? h : 320); + if (!win) return; + + logview_data_t* lv = (logview_data_t*)kzalloc(sizeof(logview_data_t)); + if (!lv) return; + + logview_reload(lv); + win->user_data = lv; + win->paint = logview_paint; + win->handle_event = app_forward_event; +} + +// =========================================================================== +// 4. KTest Runner +// =========================================================================== +typedef struct { + int selected_suite; + bool has_run; + ktest_result_t result; + char suite_name[24]; +} ktest_data_t; + +static const char* const ktest_suite_names[] = { + "(all suites)", "rbtree", "kfifo", "hashtable", + "concurrency", "page_cache", "libcore" +}; +#define KTEST_SUITE_COUNT 7 + +static void ktest_paint(gui_window_t* win) { + ktest_data_t* kd = (ktest_data_t*)win->user_data; + if (!kd) return; + + gui_widget_begin(win); + + int w = gui_widget_client_width(); + int h = gui_widget_client_height(); + char buf[64]; + + gui_icon_draw(GUI_ICON_FLASK, win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + 7); + gui_label_bold(30, 6, "In-Kernel Test Harness", GUI_THEME_PRIMARY); + + gui_label(10, 30, "Suite", GUI_THEME_TEXT_DIM); + gui_listbox(1, 10, 42, 150, 108, ktest_suite_names, KTEST_SUITE_COUNT, + &kd->selected_suite); + + if (gui_button_colored(2, 10, 156, 150, 26, "Run Tests", GUI_THEME_SUCCESS)) { + // Suite 0 is the "all" pseudo-entry; the rest map 1:1 by name. + if (kd->selected_suite == 0) { + ktest_run_all(&kd->result); + strcpy(kd->suite_name, "all suites"); + } else { + ktest_run_suite(ktest_suite_names[kd->selected_suite], &kd->result); + strncpy(kd->suite_name, ktest_suite_names[kd->selected_suite], + sizeof(kd->suite_name) - 1); + } + kd->has_run = true; + } + + int rx = 174; + gui_panel(rx, 42, w - rx - 14, 140, "Results"); + + if (!kd->has_run) { + gui_label(rx + 12, 66, "No run yet.", GUI_THEME_TEXT_DIM); + gui_label(rx + 12, 82, "Pick a suite and press", GUI_THEME_TEXT_DIM); + gui_label(rx + 12, 94, "Run Tests. Full per-case", GUI_THEME_TEXT_DIM); + gui_label(rx + 12, 106, "output goes to the serial", GUI_THEME_TEXT_DIM); + gui_label(rx + 12, 118, "console and dmesg.", GUI_THEME_TEXT_DIM); + } else { + const ktest_result_t* r = &kd->result; + bool green = (r->tests_failed == 0); + + snprintf(buf, sizeof(buf), "Suite: %s", kd->suite_name); + gui_label(rx + 12, 56, buf, GUI_THEME_TEXT_MAIN); + + snprintf(buf, sizeof(buf), "%u tests, %u passed, %u failed", + r->tests_run, r->tests_passed, r->tests_failed); + gui_label(rx + 12, 72, buf, GUI_THEME_TEXT_MUTED); + + snprintf(buf, sizeof(buf), "%u assertions, %u failures", + r->assertions, r->assertion_failures); + gui_label(rx + 12, 86, buf, GUI_THEME_TEXT_MUTED); + + snprintf(buf, sizeof(buf), "Elapsed: %llu ms", + (unsigned long long)(r->elapsed_ticks * 10)); + gui_label(rx + 12, 100, buf, GUI_THEME_TEXT_DIM); + + int pct = r->tests_run ? (int)((r->tests_passed * 100) / r->tests_run) : 0; + gui_progress_bar(rx + 12, 118, w - rx - 40, 14, pct, + green ? GUI_THEME_SUCCESS : GUI_THEME_DANGER); + + gui_badge(rx + 12, 140, green ? "ALL PASSED" : "FAILURES", + green ? GUI_THEME_SUCCESS : GUI_THEME_DANGER, GUI_COLOR_BLACK); + } + + gui_separator(10, h - 42, w - 20); + gui_label(10, h - 34, "Suites cover rbtree, kfifo, hashtable, RCU,", + GUI_THEME_TEXT_DIM); + gui_label(10, h - 22, "futex, wait queues, page cache and libcore.", + GUI_THEME_TEXT_DIM); + + gui_widget_end(); +} + +void gui_app_ktest_launch(int x, int y, int w, int h) { + gui_window_t* win = gui_wm_create_window("KTest Runner", x, y, + (w > 0) ? w : 460, (h > 0) ? h : 250); + if (!win) return; + + ktest_data_t* kd = (ktest_data_t*)kzalloc(sizeof(ktest_data_t)); + if (!kd) return; + + win->user_data = kd; + win->paint = ktest_paint; + win->handle_event = app_forward_event; +} + +// =========================================================================== +// 5. Clock & Calendar +// =========================================================================== +typedef struct { + bool show_seconds; + int view_month; // 1-12; 0 means "follow the RTC" +} clock_data_t; + +static const char* const month_names[] = { + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" +}; + +static int days_in_month(int month, int year) { + static const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + if (month < 1 || month > 12) return 30; + if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)) return 29; + return days[month - 1]; +} + +// Zeller's congruence, returning 0 = Sunday. +static int day_of_week(int day, int month, int year) { + if (month < 3) { month += 12; year -= 1; } + int k = year % 100; + int j = year / 100; + int hh = (day + (13 * (month + 1)) / 5 + k + k / 4 + j / 4 + 5 * j) % 7; + return (hh + 6) % 7; +} + +static void clock_paint(gui_window_t* win) { + clock_data_t* cd = (clock_data_t*)win->user_data; + if (!cd) return; + + rtc_time_t now; + rtc_get_time(&now); + + gui_widget_begin(win); + int w = gui_widget_client_width(); + char buf[64]; + + int month = cd->view_month ? cd->view_month : (int)now.month; + int year = (int)now.year; + + // Big digital readout + if (cd->show_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); + } + + int tw = (int)strlen(buf) * 8; + gui_gfx_draw_string_16_shadow(win->x + 1 + (w - tw) / 2, + win->y + GUI_TITLEBAR_HEIGHT + 9, + buf, GUI_THEME_PRIMARY, GUI_THEME_PRIMARY_DARK); + + snprintf(buf, sizeof(buf), "%s %u, %d", month_names[month - 1], now.day, year); + gui_label_aligned(0, 32, w, buf, GUI_THEME_TEXT_MAIN, GUI_ALIGN_CENTER); + + // Month grid + int gx = 12, gy = 50; + int cell_w = (w - 24) / 7; + int cell_h = 16; + + const char* const dows[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" }; + for (int i = 0; i < 7; i++) { + gui_label(gx + i * cell_w + (cell_w - 16) / 2, gy, dows[i], + (i == 0 || i == 6) ? GUI_THEME_DANGER : GUI_THEME_TEXT_DIM); + } + gui_separator(gx, gy + 12, w - 24); + + int first_dow = day_of_week(1, month, year); + int total_days = days_in_month(month, year); + + for (int d = 1; d <= total_days; d++) { + int slot = first_dow + d - 1; + int col = slot % 7; + int row = slot / 7; + int cx = gx + col * cell_w; + int cy = gy + 18 + row * cell_h; + + bool today = (d == (int)now.day && month == (int)now.month); + if (today) { + gui_gfx_fill_rect(win->x + 1 + cx, win->y + GUI_TITLEBAR_HEIGHT + 1 + cy - 2, + cell_w - 2, cell_h - 2, GUI_THEME_PRIMARY_DARK); + } + + snprintf(buf, sizeof(buf), "%2d", d); + gui_label(cx + (cell_w - 16) / 2, cy, buf, + today ? GUI_COLOR_WHITE + : (col == 0 || col == 6) ? GUI_THEME_TEXT_DIM + : GUI_THEME_TEXT_MUTED); + } + + int h = gui_widget_client_height(); + if (gui_button(1, 12, h - 30, 30, 22, "<")) { + cd->view_month = (month == 1) ? 12 : month - 1; + } + if (gui_button(2, 46, h - 30, 30, 22, ">")) { + cd->view_month = (month == 12) ? 1 : month + 1; + } + if (gui_button(3, 80, h - 30, 62, 22, "Today")) { + cd->view_month = 0; + } + gui_checkbox(4, 152, h - 28, "Seconds", &cd->show_seconds); + + gui_widget_end(); +} + +void gui_app_clock_launch(int x, int y, int w, int h) { + gui_window_t* win = gui_wm_create_window("Clock & Calendar", x, y, + (w > 0) ? w : 270, (h > 0) ? h : 250); + if (!win) return; + + clock_data_t* cd = (clock_data_t*)kzalloc(sizeof(clock_data_t)); + if (!cd) return; + + cd->show_seconds = true; + win->user_data = cd; + win->paint = clock_paint; + win->handle_event = app_forward_event; +} + +// =========================================================================== +// 6. Text Editor +// =========================================================================== +#define EDITOR_ROWS 14 +#define EDITOR_ROW_CHARS 56 + +typedef struct { + char lines[EDITOR_ROWS][EDITOR_ROW_CHARS + 1]; + int line_count; + int cursor_line; + char filename[48]; + bool modified; + bool word_wrap; +} editor_data_t; + +static void editor_insert_char(editor_data_t* ed, char c) { + if (ed->cursor_line >= EDITOR_ROWS) return; + + char* line = ed->lines[ed->cursor_line]; + size_t len = strlen(line); + + if (c == '\n' || c == '\r') { + if (ed->line_count < EDITOR_ROWS) { + // Push the following lines down to make room for the new one. + for (int i = ed->line_count; i > ed->cursor_line + 1; i--) { + strcpy(ed->lines[i], ed->lines[i - 1]); + } + ed->line_count++; + ed->cursor_line++; + ed->lines[ed->cursor_line][0] = '\0'; + ed->modified = true; + } + return; + } + + if (c == '\b' || (uint8_t)c == 0x7F) { + if (len > 0) { + line[len - 1] = '\0'; + ed->modified = true; + } else if (ed->cursor_line > 0) { + ed->cursor_line--; + } + return; + } + + if (c >= 32 && c <= 126 && len < EDITOR_ROW_CHARS) { + line[len] = c; + line[len + 1] = '\0'; + ed->modified = true; + } +} + +static void editor_event(gui_window_t* win, const gui_event_t* ev) { + editor_data_t* ed = (editor_data_t*)win->user_data; + + if (ed && ev->type == GUI_EVENT_KEY_DOWN) { + uint16_t key = ev->key; + if (key == KEY_UP) { + if (ed->cursor_line > 0) ed->cursor_line--; + return; + } + if (key == KEY_DOWN) { + if (ed->cursor_line < ed->line_count - 1) ed->cursor_line++; + return; + } + if (!(key & KEY_SPECIAL_FLAG)) { + editor_insert_char(ed, (char)(key & 0xFF)); + return; + } + } + + app_forward_event(win, ev); +} + +static void editor_paint(gui_window_t* win) { + editor_data_t* ed = (editor_data_t*)win->user_data; + if (!ed) return; + + gui_widget_begin(win); + + int w = gui_widget_client_width(); + int h = gui_widget_client_height(); + char buf[80]; + + // Toolbar + gui_icon_draw(GUI_ICON_FILE, win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + 6); + snprintf(buf, sizeof(buf), "%s%s", ed->filename, ed->modified ? " *" : ""); + gui_label(30, 10, buf, ed->modified ? GUI_THEME_WARNING : GUI_THEME_TEXT_MAIN); + + if (gui_button(1, w - 176, 6, 52, 20, "New")) { + memset(ed->lines, 0, sizeof(ed->lines)); + ed->line_count = 1; + ed->cursor_line = 0; + ed->modified = false; + strcpy(ed->filename, "untitled.txt"); + } + if (gui_button(2, w - 120, 6, 52, 20, "Save")) { + ed->modified = false; + gui_dialog_show(GUI_DIALOG_INFO, "Text Editor", + "This build keeps the buffer in memory only; VFS write-back " + "is not wired up yet."); + } + if (gui_button(3, w - 64, 6, 52, 20, "Wrap")) { + ed->word_wrap = !ed->word_wrap; + } + + // Text area with a gutter + int area_y = 32; + int area_h = h - area_y - 26; + int row_h = 14; + + gui_gfx_fill_rect(win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + area_y, + w - 18, area_h, 0xFF080B14); + gui_gfx_draw_rect(win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + area_y, + w - 18, area_h, GUI_THEME_BORDER); + gui_gfx_draw_line(win->x + 40, win->y + GUI_TITLEBAR_HEIGHT + area_y + 1, + win->x + 40, win->y + GUI_TITLEBAR_HEIGHT + area_y + area_h - 2, + GUI_THEME_BORDER); + + int visible = (area_h - 6) / row_h; + for (int i = 0; i < visible && i < ed->line_count; i++) { + int ry = area_y + 4 + i * row_h; + bool is_cursor = (i == ed->cursor_line); + + if (is_cursor) { + gui_gfx_fill_rect(win->x + 42, win->y + GUI_TITLEBAR_HEIGHT + ry - 2, + w - 54, row_h, 0xFF11182B); + } + + snprintf(buf, sizeof(buf), "%3d", i + 1); + gui_label(14, ry, buf, is_cursor ? GUI_THEME_PRIMARY : GUI_THEME_TEXT_DIM); + gui_label(38, ry, ed->lines[i], GUI_THEME_TEXT_MAIN); + + if (is_cursor) { + int cx = 38 + (int)strlen(ed->lines[i]) * 8; + gui_gfx_fill_rect(win->x + 1 + cx, win->y + GUI_TITLEBAR_HEIGHT + 1 + ry - 1, + 7, 10, GUI_THEME_SUCCESS); + } + } + + snprintf(buf, sizeof(buf), "Ln %d/%d Col %d %s", + ed->cursor_line + 1, ed->line_count, + (int)strlen(ed->lines[ed->cursor_line]) + 1, + ed->word_wrap ? "WRAP" : "NOWRAP"); + gui_label(10, h - 16, buf, GUI_THEME_TEXT_DIM); + + gui_widget_end(); +} + +void gui_app_editor_launch(int x, int y, int w, int h) { + gui_window_t* win = gui_wm_create_window("Text Editor", x, y, + (w > 0) ? w : 500, (h > 0) ? h : 320); + if (!win) return; + + editor_data_t* ed = (editor_data_t*)kzalloc(sizeof(editor_data_t)); + if (!ed) return; + + strcpy(ed->filename, "untitled.txt"); + strcpy(ed->lines[0], "# SUB-OS Text Editor"); + strcpy(ed->lines[1], ""); + strcpy(ed->lines[2], "Type to edit. Enter adds a line,"); + strcpy(ed->lines[3], "Up/Down move between lines."); + ed->line_count = 4; + ed->cursor_line = 3; + + win->user_data = ed; + win->paint = editor_paint; + win->handle_event = editor_event; +} diff --git a/gui/gui_desktop.c b/gui/gui_desktop.c index 54a2ae6..55ac412 100644 --- a/gui/gui_desktop.c +++ b/gui/gui_desktop.c @@ -1,250 +1,638 @@ -// High-Resolution Modern Desktop Environment & Compositor for SUB-OS (800x600) +// SUB-OS Desktop Shell: wallpaper, icon grid, taskbar, menus and compositor loop +// +// The shell owns the frame loop. Every iteration it samples the input devices, +// routes the result through the modal dialog layer, then the desktop chrome, +// then the window manager, and finally composites the scene back to front: +// wallpaper, icons, windows, taskbar, menus, dialog, cursor. + #include #include #include #include #include +#include +#include +#include +#include #include #include #include #include #include #include +#include +#include +#include #include #include #include -static bool g_start_menu_open = false; -static bool g_desktop_running = false; -static bool g_prev_left = false; +#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, 440, 280 }, + { "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 }, + { "Clock", GUI_ICON_CLOCK, gui_app_clock_launch, 460, 120, 280, 260 }, + { "Settings", GUI_ICON_SETTINGS, gui_app_settings_launch, 160, 60, 400, 320 }, + { "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. +static const int g_desktop_icons[] = { 0, 1, 2, 4, 5, 10 }; +#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 int g_selected_icon = -1; +static int g_hover_menu_item = -1; +static uint64_t g_frames = 0; +static uint8_t g_cpu_history[48]; + +static void launch_app(int index) { + if (index < 0 || index >= APP_COUNT) return; + const desktop_app_t* a = &g_apps[index]; + if (a->launch) a->launch(a->x, a->y, a->w, a->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 : 800; - int h = (fb && fb->height > 0) ? fb->height : 600; + 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); + 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_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 +// =========================================================================== + void gui_desktop_render_background(void) { int sw = gui_gfx_get_width(); - int sh = gui_gfx_get_height(); + int wh = gui_desktop_workarea_height(); - // Dark Cyberpunk / Nord Deep Gradient Wallpaper - gui_gfx_draw_gradient_v(0, 0, sw, sh - GUI_TASKBAR_HEIGHT, 0xFF0B132B, 0xFF1C2541); + gui_gfx_draw_gradient_v(0, 0, sw, wh, 0xFF0B132B, 0xFF1C2541); - // Subtle Architectural Grid Overlay - for (int y = 0; y < sh - GUI_TASKBAR_HEIGHT; y += 40) { - gui_gfx_draw_line(0, y, sw - 1, y, 0xFF141F36); - } - for (int x = 0; x < sw; x += 40) { - gui_gfx_draw_line(x, 0, x, sh - GUI_TASKBAR_HEIGHT - 1, 0xFF141F36); + if (g_show_grid) { + for (int y = 0; y < wh; y += 40) gui_gfx_draw_line(0, y, sw - 1, y, 0xFF141F36); + for (int x = 0; x < sw; x += 40) gui_gfx_draw_line(x, 0, x, wh - 1, 0xFF141F36); } - // Centered Elegant Desktop Branding - gui_gfx_draw_string_16_shadow(sw / 2 - 80, sh / 2 - 40, "SUB-OS DESKTOP", 0xFF38BDF8, 0xFF0284C7); - gui_gfx_draw_string_16(sw / 2 - 95, sh / 2 - 18, "Modular Monolithic Kernel", 0xFF94A3B8); - gui_gfx_draw_string(sw / 2 - 60, sh / 2 + 6, "v0.2.0-LTS (x86_64)", 0xFF64748B); + // Branding, offset to the right so it clears the icon column. + int bx = sw / 2 + 40; + int by = wh / 2 - 30; + gui_gfx_draw_string_16_shadow(bx - 60, by, "SUB-OS DESKTOP", 0xFF38BDF8, 0xFF0284C7); + gui_gfx_draw_string(bx - 76, by + 22, "Modular Monolithic Kernel v0.2.0-LTS", 0xFF64748B); + gui_gfx_draw_string(bx - 76, by + 36, "F5 cycle F6 cascade F7 tile F8 max", 0xFF475569); } +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 sh = gui_gfx_get_height(); - int ty = sh - GUI_TASKBAR_HEIGHT; + int ty = gui_desktop_workarea_height(); - // Taskbar Surface - gui_gfx_fill_rect(0, ty, sw, GUI_TASKBAR_HEIGHT, GUI_THEME_TASKBAR_BG); + 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 + // Start button uint32_t start_bg = g_start_menu_open ? GUI_THEME_PRIMARY_DARK : GUI_THEME_PRIMARY; - gui_gfx_fill_rect(6, ty + 4, 86, 24, start_bg); - gui_gfx_draw_rect(6, ty + 4, 86, 24, GUI_THEME_PRIMARY_DARK); - gui_gfx_draw_string_16_shadow(14, ty + 8, "# SUB-OS", GUI_COLOR_WHITE, GUI_COLOR_BLACK); + 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); - // Window Tabs in Taskbar - int tab_x = 100; - int win_count = gui_wm_get_window_count(); - for (int i = 0; i < win_count && tab_x < sw - 160; i++) { - gui_window_t* win = gui_wm_get_window(i + 1); + // One button per window, in z-order + int tab_x = 106; + int tray_x = sw - 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; - uint32_t tab_bg = win->active ? GUI_THEME_BG_ELEVATED : GUI_THEME_BG_SURFACE; + 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 ? GUI_THEME_PRIMARY : GUI_THEME_BORDER); + 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_16(tab_x + 8, ty + 8, short_title, win->active ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED); - + 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 (Clock & Telemetry) - rtc_time_t time; - rtc_get_time(&time); - char time_str[16]; - snprintf(time_str, sizeof(time_str), "%02u:%02u:%02u", time.hour, time.minute, time.second); - gui_gfx_draw_string_16_shadow(sw - 85, ty + 8, time_str, GUI_THEME_PRIMARY, GUI_COLOR_BLACK); + // 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); + } - // RAM Badge - gui_gfx_fill_rect(sw - 140, ty + 6, 45, 20, GUI_THEME_SUCCESS); - gui_gfx_draw_rect(sw - 140, ty + 6, 45, 20, 0xFF059669); - gui_gfx_draw_string_16(sw - 134, ty + 8, "24 MB", GUI_COLOR_WHITE); + 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; + + rtc_time_t now; + rtc_get_time(&now); + snprintf(buf, sizeof(buf), "%02u:%02u:%02u", now.hour, now.minute, now.second); + gui_gfx_draw_string_16_shadow(sw - 84, ty + 8, buf, GUI_THEME_PRIMARY, GUI_COLOR_BLACK); +} + +// =========================================================================== +// 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 sh = gui_gfx_get_height(); - int menu_w = 170; - int menu_h = 200; - int mx = 6; - int my = sh - GUI_TASKBAR_HEIGHT - menu_h - 6; + int mx, my; + start_menu_origin(&mx, &my); + int mh = start_menu_height(); - // Drop Shadow & Body - gui_gfx_draw_shadow(mx, my, menu_w, menu_h, 8); - gui_gfx_fill_rect(mx, my, menu_w, menu_h, GUI_THEME_BG_SURFACE); - gui_gfx_draw_rect(mx, my, menu_w, menu_h, GUI_THEME_PRIMARY); + 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); - // Menu Header - gui_gfx_fill_rect(mx + 1, my + 1, menu_w - 2, 26, GUI_THEME_PRIMARY_DARK); - gui_gfx_draw_string_16_shadow(mx + 10, my + 6, "SUB-OS Applications", GUI_COLOR_WHITE, GUI_COLOR_BLACK); + 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); - const char* items[] = { - ">_ Terminal Console", - "[#] System Monitor", - "[F] File Explorer", - "[=] Calculator", - "[*] Paint Studio", - "[i] About SUB-OS", - "[X] Exit to TTY" - }; + for (int i = 0; i < APP_COUNT; i++) { + int iy = my + 32 + i * START_ITEM_H; + bool hot = (g_hover_menu_item == i); - for (int i = 0; i < 7; i++) { - int iy = my + 34 + i * 23; - uint32_t fg = (i == 6) ? GUI_THEME_DANGER : GUI_THEME_TEXT_MAIN; - gui_gfx_draw_string_16(mx + 10, iy, items[i], fg); + 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; +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_handle_taskbar_click(int mx, int my) { - int sw = gui_gfx_get_width(); - int sh = gui_gfx_get_height(); - int ty = sh - GUI_TASKBAR_HEIGHT; +void gui_desktop_request_exit(void) { g_desktop_running = false; } - // Check Start Button - if (mx >= 6 && mx <= 92 && my >= ty + 4 && my <= ty + 28) { - gui_desktop_toggle_start_menu(); +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; } - // Check Start Menu Click if open - if (g_start_menu_open) { - int menu_w = 170; - int menu_h = 200; - int smx = 6; - int smy = sh - GUI_TASKBAR_HEIGHT - menu_h - 6; - - if (mx >= smx && mx <= smx + menu_w && my >= smy && my <= smy + menu_h) { - int item_idx = (my - (smy + 34)) / 23; - g_start_menu_open = false; - switch (item_idx) { - case 0: gui_app_terminal_launch(30, 30, 440, 270); break; - case 1: gui_app_sysmon_launch(420, 30, 330, 230); break; - case 2: gui_app_fileman_launch(60, 60, 360, 220); break; - case 3: gui_app_calc_launch(120, 80, 220, 220); break; - case 4: gui_app_paint_launch(160, 50, 340, 240); break; - case 5: gui_app_about_launch(200, 100, 320, 160); break; - case 6: g_desktop_running = false; break; // Exit GUI - default: break; - } - return; - } else { - g_start_menu_open = false; + 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; } } +} - // Check Window Tabs - int tab_x = 100; - int win_count = gui_wm_get_window_count(); - for (int i = 0; i < win_count && tab_x < sw - 160; i++) { - if (mx >= tab_x && mx <= tab_x + 100 && my >= ty + 4 && my <= ty + 28) { - gui_wm_focus_window(i + 1); - return; +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) { gui_desktop_init(); - // Launch default initial windows in spacious 800x600 layout - gui_app_sysmon_launch(430, 20, 340, 240); - gui_app_terminal_launch(20, 30, 390, 260); + // Open a default session so the desktop is not empty on first boot. + gui_app_sysmon_launch(430, 30, 340, 240); + gui_app_terminal_launch(30, 40, 380, 260); + + // 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 Environment session started (Press ESC to return to TTY)\n"); + printk(KERN_INFO "GUI: Desktop session started (ESC returns to the kernel TTY)\n"); while (g_desktop_running) { - // 1. Process Mouse Input + // --- 1. Sample input ----------------------------------------------- const mouse_state_t* ms = mouse_get_state(); - int mx = ms ? ms->x : 400; - int my = ms ? ms->y : 300; - bool left = ms ? ms->left_btn : false; + 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; - if (left && !g_prev_left) { - int sh = gui_gfx_get_height(); - if (my >= sh - GUI_TASKBAR_HEIGHT || g_start_menu_open) { - gui_desktop_handle_taskbar_click(mx, my); + // --- 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); } - } else { - gui_wm_handle_mouse(mx, my, left, right); } - g_prev_left = left; - // 2. Process Keyboard Input - uint16_t key = keyboard_get_key(); + 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) { - char c = (char)(key & 0xFF); - if (c == 27) { // Escape key - g_desktop_running = false; - break; + 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); } - gui_wm_handle_key(key); } - // 3. Render Complete Desktop Scene + // --- 4. Composite the scene, back to front -------------------------- + if ((g_frames & 0x1F) == 0) sample_cpu_history(); + 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(); gui_cursor_draw(); - // 4. Present Framebuffer to Display gui_gfx_present(); + g_frames++; } - printk(KERN_INFO "GUI: Desktop session ended, restored TTY console\n"); + printk(KERN_INFO "GUI: Desktop session ended after %llu frames, TTY restored\n", + (unsigned long long)g_frames); return 0; } diff --git a/gui/gui_dialog.c b/gui/gui_dialog.c new file mode 100644 index 0000000..c06d265 --- /dev/null +++ b/gui/gui_dialog.c @@ -0,0 +1,211 @@ +// Modal dialog layer for the SUB-OS desktop +#include +#include +#include +#include +#include +#include + +#define DIALOG_W 300 +#define DIALOG_H 132 +#define DIALOG_TITLEBAR 24 +#define DIALOG_BTN_W 84 +#define DIALOG_BTN_H 24 +#define DIALOG_MAX_LINES 4 +#define DIALOG_LINE_CHARS 28 + +typedef struct { + bool open; + gui_dialog_kind_t kind; + char title[48]; + char lines[DIALOG_MAX_LINES][DIALOG_LINE_CHARS + 1]; + int line_count; + bool has_cancel; + gui_dialog_cb_t callback; + void* ctx; + int hover_button; // 0 = none, 1 = OK, 2 = Cancel +} dialog_state_t; + +static dialog_state_t g_dlg; + +void gui_dialog_init(void) { + memset(&g_dlg, 0, sizeof(g_dlg)); +} + +// Break the message on spaces so words are not split across rows. +static void wrap_message(const char* message) { + g_dlg.line_count = 0; + if (!message) return; + + const char* p = message; + while (*p && g_dlg.line_count < DIALOG_MAX_LINES) { + int take = 0; + int last_space = -1; + + while (p[take] && take < DIALOG_LINE_CHARS) { + if (p[take] == ' ') last_space = take; + take++; + } + + if (p[take] && last_space > 0) take = last_space; + + int n = take; + if (n > DIALOG_LINE_CHARS) n = DIALOG_LINE_CHARS; + memcpy(g_dlg.lines[g_dlg.line_count], p, (size_t)n); + g_dlg.lines[g_dlg.line_count][n] = '\0'; + g_dlg.line_count++; + + p += take; + while (*p == ' ') p++; + } +} + +static void open_dialog(gui_dialog_kind_t kind, const char* title, const char* message, + bool has_cancel, gui_dialog_cb_t cb, void* ctx) { + memset(&g_dlg, 0, sizeof(g_dlg)); + g_dlg.open = true; + g_dlg.kind = kind; + g_dlg.has_cancel = has_cancel; + g_dlg.callback = cb; + g_dlg.ctx = ctx; + + strncpy(g_dlg.title, title ? title : "SUB-OS", sizeof(g_dlg.title) - 1); + wrap_message(message); +} + +void gui_dialog_show(gui_dialog_kind_t kind, const char* title, const char* message) { + open_dialog(kind, title, message, false, NULL, NULL); +} + +void gui_dialog_confirm(const char* title, const char* message, + gui_dialog_cb_t on_result, void* ctx) { + open_dialog(GUI_DIALOG_CONFIRM, title, message, true, on_result, ctx); +} + +void gui_dialog_dismiss(void) { + g_dlg.open = false; +} + +bool gui_dialog_is_open(void) { + return g_dlg.open; +} + +static void finish(gui_dialog_result_t result) { + gui_dialog_cb_t cb = g_dlg.callback; + void* ctx = g_dlg.ctx; + + // Clear before invoking so a callback may immediately open another dialog. + g_dlg.open = false; + g_dlg.callback = NULL; + + if (cb) cb(result, ctx); +} + +static void dialog_geometry(int* dx, int* dy) { + *dx = (gui_gfx_get_width() - DIALOG_W) / 2; + *dy = (gui_gfx_get_height() - DIALOG_H) / 2 - 20; +} + +bool gui_dialog_handle_mouse(int mx, int my, bool clicked) { + if (!g_dlg.open) return false; + + int dx, dy; + dialog_geometry(&dx, &dy); + + int btn_y = dy + DIALOG_H - DIALOG_BTN_H - 12; + int ok_x, cancel_x; + if (g_dlg.has_cancel) { + ok_x = dx + DIALOG_W - (DIALOG_BTN_W * 2) - 22; + cancel_x = dx + DIALOG_W - DIALOG_BTN_W - 12; + } else { + ok_x = dx + (DIALOG_W - DIALOG_BTN_W) / 2; + cancel_x = -1000; + } + + g_dlg.hover_button = 0; + if (my >= btn_y && my < btn_y + DIALOG_BTN_H) { + if (mx >= ok_x && mx < ok_x + DIALOG_BTN_W) g_dlg.hover_button = 1; + else if (g_dlg.has_cancel && mx >= cancel_x && mx < cancel_x + DIALOG_BTN_W) g_dlg.hover_button = 2; + } + + if (clicked) { + if (g_dlg.hover_button == 1) finish(GUI_DIALOG_RESULT_OK); + else if (g_dlg.hover_button == 2) finish(GUI_DIALOG_RESULT_CANCEL); + } + + // Swallow every click while modal, including ones outside the frame. + return true; +} + +bool gui_dialog_handle_key(uint16_t key) { + if (!g_dlg.open) return false; + + char c = (char)(key & 0xFF); + if (c == '\n' || c == '\r') { + finish(GUI_DIALOG_RESULT_OK); + } else if (c == 27) { // Escape + finish(g_dlg.has_cancel ? GUI_DIALOG_RESULT_CANCEL : GUI_DIALOG_RESULT_OK); + } + return true; +} + +static void draw_button(int x, int y, const char* label, bool hovered, uint32_t accent) { + gui_gfx_fill_rect(x, y, DIALOG_BTN_W, DIALOG_BTN_H, + hovered ? GUI_THEME_BG_ELEVATED : GUI_THEME_BG_SURFACE); + gui_gfx_draw_rect(x, y, DIALOG_BTN_W, DIALOG_BTN_H, hovered ? accent : GUI_THEME_BORDER); + + int tw = (int)strlen(label) * 8; + gui_gfx_draw_string(x + (DIALOG_BTN_W - tw) / 2, y + (DIALOG_BTN_H - 8) / 2, + label, hovered ? GUI_COLOR_WHITE : GUI_THEME_TEXT_MUTED); +} + +void gui_dialog_render(void) { + if (!g_dlg.open) return; + + // Dim the whole desktop so the modal reads as the only live surface. + gui_gfx_fill_rect_blend(0, 0, gui_gfx_get_width(), gui_gfx_get_height(), + GUI_COLOR_BLACK, 120); + + int dx, dy; + dialog_geometry(&dx, &dy); + + uint32_t accent; + gui_icon_id_t icon; + switch (g_dlg.kind) { + case GUI_DIALOG_WARNING: accent = GUI_THEME_WARNING; icon = GUI_ICON_WARNING; break; + case GUI_DIALOG_ERROR: accent = GUI_THEME_DANGER; icon = GUI_ICON_WARNING; break; + case GUI_DIALOG_CONFIRM: accent = GUI_THEME_ACCENT; icon = GUI_ICON_INFO; break; + default: accent = GUI_THEME_PRIMARY; icon = GUI_ICON_INFO; break; + } + + gui_gfx_draw_shadow(dx, dy, DIALOG_W, DIALOG_H, 10); + gui_gfx_fill_rect(dx, dy, DIALOG_W, DIALOG_H, GUI_THEME_BG_SURFACE); + gui_gfx_draw_rect(dx, dy, DIALOG_W, DIALOG_H, accent); + + // Title bar + gui_gfx_draw_gradient_v(dx + 1, dy + 1, DIALOG_W - 2, DIALOG_TITLEBAR - 1, + GUI_THEME_BG_ELEVATED, GUI_THEME_BG_SURFACE); + gui_gfx_draw_line(dx, dy + DIALOG_TITLEBAR, dx + DIALOG_W - 1, dy + DIALOG_TITLEBAR, accent); + gui_gfx_draw_string_16_shadow(dx + 10, dy + 4, g_dlg.title, GUI_COLOR_WHITE, GUI_COLOR_BLACK); + + // Icon + wrapped message + gui_icon_draw_scaled(icon, dx + 14, dy + DIALOG_TITLEBAR + 14, 2); + for (int i = 0; i < g_dlg.line_count; i++) { + gui_gfx_draw_string(dx + 58, dy + DIALOG_TITLEBAR + 14 + i * 13, + g_dlg.lines[i], GUI_THEME_TEXT_MAIN); + } + + int btn_y = dy + DIALOG_H - DIALOG_BTN_H - 12; + if (g_dlg.has_cancel) { + draw_button(dx + DIALOG_W - (DIALOG_BTN_W * 2) - 22, btn_y, "OK", + g_dlg.hover_button == 1, accent); + draw_button(dx + DIALOG_W - DIALOG_BTN_W - 12, btn_y, "Cancel", + g_dlg.hover_button == 2, GUI_THEME_DANGER); + } else { + draw_button(dx + (DIALOG_W - DIALOG_BTN_W) / 2, btn_y, "OK", + g_dlg.hover_button == 1, accent); + } + + gui_gfx_draw_string(dx + 12, dy + DIALOG_H - 12, "Enter = OK Esc = Dismiss", + GUI_THEME_TEXT_DIM); +} diff --git a/gui/gui_gfx.c b/gui/gui_gfx.c index f84c6cb..263a736 100644 --- a/gui/gui_gfx.c +++ b/gui/gui_gfx.c @@ -70,7 +70,7 @@ static const uint8_t font8x8[96][8] = { {0x66,0x66,0x66,0x3c,0x18,0x18,0x18,0x00}, // Y {0xfe,0x06,0x0c,0x18,0x30,0x60,0xfe,0x00}, // Z {0x3c,0x30,0x30,0x30,0x30,0x30,0x3c,0x00}, // [ - {0xc0,0x60,0x30,0x18,0x0c,0x06,0x02,0x00}, // \ + {0xc0,0x60,0x30,0x18,0x0c,0x06,0x02,0x00}, // backslash {0x3c,0x0c,0x0c,0x0c,0x0c,0x0c,0x3c,0x00}, // ] {0x10,0x38,0x6c,0xc6,0x00,0x00,0x00,0x00}, // ^ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff}, // _ diff --git a/gui/gui_icons.c b/gui/gui_icons.c new file mode 100644 index 0000000..f46a45f --- /dev/null +++ b/gui/gui_icons.c @@ -0,0 +1,365 @@ +// SUB-OS Desktop Icon Set: 16x16 palette-indexed glyphs +// +// Each icon is 16 rows of 16 hex digits, where the digit indexes the shared +// palette below. Storing the art as text keeps it editable by hand, and the +// indirection lets every glyph pick up a theme change for free. + +#include +#include +#include + +#define ICON_PALETTE_SIZE 16 + +static const uint32_t g_palette[ICON_PALETTE_SIZE] = { + 0x00000000, // 0 transparent + 0xFF0B0F19, // 1 outline / near-black + 0xFFF8FAFC, // 2 white + 0xFF94A3B8, // 3 muted grey + 0xFF475569, // 4 dark grey + 0xFF38BDF8, // 5 primary sky + 0xFF0284C7, // 6 primary dark + 0xFF34D399, // 7 success green + 0xFFFBBF24, // 8 warning amber + 0xFFF87171, // 9 danger red + 0xFF818CF8, // A accent indigo + 0xFFEC4899, // B pink + 0xFF1E293B, // C surface + 0xFF334155, // D elevated + 0xFF000000, // E pure black + 0xFFD9A441 // F folder tan +}; + +static const char* g_icons[GUI_ICON_COUNT][GUI_ICON_SIZE] = { + // GUI_ICON_TERMINAL + { + "4111111111111114", + "4CCCCCCCCCCCCCC4", + "4CCCCCCCCCCCCCC4", + "4C7CCCCCCCCCCCC4", + "4C77CCCCCCCCCCC4", + "4C777CCCCCCCCCC4", + "4C7777CCCCCCCCC4", + "4C777CCCCCCCCCC4", + "4C77CCCCCCCCCCC4", + "4C7CCCCCCCCCCCC4", + "4CCCCCCCCCCCCCC4", + "4CC77777777CCCC4", + "4CCCCCCCCCCCCCC4", + "4CCCCCCCCCCCCCC4", + "4CCCCCCCCCCCCCC4", + "4111111111111114", + }, + // GUI_ICON_MONITOR + { + "4111111111111114", + "4CCCCCCCCCCCCCC4", + "4CCCCCCCCCCCC5C4", + "4CCCCCCCCCCC5CC4", + "4CCCCCCCCC55CCC4", + "4CCCCCCCC5CCCCC4", + "4CCCCCC55CCCCCC4", + "4CCCC55CCCCCCCC4", + "4CC55CCCCCCCCCC4", + "4C5CCCCCCCCCCCC4", + "4CCCCCCCCCCCCCC4", + "4111111111111114", + "0000444444440000", + "0000004444000000", + "0000444444440000", + "0000000000000000", + }, + // GUI_ICON_FOLDER + { + "0000000000000000", + "0FFFFF0000000000", + "0F888FF000000000", + "0F88888FFFFFFF00", + "0F8888888888F000", + "0F8888888888F000", + "0F8888888888F000", + "0F8888888888F000", + "0F8888888888F000", + "0F8888888888F000", + "0F8888888888F000", + "0F8888888888F000", + "0FFFFFFFFFFFF000", + "0000000000000000", + "0000000000000000", + "0000000000000000", + }, + // GUI_ICON_CALC + { + "4111111111111114", + "4CCCCCCCCCCCCCC4", + "4C777777777777C4", + "4C777777777777C4", + "4CCCCCCCCCCCCCC4", + "4C2CC2CC2CC2CCC4", + "4CCCCCCCCCCCCCC4", + "4C2CC2CC2CC2CCC4", + "4CCCCCCCCCCCCCC4", + "4C2CC2CC2CC2CCC4", + "4CCCCCCCCCCCCCC4", + "4C2CC2CC2CC5CCC4", + "4CCCCCCCCCCCCCC4", + "4CCCCCCCCCCCCCC4", + "4CCCCCCCCCCCCCC4", + "4111111111111114", + }, + // GUI_ICON_PAINT + { + "0000000000000000", + "0000333333000000", + "0003222222300000", + "0032955992230000", + "0329999999923000", + "0329B99997923000", + "0329999999923000", + "0032999999230000", + "0003299992300000", + "0000322223000000", + "0000033330000000", + "0000004440000000", + "0000004440000000", + "0000004440000000", + "0000000000000000", + "0000000000000000", + }, + // GUI_ICON_INFO + { + "0000055555000000", + "0000555555550000", + "0005566665550000", + "0055666666655000", + "0556662266665500", + "0556662266665500", + "0556666666665500", + "0556662266665500", + "0556622226665500", + "0556666226665500", + "0556666226665500", + "0556662222265500", + "0055666666655000", + "0005566665550000", + "0000555555550000", + "0000055555000000", + }, + // GUI_ICON_SETTINGS + { + "0000034443000000", + "0000034443000000", + "0003344444330000", + "0034444444443000", + "0344444444444300", + "3444442224444430", + "4444422222444444", + "4444422222444444", + "4444422222444444", + "3444442224444430", + "0344444444444300", + "0034444444443000", + "0003344444330000", + "0000034443000000", + "0000034443000000", + "0000000000000000", + }, + // GUI_ICON_TASKS + { + "0000000000000000", + "0555555555555550", + "0500000000000050", + "0507000000000050", + "0507700000000050", + "0507770000000050", + "0507777000000050", + "0500000000000050", + "0508880000000050", + "0508888000000050", + "0500000000000050", + "0509990000000050", + "0509999000000050", + "0500000000000050", + "0555555555555550", + "0000000000000000", + }, + // GUI_ICON_LOG + { + "0022222222222000", + "0244444444442000", + "0240000000042000", + "0244444444442000", + "0240000000042000", + "0244444444442000", + "0240000000042000", + "0244444444442000", + "0240000000042000", + "0244444444442000", + "0240000000042000", + "0244444444442000", + "0240000000042000", + "0244444444442000", + "0022222222222000", + "0000000000000000", + }, + // GUI_ICON_FLASK + { + "0000022222000000", + "0000020002000000", + "0000020002000000", + "0000020002000000", + "0000020002000000", + "0000200000200000", + "0000200000200000", + "0002000000020000", + "0002077777020000", + "0020777777702000", + "0020777777702000", + "0207777777770200", + "0207777777770200", + "0027777777772000", + "0002222222220000", + "0000000000000000", + }, + // GUI_ICON_CLOCK + { + "0000055555000000", + "0005522222550000", + "0052222222225000", + "0522222222222500", + "0522221222222500", + "5222221222222250", + "5222221222222250", + "5222221222222250", + "5222221111122250", + "5222222222222250", + "5222222222222250", + "0522222222222500", + "0522222222222500", + "0052222222225000", + "0005522222550000", + "0000055555000000", + }, + // GUI_ICON_FILE + { + "0022222222200000", + "0244444444200000", + "0244444444420000", + "0244444444442000", + "0244444444444200", + "0244444444444200", + "0244444444444200", + "0244444444444200", + "0244444444444200", + "0244444444444200", + "0244444444444200", + "0244444444444200", + "0244444444444200", + "0244444444444200", + "0222222222222200", + "0000000000000000", + }, + // GUI_ICON_POWER + { + "0000009900000000", + "0000009900000000", + "0000999999000000", + "0009900009900000", + "0099009900990000", + "0990009900099000", + "0990009900099000", + "9900009900009900", + "9900009900009900", + "9900000000009900", + "9900000000009900", + "0990000000099000", + "0990000000099000", + "0099000000990000", + "0009999999900000", + "0000099999000000", + }, + // GUI_ICON_WARNING + { + "0000008800000000", + "0000008800000000", + "0000088880000000", + "0000088880000000", + "0000881188000000", + "0000881188000000", + "0008811118800000", + "0008811118800000", + "0088111111880000", + "0088111111880000", + "0881111111188000", + "0881118811188000", + "8811118811118800", + "8811118811118800", + "8888888888888888", + "8888888888888888", + },}; + +static const char* g_icon_names[GUI_ICON_COUNT] = { + "Terminal", "Monitor", "Folder", "Calculator", "Paint", "Info", + "Settings", "Tasks", "Log", "Tests", "Clock", "File", "Power", "Warning" +}; + +static uint8_t hex_index(char c) { + if (c >= '0' && c <= '9') return (uint8_t)(c - '0'); + if (c >= 'A' && c <= 'F') return (uint8_t)(c - 'A' + 10); + if (c >= 'a' && c <= 'f') return (uint8_t)(c - 'a' + 10); + return 0; +} + +void gui_icons_init(void) { + // The glyph table is static const; nothing to build at runtime. +} + +const char* gui_icon_name(gui_icon_id_t id) { + if (id < 0 || id >= GUI_ICON_COUNT) return "Unknown"; + return g_icon_names[id]; +} + +void gui_icon_draw_scaled(gui_icon_id_t id, int x, int y, int scale) { + if (id < 0 || id >= GUI_ICON_COUNT) return; + if (scale < 1) scale = 1; + + for (int row = 0; row < GUI_ICON_SIZE; row++) { + const char* line = g_icons[id][row]; + if (!line) continue; + + for (int col = 0; col < GUI_ICON_SIZE; col++) { + uint32_t color = g_palette[hex_index(line[col])]; + if ((color >> 24) == 0) continue; // Transparent pixel + + if (scale == 1) { + gui_gfx_draw_pixel(x + col, y + row, color); + } else { + gui_gfx_fill_rect(x + col * scale, y + row * scale, scale, scale, color); + } + } + } +} + +void gui_icon_draw(gui_icon_id_t id, int x, int y) { + gui_icon_draw_scaled(id, x, y, 1); +} + +void gui_icon_draw_tinted(gui_icon_id_t id, int x, int y, int scale, uint32_t tint) { + if (id < 0 || id >= GUI_ICON_COUNT) return; + if (scale < 1) scale = 1; + + // Every opaque pixel becomes the tint; the glyph acts purely as a stencil. + for (int row = 0; row < GUI_ICON_SIZE; row++) { + const char* line = g_icons[id][row]; + if (!line) continue; + + for (int col = 0; col < GUI_ICON_SIZE; col++) { + if ((g_palette[hex_index(line[col])] >> 24) == 0) continue; + + if (scale == 1) { + gui_gfx_draw_pixel(x + col, y + row, tint); + } else { + gui_gfx_fill_rect(x + col * scale, y + row * scale, scale, scale, tint); + } + } + } +} diff --git a/gui/gui_widgets.c b/gui/gui_widgets.c new file mode 100644 index 0000000..4a1bd0d --- /dev/null +++ b/gui/gui_widgets.c @@ -0,0 +1,499 @@ +// SUB-WT: Immediate-mode widget toolkit for the SUB-OS desktop +#include +#include +#include +#include +#include +#include + +static gui_widget_input_t g_input; +static gui_window_t* g_win = NULL; +static int g_origin_x = 0; +static int g_origin_y = 0; +static int g_client_w = 0; +static int g_client_h = 0; + +// Screen-space helpers: widgets are authored in client coordinates. +static inline int SX(int x) { return g_origin_x + x; } +static inline int SY(int y) { return g_origin_y + y; } + +static bool hit_test(int x, int y, int w, int h) { + return g_input.mouse_x >= x && g_input.mouse_x < x + w && + g_input.mouse_y >= y && g_input.mouse_y < y + h; +} + +static int text_width(const char* s) { + return s ? (int)strlen(s) * GUI_FONT_W : 0; +} + +void gui_widget_feed_event(gui_window_t* win, const gui_event_t* ev) { + if (!ev) return; + + switch (ev->type) { + case GUI_EVENT_MOUSE_DOWN: + g_input.mouse_x = ev->rel_x; + g_input.mouse_y = ev->rel_y; + g_input.mouse_down = true; + g_input.mouse_clicked = true; + break; + case GUI_EVENT_MOUSE_UP: + g_input.mouse_x = ev->rel_x; + g_input.mouse_y = ev->rel_y; + g_input.mouse_down = false; + g_input.mouse_released = true; + g_input.active_id = 0; + break; + case GUI_EVENT_MOUSE_MOVE: + g_input.mouse_x = ev->rel_x; + g_input.mouse_y = ev->rel_y; + g_input.mouse_down = ev->btn_left; + break; + case GUI_EVENT_KEY_DOWN: + g_input.key = ev->key; + break; + case GUI_EVENT_CLOSE: + g_input.focus_id = 0; + g_input.active_id = 0; + break; + default: + break; + } + (void)win; +} + +void gui_widget_begin(gui_window_t* win) { + g_win = win; + if (!win) return; + + g_origin_x = win->x + 1; + g_origin_y = win->y + GUI_TITLEBAR_HEIGHT + 1; + g_client_w = win->width - 2; + g_client_h = win->height - GUI_TITLEBAR_HEIGHT - 2; + g_input.hot_id = 0; +} + +void gui_widget_end(void) { + // One-shot inputs live for exactly one paint pass; anything not claimed + // by a widget this frame is discarded rather than replayed later. + g_input.mouse_clicked = false; + g_input.mouse_released = false; + g_input.key = 0; + g_win = NULL; +} + +const gui_widget_input_t* gui_widget_get_input(void) { return &g_input; } +void gui_widget_set_focus(int id) { g_input.focus_id = id; } +int gui_widget_client_width(void) { return g_client_w; } +int gui_widget_client_height(void) { return g_client_h; } + +// =========================================================================== +// Static content +// =========================================================================== + +void gui_label(int x, int y, const char* text, uint32_t color) { + gui_gfx_draw_string(SX(x), SY(y), text, color); +} + +void gui_label_bold(int x, int y, const char* text, uint32_t color) { + gui_gfx_draw_string_16_shadow(SX(x), SY(y), text, color, GUI_COLOR_BLACK); +} + +void gui_label_aligned(int x, int y, int w, const char* text, uint32_t color, gui_align_t align) { + int tw = text_width(text); + int tx = x; + if (align == GUI_ALIGN_CENTER) tx = x + (w - tw) / 2; + else if (align == GUI_ALIGN_RIGHT) tx = x + w - tw; + gui_gfx_draw_string(SX(tx), SY(y), text, color); +} + +void gui_panel(int x, int y, int w, int h, const char* title) { + gui_gfx_fill_rect(SX(x), SY(y), w, h, GUI_THEME_BG_DARK); + gui_gfx_draw_rect(SX(x), SY(y), w, h, GUI_THEME_BORDER); + + if (title && *title) { + // Caption sits astride the top border, so clear the strip behind it. + int tw = text_width(title) + 8; + gui_gfx_fill_rect(SX(x + 8), SY(y) - 4, tw, 9, GUI_THEME_BG_SURFACE); + gui_gfx_draw_string(SX(x + 12), SY(y) - 4, title, GUI_THEME_TEXT_MUTED); + } +} + +void gui_separator(int x, int y, int w) { + gui_gfx_draw_line(SX(x), SY(y), SX(x + w - 1), SY(y), GUI_THEME_BORDER); +} + +void gui_progress_bar(int x, int y, int w, int h, int percent, uint32_t fill) { + if (percent < 0) percent = 0; + if (percent > 100) percent = 100; + + gui_gfx_fill_rect(SX(x), SY(y), w, h, GUI_THEME_BG_DARK); + int filled = (w - 2) * percent / 100; + if (filled > 0) gui_gfx_fill_rect(SX(x + 1), SY(y + 1), filled, h - 2, fill); + gui_gfx_draw_rect(SX(x), SY(y), w, h, GUI_THEME_BORDER); +} + +void gui_sparkline(int x, int y, int w, int h, const uint8_t* samples, int count, uint32_t color) { + gui_gfx_fill_rect(SX(x), SY(y), w, h, GUI_THEME_BG_DARK); + gui_gfx_draw_rect(SX(x), SY(y), w, h, GUI_THEME_BORDER); + if (!samples || count < 2) return; + + // Horizontal guides at 25/50/75%. + for (int q = 1; q < 4; q++) { + int gy = SY(y) + h * q / 4; + gui_gfx_draw_line(SX(x) + 1, gy, SX(x) + w - 2, gy, 0xFF1E293B); + } + + int usable_h = h - 4; + for (int i = 0; i < count - 1; i++) { + int x0 = SX(x) + 1 + (i * (w - 2)) / (count - 1); + int x1 = SX(x) + 1 + ((i + 1) * (w - 2)) / (count - 1); + int y0 = SY(y) + h - 2 - (samples[i] * usable_h) / 100; + int y1 = SY(y) + h - 2 - (samples[i + 1] * usable_h) / 100; + gui_gfx_draw_line(x0, y0, x1, y1, color); + } +} + +void gui_badge(int x, int y, const char* text, uint32_t bg, uint32_t fg) { + int w = text_width(text) + 10; + gui_gfx_fill_rect(SX(x), SY(y), w, 14, bg); + gui_gfx_draw_rect(SX(x), SY(y), w, 14, GUI_THEME_BORDER); + gui_gfx_draw_string(SX(x + 5), SY(y + 3), text, fg); +} + +// =========================================================================== +// Interactive controls +// =========================================================================== + +bool gui_button_colored(int id, int x, int y, int w, int h, const char* label, uint32_t accent) { + bool hovered = hit_test(x, y, w, h); + bool pressed = hovered && g_input.mouse_down; + bool clicked = false; + + if (hovered) { + g_input.hot_id = id; + if (g_input.mouse_clicked) { + g_input.mouse_clicked = false; // Claim the click + g_input.active_id = id; + g_input.focus_id = 0; + clicked = true; + } + } + + uint32_t face = pressed ? GUI_THEME_BG_DARK + : hovered ? GUI_THEME_BG_ELEVATED + : GUI_THEME_BG_SURFACE; + uint32_t border = hovered ? accent : GUI_THEME_BORDER; + + gui_gfx_fill_rect(SX(x), SY(y), w, h, face); + gui_gfx_draw_rect(SX(x), SY(y), w, h, border); + + // A pressed button shifts its label a pixel to sell the depression. + int tx = x + (w - text_width(label)) / 2 + (pressed ? 1 : 0); + int ty = y + (h - GUI_FONT_H) / 2 + (pressed ? 1 : 0); + gui_gfx_draw_string(SX(tx), SY(ty), label, hovered ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED); + + return clicked; +} + +bool gui_button(int id, int x, int y, int w, int h, const char* label) { + return gui_button_colored(id, x, y, w, h, label, GUI_THEME_PRIMARY); +} + +bool gui_checkbox(int id, int x, int y, const char* label, bool* value) { + if (!value) return false; + + int box = 14; + int w = box + 6 + text_width(label); + bool hovered = hit_test(x, y, w, box); + bool changed = false; + + if (hovered && g_input.mouse_clicked) { + g_input.mouse_clicked = false; + *value = !*value; + changed = true; + } + + gui_gfx_fill_rect(SX(x), SY(y), box, box, GUI_THEME_BG_DARK); + gui_gfx_draw_rect(SX(x), SY(y), box, box, hovered ? GUI_THEME_PRIMARY : GUI_THEME_BORDER); + + if (*value) { + // Two strokes form the tick. + gui_gfx_draw_line(SX(x + 3), SY(y + 7), SX(x + 6), SY(y + 10), GUI_THEME_SUCCESS); + gui_gfx_draw_line(SX(x + 6), SY(y + 10), SX(x + 11), SY(y + 3), GUI_THEME_SUCCESS); + } + + gui_gfx_draw_string(SX(x + box + 6), SY(y + 3), label, + hovered ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED); + return changed; +} + +bool gui_radio(int id, int x, int y, const char* label, int* selected, int this_value) { + if (!selected) return false; + + int box = 12; + int w = box + 6 + text_width(label); + bool hovered = hit_test(x, y, w, box); + bool changed = false; + + if (hovered && g_input.mouse_clicked) { + g_input.mouse_clicked = false; + if (*selected != this_value) { + *selected = this_value; + changed = true; + } + } + + gui_gfx_draw_rounded_rect(SX(x), SY(y), box, box, 5, + hovered ? GUI_THEME_PRIMARY : GUI_THEME_BORDER, false); + if (*selected == this_value) { + gui_gfx_fill_rect(SX(x + 4), SY(y + 4), 4, 4, GUI_THEME_PRIMARY); + } + + gui_gfx_draw_string(SX(x + box + 6), SY(y + 2), label, + hovered ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED); + (void)id; + return changed; +} + +bool gui_slider(int id, int x, int y, int w, int* value, int min_val, int max_val) { + if (!value || max_val <= min_val) return false; + + int track_h = 6; + int knob_w = 10; + int hit_h = 18; + bool hovered = hit_test(x, y - 4, w, hit_h); + bool changed = false; + + if (hovered && g_input.mouse_clicked) { + g_input.mouse_clicked = false; + g_input.active_id = id; + } + + // Dragging continues while the button is held, even off the track. + if (g_input.active_id == id && g_input.mouse_down) { + int rel = g_input.mouse_x - x - knob_w / 2; + int span = w - knob_w; + if (span < 1) span = 1; + if (rel < 0) rel = 0; + if (rel > span) rel = span; + + int nv = min_val + (rel * (max_val - min_val)) / span; + if (nv != *value) { + *value = nv; + changed = true; + } + } + + if (*value < min_val) *value = min_val; + if (*value > max_val) *value = max_val; + + int track_y = y + (hit_h - track_h) / 2 - 4; + gui_gfx_fill_rect(SX(x), SY(track_y), w, track_h, GUI_THEME_BG_DARK); + gui_gfx_draw_rect(SX(x), SY(track_y), w, track_h, GUI_THEME_BORDER); + + int span = w - knob_w; + int knob_x = x + ((*value - min_val) * span) / (max_val - min_val); + gui_gfx_fill_rect(SX(x + 1), SY(track_y + 1), knob_x - x, track_h - 2, GUI_THEME_PRIMARY_DARK); + gui_gfx_fill_rect(SX(knob_x), SY(y - 3), knob_w, 16, + (g_input.active_id == id) ? GUI_THEME_PRIMARY : GUI_THEME_BG_ELEVATED); + gui_gfx_draw_rect(SX(knob_x), SY(y - 3), knob_w, 16, GUI_THEME_PRIMARY); + + return changed; +} + +bool gui_listbox(int id, int x, int y, int w, int h, const char* const* items, int count, int* selected) { + if (!items || !selected) return false; + + gui_gfx_fill_rect(SX(x), SY(y), w, h, GUI_THEME_BG_DARK); + gui_gfx_draw_rect(SX(x), SY(y), w, h, GUI_THEME_BORDER); + + int row_h = GUI_WIDGET_ROW_H - 4; + int visible = (h - 4) / row_h; + bool changed = false; + + // Scroll just enough to keep the selection on screen. + int first = 0; + if (*selected >= visible) first = *selected - visible + 1; + if (first > count - visible) first = count - visible; + if (first < 0) first = 0; + + for (int i = 0; i < visible && first + i < count; i++) { + int idx = first + i; + int ry = y + 2 + i * row_h; + bool hovered = hit_test(x + 1, ry, w - 2, row_h); + + if (hovered && g_input.mouse_clicked) { + g_input.mouse_clicked = false; + g_input.focus_id = id; + if (*selected != idx) { + *selected = idx; + changed = true; + } + } + + if (idx == *selected) { + gui_gfx_fill_rect(SX(x + 1), SY(ry), w - 2, row_h, GUI_THEME_PRIMARY_DARK); + } else if (hovered) { + gui_gfx_fill_rect(SX(x + 1), SY(ry), w - 2, row_h, GUI_THEME_BG_ELEVATED); + } + + uint32_t fg = (idx == *selected) ? GUI_COLOR_WHITE : GUI_THEME_TEXT_MUTED; + gui_gfx_draw_string(SX(x + 6), SY(ry + (row_h - GUI_FONT_H) / 2), items[idx], fg); + } + + // Arrow keys move the selection while this list holds focus. + if (g_input.focus_id == id && g_input.key) { + if (g_input.key == KEY_UP && *selected > 0) { + (*selected)--; + changed = true; + g_input.key = 0; + } else if (g_input.key == KEY_DOWN && *selected < count - 1) { + (*selected)++; + changed = true; + g_input.key = 0; + } + } + + if (count > visible) { + gui_gfx_draw_string(SX(x + w - 14), SY(y + 3), "^", GUI_THEME_TEXT_DIM); + gui_gfx_draw_string(SX(x + w - 14), SY(y + h - 11), "v", GUI_THEME_TEXT_DIM); + } + + return changed; +} + +bool gui_textfield(int id, int x, int y, int w, char* buffer, size_t capacity) { + if (!buffer || capacity == 0) return false; + + int h = 20; + bool hovered = hit_test(x, y, w, h); + bool focused = (g_input.focus_id == id); + bool changed = false; + + if (hovered && g_input.mouse_clicked) { + g_input.mouse_clicked = false; + g_input.focus_id = id; + focused = true; + } + + if (focused && g_input.key) { + uint16_t key = g_input.key; + char c = (char)(key & 0xFF); + size_t len = strlen(buffer); + + if (!(key & KEY_SPECIAL_FLAG)) { + if (c == '\b' || (uint8_t)c == 0x7F) { + if (len > 0) { + buffer[len - 1] = '\0'; + changed = true; + } + g_input.key = 0; + } else if (c >= 32 && c <= 126 && len + 1 < capacity) { + buffer[len] = c; + buffer[len + 1] = '\0'; + changed = true; + g_input.key = 0; + } + } + } + + gui_gfx_fill_rect(SX(x), SY(y), w, h, GUI_THEME_BG_DARK); + gui_gfx_draw_rect(SX(x), SY(y), w, h, + focused ? GUI_THEME_PRIMARY : (hovered ? GUI_THEME_ACCENT : GUI_THEME_BORDER)); + + // Show the tail of the text when it overflows the field. + int max_chars = (w - 10) / GUI_FONT_W; + int len = (int)strlen(buffer); + const char* view = (len > max_chars) ? buffer + (len - max_chars) : buffer; + + gui_gfx_draw_string(SX(x + 5), SY(y + (h - GUI_FONT_H) / 2), view, GUI_THEME_TEXT_MAIN); + + if (focused) { + int cx = x + 5 + (int)strlen(view) * GUI_FONT_W; + gui_gfx_fill_rect(SX(cx), SY(y + 4), 2, h - 8, GUI_THEME_SUCCESS); + } + + return changed; +} + +int gui_tabbar(int id, int x, int y, int w, const char* const* labels, int count, int* active) { + if (!labels || count <= 0 || !active) return -1; + + int tab_w = w / count; + int tab_h = 22; + + for (int i = 0; i < count; i++) { + int tx = x + i * tab_w; + bool hovered = hit_test(tx, y, tab_w, tab_h); + bool is_active = (*active == i); + + if (hovered && g_input.mouse_clicked) { + g_input.mouse_clicked = false; + *active = i; + is_active = true; + } + + uint32_t bg = is_active ? GUI_THEME_BG_SURFACE + : hovered ? GUI_THEME_BG_ELEVATED + : GUI_THEME_BG_DARK; + gui_gfx_fill_rect(SX(tx), SY(y), tab_w, tab_h, bg); + gui_gfx_draw_rect(SX(tx), SY(y), tab_w, tab_h, GUI_THEME_BORDER); + + if (is_active) { + // Active tab gets an accent underline and merges with the body. + gui_gfx_fill_rect(SX(tx + 1), SY(y), tab_w - 2, 2, GUI_THEME_PRIMARY); + gui_gfx_draw_line(SX(tx + 1), SY(y + tab_h - 1), SX(tx + tab_w - 2), + SY(y + tab_h - 1), GUI_THEME_BG_SURFACE); + } + + int tw = text_width(labels[i]); + gui_gfx_draw_string(SX(tx + (tab_w - tw) / 2), SY(y + (tab_h - GUI_FONT_H) / 2), + labels[i], is_active ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_DIM); + } + + (void)id; + return *active; +} + +bool gui_scrollbar(int id, int x, int y, int h, int* offset, int total, int visible) { + if (!offset || total <= visible || visible <= 0) return false; + + int w = 10; + gui_gfx_fill_rect(SX(x), SY(y), w, h, GUI_THEME_BG_DARK); + gui_gfx_draw_rect(SX(x), SY(y), w, h, GUI_THEME_BORDER); + + int max_offset = total - visible; + int thumb_h = (h * visible) / total; + if (thumb_h < 12) thumb_h = 12; + + bool hovered = hit_test(x, y, w, h); + bool changed = false; + + if (hovered && g_input.mouse_clicked) { + g_input.mouse_clicked = false; + g_input.active_id = id; + } + + if (g_input.active_id == id && g_input.mouse_down) { + int span = h - thumb_h; + if (span < 1) span = 1; + int rel = g_input.mouse_y - y - thumb_h / 2; + if (rel < 0) rel = 0; + if (rel > span) rel = span; + + int nv = (rel * max_offset) / span; + if (nv != *offset) { + *offset = nv; + changed = true; + } + } + + if (*offset < 0) *offset = 0; + if (*offset > max_offset) *offset = max_offset; + + int thumb_y = y + ((h - thumb_h) * (*offset)) / (max_offset ? max_offset : 1); + gui_gfx_fill_rect(SX(x + 1), SY(thumb_y), w - 2, thumb_h, + (g_input.active_id == id) ? GUI_THEME_PRIMARY : GUI_THEME_BG_ELEVATED); + + return changed; +} diff --git a/gui/gui_wm.c b/gui/gui_wm.c index 0da0324..6b332c7 100644 --- a/gui/gui_wm.c +++ b/gui/gui_wm.c @@ -1,21 +1,55 @@ -// SUB-OS Window Manager (SUB-WM) Compositor +// SUB-WM: window manager and software compositor for SUB-OS +// +// Windows live in a fixed slot array; z-order is a separate array of ids with +// the topmost window last. Interaction is edge-driven: a press picks a target +// and latches it into a drag or resize gesture, and the gesture then owns the +// pointer until the button comes back up. + #include +#include +#include #include #include +#define BTN_SIZE 14 +#define BTN_GAP 4 +#define SNAP_MARGIN 8 + static gui_window_t g_windows[GUI_MAX_WINDOWS]; -static int g_window_order[GUI_MAX_WINDOWS]; // Z-order (top window is at index g_num_windows - 1) -static int g_num_windows = 0; -static int g_next_win_id = 1; -static int g_drag_win_id = -1; +static int g_window_order[GUI_MAX_WINDOWS]; +static int g_num_windows = 0; +static int g_next_win_id = 1; + +static int g_drag_win_id = -1; +static int g_resize_win_id = -1; static bool g_prev_btn_left = false; +static int g_last_mouse_x = 0; +static int g_last_mouse_y = 0; + +static int g_work_w = GUI_DEFAULT_WIDTH; +static int g_work_h = GUI_DEFAULT_HEIGHT; + +// Anchor of the in-flight resize gesture, captured on the press edge. +static int g_resize_start_w = 0; +static int g_resize_start_h = 0; +static int g_resize_start_mx = 0; +static int g_resize_start_my = 0; void gui_wm_init(void) { memset(g_windows, 0, sizeof(g_windows)); - g_num_windows = 0; - g_next_win_id = 1; - g_drag_win_id = -1; + memset(g_window_order, 0, sizeof(g_window_order)); + g_num_windows = 0; + g_next_win_id = 1; + g_drag_win_id = -1; + g_resize_win_id = -1; g_prev_btn_left = false; + g_work_w = gui_gfx_get_width(); + g_work_h = gui_gfx_get_height(); +} + +void gui_wm_set_workarea(int width, int height) { + if (width > 0) g_work_w = width; + if (height > 0) g_work_h = height; } gui_window_t* gui_wm_create_window(const char* title, int x, int y, int w, int h) { @@ -23,43 +57,46 @@ gui_window_t* gui_wm_create_window(const char* title, int x, int y, int w, int h int slot = -1; for (int i = 0; i < GUI_MAX_WINDOWS; i++) { - if (g_windows[i].id == 0) { - slot = i; - break; - } + if (g_windows[i].id == 0) { slot = i; break; } } if (slot == -1) return NULL; gui_window_t* win = &g_windows[slot]; + memset(win, 0, sizeof(*win)); + win->id = g_next_win_id++; strncpy(win->title, title ? title : "Window", sizeof(win->title) - 1); - win->x = x; - win->y = y; - win->width = (w > 100) ? w : 100; - win->height = (h > 60) ? h : 60; - win->visible = true; - win->minimized = false; - win->active = true; - win->is_dragging = false; - win->user_data = NULL; - win->paint = NULL; - win->handle_event = NULL; - g_window_order[g_num_windows] = win->id; - g_num_windows++; + win->width = (w > GUI_MIN_WINDOW_W) ? w : GUI_MIN_WINDOW_W; + win->height = (h > GUI_MIN_WINDOW_H) ? h : GUI_MIN_WINDOW_H; + // Keep new windows fully on screen even if the caller asked otherwise. + if (x < 0) x = 0; + if (y < 0) y = 0; + if (x + win->width > g_work_w) x = g_work_w - win->width; + if (y + win->height > g_work_h) y = g_work_h - win->height; + win->x = (x > 0) ? x : 0; + win->y = (y > 0) ? y : 0; + + win->visible = true; + win->resizable = true; + win->active = true; + win->snap = GUI_SNAP_NONE; + + g_window_order[g_num_windows++] = win->id; gui_wm_focus_window(win->id); return win; } -void gui_wm_destroy_window(int win_id) { - int idx = -1; +static int order_index_of(int win_id) { for (int i = 0; i < g_num_windows; i++) { - if (g_window_order[i] == win_id) { - idx = i; - break; - } + if (g_window_order[i] == win_id) return i; } + return -1; +} + +void gui_wm_destroy_window(int win_id) { + int idx = order_index_of(win_id); if (idx != -1) { for (int i = idx; i < g_num_windows - 1; i++) { g_window_order[i] = g_window_order[i + 1]; @@ -68,29 +105,24 @@ void gui_wm_destroy_window(int win_id) { } for (int i = 0; i < GUI_MAX_WINDOWS; i++) { - if (g_windows[i].id == win_id) { - if (g_windows[i].handle_event) { - gui_event_t ev = { .type = GUI_EVENT_CLOSE }; - g_windows[i].handle_event(&g_windows[i], &ev); - } - memset(&g_windows[i], 0, sizeof(gui_window_t)); - break; + if (g_windows[i].id != win_id) continue; + + if (g_windows[i].handle_event) { + gui_event_t ev = { .type = GUI_EVENT_CLOSE }; + g_windows[i].handle_event(&g_windows[i], &ev); } + memset(&g_windows[i], 0, sizeof(gui_window_t)); + break; } - if (g_num_windows > 0) { - gui_wm_focus_window(g_window_order[g_num_windows - 1]); - } + if (g_drag_win_id == win_id) g_drag_win_id = -1; + if (g_resize_win_id == win_id) g_resize_win_id = -1; + + if (g_num_windows > 0) gui_wm_focus_window(g_window_order[g_num_windows - 1]); } void gui_wm_focus_window(int win_id) { - int idx = -1; - for (int i = 0; i < g_num_windows; i++) { - if (g_window_order[i] == win_id) { - idx = i; - break; - } - } + int idx = order_index_of(win_id); if (idx != -1 && idx != g_num_windows - 1) { int target = g_window_order[idx]; for (int i = idx; i < g_num_windows - 1; i++) { @@ -100,15 +132,15 @@ void gui_wm_focus_window(int win_id) { } for (int i = 0; i < GUI_MAX_WINDOWS; i++) { - if (g_windows[i].id != 0) { - bool is_active = (g_windows[i].id == win_id); - if (g_windows[i].active != is_active) { - g_windows[i].active = is_active; - if (is_active && g_windows[i].handle_event) { - gui_event_t ev = { .type = GUI_EVENT_FOCUS }; - g_windows[i].handle_event(&g_windows[i], &ev); - } - } + if (g_windows[i].id == 0) continue; + + bool is_active = (g_windows[i].id == win_id); + if (g_windows[i].active == is_active) continue; + + g_windows[i].active = is_active; + if (is_active && g_windows[i].handle_event) { + gui_event_t ev = { .type = GUI_EVENT_FOCUS }; + g_windows[i].handle_event(&g_windows[i], &ev); } } } @@ -120,116 +152,381 @@ gui_window_t* gui_wm_get_window(int win_id) { return NULL; } -gui_window_t* gui_wm_get_active_window(void) { - if (g_num_windows == 0) return NULL; - int top_id = g_window_order[g_num_windows - 1]; - return gui_wm_get_window(top_id); +gui_window_t* gui_wm_get_window_at_index(int index) { + if (index < 0 || index >= g_num_windows) return NULL; + return gui_wm_get_window(g_window_order[index]); } -int gui_wm_get_window_count(void) { - return g_num_windows; +gui_window_t* gui_wm_get_active_window(void) { + if (g_num_windows == 0) return NULL; + return gui_wm_get_window(g_window_order[g_num_windows - 1]); +} + +int gui_wm_get_window_count(void) { return g_num_windows; } + +// --------------------------------------------------------------------------- +// Window state transitions +// --------------------------------------------------------------------------- + +static void save_geometry(gui_window_t* win) { + if (win->snap != GUI_SNAP_NONE) return; // Already holding a saved rect + win->saved_x = win->x; + win->saved_y = win->y; + win->saved_width = win->width; + win->saved_height = win->height; +} + +static void emit_resize(gui_window_t* win) { + if (!win->handle_event) return; + gui_event_t ev = { .type = GUI_EVENT_RESIZE }; + win->handle_event(win, &ev); +} + +void gui_wm_snap_window(int win_id, gui_snap_t snap) { + gui_window_t* win = gui_wm_get_window(win_id); + if (!win) return; + + if (snap == GUI_SNAP_NONE) { + gui_wm_restore_window(win_id); + return; + } + + save_geometry(win); + win->snap = snap; + + switch (snap) { + case GUI_SNAP_LEFT: + win->x = 0; win->y = 0; + win->width = g_work_w / 2; win->height = g_work_h; + break; + case GUI_SNAP_RIGHT: + win->x = g_work_w / 2; win->y = 0; + win->width = g_work_w - g_work_w / 2; win->height = g_work_h; + break; + default: // GUI_SNAP_MAXIMIZED + win->x = 0; win->y = 0; + win->width = g_work_w; win->height = g_work_h; + break; + } + + emit_resize(win); +} + +void gui_wm_restore_window(int win_id) { + gui_window_t* win = gui_wm_get_window(win_id); + if (!win) return; + + win->minimized = false; + win->visible = true; + + if (win->snap != GUI_SNAP_NONE) { + win->x = win->saved_x; + win->y = win->saved_y; + win->width = win->saved_width; + win->height = win->saved_height; + win->snap = GUI_SNAP_NONE; + emit_resize(win); + } +} + +void gui_wm_toggle_maximize(int win_id) { + gui_window_t* win = gui_wm_get_window(win_id); + if (!win) return; + + if (win->snap == GUI_SNAP_MAXIMIZED) gui_wm_restore_window(win_id); + else gui_wm_snap_window(win_id, GUI_SNAP_MAXIMIZED); +} + +void gui_wm_minimize_window(int win_id) { + gui_window_t* win = gui_wm_get_window(win_id); + if (!win) return; + + win->minimized = true; + + // Focus drops to the next window that is still on screen. + for (int i = g_num_windows - 1; i >= 0; i--) { + gui_window_t* other = gui_wm_get_window(g_window_order[i]); + if (other && other->id != win_id && !other->minimized && other->visible) { + gui_wm_focus_window(other->id); + return; + } + } + win->active = false; +} + +void gui_wm_close_active(void) { + gui_window_t* active = gui_wm_get_active_window(); + if (active) gui_wm_destroy_window(active->id); +} + +void gui_wm_cycle_focus(void) { + if (g_num_windows < 2) return; + + // Walk down from the top and promote the first visible window below it. + for (int i = g_num_windows - 2; i >= 0; i--) { + gui_window_t* win = gui_wm_get_window(g_window_order[i]); + if (win && win->visible) { + if (win->minimized) win->minimized = false; + gui_wm_focus_window(win->id); + return; + } + } +} + +// After a bulk layout change nothing may hold focus (every window could have +// been minimized). Promote the topmost visible window so the desktop always +// has a keyboard target. +static void ensure_focus(void) { + if (gui_wm_get_active_window() && gui_wm_get_active_window()->active) return; + + for (int i = g_num_windows - 1; i >= 0; i--) { + gui_window_t* win = gui_wm_get_window(g_window_order[i]); + if (win && win->visible && !win->minimized) { + gui_wm_focus_window(win->id); + return; + } + } +} + +void gui_wm_cascade_windows(void) { + int step = 26; + int n = 0; + + for (int i = 0; i < g_num_windows; i++) { + gui_window_t* win = gui_wm_get_window(g_window_order[i]); + if (!win || !win->visible) continue; + + win->minimized = false; + win->snap = GUI_SNAP_NONE; + win->x = 20 + n * step; + win->y = 20 + n * step; + + if (win->x + win->width > g_work_w) win->x = g_work_w - win->width; + if (win->y + win->height > g_work_h) win->y = g_work_h - win->height; + if (win->x < 0) win->x = 0; + if (win->y < 0) win->y = 0; + + emit_resize(win); + n++; + } + + ensure_focus(); +} + +void gui_wm_tile_windows(void) { + int visible = 0; + for (int i = 0; i < g_num_windows; i++) { + gui_window_t* win = gui_wm_get_window(g_window_order[i]); + if (win && win->visible && !win->minimized) visible++; + } + if (visible == 0) return; + + // Lay out on the squarest grid that fits every window. + int cols = 1; + while (cols * cols < visible) cols++; + int rows = (visible + cols - 1) / cols; + + int cell_w = g_work_w / cols; + int cell_h = g_work_h / rows; + int n = 0; + + for (int i = 0; i < g_num_windows; i++) { + gui_window_t* win = gui_wm_get_window(g_window_order[i]); + if (!win || !win->visible || win->minimized) continue; + + win->snap = GUI_SNAP_NONE; + win->x = (n % cols) * cell_w; + win->y = (n / cols) * cell_h; + win->width = cell_w - 4; + win->height = cell_h - 4; + + if (win->width < GUI_MIN_WINDOW_W) win->width = GUI_MIN_WINDOW_W; + if (win->height < GUI_MIN_WINDOW_H) win->height = GUI_MIN_WINDOW_H; + + emit_resize(win); + n++; + } + + ensure_focus(); +} + +// --------------------------------------------------------------------------- +// Hit testing and pointer gestures +// --------------------------------------------------------------------------- + +// Title bar buttons run right to left: 0 = close, 1 = maximize, 2 = minimize. +static void button_rect(const gui_window_t* win, int which, int* bx, int* by) { + *by = win->y + (GUI_TITLEBAR_HEIGHT - BTN_SIZE) / 2; + *bx = win->x + win->width - (BTN_SIZE + BTN_GAP) * (which + 1) - 2; +} + +static bool point_in(int px, int py, int x, int y, int w, int h) { + return px >= x && px < x + w && py >= y && py < y + h; +} + +static bool in_resize_grip(const gui_window_t* win, int mx, int my) { + if (!win->resizable || win->snap != GUI_SNAP_NONE) return false; + return point_in(mx, my, + win->x + win->width - GUI_RESIZE_GRIP, + win->y + win->height - GUI_RESIZE_GRIP, + GUI_RESIZE_GRIP, GUI_RESIZE_GRIP); +} + +static void deliver(gui_window_t* win, gui_event_type_t type, int mx, int my, + bool left, bool right) { + if (!win->handle_event) return; + + gui_event_t ev = { + .type = type, + .mouse_x = mx, + .mouse_y = my, + .rel_x = mx - win->x, + .rel_y = my - (win->y + GUI_TITLEBAR_HEIGHT), + .btn_left = left, + .btn_right = right + }; + win->handle_event(win, &ev); +} + +// A titlebar press: returns true if a button consumed it. +static bool handle_titlebar_press(gui_window_t* win, int mx, int my) { + int bx, by; + + button_rect(win, 0, &bx, &by); + if (point_in(mx, my, bx, by, BTN_SIZE, BTN_SIZE)) { + gui_wm_destroy_window(win->id); + return true; + } + + button_rect(win, 1, &bx, &by); + if (point_in(mx, my, bx, by, BTN_SIZE, BTN_SIZE)) { + gui_wm_toggle_maximize(win->id); + return true; + } + + button_rect(win, 2, &bx, &by); + if (point_in(mx, my, bx, by, BTN_SIZE, BTN_SIZE)) { + gui_wm_minimize_window(win->id); + return true; + } + + // Anywhere else on the bar starts a drag. Dragging a snapped window pops + // it back to its saved size first, centred under the cursor. + if (win->snap != GUI_SNAP_NONE) { + int grab_ratio = (win->width > 0) ? ((mx - win->x) * 100) / win->width : 50; + gui_wm_restore_window(win->id); + win->x = mx - (win->width * grab_ratio) / 100; + win->y = my - GUI_TITLEBAR_HEIGHT / 2; + if (win->y < 0) win->y = 0; + } + + g_drag_win_id = win->id; + win->is_dragging = true; + win->drag_off_x = mx - win->x; + win->drag_off_y = my - win->y; + return true; +} + +void gui_wm_sync_button_state(bool btn_left) { + g_prev_btn_left = btn_left; } void gui_wm_handle_mouse(int mx, int my, bool btn_left, bool btn_right) { - // Handle ongoing drag - if (g_drag_win_id != -1) { - if (!btn_left) { - gui_window_t* win = gui_wm_get_window(g_drag_win_id); - if (win) win->is_dragging = false; - g_drag_win_id = -1; + g_last_mouse_x = mx; + g_last_mouse_y = my; + + // --- An in-flight resize owns the pointer until the button is released -- + if (g_resize_win_id != -1) { + gui_window_t* win = gui_wm_get_window(g_resize_win_id); + if (!btn_left || !win) { + if (win) { win->is_resizing = false; emit_resize(win); } + g_resize_win_id = -1; } else { - gui_window_t* win = gui_wm_get_window(g_drag_win_id); - if (win) { - win->x = mx - win->drag_off_x; - win->y = my - win->drag_off_y; - if (win->y < 0) win->y = 0; - if (win->x + win->width < 40) win->x = 40 - win->width; - } + int nw = g_resize_start_w + (mx - g_resize_start_mx); + int nh = g_resize_start_h + (my - g_resize_start_my); + + if (nw < GUI_MIN_WINDOW_W) nw = GUI_MIN_WINDOW_W; + if (nh < GUI_MIN_WINDOW_H) nh = GUI_MIN_WINDOW_H; + if (win->x + nw > g_work_w) nw = g_work_w - win->x; + if (win->y + nh > g_work_h) nh = g_work_h - win->y; + + win->width = nw; + win->height = nh; g_prev_btn_left = btn_left; return; } } - // Process top-down clicks + // --- An in-flight drag likewise owns the pointer ------------------------ + if (g_drag_win_id != -1) { + gui_window_t* win = gui_wm_get_window(g_drag_win_id); + if (!btn_left || !win) { + if (win) { + win->is_dragging = false; + + // Releasing against a screen edge snaps the window there. + if (mx <= SNAP_MARGIN) gui_wm_snap_window(win->id, GUI_SNAP_LEFT); + else if (mx >= g_work_w - SNAP_MARGIN) gui_wm_snap_window(win->id, GUI_SNAP_RIGHT); + else if (my <= SNAP_MARGIN) gui_wm_snap_window(win->id, GUI_SNAP_MAXIMIZED); + } + g_drag_win_id = -1; + } else { + win->x = mx - win->drag_off_x; + win->y = my - win->drag_off_y; + + // Keep at least a strip of the title bar reachable. + if (win->y < 0) win->y = 0; + if (win->y > g_work_h - GUI_TITLEBAR_HEIGHT) win->y = g_work_h - GUI_TITLEBAR_HEIGHT; + if (win->x + win->width < 60) win->x = 60 - win->width; + if (win->x > g_work_w - 60) win->x = g_work_w - 60; + + g_prev_btn_left = btn_left; + return; + } + } + + // --- Press edge: pick a target from the top of the z-order down --------- if (btn_left && !g_prev_btn_left) { for (int i = g_num_windows - 1; i >= 0; i--) { - int win_id = g_window_order[i]; - gui_window_t* win = gui_wm_get_window(win_id); + gui_window_t* win = gui_wm_get_window(g_window_order[i]); if (!win || !win->visible || win->minimized) continue; + if (!point_in(mx, my, win->x, win->y, win->width, win->height)) continue; - // Check titlebar hit - if (mx >= win->x && mx < win->x + win->width && - my >= win->y && my < win->y + GUI_TITLEBAR_HEIGHT) { - - gui_wm_focus_window(win->id); + gui_wm_focus_window(win->id); - // Close button [X] at (win->x + win->width - 20, win->y + 5, 14x14) - if (mx >= win->x + win->width - 20 && mx <= win->x + win->width - 6) { - gui_wm_destroy_window(win->id); - g_prev_btn_left = btn_left; - return; - } - - // Titlebar drag start - g_drag_win_id = win->id; - win->is_dragging = true; - win->drag_off_x = mx - win->x; - win->drag_off_y = my - win->y; + if (in_resize_grip(win, mx, my)) { + g_resize_win_id = win->id; + win->is_resizing = true; + g_resize_start_w = win->width; + g_resize_start_h = win->height; + g_resize_start_mx = mx; + g_resize_start_my = my; g_prev_btn_left = btn_left; return; } - // Check client area hit - if (mx >= win->x && mx < win->x + win->width && - my >= win->y + GUI_TITLEBAR_HEIGHT && my < win->y + win->height) { - - gui_wm_focus_window(win->id); - - if (win->handle_event) { - gui_event_t ev = { - .type = GUI_EVENT_MOUSE_DOWN, - .mouse_x = mx, - .mouse_y = my, - .rel_x = mx - win->x, - .rel_y = my - (win->y + GUI_TITLEBAR_HEIGHT), - .btn_left = btn_left, - .btn_right = btn_right - }; - win->handle_event(win, &ev); - } + if (my < win->y + GUI_TITLEBAR_HEIGHT) { + handle_titlebar_press(win, mx, my); g_prev_btn_left = btn_left; return; } + + deliver(win, GUI_EVENT_MOUSE_DOWN, mx, my, btn_left, btn_right); + g_prev_btn_left = btn_left; + return; } - } else if (!btn_left && g_prev_btn_left) { - // Mouse Up event - gui_window_t* active = gui_wm_get_active_window(); - if (active && active->handle_event) { - gui_event_t ev = { - .type = GUI_EVENT_MOUSE_UP, - .mouse_x = mx, - .mouse_y = my, - .rel_x = mx - active->x, - .rel_y = my - (active->y + GUI_TITLEBAR_HEIGHT), - .btn_left = btn_left, - .btn_right = btn_right - }; - active->handle_event(active, &ev); - } - } else { - // Mouse Move event - gui_window_t* active = gui_wm_get_active_window(); - if (active && active->handle_event) { - gui_event_t ev = { - .type = GUI_EVENT_MOUSE_MOVE, - .mouse_x = mx, - .mouse_y = my, - .rel_x = mx - active->x, - .rel_y = my - (active->y + GUI_TITLEBAR_HEIGHT), - .btn_left = btn_left, - .btn_right = btn_right - }; - active->handle_event(active, &ev); + + g_prev_btn_left = btn_left; + return; + } + + // --- Release and move edges go to the focused window -------------------- + gui_window_t* active = gui_wm_get_active_window(); + if (active && !active->minimized) { + if (!btn_left && g_prev_btn_left) { + deliver(active, GUI_EVENT_MOUSE_UP, mx, my, btn_left, btn_right); + } else { + deliver(active, GUI_EVENT_MOUSE_MOVE, mx, my, btn_left, btn_right); } } @@ -237,56 +534,125 @@ void gui_wm_handle_mouse(int mx, int my, bool btn_left, bool btn_right) { } void gui_wm_handle_key(uint16_t key) { + // Function keys drive the window manager itself before the app sees them. + switch (key) { + case KEY_F5: gui_wm_cycle_focus(); return; + case KEY_F6: gui_wm_cascade_windows(); return; + case KEY_F7: gui_wm_tile_windows(); return; + case KEY_F8: { + gui_window_t* a = gui_wm_get_active_window(); + if (a) gui_wm_toggle_maximize(a->id); + return; + } + case KEY_F9: { + gui_window_t* a = gui_wm_get_active_window(); + if (a) gui_wm_minimize_window(a->id); + return; + } + default: break; + } + gui_window_t* active = gui_wm_get_active_window(); - if (active && active->handle_event) { - gui_event_t ev = { - .type = GUI_EVENT_KEY_DOWN, - .key = key - }; + if (active && active->handle_event && !active->minimized) { + gui_event_t ev = { .type = GUI_EVENT_KEY_DOWN, .key = key }; active->handle_event(active, &ev); } } -void gui_wm_render(void) { - // Render windows from bottom to top - for (int i = 0; i < g_num_windows; i++) { - int win_id = g_window_order[i]; - gui_window_t* win = gui_wm_get_window(win_id); - if (!win || !win->visible || win->minimized) continue; +// --------------------------------------------------------------------------- +// Compositing +// --------------------------------------------------------------------------- - // Window Drop Shadow - gui_gfx_draw_shadow(win->x, win->y, win->width, win->height, 6); +static void draw_titlebar_buttons(const gui_window_t* win) { + int bx, by; - // Window Outer Border - uint32_t border_col = win->active ? GUI_THEME_PRIMARY : GUI_THEME_BORDER; - gui_gfx_draw_rect(win->x, win->y, win->width, win->height, border_col); + // Close + button_rect(win, 0, &bx, &by); + bool hot = point_in(g_last_mouse_x, g_last_mouse_y, bx, by, BTN_SIZE, BTN_SIZE); + gui_gfx_fill_rect(bx, by, BTN_SIZE, BTN_SIZE, hot ? 0xFFFF6B6B : GUI_THEME_BTN_CLOSE); + gui_gfx_draw_rect(bx, by, BTN_SIZE, BTN_SIZE, 0xFF991B1B); + gui_gfx_draw_line(bx + 4, by + 4, bx + BTN_SIZE - 5, by + BTN_SIZE - 5, GUI_COLOR_WHITE); + gui_gfx_draw_line(bx + BTN_SIZE - 5, by + 4, bx + 4, by + BTN_SIZE - 5, GUI_COLOR_WHITE); - // Titlebar Background Gradient - uint32_t top_bg = win->active ? 0xFF1E293B : 0xFF0F172A; - uint32_t bot_bg = win->active ? 0xFF0F172A : 0xFF0A0E1A; - gui_gfx_draw_gradient_v(win->x + 1, win->y + 1, win->width - 2, GUI_TITLEBAR_HEIGHT - 1, top_bg, bot_bg); + // Maximize / restore + button_rect(win, 1, &bx, &by); + hot = point_in(g_last_mouse_x, g_last_mouse_y, bx, by, BTN_SIZE, BTN_SIZE); + gui_gfx_fill_rect(bx, by, BTN_SIZE, BTN_SIZE, hot ? 0xFF34D399 : GUI_THEME_BTN_MAXIMIZE); + gui_gfx_draw_rect(bx, by, BTN_SIZE, BTN_SIZE, 0xFF047857); + if (win->snap == GUI_SNAP_MAXIMIZED) { + // Two offset outlines read as "restore down". + gui_gfx_draw_rect(bx + 3, by + 5, 6, 5, GUI_COLOR_WHITE); + gui_gfx_draw_rect(bx + 5, by + 3, 6, 5, GUI_COLOR_WHITE); + } else { + gui_gfx_draw_rect(bx + 4, by + 4, BTN_SIZE - 8, BTN_SIZE - 8, GUI_COLOR_WHITE); + } - // Titlebar Separator - gui_gfx_draw_line(win->x, win->y + GUI_TITLEBAR_HEIGHT, win->x + win->width - 1, win->y + GUI_TITLEBAR_HEIGHT, border_col); + // Minimize + button_rect(win, 2, &bx, &by); + hot = point_in(g_last_mouse_x, g_last_mouse_y, bx, by, BTN_SIZE, BTN_SIZE); + gui_gfx_fill_rect(bx, by, BTN_SIZE, BTN_SIZE, hot ? 0xFFFCD34D : GUI_THEME_BTN_MINIMIZE); + gui_gfx_draw_rect(bx, by, BTN_SIZE, BTN_SIZE, 0xFFB45309); + gui_gfx_fill_rect(bx + 4, by + BTN_SIZE - 6, BTN_SIZE - 8, 2, GUI_COLOR_WHITE); +} - // Titlebar Window Title Text (Large sharp 8x16 font) - uint32_t title_fg = win->active ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED; - gui_gfx_draw_string_16_shadow(win->x + 8, win->y + 4, win->title, title_fg, GUI_COLOR_BLACK); +static void draw_resize_grip(const gui_window_t* win) { + if (!win->resizable || win->snap != GUI_SNAP_NONE) return; - // Close Button [X] - int close_x = win->x + win->width - 20; - int close_y = win->y + 5; - gui_gfx_fill_rect(close_x, close_y, 14, 14, GUI_THEME_BTN_CLOSE); - gui_gfx_draw_rect(close_x, close_y, 14, 14, 0xFF991B1B); - gui_gfx_draw_line(close_x + 3, close_y + 3, close_x + 10, close_y + 10, GUI_COLOR_WHITE); - gui_gfx_draw_line(close_x + 10, close_y + 3, close_x + 3, close_y + 10, GUI_COLOR_WHITE); + int gx = win->x + win->width - GUI_RESIZE_GRIP; + int gy = win->y + win->height - GUI_RESIZE_GRIP; + uint32_t col = win->is_resizing ? GUI_THEME_PRIMARY : GUI_THEME_BORDER; - // Client Area Background - gui_gfx_fill_rect(win->x + 1, win->y + GUI_TITLEBAR_HEIGHT + 1, win->width - 2, win->height - GUI_TITLEBAR_HEIGHT - 2, GUI_THEME_BG_SURFACE); + // Three diagonal hatches, the conventional grip affordance. + for (int i = 0; i < 3; i++) { + int off = 3 + i * 3; + gui_gfx_draw_line(gx + off, gy + GUI_RESIZE_GRIP - 2, + gx + GUI_RESIZE_GRIP - 2, gy + off, col); + } +} - // Custom Window Paint Routine - if (win->paint) { - win->paint(win); +void gui_wm_render(void) { + // Bottom-to-top painter's algorithm; the topmost window wins overlaps. + for (int i = 0; i < g_num_windows; i++) { + gui_window_t* win = gui_wm_get_window(g_window_order[i]); + if (!win || !win->visible || win->minimized) continue; + + gui_gfx_draw_shadow(win->x, win->y, win->width, win->height, 6); + + uint32_t border = win->active ? GUI_THEME_PRIMARY : GUI_THEME_BORDER; + gui_gfx_draw_rect(win->x, win->y, win->width, win->height, border); + + uint32_t top_bg = win->active ? 0xFF1E293B : 0xFF0F172A; + uint32_t bot_bg = win->active ? 0xFF0F172A : 0xFF0A0E1A; + gui_gfx_draw_gradient_v(win->x + 1, win->y + 1, win->width - 2, + GUI_TITLEBAR_HEIGHT - 1, top_bg, bot_bg); + gui_gfx_draw_line(win->x, win->y + GUI_TITLEBAR_HEIGHT, + win->x + win->width - 1, win->y + GUI_TITLEBAR_HEIGHT, border); + + // Truncate the title so it never runs under the buttons. + int room = win->width - (BTN_SIZE + BTN_GAP) * 3 - 20; + int max_chars = room / 8; + if (max_chars < 1) max_chars = 1; + if (max_chars > 62) max_chars = 62; + + char shown[64]; + strncpy(shown, win->title, (size_t)max_chars); + shown[max_chars] = '\0'; + if ((int)strlen(win->title) > max_chars && max_chars > 3) { + shown[max_chars - 1] = '.'; + shown[max_chars - 2] = '.'; } + + uint32_t title_fg = win->active ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED; + gui_gfx_draw_string_16_shadow(win->x + 8, win->y + 4, shown, title_fg, GUI_COLOR_BLACK); + + draw_titlebar_buttons(win); + + gui_gfx_fill_rect(win->x + 1, win->y + GUI_TITLEBAR_HEIGHT + 1, + win->width - 2, win->height - GUI_TITLEBAR_HEIGHT - 2, + GUI_THEME_BG_SURFACE); + + if (win->paint) win->paint(win); + + draw_resize_grip(win); } } diff --git a/include/gui/gui.h b/include/gui/gui.h index e33636c..c1fddc0 100644 --- a/include/gui/gui.h +++ b/include/gui/gui.h @@ -7,6 +7,10 @@ #include #include #include +#include +#include +#include +#include void gui_init(void); int gui_start_desktop(void); diff --git a/include/gui/gui_apps_ext.h b/include/gui/gui_apps_ext.h new file mode 100644 index 0000000..9542070 --- /dev/null +++ b/include/gui/gui_apps_ext.h @@ -0,0 +1,16 @@ +#ifndef _GUI_APPS_EXT_H +#define _GUI_APPS_EXT_H + +#include + +// Second-generation desktop applications, built on the SUB-WT widget toolkit +// rather than raw framebuffer calls. + +void gui_app_settings_launch(int x, int y, int w, int h); +void gui_app_taskmgr_launch(int x, int y, int w, int h); +void gui_app_logviewer_launch(int x, int y, int w, int h); +void gui_app_ktest_launch(int x, int y, int w, int h); +void gui_app_clock_launch(int x, int y, int w, int h); +void gui_app_editor_launch(int x, int y, int w, int h); + +#endif // _GUI_APPS_EXT_H diff --git a/include/gui/gui_desktop.h b/include/gui/gui_desktop.h index 6179e68..5e143f5 100644 --- a/include/gui/gui_desktop.h +++ b/include/gui/gui_desktop.h @@ -4,16 +4,31 @@ #include #include -#define GUI_TASKBAR_HEIGHT 32 +#define GUI_TASKBAR_HEIGHT 32 +#define GUI_DESKTOP_ICON_W 72 +#define GUI_DESKTOP_ICON_H 64 void gui_desktop_init(void); + void gui_desktop_render_background(void); +void gui_desktop_render_icons(void); void gui_desktop_render_taskbar(void); void gui_desktop_render_start_menu(void); +void gui_desktop_render_context_menu(void); + void gui_desktop_toggle_start_menu(void); bool gui_desktop_is_start_menu_open(void); -void gui_desktop_handle_taskbar_click(int mx, int my); +void gui_desktop_open_context_menu(int mx, int my); +void gui_desktop_close_menus(void); -int gui_desktop_run(void); +// Returns true if the click was consumed by desktop chrome (taskbar, menus, +// icons) rather than falling through to the window manager. +bool gui_desktop_handle_click(int mx, int my); + +// Height of the area windows may occupy (screen minus taskbar). +int gui_desktop_workarea_height(void); + +void gui_desktop_request_exit(void); +int gui_desktop_run(void); #endif // _GUI_DESKTOP_H diff --git a/include/gui/gui_dialog.h b/include/gui/gui_dialog.h new file mode 100644 index 0000000..cb38bca --- /dev/null +++ b/include/gui/gui_dialog.h @@ -0,0 +1,42 @@ +#ifndef _GUI_DIALOG_H +#define _GUI_DIALOG_H + +#include +#include +#include + +// Modal dialogs. Only one is on screen at a time; while it is up the desktop +// dims behind it and the compositor routes every click to the dialog, so no +// window underneath can be interacted with. + +typedef enum { + GUI_DIALOG_INFO, + GUI_DIALOG_WARNING, + GUI_DIALOG_CONFIRM, + GUI_DIALOG_ERROR +} gui_dialog_kind_t; + +typedef enum { + GUI_DIALOG_RESULT_NONE = 0, + GUI_DIALOG_RESULT_OK, + GUI_DIALOG_RESULT_CANCEL +} gui_dialog_result_t; + +typedef void (*gui_dialog_cb_t)(gui_dialog_result_t result, void* ctx); + +void gui_dialog_init(void); + +void gui_dialog_show(gui_dialog_kind_t kind, const char* title, const char* message); +void gui_dialog_confirm(const char* title, const char* message, + gui_dialog_cb_t on_result, void* ctx); +void gui_dialog_dismiss(void); + +bool gui_dialog_is_open(void); + +// Returns true when the dialog consumed the input. +bool gui_dialog_handle_mouse(int mx, int my, bool clicked); +bool gui_dialog_handle_key(uint16_t key); + +void gui_dialog_render(void); + +#endif // _GUI_DIALOG_H diff --git a/include/gui/gui_icons.h b/include/gui/gui_icons.h new file mode 100644 index 0000000..957016b --- /dev/null +++ b/include/gui/gui_icons.h @@ -0,0 +1,40 @@ +#ifndef _GUI_ICONS_H +#define _GUI_ICONS_H + +#include +#include + +// 16x16 palette-indexed icon set. Each glyph stores one nibble per pixel +// indexing a 16-entry ARGB palette, so the whole set costs 128 bytes an icon +// and can be recoloured per theme without touching the bitmaps. + +#define GUI_ICON_SIZE 16 + +typedef enum { + GUI_ICON_TERMINAL = 0, + GUI_ICON_MONITOR, + GUI_ICON_FOLDER, + GUI_ICON_CALC, + GUI_ICON_PAINT, + GUI_ICON_INFO, + GUI_ICON_SETTINGS, + GUI_ICON_TASKS, + GUI_ICON_LOG, + GUI_ICON_FLASK, + GUI_ICON_CLOCK, + GUI_ICON_FILE, + GUI_ICON_POWER, + GUI_ICON_WARNING, + GUI_ICON_COUNT +} gui_icon_id_t; + +void gui_icons_init(void); + +// Draw at native 16x16, or scaled by an integer factor (2 = 32x32). +void gui_icon_draw(gui_icon_id_t id, int x, int y); +void gui_icon_draw_scaled(gui_icon_id_t id, int x, int y, int scale); +void gui_icon_draw_tinted(gui_icon_id_t id, int x, int y, int scale, uint32_t tint); + +const char* gui_icon_name(gui_icon_id_t id); + +#endif // _GUI_ICONS_H diff --git a/include/gui/gui_widgets.h b/include/gui/gui_widgets.h new file mode 100644 index 0000000..9759c6f --- /dev/null +++ b/include/gui/gui_widgets.h @@ -0,0 +1,86 @@ +#ifndef _GUI_WIDGETS_H +#define _GUI_WIDGETS_H + +#include +#include +#include +#include +#include +#include + +// SUB-WT: immediate-mode widget toolkit for SUB-OS desktop applications. +// +// An app feeds the toolkit whatever event the window manager delivered +// (gui_widget_feed_event) and then simply calls widget functions from inside +// its paint routine. Each call both draws the control and reports what the +// user did with it, so no retained widget tree has to be kept in sync with +// the window's state. +// +// static void my_paint(gui_window_t* win) { +// gui_widget_begin(win); +// if (gui_button(1, 10, 10, 90, 24, "Refresh")) do_refresh(); +// gui_checkbox(2, 10, 44, "Verbose", &verbose); +// gui_widget_end(); +// } + +#define GUI_FONT_W 8 +#define GUI_FONT_H 8 +#define GUI_FONT16_H 16 +#define GUI_WIDGET_ROW_H 22 + +typedef enum { + GUI_ALIGN_LEFT, + GUI_ALIGN_CENTER, + GUI_ALIGN_RIGHT +} gui_align_t; + +// Per-frame input snapshot the widgets consult. +typedef struct { + int mouse_x; // Window-relative, client area origin + int mouse_y; + bool mouse_down; // Button currently held + bool mouse_clicked; // Press edge, consumed by the first widget hit + bool mouse_released; + uint16_t key; // Pending keystroke, consumed by the focused field + int hot_id; // Widget under the cursor this frame + int active_id; // Widget holding the press + int focus_id; // Widget receiving keystrokes +} gui_widget_input_t; + +// Called from an app's handle_event to record what the WM delivered. +void gui_widget_feed_event(gui_window_t* win, const gui_event_t* ev); + +// Frame scope. Everything between these two is drawn in client-area +// coordinates: (0,0) is the top-left pixel below the title bar. +void gui_widget_begin(gui_window_t* win); +void gui_widget_end(void); + +const gui_widget_input_t* gui_widget_get_input(void); +void gui_widget_set_focus(int id); +int gui_widget_client_width(void); +int gui_widget_client_height(void); + +// --- Static content --------------------------------------------------------- +void gui_label(int x, int y, const char* text, uint32_t color); +void gui_label_bold(int x, int y, const char* text, uint32_t color); +void gui_label_aligned(int x, int y, int w, const char* text, uint32_t color, gui_align_t align); +void gui_panel(int x, int y, int w, int h, const char* title); +void gui_separator(int x, int y, int w); +void gui_progress_bar(int x, int y, int w, int h, int percent, uint32_t fill); +void gui_sparkline(int x, int y, int w, int h, const uint8_t* samples, int count, uint32_t color); +void gui_badge(int x, int y, const char* text, uint32_t bg, uint32_t fg); + +// --- Interactive controls --------------------------------------------------- +// Every control takes a caller-assigned id that must be unique and stable +// within one window. +bool gui_button(int id, int x, int y, int w, int h, const char* label); +bool gui_button_colored(int id, int x, int y, int w, int h, const char* label, uint32_t accent); +bool gui_checkbox(int id, int x, int y, const char* label, bool* value); +bool gui_radio(int id, int x, int y, const char* label, int* selected, int this_value); +bool gui_slider(int id, int x, int y, int w, int* value, int min_val, int max_val); +bool gui_listbox(int id, int x, int y, int w, int h, const char* const* items, int count, int* selected); +bool gui_textfield(int id, int x, int y, int w, char* buffer, size_t capacity); +int gui_tabbar(int id, int x, int y, int w, const char* const* labels, int count, int* active); +bool gui_scrollbar(int id, int x, int y, int h, int* offset, int total, int visible); + +#endif // _GUI_WIDGETS_H diff --git a/include/gui/gui_wm.h b/include/gui/gui_wm.h index f60ec1b..e3aa234 100644 --- a/include/gui/gui_wm.h +++ b/include/gui/gui_wm.h @@ -7,8 +7,11 @@ #include #include -#define GUI_MAX_WINDOWS 16 -#define GUI_TITLEBAR_HEIGHT 24 +#define GUI_MAX_WINDOWS 16 +#define GUI_TITLEBAR_HEIGHT 24 +#define GUI_RESIZE_GRIP 12 +#define GUI_MIN_WINDOW_W 160 +#define GUI_MIN_WINDOW_H 100 typedef enum { GUI_EVENT_MOUSE_MOVE, @@ -16,6 +19,7 @@ typedef enum { GUI_EVENT_MOUSE_UP, GUI_EVENT_KEY_DOWN, GUI_EVENT_FOCUS, + GUI_EVENT_RESIZE, GUI_EVENT_CLOSE } gui_event_type_t; @@ -30,19 +34,40 @@ typedef struct { uint16_t key; } gui_event_t; +// Where a window sits relative to the screen edges. +typedef enum { + GUI_SNAP_NONE = 0, + GUI_SNAP_LEFT, + GUI_SNAP_RIGHT, + GUI_SNAP_MAXIMIZED +} gui_snap_t; + typedef struct gui_window { - int id; + int id; char title[64]; - int x; - int y; - int width; - int height; + int x; + int y; + int width; + int height; + bool active; bool minimized; bool visible; + bool resizable; + + // Drag / resize interaction state bool is_dragging; - int drag_off_x; - int drag_off_y; + bool is_resizing; + int drag_off_x; + int drag_off_y; + + // Geometry remembered across maximize / snap so restore is exact + gui_snap_t snap; + int saved_x; + int saved_y; + int saved_width; + int saved_height; + void* user_data; void (*paint)(struct gui_window* win); @@ -50,12 +75,34 @@ typedef struct gui_window { } gui_window_t; void gui_wm_init(void); + +// The work area excludes the taskbar; windows maximize and snap inside it. +void gui_wm_set_workarea(int width, int height); + gui_window_t* gui_wm_create_window(const char* title, int x, int y, int w, int h); -void gui_wm_destroy_window(int win_id); -void gui_wm_focus_window(int win_id); +void gui_wm_destroy_window(int win_id); +void gui_wm_focus_window(int win_id); gui_window_t* gui_wm_get_window(int win_id); gui_window_t* gui_wm_get_active_window(void); -int gui_wm_get_window_count(void); +int gui_wm_get_window_count(void); + +// Enumerate in z-order, bottom-most first. Returns NULL past the end. +gui_window_t* gui_wm_get_window_at_index(int index); + +void gui_wm_minimize_window(int win_id); +void gui_wm_restore_window(int win_id); +void gui_wm_toggle_maximize(int win_id); +void gui_wm_snap_window(int win_id, gui_snap_t snap); +void gui_wm_close_active(void); +void gui_wm_cycle_focus(void); +void gui_wm_cascade_windows(void); +void gui_wm_tile_windows(void); + +// Keep the WM's press-edge detector in step when another layer (a menu, a +// modal dialog) consumed the click instead. Without this the WM still holds +// the pre-press state and reads the still-held button as a fresh press on the +// following frame, stealing focus from whatever the click just opened. +void gui_wm_sync_button_state(bool btn_left); void gui_wm_handle_mouse(int x, int y, bool btn_left, bool btn_right); void gui_wm_handle_key(uint16_t key);