// 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 #include #include #include #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; } }