886 lines
29 KiB
C
886 lines
29 KiB
C
// dvx_app.c — Layer 5: Application API for DV/X GUI
|
|
|
|
#include "dvxApp.h"
|
|
#include "dvxFont.h"
|
|
#include "dvxCursor.h"
|
|
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <dpmi.h>
|
|
|
|
#define DBLCLICK_THRESHOLD (CLOCKS_PER_SEC / 2)
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static void compositeAndFlush(AppContextT *ctx);
|
|
static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y);
|
|
static void dispatchEvents(AppContextT *ctx);
|
|
static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y);
|
|
static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons);
|
|
static void initColorScheme(AppContextT *ctx);
|
|
static void initMouse(AppContextT *ctx);
|
|
static void pollKeyboard(AppContextT *ctx);
|
|
static void pollMouse(AppContextT *ctx);
|
|
static void updateCursorShape(AppContextT *ctx);
|
|
|
|
|
|
// ============================================================
|
|
// compositeAndFlush
|
|
// ============================================================
|
|
|
|
static void compositeAndFlush(AppContextT *ctx) {
|
|
DisplayT *d = &ctx->display;
|
|
BlitOpsT *ops = &ctx->blitOps;
|
|
DirtyListT *dl = &ctx->dirty;
|
|
WindowStackT *ws = &ctx->stack;
|
|
|
|
dirtyListMerge(dl);
|
|
|
|
for (int32_t i = 0; i < dl->count; i++) {
|
|
RectT *dr = &dl->rects[i];
|
|
|
|
// Clip dirty rect to screen bounds
|
|
if (dr->x < 0) { dr->w += dr->x; dr->x = 0; }
|
|
if (dr->y < 0) { dr->h += dr->y; dr->y = 0; }
|
|
if (dr->x + dr->w > d->width) { dr->w = d->width - dr->x; }
|
|
if (dr->y + dr->h > d->height) { dr->h = d->height - dr->y; }
|
|
if (dr->w <= 0 || dr->h <= 0) { continue; }
|
|
|
|
// Set clip rect to this dirty rect
|
|
setClipRect(d, dr->x, dr->y, dr->w, dr->h);
|
|
|
|
// 1. Draw desktop background
|
|
rectFill(d, ops, dr->x, dr->y, dr->w, dr->h, ctx->colors.desktop);
|
|
|
|
// 2. Walk window stack bottom-to-top
|
|
for (int32_t j = 0; j < ws->count; j++) {
|
|
WindowT *win = ws->windows[j];
|
|
|
|
if (!win->visible || win->minimized) {
|
|
continue;
|
|
}
|
|
|
|
// Check if window intersects this dirty rect
|
|
RectT winRect = {win->x, win->y, win->w, win->h};
|
|
RectT isect;
|
|
|
|
if (!rectIntersect(dr, &winRect, &isect)) {
|
|
continue;
|
|
}
|
|
|
|
wmDrawChrome(d, ops, &ctx->font, &ctx->colors, win, dr);
|
|
wmDrawContent(d, ops, win, dr);
|
|
if (win->vScroll || win->hScroll) {
|
|
wmDrawScrollbars(d, ops, &ctx->colors, win, dr);
|
|
}
|
|
}
|
|
|
|
// 2b. Draw minimized window icons
|
|
wmDrawMinimizedIcons(d, ops, &ctx->colors, ws, dr);
|
|
|
|
// 3. Draw popup menu if active
|
|
if (ctx->popup.active) {
|
|
// Draw popup dropdown
|
|
RectT popRect = {
|
|
ctx->popup.popupX, ctx->popup.popupY,
|
|
ctx->popup.popupW, ctx->popup.popupH
|
|
};
|
|
RectT popIsect;
|
|
|
|
if (rectIntersect(dr, &popRect, &popIsect)) {
|
|
setClipRect(d, dr->x, dr->y, dr->w, dr->h);
|
|
|
|
// Find the window and menu
|
|
for (int32_t j = 0; j < ws->count; j++) {
|
|
if (ws->windows[j]->id == ctx->popup.windowId) {
|
|
WindowT *win = ws->windows[j];
|
|
MenuBarT *bar = win->menuBar;
|
|
|
|
if (bar && ctx->popup.menuIdx < bar->menuCount) {
|
|
MenuT *menu = &bar->menus[ctx->popup.menuIdx];
|
|
|
|
// Draw popup background
|
|
BevelStyleT popBevel;
|
|
popBevel.highlight = ctx->colors.windowHighlight;
|
|
popBevel.shadow = ctx->colors.windowShadow;
|
|
popBevel.face = ctx->colors.menuBg;
|
|
popBevel.width = 2;
|
|
drawBevel(d, ops, ctx->popup.popupX, ctx->popup.popupY,
|
|
ctx->popup.popupW, ctx->popup.popupH, &popBevel);
|
|
|
|
// Draw menu items
|
|
int32_t itemY = ctx->popup.popupY + 2;
|
|
|
|
for (int32_t k = 0; k < menu->itemCount; k++) {
|
|
MenuItemT *item = &menu->items[k];
|
|
|
|
if (item->separator) {
|
|
drawHLine(d, ops, ctx->popup.popupX + 2,
|
|
itemY + ctx->font.charHeight / 2,
|
|
ctx->popup.popupW - 4,
|
|
ctx->colors.windowShadow);
|
|
itemY += ctx->font.charHeight;
|
|
continue;
|
|
}
|
|
|
|
uint32_t bg = ctx->colors.menuBg;
|
|
uint32_t fg = ctx->colors.menuFg;
|
|
|
|
if (k == ctx->popup.hoverItem) {
|
|
bg = ctx->colors.menuHighlightBg;
|
|
fg = ctx->colors.menuHighlightFg;
|
|
}
|
|
|
|
rectFill(d, ops, ctx->popup.popupX + 2, itemY,
|
|
ctx->popup.popupW - 4, ctx->font.charHeight, bg);
|
|
drawText(d, ops, &ctx->font,
|
|
ctx->popup.popupX + CHROME_TITLE_PAD + 2, itemY,
|
|
item->label, fg, bg, true);
|
|
|
|
itemY += ctx->font.charHeight;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Draw cursor
|
|
drawCursorAt(ctx, ctx->mouseX, ctx->mouseY);
|
|
|
|
// 5. Flush this dirty rect to LFB
|
|
flushRect(d, ops, dr);
|
|
}
|
|
|
|
resetClipRect(d);
|
|
dirtyListClear(dl);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dirtyCursorArea
|
|
// ============================================================
|
|
|
|
static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y) {
|
|
// Dirty the union of all cursor shapes at this position to handle shape changes
|
|
// All cursors are 16x16 with hotspot at either (0,0) or (7,7), so worst case
|
|
// covers from (x-7, y-7) to (x+15, y+15) = 23x23 area
|
|
dirtyListAdd(&ctx->dirty, x - 7, y - 7, 23, 23);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dispatchEvents
|
|
// ============================================================
|
|
|
|
static void dispatchEvents(AppContextT *ctx) {
|
|
int32_t mx = ctx->mouseX;
|
|
int32_t my = ctx->mouseY;
|
|
int32_t buttons = ctx->mouseButtons;
|
|
int32_t prevBtn = ctx->prevMouseButtons;
|
|
|
|
// Mouse movement always dirties old and new cursor positions
|
|
if (mx != ctx->prevMouseX || my != ctx->prevMouseY) {
|
|
dirtyCursorArea(ctx, ctx->prevMouseX, ctx->prevMouseY);
|
|
dirtyCursorArea(ctx, mx, my);
|
|
}
|
|
|
|
// Update cursor shape based on what the mouse is hovering over
|
|
updateCursorShape(ctx);
|
|
|
|
// Handle active drag
|
|
if (ctx->stack.dragWindow >= 0) {
|
|
if (buttons & 1) {
|
|
wmDragMove(&ctx->stack, &ctx->dirty, mx, my);
|
|
} else {
|
|
wmDragEnd(&ctx->stack);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Handle active resize
|
|
if (ctx->stack.resizeWindow >= 0) {
|
|
if (buttons & 1) {
|
|
wmResizeMove(&ctx->stack, &ctx->dirty, &ctx->display, mx, my);
|
|
} else {
|
|
wmResizeEnd(&ctx->stack);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Handle active scrollbar thumb drag
|
|
if (ctx->stack.scrollWindow >= 0) {
|
|
if (buttons & 1) {
|
|
wmScrollbarDrag(&ctx->stack, &ctx->dirty, mx, my);
|
|
} else {
|
|
wmScrollbarEnd(&ctx->stack);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Handle popup menu interaction
|
|
if (ctx->popup.active) {
|
|
// Check if mouse is inside popup
|
|
if (mx >= ctx->popup.popupX && mx < ctx->popup.popupX + ctx->popup.popupW &&
|
|
my >= ctx->popup.popupY && my < ctx->popup.popupY + ctx->popup.popupH) {
|
|
|
|
// Find which item is hovered
|
|
int32_t relY = my - ctx->popup.popupY - 2;
|
|
int32_t itemIdx = relY / ctx->font.charHeight;
|
|
|
|
if (itemIdx != ctx->popup.hoverItem) {
|
|
ctx->popup.hoverItem = itemIdx;
|
|
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
|
|
ctx->popup.popupW, ctx->popup.popupH);
|
|
}
|
|
|
|
// Click on item
|
|
if ((buttons & 1) && !(prevBtn & 1)) {
|
|
// Find the window and menu
|
|
for (int32_t j = 0; j < ctx->stack.count; j++) {
|
|
if (ctx->stack.windows[j]->id == ctx->popup.windowId) {
|
|
WindowT *win = ctx->stack.windows[j];
|
|
MenuBarT *bar = win->menuBar;
|
|
|
|
if (bar && ctx->popup.menuIdx < bar->menuCount) {
|
|
MenuT *menu = &bar->menus[ctx->popup.menuIdx];
|
|
|
|
if (itemIdx >= 0 && itemIdx < menu->itemCount) {
|
|
MenuItemT *item = &menu->items[itemIdx];
|
|
|
|
if (item->enabled && !item->separator && win->onMenu) {
|
|
win->onMenu(win, item->id);
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Close popup
|
|
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
|
|
ctx->popup.popupW, ctx->popup.popupH);
|
|
ctx->popup.active = false;
|
|
}
|
|
} else if ((buttons & 1) && !(prevBtn & 1)) {
|
|
// Click outside popup — close it
|
|
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
|
|
ctx->popup.popupW, ctx->popup.popupH);
|
|
ctx->popup.active = false;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Handle button press
|
|
if ((buttons & 1) && !(prevBtn & 1)) {
|
|
handleMouseButton(ctx, mx, my, buttons);
|
|
}
|
|
|
|
// Handle button release on content — send to focused window
|
|
if (!(buttons & 1) && (prevBtn & 1)) {
|
|
if (ctx->stack.focusedIdx >= 0) {
|
|
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
|
|
|
|
if (win->onMouse) {
|
|
int32_t relX = mx - win->x - win->contentX;
|
|
int32_t relY = my - win->y - win->contentY;
|
|
win->onMouse(win, relX, relY, buttons);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mouse movement in content area — send to focused window
|
|
if ((mx != ctx->prevMouseX || my != ctx->prevMouseY) &&
|
|
ctx->stack.focusedIdx >= 0 && (buttons & 1)) {
|
|
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
|
|
|
|
if (win->onMouse) {
|
|
int32_t relX = mx - win->x - win->contentX;
|
|
int32_t relY = my - win->y - win->contentY;
|
|
win->onMouse(win, relX, relY, buttons);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// drawCursorAt
|
|
// ============================================================
|
|
|
|
static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y) {
|
|
const CursorT *cur = &ctx->cursors[ctx->cursorId];
|
|
|
|
drawMaskedBitmap(&ctx->display, &ctx->blitOps,
|
|
x - cur->hotX, y - cur->hotY,
|
|
cur->width, cur->height,
|
|
cur->andMask, cur->xorData,
|
|
ctx->cursorFg, ctx->cursorBg);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxCreateWindow
|
|
// ============================================================
|
|
|
|
WindowT *dvxCreateWindow(AppContextT *ctx, const char *title,
|
|
int32_t x, int32_t y, int32_t w, int32_t h,
|
|
bool resizable) {
|
|
WindowT *win = wmCreateWindow(&ctx->stack, &ctx->display,
|
|
title, x, y, w, h, resizable);
|
|
|
|
if (win) {
|
|
// Raise and focus
|
|
int32_t idx = ctx->stack.count - 1;
|
|
wmSetFocus(&ctx->stack, &ctx->dirty, idx);
|
|
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
|
|
}
|
|
|
|
return win;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxDestroyWindow
|
|
// ============================================================
|
|
|
|
void dvxDestroyWindow(AppContextT *ctx, WindowT *win) {
|
|
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
|
|
wmDestroyWindow(&ctx->stack, win);
|
|
|
|
// Focus the new top window
|
|
if (ctx->stack.count > 0) {
|
|
wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxGetBlitOps
|
|
// ============================================================
|
|
|
|
const BlitOpsT *dvxGetBlitOps(const AppContextT *ctx) {
|
|
return &ctx->blitOps;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxGetColors
|
|
// ============================================================
|
|
|
|
const ColorSchemeT *dvxGetColors(const AppContextT *ctx) {
|
|
return &ctx->colors;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxGetDisplay
|
|
// ============================================================
|
|
|
|
DisplayT *dvxGetDisplay(AppContextT *ctx) {
|
|
return &ctx->display;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxGetFont
|
|
// ============================================================
|
|
|
|
const BitmapFontT *dvxGetFont(const AppContextT *ctx) {
|
|
return &ctx->font;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxInit
|
|
// ============================================================
|
|
|
|
int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
|
|
memset(ctx, 0, sizeof(*ctx));
|
|
|
|
// Initialize video
|
|
if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
// Initialize blit ops
|
|
drawInit(&ctx->blitOps, &ctx->display);
|
|
|
|
// Initialize window stack
|
|
wmInit(&ctx->stack);
|
|
|
|
// Initialize dirty list
|
|
dirtyListInit(&ctx->dirty);
|
|
|
|
// Set up font (use 8x16)
|
|
ctx->font = dvxFont8x16;
|
|
|
|
// Set up cursors
|
|
memcpy(ctx->cursors, dvxCursors, sizeof(dvxCursors));
|
|
ctx->cursorId = CURSOR_ARROW;
|
|
|
|
// Initialize colors
|
|
initColorScheme(ctx);
|
|
|
|
// Cache cursor colors
|
|
ctx->cursorFg = packColor(&ctx->display, 255, 255, 255);
|
|
ctx->cursorBg = packColor(&ctx->display, 0, 0, 0);
|
|
|
|
// Initialize mouse
|
|
initMouse(ctx);
|
|
|
|
ctx->running = true;
|
|
ctx->lastIconClickId = -1;
|
|
ctx->lastIconClickTime = 0;
|
|
ctx->lastCloseClickId = -1;
|
|
ctx->lastCloseClickTime = 0;
|
|
|
|
// Dirty the entire screen for first paint
|
|
dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxInvalidateRect
|
|
// ============================================================
|
|
|
|
void dvxInvalidateRect(AppContextT *ctx, WindowT *win,
|
|
int32_t x, int32_t y, int32_t w, int32_t h) {
|
|
// Convert from content-relative to screen coordinates
|
|
int32_t screenX = win->x + win->contentX + x;
|
|
int32_t screenY = win->y + win->contentY + y;
|
|
|
|
dirtyListAdd(&ctx->dirty, screenX, screenY, w, h);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxInvalidateWindow
|
|
// ============================================================
|
|
|
|
void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) {
|
|
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxQuit
|
|
// ============================================================
|
|
|
|
void dvxQuit(AppContextT *ctx) {
|
|
ctx->running = false;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxRun
|
|
// ============================================================
|
|
|
|
void dvxRun(AppContextT *ctx) {
|
|
while (ctx->running) {
|
|
pollMouse(ctx);
|
|
pollKeyboard(ctx);
|
|
dispatchEvents(ctx);
|
|
|
|
if (ctx->dirty.count > 0) {
|
|
compositeAndFlush(ctx);
|
|
} else {
|
|
// Nothing to do — yield timeslice
|
|
__dpmi_yield();
|
|
}
|
|
|
|
ctx->prevMouseX = ctx->mouseX;
|
|
ctx->prevMouseY = ctx->mouseY;
|
|
ctx->prevMouseButtons = ctx->mouseButtons;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxShutdown
|
|
// ============================================================
|
|
|
|
void dvxShutdown(AppContextT *ctx) {
|
|
// Destroy all remaining windows
|
|
while (ctx->stack.count > 0) {
|
|
wmDestroyWindow(&ctx->stack, ctx->stack.windows[ctx->stack.count - 1]);
|
|
}
|
|
|
|
videoShutdown(&ctx->display);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxSetTitle
|
|
// ============================================================
|
|
|
|
void dvxSetTitle(AppContextT *ctx, WindowT *win, const char *title) {
|
|
wmSetTitle(win, &ctx->dirty, title);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxSetWindowIcon
|
|
// ============================================================
|
|
|
|
int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path) {
|
|
return wmSetIcon(win, path, &ctx->display);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// handleMouseButton
|
|
// ============================================================
|
|
|
|
static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons) {
|
|
// Check for click on minimized icon first
|
|
int32_t iconIdx = wmMinimizedIconHit(&ctx->stack, &ctx->display, mx, my);
|
|
|
|
if (iconIdx >= 0) {
|
|
WindowT *iconWin = ctx->stack.windows[iconIdx];
|
|
clock_t now = clock();
|
|
|
|
if (ctx->lastIconClickId == iconWin->id &&
|
|
(now - ctx->lastIconClickTime) < DBLCLICK_THRESHOLD) {
|
|
// Double-click: restore minimized window
|
|
// Dirty the entire icon strip area
|
|
dirtyListAdd(&ctx->dirty, 0,
|
|
ctx->display.height - ICON_TOTAL_SIZE - ICON_SPACING,
|
|
ctx->display.width, ICON_TOTAL_SIZE + ICON_SPACING);
|
|
wmRestoreMinimized(&ctx->stack, &ctx->dirty, iconWin);
|
|
ctx->lastIconClickId = -1;
|
|
} else {
|
|
// First click — record for double-click detection
|
|
ctx->lastIconClickTime = now;
|
|
ctx->lastIconClickId = iconWin->id;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
int32_t hitPart;
|
|
int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);
|
|
|
|
if (hitIdx < 0) {
|
|
return; // clicked on desktop
|
|
}
|
|
|
|
WindowT *win = ctx->stack.windows[hitIdx];
|
|
|
|
// Raise and focus if not already
|
|
if (hitIdx != ctx->stack.focusedIdx) {
|
|
wmRaiseWindow(&ctx->stack, &ctx->dirty, hitIdx);
|
|
// After raise, the window is now at count-1
|
|
hitIdx = ctx->stack.count - 1;
|
|
wmSetFocus(&ctx->stack, &ctx->dirty, hitIdx);
|
|
}
|
|
|
|
switch (hitPart) {
|
|
case 0: // content
|
|
if (win->onMouse) {
|
|
int32_t relX = mx - win->x - win->contentX;
|
|
int32_t relY = my - win->y - win->contentY;
|
|
win->onMouse(win, relX, relY, buttons);
|
|
}
|
|
break;
|
|
|
|
case 1: // title bar — start drag
|
|
wmDragBegin(&ctx->stack, hitIdx, mx, my);
|
|
break;
|
|
|
|
case 2: // close button (double-click to close)
|
|
{
|
|
clock_t now = clock();
|
|
|
|
if (ctx->lastCloseClickId == win->id &&
|
|
(now - ctx->lastCloseClickTime) < DBLCLICK_THRESHOLD) {
|
|
ctx->lastCloseClickId = -1;
|
|
|
|
if (win->onClose) {
|
|
win->onClose(win);
|
|
} else {
|
|
dvxDestroyWindow(ctx, win);
|
|
}
|
|
} else {
|
|
ctx->lastCloseClickTime = now;
|
|
ctx->lastCloseClickId = win->id;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 3: // resize edge
|
|
{
|
|
int32_t edge = wmResizeEdgeHit(win, mx, my);
|
|
wmResizeBegin(&ctx->stack, hitIdx, edge, mx, my);
|
|
}
|
|
break;
|
|
|
|
case 4: // menu bar
|
|
{
|
|
// Determine which menu was clicked
|
|
if (!win->menuBar) {
|
|
break;
|
|
}
|
|
|
|
int32_t relX = mx - win->x;
|
|
|
|
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
|
|
MenuT *menu = &win->menuBar->menus[i];
|
|
|
|
if (relX >= menu->barX && relX < menu->barX + menu->barW) {
|
|
// Open popup
|
|
ctx->popup.active = true;
|
|
ctx->popup.windowId = win->id;
|
|
ctx->popup.menuIdx = i;
|
|
ctx->popup.popupX = win->x + menu->barX;
|
|
ctx->popup.popupY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT + CHROME_MENU_HEIGHT;
|
|
ctx->popup.hoverItem = -1;
|
|
|
|
// Calculate popup size
|
|
int32_t maxW = 0;
|
|
|
|
for (int32_t k = 0; k < menu->itemCount; k++) {
|
|
int32_t itemW = textWidth(&ctx->font, menu->items[k].label);
|
|
|
|
if (itemW > maxW) {
|
|
maxW = itemW;
|
|
}
|
|
}
|
|
|
|
ctx->popup.popupW = maxW + CHROME_TITLE_PAD * 2 + 8;
|
|
ctx->popup.popupH = menu->itemCount * ctx->font.charHeight + 4;
|
|
|
|
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
|
|
ctx->popup.popupW, ctx->popup.popupH);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 5: // vertical scrollbar
|
|
wmScrollbarClick(&ctx->stack, &ctx->dirty, hitIdx, 0, mx, my);
|
|
break;
|
|
|
|
case 6: // horizontal scrollbar
|
|
wmScrollbarClick(&ctx->stack, &ctx->dirty, hitIdx, 1, mx, my);
|
|
break;
|
|
|
|
case 7: // minimize
|
|
wmMinimize(&ctx->stack, &ctx->dirty, win);
|
|
// Dirty the icon strip area so the new icon gets drawn
|
|
dirtyListAdd(&ctx->dirty, 0,
|
|
ctx->display.height - ICON_TOTAL_SIZE - ICON_SPACING,
|
|
ctx->display.width, ICON_TOTAL_SIZE + ICON_SPACING);
|
|
break;
|
|
|
|
case 8: // maximize / restore
|
|
if (win->maximized) {
|
|
wmRestore(&ctx->stack, &ctx->dirty, &ctx->display, win);
|
|
} else {
|
|
wmMaximize(&ctx->stack, &ctx->dirty, &ctx->display, win);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// initColorScheme
|
|
// ============================================================
|
|
|
|
static void initColorScheme(AppContextT *ctx) {
|
|
DisplayT *d = &ctx->display;
|
|
|
|
// GEOS Ensemble Motif-style color scheme
|
|
ctx->colors.desktop = packColor(d, 0, 128, 128); // GEOS teal desktop
|
|
ctx->colors.windowFace = packColor(d, 192, 192, 192); // standard Motif grey
|
|
ctx->colors.windowHighlight = packColor(d, 255, 255, 255);
|
|
ctx->colors.windowShadow = packColor(d, 80, 80, 80);
|
|
ctx->colors.activeTitleBg = packColor(d, 48, 48, 48); // GEOS dark charcoal
|
|
ctx->colors.activeTitleFg = packColor(d, 255, 255, 255);
|
|
ctx->colors.inactiveTitleBg = packColor(d, 160, 160, 160); // lighter grey
|
|
ctx->colors.inactiveTitleFg = packColor(d, 64, 64, 64);
|
|
ctx->colors.contentBg = packColor(d, 255, 255, 255);
|
|
ctx->colors.contentFg = packColor(d, 0, 0, 0);
|
|
ctx->colors.menuBg = packColor(d, 192, 192, 192);
|
|
ctx->colors.menuFg = packColor(d, 0, 0, 0);
|
|
ctx->colors.menuHighlightBg = packColor(d, 48, 48, 48);
|
|
ctx->colors.menuHighlightFg = packColor(d, 255, 255, 255);
|
|
ctx->colors.buttonFace = packColor(d, 192, 192, 192);
|
|
ctx->colors.scrollbarBg = packColor(d, 192, 192, 192);
|
|
ctx->colors.scrollbarFg = packColor(d, 128, 128, 128);
|
|
ctx->colors.scrollbarTrough = packColor(d, 160, 160, 160); // GEOS lighter trough
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// initMouse
|
|
// ============================================================
|
|
|
|
static void initMouse(AppContextT *ctx) {
|
|
__dpmi_regs r;
|
|
|
|
// Reset mouse driver
|
|
memset(&r, 0, sizeof(r));
|
|
r.x.ax = 0x0000;
|
|
__dpmi_int(0x33, &r);
|
|
|
|
// Set horizontal range to match screen width
|
|
memset(&r, 0, sizeof(r));
|
|
r.x.ax = 0x0007;
|
|
r.x.cx = 0;
|
|
r.x.dx = ctx->display.width - 1;
|
|
__dpmi_int(0x33, &r);
|
|
|
|
// Set vertical range to match screen height
|
|
memset(&r, 0, sizeof(r));
|
|
r.x.ax = 0x0008;
|
|
r.x.cx = 0;
|
|
r.x.dx = ctx->display.height - 1;
|
|
__dpmi_int(0x33, &r);
|
|
|
|
// Position cursor at center of screen
|
|
ctx->mouseX = ctx->display.width / 2;
|
|
ctx->mouseY = ctx->display.height / 2;
|
|
ctx->prevMouseX = ctx->mouseX;
|
|
ctx->prevMouseY = ctx->mouseY;
|
|
|
|
// Set mouse position
|
|
memset(&r, 0, sizeof(r));
|
|
r.x.ax = 0x0004;
|
|
r.x.cx = ctx->mouseX;
|
|
r.x.dx = ctx->mouseY;
|
|
__dpmi_int(0x33, &r);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// pollKeyboard
|
|
// ============================================================
|
|
|
|
static void pollKeyboard(AppContextT *ctx) {
|
|
__dpmi_regs r;
|
|
|
|
// Check if key is available (INT 16h, function 01h)
|
|
while (1) {
|
|
memset(&r, 0, sizeof(r));
|
|
r.x.ax = 0x0100;
|
|
__dpmi_int(0x16, &r);
|
|
|
|
// Zero flag set = no key available
|
|
if (r.x.flags & 0x40) {
|
|
break;
|
|
}
|
|
|
|
// Read the key (INT 16h, function 00h)
|
|
memset(&r, 0, sizeof(r));
|
|
r.x.ax = 0x0000;
|
|
__dpmi_int(0x16, &r);
|
|
|
|
int32_t scancode = (r.x.ax >> 8) & 0xFF;
|
|
int32_t ascii = r.x.ax & 0xFF;
|
|
|
|
// ESC = quit
|
|
if (ascii == 0x1B) {
|
|
dvxQuit(ctx);
|
|
return;
|
|
}
|
|
|
|
// Send to focused window
|
|
if (ctx->stack.focusedIdx >= 0) {
|
|
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
|
|
|
|
if (win->onKey) {
|
|
win->onKey(win, ascii ? ascii : (scancode | 0x100), 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// pollMouse
|
|
// ============================================================
|
|
|
|
static void pollMouse(AppContextT *ctx) {
|
|
__dpmi_regs r;
|
|
|
|
memset(&r, 0, sizeof(r));
|
|
r.x.ax = 0x0003;
|
|
__dpmi_int(0x33, &r);
|
|
|
|
ctx->mouseX = r.x.cx;
|
|
ctx->mouseY = r.x.dx;
|
|
ctx->mouseButtons = r.x.bx;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// updateCursorShape
|
|
// ============================================================
|
|
|
|
static void updateCursorShape(AppContextT *ctx) {
|
|
int32_t newCursor = CURSOR_ARROW;
|
|
int32_t mx = ctx->mouseX;
|
|
int32_t my = ctx->mouseY;
|
|
|
|
// During active resize, keep the resize cursor
|
|
if (ctx->stack.resizeWindow >= 0) {
|
|
int32_t edge = ctx->stack.resizeEdge;
|
|
bool horiz = (edge & (RESIZE_LEFT | RESIZE_RIGHT)) != 0;
|
|
bool vert = (edge & (RESIZE_TOP | RESIZE_BOTTOM)) != 0;
|
|
|
|
if (horiz && vert) {
|
|
if ((edge & RESIZE_LEFT && edge & RESIZE_TOP) ||
|
|
(edge & RESIZE_RIGHT && edge & RESIZE_BOTTOM)) {
|
|
newCursor = CURSOR_RESIZE_DIAG_NWSE;
|
|
} else {
|
|
newCursor = CURSOR_RESIZE_DIAG_NESW;
|
|
}
|
|
} else if (horiz) {
|
|
newCursor = CURSOR_RESIZE_H;
|
|
} else {
|
|
newCursor = CURSOR_RESIZE_V;
|
|
}
|
|
}
|
|
// Not in an active drag/resize — check what we're hovering
|
|
else if (ctx->stack.dragWindow < 0 && ctx->stack.scrollWindow < 0) {
|
|
int32_t hitPart;
|
|
int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);
|
|
|
|
if (hitIdx >= 0 && hitPart == 3) {
|
|
// Hovering over a resize edge
|
|
WindowT *win = ctx->stack.windows[hitIdx];
|
|
int32_t edge = wmResizeEdgeHit(win, mx, my);
|
|
bool horiz = (edge & (RESIZE_LEFT | RESIZE_RIGHT)) != 0;
|
|
bool vert = (edge & (RESIZE_TOP | RESIZE_BOTTOM)) != 0;
|
|
|
|
if (horiz && vert) {
|
|
if ((edge & RESIZE_LEFT && edge & RESIZE_TOP) ||
|
|
(edge & RESIZE_RIGHT && edge & RESIZE_BOTTOM)) {
|
|
newCursor = CURSOR_RESIZE_DIAG_NWSE;
|
|
} else {
|
|
newCursor = CURSOR_RESIZE_DIAG_NESW;
|
|
}
|
|
} else if (horiz) {
|
|
newCursor = CURSOR_RESIZE_H;
|
|
} else if (vert) {
|
|
newCursor = CURSOR_RESIZE_V;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If cursor shape changed, dirty the cursor area
|
|
if (newCursor != ctx->cursorId) {
|
|
dirtyCursorArea(ctx, mx, my);
|
|
ctx->cursorId = newCursor;
|
|
}
|
|
}
|