mirror of
https://github.com/subhobhai943/SUB-OS.git
synced 2026-10-25 16:52:29 +00:00
GUI polish / HD: - Default display mode raised to 1920x1080 (video=1920x1080); make run/vnc use vgamem_mb=64 and -m 512M. The growable heap absorbs the larger backbuffer, shadow and wallpaper (observed: one 16 MB growth to a 48 MB heap). - Wallpaper: three-stop vertical gradient, a soft concentric-frame edge vignette for depth, a finer faint grid, and a centred brand mark. - Window chrome: brighter/saturated titlebar gradient when focused, an accent underline on the active window, and larger/softer drop shadows. - Default session windows are placed and sized for the HD work area. - Live FPS pill in the taskbar tray (green/amber/red by rate); tray widened so FPS + CPU trace + memory badge + clock no longer overlap. Kernel: - kernel/time/ktime.c + include/kernel/ktime.h (new): a unified monotonic high-resolution clock that prefers the HPET (nanosecond) and falls back to the 100 Hz PIT tick. Wired ktime_init() into boot; drives the compositor's FPS sampling. New `ktime` LazyBox applet reports the source and uptime. Verified live (screenshotted): boots to 1920x1080, KTIME reports "source: HPET, nanosecond resolution", the polished desktop renders with the active-window accent and vignette, and the tray shows a live FPS reading. Clean build on x86_64/aarch64/armv8i; 0 panics / 0 OOM. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01Fh5z6V7nGPbHvqVG5p4CXX
668 lines
22 KiB
C
668 lines
22 KiB
C
// 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 <gui/gui_wm.h>
|
|
#include <gui/gui_icons.h>
|
|
#include <drivers/keyboard.h>
|
|
#include <lib/string.h>
|
|
#include <mm/kmalloc.h>
|
|
|
|
#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];
|
|
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));
|
|
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) {
|
|
if (g_num_windows >= GUI_MAX_WINDOWS) return NULL;
|
|
|
|
int slot = -1;
|
|
for (int i = 0; i < GUI_MAX_WINDOWS; i++) {
|
|
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->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;
|
|
}
|
|
|
|
static int order_index_of(int win_id) {
|
|
for (int i = 0; i < g_num_windows; i++) {
|
|
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];
|
|
}
|
|
g_num_windows--;
|
|
}
|
|
|
|
for (int i = 0; i < GUI_MAX_WINDOWS; i++) {
|
|
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_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 = 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++) {
|
|
g_window_order[i] = g_window_order[i + 1];
|
|
}
|
|
g_window_order[g_num_windows - 1] = target;
|
|
}
|
|
|
|
for (int i = 0; i < GUI_MAX_WINDOWS; i++) {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
gui_window_t* gui_wm_get_window(int win_id) {
|
|
for (int i = 0; i < GUI_MAX_WINDOWS; i++) {
|
|
if (g_windows[i].id == win_id) return &g_windows[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
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]);
|
|
}
|
|
|
|
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) {
|
|
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 {
|
|
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;
|
|
}
|
|
}
|
|
|
|
// --- 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--) {
|
|
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;
|
|
|
|
gui_wm_focus_window(win->id);
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
g_prev_btn_left = btn_left;
|
|
}
|
|
|
|
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 && !active->minimized) {
|
|
gui_event_t ev = { .type = GUI_EVENT_KEY_DOWN, .key = key };
|
|
active->handle_event(active, &ev);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Compositing
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static void draw_titlebar_buttons(const gui_window_t* win) {
|
|
int bx, by;
|
|
|
|
// 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);
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
static void draw_resize_grip(const gui_window_t* win) {
|
|
if (!win->resizable || win->snap != GUI_SNAP_NONE) return;
|
|
|
|
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;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
// A larger, softer drop shadow reads as real depth on the HD desktop.
|
|
gui_gfx_draw_shadow(win->x, win->y, win->width, win->height,
|
|
win->active ? 12 : 7);
|
|
|
|
uint32_t border = win->active ? GUI_THEME_PRIMARY : GUI_THEME_BORDER;
|
|
gui_gfx_draw_rect(win->x, win->y, win->width, win->height, border);
|
|
|
|
// Brighter, more saturated titlebar when focused; muted when not.
|
|
uint32_t top_bg = win->active ? 0xFF24345A : 0xFF141C2E;
|
|
uint32_t bot_bg = win->active ? 0xFF141E38 : 0xFF0B1120;
|
|
gui_gfx_draw_gradient_v(win->x + 1, win->y + 1, win->width - 2,
|
|
GUI_TITLEBAR_HEIGHT - 1, top_bg, bot_bg);
|
|
|
|
// Accent underline on the active window's titlebar; plain divider otherwise.
|
|
if (win->active) {
|
|
gui_gfx_fill_rect(win->x + 1, win->y + GUI_TITLEBAR_HEIGHT - 1,
|
|
win->width - 2, 2, GUI_THEME_PRIMARY);
|
|
}
|
|
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);
|
|
}
|
|
}
|