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);