DVX_GUI/dvx/dvxApp.c

1919 lines
66 KiB
C

// dvx_app.c — Layer 5: Application API for DV/X GUI
#include "dvxApp.h"
#include "dvxWidget.h"
#include "widgets/widgetInternal.h"
#include "dvxFont.h"
#include "dvxCursor.h"
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <dpmi.h>
#define DBLCLICK_THRESHOLD (CLOCKS_PER_SEC / 2)
#define ICON_REFRESH_INTERVAL 8
#define KB_MOVE_STEP 8
// ============================================================
// Prototypes
// ============================================================
static void closeSysMenu(AppContextT *ctx);
static void compositeAndFlush(AppContextT *ctx);
static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y);
static bool dispatchAccelKey(AppContextT *ctx, char key);
static void dispatchEvents(AppContextT *ctx);
static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y);
static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd);
static WindowT *findWindowById(AppContextT *ctx, int32_t id);
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 openSysMenu(AppContextT *ctx, WindowT *win);
static void pollAnsiTermWidgets(AppContextT *ctx);
static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win);
static void pollKeyboard(AppContextT *ctx);
static void pollMouse(AppContextT *ctx);
static void refreshMinimizedIcons(AppContextT *ctx);
static void updateCursorShape(AppContextT *ctx);
// Button pressed via accelerator key — separate from sPressedButton (mouse)
static WidgetT *sAccelPressedBtn = NULL;
// Alt+key scan code to ASCII lookup table (indexed by scan code)
// BIOS INT 16h returns these scan codes with ascii=0 for Alt+key combos
static const char sAltScanToAscii[256] = {
// Alt+letters
[0x10] = 'q', [0x11] = 'w', [0x12] = 'e', [0x13] = 'r',
[0x14] = 't', [0x15] = 'y', [0x16] = 'u', [0x17] = 'i',
[0x18] = 'o', [0x19] = 'p', [0x1E] = 'a', [0x1F] = 's',
[0x20] = 'd', [0x21] = 'f', [0x22] = 'g', [0x23] = 'h',
[0x24] = 'j', [0x25] = 'k', [0x26] = 'l', [0x2C] = 'z',
[0x2D] = 'x', [0x2E] = 'c', [0x2F] = 'v', [0x30] = 'b',
[0x31] = 'n', [0x32] = 'm',
// Alt+digits
[0x78] = '1', [0x79] = '2', [0x7A] = '3', [0x7B] = '4',
[0x7C] = '5', [0x7D] = '6', [0x7E] = '7', [0x7F] = '8',
[0x80] = '9', [0x81] = '0',
};
// ============================================================
// closeSysMenu
// ============================================================
static void closeSysMenu(AppContextT *ctx) {
if (ctx->sysMenu.active) {
dirtyListAdd(&ctx->dirty, ctx->sysMenu.popupX, ctx->sysMenu.popupY,
ctx->sysMenu.popupW, ctx->sysMenu.popupH);
ctx->sysMenu.active = false;
}
}
// ============================================================
// 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. Draw minimized window icons (under all windows)
wmDrawMinimizedIcons(d, ops, &ctx->colors, ws, dr);
// 3. 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);
}
}
// 4. 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);
drawTextAccel(d, ops, &ctx->font,
ctx->popup.popupX + CHROME_TITLE_PAD + 2, itemY,
item->label, fg, bg, true);
itemY += ctx->font.charHeight;
}
}
break;
}
}
}
}
// 4b. Draw system menu if active
if (ctx->sysMenu.active) {
RectT smRect = { ctx->sysMenu.popupX, ctx->sysMenu.popupY, ctx->sysMenu.popupW, ctx->sysMenu.popupH };
RectT smIsect;
if (rectIntersect(dr, &smRect, &smIsect)) {
setClipRect(d, dr->x, dr->y, dr->w, dr->h);
BevelStyleT smBevel;
smBevel.highlight = ctx->colors.windowHighlight;
smBevel.shadow = ctx->colors.windowShadow;
smBevel.face = ctx->colors.menuBg;
smBevel.width = 2;
drawBevel(d, ops, ctx->sysMenu.popupX, ctx->sysMenu.popupY, ctx->sysMenu.popupW, ctx->sysMenu.popupH, &smBevel);
int32_t itemY = ctx->sysMenu.popupY + 2;
for (int32_t k = 0; k < ctx->sysMenu.itemCount; k++) {
SysMenuItemT *item = &ctx->sysMenu.items[k];
if (item->separator) {
drawHLine(d, ops, ctx->sysMenu.popupX + 2, itemY + ctx->font.charHeight / 2, ctx->sysMenu.popupW - 4, ctx->colors.windowShadow);
itemY += ctx->font.charHeight;
continue;
}
uint32_t bg = ctx->colors.menuBg;
uint32_t fg = item->enabled ? ctx->colors.menuFg : ctx->colors.windowShadow;
if (k == ctx->sysMenu.hoverItem && item->enabled) {
bg = ctx->colors.menuHighlightBg;
fg = ctx->colors.menuHighlightFg;
}
rectFill(d, ops, ctx->sysMenu.popupX + 2, itemY, ctx->sysMenu.popupW - 4, ctx->font.charHeight, bg);
drawTextAccel(d, ops, &ctx->font, ctx->sysMenu.popupX + CHROME_TITLE_PAD + 2, itemY, item->label, fg, bg, true);
itemY += ctx->font.charHeight;
}
}
}
// 5. Draw cursor
drawCursorAt(ctx, ctx->mouseX, ctx->mouseY);
// 6. Flush this dirty rect to LFB
flushRect(d, 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);
}
// ============================================================
// dispatchAccelKey — route Alt+key to menu or widget
// ============================================================
static bool dispatchAccelKey(AppContextT *ctx, char key) {
if (ctx->stack.focusedIdx < 0) {
return false;
}
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
// Check menu bar first
if (win->menuBar) {
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
MenuT *menu = &win->menuBar->menus[i];
if (menu->accelKey == key) {
// Close existing popup first
if (ctx->popup.active) {
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
}
// Open this menu's 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 = textWidthAccel(&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);
return true;
}
}
}
// Check widget tree
if (win->widgetRoot) {
WidgetT *target = widgetFindByAccel(win->widgetRoot, key);
if (target) {
switch (target->type) {
case WidgetButtonE:
target->as.button.pressed = true;
sAccelPressedBtn = target;
wgtInvalidate(target);
return true;
case WidgetCheckboxE:
widgetCheckboxOnMouse(target);
wgtInvalidate(target);
return true;
case WidgetRadioE:
widgetRadioOnMouse(target);
wgtInvalidate(target);
return true;
case WidgetImageButtonE:
if (target->onClick) {
target->onClick(target);
}
wgtInvalidate(target);
return true;
case WidgetTabPageE:
{
// Close any open dropdown/combobox popup
if (sOpenPopup) {
if (sOpenPopup->type == WidgetDropdownE) {
sOpenPopup->as.dropdown.open = false;
} else if (sOpenPopup->type == WidgetComboBoxE) {
sOpenPopup->as.comboBox.open = false;
}
sOpenPopup = NULL;
}
// Activate this tab in its parent TabControl
if (target->parent && target->parent->type == WidgetTabControlE) {
int32_t tabIdx = 0;
for (WidgetT *c = target->parent->firstChild; c; c = c->nextSibling) {
if (c == target) {
wgtTabControlSetActive(target->parent, tabIdx);
wgtInvalidate(win->widgetRoot);
break;
}
if (c->type == WidgetTabPageE) {
tabIdx++;
}
}
}
return true;
}
case WidgetDropdownE:
target->as.dropdown.open = true;
target->as.dropdown.hoverIdx = target->as.dropdown.selectedIdx;
sOpenPopup = target;
widgetClearFocus(win->widgetRoot);
target->focused = true;
wgtInvalidate(win->widgetRoot);
return true;
case WidgetComboBoxE:
target->as.comboBox.open = true;
target->as.comboBox.hoverIdx = target->as.comboBox.selectedIdx;
sOpenPopup = target;
widgetClearFocus(win->widgetRoot);
target->focused = true;
wgtInvalidate(win->widgetRoot);
return true;
case WidgetLabelE:
case WidgetFrameE:
{
// Focus the next focusable widget
WidgetT *next = widgetFindNextFocusable(win->widgetRoot, target);
if (next) {
widgetClearFocus(win->widgetRoot);
next->focused = true;
// Open dropdown/combobox if that's the focused target
if (next->type == WidgetDropdownE) {
next->as.dropdown.open = true;
next->as.dropdown.hoverIdx = next->as.dropdown.selectedIdx;
sOpenPopup = next;
} else if (next->type == WidgetComboBoxE) {
next->as.comboBox.open = true;
next->as.comboBox.hoverIdx = next->as.comboBox.selectedIdx;
sOpenPopup = next;
}
wgtInvalidate(win->widgetRoot);
}
return true;
}
default:
// For focusable widgets, just focus them
if (widgetIsFocusable(target->type)) {
widgetClearFocus(win->widgetRoot);
target->focused = true;
wgtInvalidate(win->widgetRoot);
return true;
}
break;
}
}
}
return false;
}
// ============================================================
// 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 system menu interaction
if (ctx->sysMenu.active) {
if (mx >= ctx->sysMenu.popupX && mx < ctx->sysMenu.popupX + ctx->sysMenu.popupW &&
my >= ctx->sysMenu.popupY && my < ctx->sysMenu.popupY + ctx->sysMenu.popupH) {
// Hover tracking
int32_t relY = my - ctx->sysMenu.popupY - 2;
int32_t itemIdx = relY / ctx->font.charHeight;
if (itemIdx >= 0 && itemIdx < ctx->sysMenu.itemCount && ctx->sysMenu.items[itemIdx].separator) {
itemIdx = -1;
}
if (itemIdx != ctx->sysMenu.hoverItem) {
ctx->sysMenu.hoverItem = itemIdx;
dirtyListAdd(&ctx->dirty, ctx->sysMenu.popupX, ctx->sysMenu.popupY, ctx->sysMenu.popupW, ctx->sysMenu.popupH);
}
// Click on item
if ((buttons & 1) && !(prevBtn & 1)) {
if (itemIdx >= 0 && itemIdx < ctx->sysMenu.itemCount) {
SysMenuItemT *item = &ctx->sysMenu.items[itemIdx];
if (item->enabled && !item->separator) {
executeSysMenuCmd(ctx, item->cmd);
}
}
}
return;
}
// Click outside system menu — close it, let event fall through
if ((buttons & 1) && !(prevBtn & 1)) {
closeSysMenu(ctx);
}
}
// 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);
}
}
// ============================================================
// dvxFitWindow
// ============================================================
void dvxFitWindow(AppContextT *ctx, WindowT *win) {
if (!ctx || !win || !win->widgetRoot) {
return;
}
// Measure the widget tree to get minimum content size
widgetCalcMinSizeTree(win->widgetRoot, &ctx->font);
int32_t contentW = win->widgetRoot->calcMinW;
int32_t contentH = win->widgetRoot->calcMinH;
// Compute chrome overhead
int32_t topChrome = CHROME_TOTAL_TOP;
if (win->menuBar) {
topChrome += CHROME_MENU_HEIGHT;
}
int32_t newW = contentW + CHROME_TOTAL_SIDE * 2;
int32_t newH = contentH + topChrome + CHROME_TOTAL_BOTTOM;
// Dirty old position
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
// Resize
win->w = newW;
win->h = newH;
wmUpdateContentRect(win);
wmReallocContentBuf(win, &ctx->display);
// Dirty new position
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
}
// ============================================================
// 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 (dvxUpdate(ctx)) {
// dvxUpdate returns false when the GUI wants to exit
}
}
// ============================================================
// dvxUpdate
// ============================================================
bool dvxUpdate(AppContextT *ctx) {
if (!ctx->running) {
return false;
}
pollMouse(ctx);
pollKeyboard(ctx);
dispatchEvents(ctx);
pollAnsiTermWidgets(ctx);
// Periodically refresh one minimized window thumbnail (staggered)
ctx->frameCount++;
if (ctx->frameCount % ICON_REFRESH_INTERVAL == 0) {
refreshMinimizedIcons(ctx);
}
if (ctx->dirty.count > 0) {
compositeAndFlush(ctx);
} else if (ctx->idleCallback) {
ctx->idleCallback(ctx->idleCtx);
} else {
__dpmi_yield();
}
// After compositing, release accel-pressed button (one frame of animation)
if (sAccelPressedBtn) {
sAccelPressedBtn->as.button.pressed = false;
if (sAccelPressedBtn->onClick) {
sAccelPressedBtn->onClick(sAccelPressedBtn);
}
wgtInvalidate(sAccelPressedBtn);
sAccelPressedBtn = NULL;
}
ctx->prevMouseX = ctx->mouseX;
ctx->prevMouseY = ctx->mouseY;
ctx->prevMouseButtons = ctx->mouseButtons;
return ctx->running;
}
// ============================================================
// 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);
}
// ============================================================
// executeSysMenuCmd
// ============================================================
static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd) {
WindowT *win = findWindowById(ctx, ctx->sysMenu.windowId);
closeSysMenu(ctx);
if (!win) {
return;
}
switch (cmd) {
case SysMenuRestoreE:
if (win->maximized) {
wmRestore(&ctx->stack, &ctx->dirty, &ctx->display, win);
}
break;
case SysMenuMoveE:
ctx->kbMoveResize.mode = KbModeMoveE;
ctx->kbMoveResize.windowId = win->id;
ctx->kbMoveResize.origX = win->x;
ctx->kbMoveResize.origY = win->y;
break;
case SysMenuSizeE:
ctx->kbMoveResize.mode = KbModeResizeE;
ctx->kbMoveResize.windowId = win->id;
ctx->kbMoveResize.origX = win->x;
ctx->kbMoveResize.origY = win->y;
ctx->kbMoveResize.origW = win->w;
ctx->kbMoveResize.origH = win->h;
break;
case SysMenuMinimizeE:
wmMinimize(&ctx->stack, &ctx->dirty, win);
dirtyListAdd(&ctx->dirty, 0, ctx->display.height - ICON_TOTAL_SIZE - ICON_SPACING, ctx->display.width, ICON_TOTAL_SIZE + ICON_SPACING);
break;
case SysMenuMaximizeE:
if (win->resizable && !win->maximized) {
wmMaximize(&ctx->stack, &ctx->dirty, &ctx->display, win);
}
break;
case SysMenuCloseE:
if (win->onClose) {
win->onClose(win);
} else {
dvxDestroyWindow(ctx, win);
}
break;
}
}
// ============================================================
// findWindowById
// ============================================================
static WindowT *findWindowById(AppContextT *ctx, int32_t id) {
for (int32_t i = 0; i < ctx->stack.count; i++) {
if (ctx->stack.windows[i]->id == id) {
return ctx->stack.windows[i];
}
}
return NULL;
}
// ============================================================
// 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, single-click opens system menu)
{
clock_t now = clock();
if (ctx->lastCloseClickId == win->id &&
(now - ctx->lastCloseClickTime) < DBLCLICK_THRESHOLD) {
ctx->lastCloseClickId = -1;
closeSysMenu(ctx);
if (win->onClose) {
win->onClose(win);
} else {
dvxDestroyWindow(ctx, win);
}
} else {
ctx->lastCloseClickTime = now;
ctx->lastCloseClickId = win->id;
openSysMenu(ctx, win);
}
}
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 = textWidthAccel(&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, 128, 128, 128);
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, 192, 192, 192);
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);
}
// ============================================================
// openSysMenu
// ============================================================
static void openSysMenu(AppContextT *ctx, WindowT *win) {
// Close any existing popup menus first
if (ctx->popup.active) {
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
ctx->popup.active = false;
}
if (ctx->sysMenu.active) {
closeSysMenu(ctx);
return;
}
ctx->sysMenu.itemCount = 0;
ctx->sysMenu.windowId = win->id;
// Restore — enabled only when maximized
SysMenuItemT *item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
strncpy(item->label, "&Restore", MAX_MENU_LABEL - 1);
item->cmd = SysMenuRestoreE;
item->separator = false;
item->enabled = win->maximized;
item->accelKey = accelParse(item->label);
// Move — disabled when maximized
item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
strncpy(item->label, "&Move", MAX_MENU_LABEL - 1);
item->cmd = SysMenuMoveE;
item->separator = false;
item->enabled = !win->maximized;
item->accelKey = accelParse(item->label);
// Size — only if resizable and not maximized
item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
strncpy(item->label, "&Size", MAX_MENU_LABEL - 1);
item->cmd = SysMenuSizeE;
item->separator = false;
item->enabled = win->resizable && !win->maximized;
item->accelKey = accelParse(item->label);
// Minimize
item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
strncpy(item->label, "Mi&nimize", MAX_MENU_LABEL - 1);
item->cmd = SysMenuMinimizeE;
item->separator = false;
item->enabled = true;
item->accelKey = accelParse(item->label);
// Maximize — only if resizable and not maximized
item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
strncpy(item->label, "Ma&ximize", MAX_MENU_LABEL - 1);
item->cmd = SysMenuMaximizeE;
item->separator = false;
item->enabled = win->resizable && !win->maximized;
item->accelKey = accelParse(item->label);
// Separator
item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
memset(item, 0, sizeof(*item));
item->separator = true;
// Close
item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
strncpy(item->label, "&Close", MAX_MENU_LABEL - 1);
item->cmd = SysMenuCloseE;
item->separator = false;
item->enabled = true;
item->accelKey = accelParse(item->label);
// Compute popup geometry — position below the close gadget
ctx->sysMenu.popupX = win->x + CHROME_BORDER_WIDTH;
ctx->sysMenu.popupY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT;
if (win->menuBar) {
ctx->sysMenu.popupY += CHROME_MENU_HEIGHT;
}
int32_t maxW = 0;
for (int32_t i = 0; i < ctx->sysMenu.itemCount; i++) {
if (!ctx->sysMenu.items[i].separator) {
int32_t w = textWidthAccel(&ctx->font, ctx->sysMenu.items[i].label);
if (w > maxW) {
maxW = w;
}
}
}
ctx->sysMenu.popupW = maxW + CHROME_TITLE_PAD * 2 + 8;
ctx->sysMenu.popupH = ctx->sysMenu.itemCount * ctx->font.charHeight + 4;
ctx->sysMenu.hoverItem = -1;
ctx->sysMenu.active = true;
dirtyListAdd(&ctx->dirty, ctx->sysMenu.popupX, ctx->sysMenu.popupY, ctx->sysMenu.popupW, ctx->sysMenu.popupH);
}
// ============================================================
// pollAnsiTermWidgets — poll and repaint all ANSI term widgets
// ============================================================
static void pollAnsiTermWidgets(AppContextT *ctx) {
for (int32_t i = 0; i < ctx->stack.count; i++) {
WindowT *win = ctx->stack.windows[i];
if (win->widgetRoot) {
pollAnsiTermWidgetsWalk(ctx, win->widgetRoot, win);
}
}
}
// ============================================================
// pollAnsiTermWidgetsWalk — recursive helper
// ============================================================
static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win) {
if (w->type == WidgetAnsiTermE) {
wgtAnsiTermPoll(w);
if (wgtAnsiTermRepaint(w) > 0) {
win->contentDirty = true;
dvxInvalidateWindow(ctx, win);
}
}
for (WidgetT *child = w->firstChild; child; child = child->nextSibling) {
pollAnsiTermWidgetsWalk(ctx, child, win);
}
}
// ============================================================
// pollKeyboard
// ============================================================
static void pollKeyboard(AppContextT *ctx) {
__dpmi_regs r;
// Check if key is available (INT 16h, enhanced function 11h)
while (1) {
memset(&r, 0, sizeof(r));
r.x.ax = 0x1100;
__dpmi_int(0x16, &r);
// Zero flag set = no key available
if (r.x.flags & 0x40) {
break;
}
// Read the key (INT 16h, enhanced function 10h)
memset(&r, 0, sizeof(r));
r.x.ax = 0x1000;
__dpmi_int(0x16, &r);
int32_t scancode = (r.x.ax >> 8) & 0xFF;
int32_t ascii = r.x.ax & 0xFF;
// Enhanced INT 16h returns ascii=0xE0 for grey/extended keys
// (arrows, Home, End, Insert, Delete, etc. on 101-key keyboards).
// Normalize to 0 so all extended key checks work uniformly.
if (ascii == 0xE0) {
ascii = 0;
}
// Keyboard move/resize mode — intercept all keys
if (ctx->kbMoveResize.mode != KbModeNoneE) {
WindowT *kbWin = findWindowById(ctx, ctx->kbMoveResize.windowId);
if (!kbWin) {
ctx->kbMoveResize.mode = KbModeNoneE;
continue;
}
if (ascii == 0x1B) {
// Cancel — restore original position/size
dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
kbWin->x = ctx->kbMoveResize.origX;
kbWin->y = ctx->kbMoveResize.origY;
if (ctx->kbMoveResize.mode == KbModeResizeE) {
kbWin->w = ctx->kbMoveResize.origW;
kbWin->h = ctx->kbMoveResize.origH;
wmUpdateContentRect(kbWin);
wmReallocContentBuf(kbWin, &ctx->display);
if (kbWin->onResize) {
kbWin->onResize(kbWin, kbWin->contentW, kbWin->contentH);
}
if (kbWin->onPaint) {
kbWin->onPaint(kbWin, NULL);
}
}
dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
ctx->kbMoveResize.mode = KbModeNoneE;
continue;
}
if (ascii == 0x0D) {
// Confirm
ctx->kbMoveResize.mode = KbModeNoneE;
continue;
}
if (ctx->kbMoveResize.mode == KbModeMoveE) {
if (ascii == 0 && scancode == 0x48) {
dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
kbWin->y -= KB_MOVE_STEP;
dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
} else if (ascii == 0 && scancode == 0x50) {
dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
kbWin->y += KB_MOVE_STEP;
dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
} else if (ascii == 0 && scancode == 0x4B) {
dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
kbWin->x -= KB_MOVE_STEP;
dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
} else if (ascii == 0 && scancode == 0x4D) {
dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
kbWin->x += KB_MOVE_STEP;
dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
}
} else {
// KbModeResizeE
int32_t newW = kbWin->w;
int32_t newH = kbWin->h;
if (ascii == 0 && scancode == 0x4D) {
newW += KB_MOVE_STEP;
} else if (ascii == 0 && scancode == 0x4B) {
newW -= KB_MOVE_STEP;
} else if (ascii == 0 && scancode == 0x50) {
newH += KB_MOVE_STEP;
} else if (ascii == 0 && scancode == 0x48) {
newH -= KB_MOVE_STEP;
}
if (newW < MIN_WINDOW_W) {
newW = MIN_WINDOW_W;
}
if (newH < MIN_WINDOW_H) {
newH = MIN_WINDOW_H;
}
if (kbWin->maxW > 0 && newW > kbWin->maxW) {
newW = kbWin->maxW;
}
if (kbWin->maxH > 0 && newH > kbWin->maxH) {
newH = kbWin->maxH;
}
if (newW != kbWin->w || newH != kbWin->h) {
dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
kbWin->w = newW;
kbWin->h = newH;
wmUpdateContentRect(kbWin);
wmReallocContentBuf(kbWin, &ctx->display);
if (kbWin->onResize) {
kbWin->onResize(kbWin, kbWin->contentW, kbWin->contentH);
}
if (kbWin->onPaint) {
kbWin->onPaint(kbWin, NULL);
}
dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h);
}
}
continue;
}
// Alt+Space — open/close system menu
// Enhanced INT 16h: Alt+Space returns scancode 0x02, ascii 0x20
if (scancode == 0x02 && ascii == 0x20) {
if (ctx->sysMenu.active) {
closeSysMenu(ctx);
} else if (ctx->stack.focusedIdx >= 0) {
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
openSysMenu(ctx, win);
}
continue;
}
// System menu keyboard navigation
if (ctx->sysMenu.active) {
// Alt+key — close system menu and let it fall through to accel dispatch
if (ascii == 0 && scancode < 256 && sAltScanToAscii[scancode]) {
closeSysMenu(ctx);
// Fall through to dispatchAccelKey below
} else if (ascii == 0x1B) {
closeSysMenu(ctx);
continue;
} else if (ascii == 0 && scancode == 0x48) {
// Up arrow
int32_t idx = ctx->sysMenu.hoverItem;
for (int32_t tries = 0; tries < ctx->sysMenu.itemCount; tries++) {
idx--;
if (idx < 0) {
idx = ctx->sysMenu.itemCount - 1;
}
if (!ctx->sysMenu.items[idx].separator) {
break;
}
}
ctx->sysMenu.hoverItem = idx;
dirtyListAdd(&ctx->dirty, ctx->sysMenu.popupX, ctx->sysMenu.popupY, ctx->sysMenu.popupW, ctx->sysMenu.popupH);
continue;
} else if (ascii == 0 && scancode == 0x50) {
// Down arrow
int32_t idx = ctx->sysMenu.hoverItem;
for (int32_t tries = 0; tries < ctx->sysMenu.itemCount; tries++) {
idx++;
if (idx >= ctx->sysMenu.itemCount) {
idx = 0;
}
if (!ctx->sysMenu.items[idx].separator) {
break;
}
}
ctx->sysMenu.hoverItem = idx;
dirtyListAdd(&ctx->dirty, ctx->sysMenu.popupX, ctx->sysMenu.popupY, ctx->sysMenu.popupW, ctx->sysMenu.popupH);
continue;
} else if (ascii == 0x0D) {
// Enter — execute selected item
if (ctx->sysMenu.hoverItem >= 0 && ctx->sysMenu.hoverItem < ctx->sysMenu.itemCount) {
SysMenuItemT *item = &ctx->sysMenu.items[ctx->sysMenu.hoverItem];
if (item->enabled && !item->separator) {
executeSysMenuCmd(ctx, item->cmd);
}
} else {
closeSysMenu(ctx);
}
continue;
} else if (ascii != 0) {
// Accelerator key match
char lc = (char)tolower(ascii);
for (int32_t k = 0; k < ctx->sysMenu.itemCount; k++) {
if (ctx->sysMenu.items[k].accelKey == lc && ctx->sysMenu.items[k].enabled && !ctx->sysMenu.items[k].separator) {
executeSysMenuCmd(ctx, ctx->sysMenu.items[k].cmd);
goto nextKey;
}
}
// No sys menu match — try menu bar accelerators
closeSysMenu(ctx);
if (ctx->stack.focusedIdx >= 0) {
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
if (win->menuBar) {
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
if (win->menuBar->menus[i].accelKey == lc) {
dispatchAccelKey(ctx, lc);
goto nextKey;
}
}
}
}
continue;
} else {
continue;
}
}
// Check for Alt+key (BIOS returns ascii=0 with specific scancodes)
if (ascii == 0 && scancode < 256 && sAltScanToAscii[scancode]) {
char accelKey = sAltScanToAscii[scancode];
if (dispatchAccelKey(ctx, accelKey)) {
continue;
}
}
// Popup menu keyboard navigation (arrows, enter, esc)
if (ctx->popup.active && ascii == 0) {
// Up arrow
if (scancode == 0x48) {
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
if (win && win->menuBar && ctx->popup.menuIdx < win->menuBar->menuCount) {
MenuT *menu = &win->menuBar->menus[ctx->popup.menuIdx];
int32_t idx = ctx->popup.hoverItem;
for (int32_t tries = 0; tries < menu->itemCount; tries++) {
idx--;
if (idx < 0) {
idx = menu->itemCount - 1;
}
if (!menu->items[idx].separator) {
break;
}
}
ctx->popup.hoverItem = idx;
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
}
continue;
}
// Down arrow
if (scancode == 0x50) {
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
if (win && win->menuBar && ctx->popup.menuIdx < win->menuBar->menuCount) {
MenuT *menu = &win->menuBar->menus[ctx->popup.menuIdx];
int32_t idx = ctx->popup.hoverItem;
for (int32_t tries = 0; tries < menu->itemCount; tries++) {
idx++;
if (idx >= menu->itemCount) {
idx = 0;
}
if (!menu->items[idx].separator) {
break;
}
}
ctx->popup.hoverItem = idx;
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
}
continue;
}
// Left arrow — switch to previous menu
if (scancode == 0x4B) {
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
if (win && win->menuBar && win->menuBar->menuCount > 1) {
int32_t newIdx = ctx->popup.menuIdx - 1;
if (newIdx < 0) {
newIdx = win->menuBar->menuCount - 1;
}
dispatchAccelKey(ctx, win->menuBar->menus[newIdx].accelKey);
}
continue;
}
// Right arrow — switch to next menu
if (scancode == 0x4D) {
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
if (win && win->menuBar && win->menuBar->menuCount > 1) {
int32_t newIdx = ctx->popup.menuIdx + 1;
if (newIdx >= win->menuBar->menuCount) {
newIdx = 0;
}
dispatchAccelKey(ctx, win->menuBar->menus[newIdx].accelKey);
}
continue;
}
}
// Enter executes highlighted popup menu item
if (ctx->popup.active && ascii == 0x0D) {
if (ctx->popup.hoverItem >= 0) {
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
if (win && win->menuBar && ctx->popup.menuIdx < win->menuBar->menuCount) {
MenuT *menu = &win->menuBar->menus[ctx->popup.menuIdx];
if (ctx->popup.hoverItem < menu->itemCount) {
MenuItemT *item = &menu->items[ctx->popup.hoverItem];
if (item->enabled && !item->separator && win->onMenu) {
win->onMenu(win, item->id);
}
}
}
}
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
ctx->popup.active = false;
continue;
}
// Check for plain key accelerator in open popup menu
if (ctx->popup.active && ascii != 0) {
char lc = (ascii >= 'A' && ascii <= 'Z') ? (char)(ascii + 32) : (char)ascii;
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];
// Try matching an item in the current popup
for (int32_t k = 0; k < menu->itemCount; k++) {
MenuItemT *item = &menu->items[k];
if (item->accelKey == lc && item->enabled && !item->separator) {
if (win->onMenu) {
win->onMenu(win, item->id);
}
// Close popup
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
ctx->popup.active = false;
goto nextKey;
}
}
// No match in current popup — try switching to another menu
for (int32_t i = 0; i < bar->menuCount; i++) {
if (bar->menus[i].accelKey == lc && i != ctx->popup.menuIdx) {
dispatchAccelKey(ctx, lc);
goto nextKey;
}
}
}
break;
}
}
}
// ESC closes open dropdown/combobox popup
if (sOpenPopup && ascii == 0x1B) {
// Dirty the popup list area
WindowT *popWin = sOpenPopup->window;
int32_t popX;
int32_t popY;
int32_t popW;
int32_t popH;
widgetDropdownPopupRect(sOpenPopup, &ctx->font, popWin->contentH, &popX, &popY, &popW, &popH);
dirtyListAdd(&ctx->dirty, popWin->x + popWin->contentX + popX, popWin->y + popWin->contentY + popY, popW, popH);
WidgetT *closing = sOpenPopup;
sOpenPopup = NULL;
if (closing->type == WidgetDropdownE) {
closing->as.dropdown.open = false;
} else if (closing->type == WidgetComboBoxE) {
closing->as.comboBox.open = false;
}
wgtInvalidate(closing);
continue;
}
// ESC closes popup menu
if (ctx->popup.active && ascii == 0x1B) {
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
ctx->popup.active = false;
continue;
}
// 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);
}
}
continue;
nextKey:;
}
}
// ============================================================
// 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;
}
// ============================================================
// refreshMinimizedIcons
// ============================================================
//
// Dirty the next minimized window icon whose content has changed
// since the last refresh. Only considers windows without custom
// iconData. Called every ICON_REFRESH_INTERVAL frames to stagger.
static void refreshMinimizedIcons(AppContextT *ctx) {
WindowStackT *ws = &ctx->stack;
DisplayT *d = &ctx->display;
int32_t count = 0;
int32_t iconIdx = 0;
for (int32_t i = 0; i < ws->count; i++) {
WindowT *win = ws->windows[i];
if (!win->visible || !win->minimized) {
continue;
}
if (!win->iconData && win->contentDirty) {
if (count >= ctx->iconRefreshIdx) {
int32_t ix = ICON_SPACING + iconIdx * (ICON_TOTAL_SIZE + ICON_SPACING);
int32_t iy = d->height - ICON_TOTAL_SIZE - ICON_SPACING;
dirtyListAdd(&ctx->dirty, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE);
win->contentDirty = false;
ctx->iconRefreshIdx = count + 1;
return;
}
count++;
}
iconIdx++;
}
// Wrapped past the end — reset for next cycle
ctx->iconRefreshIdx = 0;
}
// ============================================================
// 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;
}
}