2868 lines
99 KiB
C
2868 lines
99 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>
|
|
#include <signal.h>
|
|
|
|
#define DBLCLICK_THRESHOLD (CLOCKS_PER_SEC / 2)
|
|
#define ICON_REFRESH_INTERVAL 8
|
|
#define KB_MOVE_STEP 8
|
|
#define MENU_CHECK_WIDTH 14
|
|
#define SUBMENU_ARROW_WIDTH 12
|
|
#define TOOLTIP_DELAY_MS 500
|
|
#define TOOLTIP_PAD 3
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph);
|
|
static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t modifiers);
|
|
static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx);
|
|
static void closeAllPopups(AppContextT *ctx);
|
|
static void closePopupLevel(AppContextT *ctx);
|
|
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 drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, const MenuT *menu, int32_t px, int32_t py, int32_t pw, int32_t ph, int32_t hoverItem, const RectT *clipTo);
|
|
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 openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY);
|
|
static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx);
|
|
static void openSubMenu(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);
|
|
static void updateTooltip(AppContextT *ctx);
|
|
|
|
// Button pressed via keyboard — shared with widgetEvent.c for Space/Enter
|
|
WidgetT *sKeyPressedBtn = 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',
|
|
};
|
|
|
|
|
|
// ============================================================
|
|
// calcPopupSize — compute popup width and height for a menu
|
|
// ============================================================
|
|
|
|
static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph) {
|
|
int32_t maxW = 0;
|
|
bool hasSub = false;
|
|
bool hasCheck = false;
|
|
|
|
for (int32_t k = 0; k < menu->itemCount; k++) {
|
|
int32_t itemW = textWidthAccel(&ctx->font, menu->items[k].label);
|
|
|
|
if (itemW > maxW) {
|
|
maxW = itemW;
|
|
}
|
|
|
|
if (menu->items[k].subMenu) {
|
|
hasSub = true;
|
|
}
|
|
|
|
if (menu->items[k].type == MenuItemCheckE || menu->items[k].type == MenuItemRadioE) {
|
|
hasCheck = true;
|
|
}
|
|
}
|
|
|
|
*pw = maxW + CHROME_TITLE_PAD * 2 + 8 + (hasSub ? SUBMENU_ARROW_WIDTH : 0) + (hasCheck ? MENU_CHECK_WIDTH : 0);
|
|
*ph = menu->itemCount * ctx->font.charHeight + 4;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// checkAccelTable — test key against window's accelerator table
|
|
// ============================================================
|
|
|
|
static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t modifiers) {
|
|
if (!win->accelTable || !win->onMenu) {
|
|
return false;
|
|
}
|
|
|
|
// Normalize Ctrl+letter: BIOS returns ASCII 0x01-0x1A for Ctrl+A..Z
|
|
// Map back to uppercase letter for matching
|
|
int32_t matchKey = key;
|
|
|
|
if ((modifiers & ACCEL_CTRL) && matchKey >= 0x01 && matchKey <= 0x1A) {
|
|
matchKey = matchKey + 'A' - 1;
|
|
}
|
|
|
|
// Uppercase for case-insensitive letter matching
|
|
if (matchKey >= 'a' && matchKey <= 'z') {
|
|
matchKey = matchKey - 32;
|
|
}
|
|
|
|
int32_t requiredMods = modifiers & (ACCEL_CTRL | ACCEL_ALT);
|
|
AccelTableT *table = win->accelTable;
|
|
|
|
for (int32_t i = 0; i < table->count; i++) {
|
|
AccelEntryT *e = &table->entries[i];
|
|
|
|
// Uppercase the entry key too
|
|
int32_t entryKey = e->key;
|
|
|
|
if (entryKey >= 'a' && entryKey <= 'z') {
|
|
entryKey = entryKey - 32;
|
|
}
|
|
|
|
int32_t entryMods = e->modifiers & (ACCEL_CTRL | ACCEL_ALT);
|
|
|
|
if (entryKey == matchKey && entryMods == requiredMods) {
|
|
win->onMenu(win, e->cmdId);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
(void)ctx;
|
|
return false;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// clickMenuCheckRadio — toggle check or select radio on click
|
|
// ============================================================
|
|
|
|
static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx) {
|
|
MenuItemT *item = &menu->items[itemIdx];
|
|
|
|
if (item->type == MenuItemCheckE) {
|
|
item->checked = !item->checked;
|
|
} else if (item->type == MenuItemRadioE) {
|
|
// Uncheck all radio items in the same group (consecutive radio items)
|
|
// Search backward to find group start
|
|
int32_t groupStart = itemIdx;
|
|
|
|
while (groupStart > 0 && menu->items[groupStart - 1].type == MenuItemRadioE) {
|
|
groupStart--;
|
|
}
|
|
|
|
// Search forward to find group end
|
|
int32_t groupEnd = itemIdx;
|
|
|
|
while (groupEnd < menu->itemCount - 1 && menu->items[groupEnd + 1].type == MenuItemRadioE) {
|
|
groupEnd++;
|
|
}
|
|
|
|
// Uncheck all in group, check the clicked one
|
|
for (int32_t i = groupStart; i <= groupEnd; i++) {
|
|
menu->items[i].checked = (i == itemIdx);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// closeAllPopups — dirty all popup levels and deactivate
|
|
// ============================================================
|
|
|
|
static void closeAllPopups(AppContextT *ctx) {
|
|
if (!ctx->popup.active) {
|
|
return;
|
|
}
|
|
|
|
// Dirty current (deepest) level
|
|
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
|
|
ctx->popup.popupW, ctx->popup.popupH);
|
|
|
|
// Dirty all parent levels
|
|
for (int32_t i = 0; i < ctx->popup.depth; i++) {
|
|
PopupLevelT *pl = &ctx->popup.parentStack[i];
|
|
dirtyListAdd(&ctx->dirty, pl->popupX, pl->popupY, pl->popupW, pl->popupH);
|
|
}
|
|
|
|
ctx->popup.active = false;
|
|
ctx->popup.depth = 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// closePopupLevel — close one submenu level (or deactivate if top)
|
|
// ============================================================
|
|
|
|
static void closePopupLevel(AppContextT *ctx) {
|
|
if (!ctx->popup.active) {
|
|
return;
|
|
}
|
|
|
|
// Dirty current level
|
|
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
|
|
ctx->popup.popupW, ctx->popup.popupH);
|
|
|
|
if (ctx->popup.depth > 0) {
|
|
// Pop parent
|
|
ctx->popup.depth--;
|
|
PopupLevelT *pl = &ctx->popup.parentStack[ctx->popup.depth];
|
|
ctx->popup.menu = pl->menu;
|
|
ctx->popup.menuIdx = pl->menuIdx;
|
|
ctx->popup.popupX = pl->popupX;
|
|
ctx->popup.popupY = pl->popupY;
|
|
ctx->popup.popupW = pl->popupW;
|
|
ctx->popup.popupH = pl->popupH;
|
|
ctx->popup.hoverItem = pl->hoverItem;
|
|
} else {
|
|
ctx->popup.active = false;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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 (all levels)
|
|
if (ctx->popup.active) {
|
|
// Draw parent levels first (bottom to top)
|
|
for (int32_t lvl = 0; lvl < ctx->popup.depth; lvl++) {
|
|
PopupLevelT *pl = &ctx->popup.parentStack[lvl];
|
|
drawPopupLevel(ctx, d, ops, pl->menu, pl->popupX, pl->popupY, pl->popupW, pl->popupH, pl->hoverItem, dr);
|
|
}
|
|
|
|
// Draw current (deepest) level
|
|
drawPopupLevel(ctx, d, ops, ctx->popup.menu, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH, ctx->popup.hoverItem, dr);
|
|
}
|
|
|
|
// 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)) {
|
|
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 tooltip
|
|
if (ctx->tooltipText) {
|
|
RectT ttRect = { ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH };
|
|
RectT ttIsect;
|
|
|
|
if (rectIntersect(dr, &ttRect, &ttIsect)) {
|
|
rectFill(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.menuBg);
|
|
drawHLine(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->colors.contentFg);
|
|
drawHLine(d, ops, ctx->tooltipX, ctx->tooltipY + ctx->tooltipH - 1, ctx->tooltipW, ctx->colors.contentFg);
|
|
drawVLine(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipH, ctx->colors.contentFg);
|
|
drawVLine(d, ops, ctx->tooltipX + ctx->tooltipW - 1, ctx->tooltipY, ctx->tooltipH, ctx->colors.contentFg);
|
|
drawText(d, ops, &ctx->font, ctx->tooltipX + TOOLTIP_PAD, ctx->tooltipY + TOOLTIP_PAD, ctx->tooltipText, ctx->colors.menuFg, ctx->colors.menuBg, true);
|
|
}
|
|
}
|
|
|
|
// 6. 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++) {
|
|
if (win->menuBar->menus[i].accelKey == key) {
|
|
openPopupAtMenu(ctx, win, i);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check widget tree
|
|
if (win->widgetRoot) {
|
|
WidgetT *target = widgetFindByAccel(win->widgetRoot, key);
|
|
|
|
if (target) {
|
|
switch (target->type) {
|
|
case WidgetButtonE:
|
|
if (sFocusedWidget) { sFocusedWidget->focused = false; }
|
|
sFocusedWidget = target;
|
|
target->focused = true;
|
|
target->as.button.pressed = true;
|
|
sKeyPressedBtn = target;
|
|
wgtInvalidate(target);
|
|
return true;
|
|
|
|
case WidgetCheckboxE:
|
|
if (sFocusedWidget) { sFocusedWidget->focused = false; }
|
|
widgetCheckboxOnMouse(target, win->widgetRoot, 0, 0);
|
|
sFocusedWidget = target;
|
|
wgtInvalidate(target);
|
|
return true;
|
|
|
|
case WidgetRadioE:
|
|
if (sFocusedWidget) { sFocusedWidget->focused = false; }
|
|
widgetRadioOnMouse(target, win->widgetRoot, 0, 0);
|
|
sFocusedWidget = target;
|
|
wgtInvalidate(target);
|
|
return true;
|
|
|
|
case WidgetImageButtonE:
|
|
if (sFocusedWidget) { sFocusedWidget->focused = false; }
|
|
sFocusedWidget = target;
|
|
target->focused = true;
|
|
target->as.imageButton.pressed = true;
|
|
sKeyPressedBtn = 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;
|
|
if (sFocusedWidget) { sFocusedWidget->focused = false; }
|
|
sFocusedWidget = target;
|
|
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;
|
|
if (sFocusedWidget) { sFocusedWidget->focused = false; }
|
|
sFocusedWidget = target;
|
|
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) {
|
|
if (sFocusedWidget) { sFocusedWidget->focused = false; }
|
|
sFocusedWidget = next;
|
|
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)) {
|
|
if (sFocusedWidget) { sFocusedWidget->focused = false; }
|
|
sFocusedWidget = target;
|
|
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 (with cascading submenu support)
|
|
if (ctx->popup.active) {
|
|
// Check if mouse is inside current (deepest) popup level
|
|
bool inCurrent = (mx >= ctx->popup.popupX && mx < ctx->popup.popupX + ctx->popup.popupW &&
|
|
my >= ctx->popup.popupY && my < ctx->popup.popupY + ctx->popup.popupH);
|
|
|
|
if (inCurrent) {
|
|
// Hover tracking in current level
|
|
int32_t relY = my - ctx->popup.popupY - 2;
|
|
int32_t itemIdx = relY / ctx->font.charHeight;
|
|
|
|
if (itemIdx < 0) {
|
|
itemIdx = 0;
|
|
}
|
|
|
|
if (ctx->popup.menu && itemIdx >= ctx->popup.menu->itemCount) {
|
|
itemIdx = ctx->popup.menu->itemCount - 1;
|
|
}
|
|
|
|
if (itemIdx != ctx->popup.hoverItem) {
|
|
int32_t prevHover = ctx->popup.hoverItem;
|
|
ctx->popup.hoverItem = itemIdx;
|
|
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
|
|
ctx->popup.popupW, ctx->popup.popupH);
|
|
|
|
// If hovering a submenu item, open the submenu
|
|
if (ctx->popup.menu && itemIdx >= 0 && itemIdx < ctx->popup.menu->itemCount) {
|
|
MenuItemT *hItem = &ctx->popup.menu->items[itemIdx];
|
|
|
|
if (hItem->subMenu && hItem->enabled) {
|
|
openSubMenu(ctx);
|
|
}
|
|
}
|
|
|
|
// If we moved away from a submenu item to a non-submenu item,
|
|
// close any child submenus that were opened from this level
|
|
if (prevHover >= 0 && ctx->popup.menu && prevHover < ctx->popup.menu->itemCount) {
|
|
// Already handled: openSubMenu replaces the child, and if current
|
|
// item is not a submenu, no child opens. But we may still have
|
|
// a stale child — check if depth was increased by a previous hover.
|
|
// This case is handled below by the parent-level hit test popping levels.
|
|
}
|
|
}
|
|
|
|
// Click on item in current level
|
|
if ((buttons & 1) && !(prevBtn & 1)) {
|
|
if (ctx->popup.menu && itemIdx >= 0 && itemIdx < ctx->popup.menu->itemCount) {
|
|
MenuItemT *item = &ctx->popup.menu->items[itemIdx];
|
|
|
|
if (item->subMenu && item->enabled) {
|
|
// Clicking a submenu item opens it (already open from hover, but ensure)
|
|
openSubMenu(ctx);
|
|
} else if (item->enabled && !item->separator) {
|
|
// Toggle check/radio state before closing
|
|
if (item->type == MenuItemCheckE || item->type == MenuItemRadioE) {
|
|
clickMenuCheckRadio(ctx->popup.menu, itemIdx);
|
|
}
|
|
|
|
// Close popup BEFORE calling onMenu (onMenu may run a nested event loop)
|
|
int32_t menuId = item->id;
|
|
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
|
closeAllPopups(ctx);
|
|
|
|
if (win && win->onMenu) {
|
|
win->onMenu(win, menuId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Mouse is not in current popup — check parent levels (deepest first)
|
|
bool inParent = false;
|
|
|
|
for (int32_t lvl = ctx->popup.depth - 1; lvl >= 0; lvl--) {
|
|
PopupLevelT *pl = &ctx->popup.parentStack[lvl];
|
|
|
|
if (mx >= pl->popupX && mx < pl->popupX + pl->popupW &&
|
|
my >= pl->popupY && my < pl->popupY + pl->popupH) {
|
|
|
|
// Close all levels deeper than this one
|
|
while (ctx->popup.depth > lvl + 1) {
|
|
closePopupLevel(ctx);
|
|
}
|
|
|
|
// Now close the level that was the "current" when we entered this parent
|
|
closePopupLevel(ctx);
|
|
|
|
// Now current level IS this parent — update hover
|
|
int32_t relY = my - ctx->popup.popupY - 2;
|
|
int32_t itemIdx = relY / ctx->font.charHeight;
|
|
|
|
if (itemIdx < 0) {
|
|
itemIdx = 0;
|
|
}
|
|
|
|
if (ctx->popup.menu && itemIdx >= ctx->popup.menu->itemCount) {
|
|
itemIdx = ctx->popup.menu->itemCount - 1;
|
|
}
|
|
|
|
ctx->popup.hoverItem = itemIdx;
|
|
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
|
|
ctx->popup.popupW, ctx->popup.popupH);
|
|
|
|
// If the newly hovered item has a submenu, open it
|
|
if (ctx->popup.menu && itemIdx >= 0 && itemIdx < ctx->popup.menu->itemCount) {
|
|
MenuItemT *hItem = &ctx->popup.menu->items[itemIdx];
|
|
|
|
if (hItem->subMenu && hItem->enabled) {
|
|
openSubMenu(ctx);
|
|
}
|
|
}
|
|
|
|
inParent = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!inParent) {
|
|
if (ctx->popup.isContextMenu) {
|
|
// Context menu: any click outside closes it
|
|
if ((buttons & 1) && !(prevBtn & 1)) {
|
|
closeAllPopups(ctx);
|
|
}
|
|
} else {
|
|
// Menu bar popup: check if mouse is on the menu bar for switching
|
|
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
|
|
|
if (win && win->menuBar) {
|
|
int32_t barY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT;
|
|
|
|
if (my >= barY && my < barY + CHROME_MENU_HEIGHT &&
|
|
mx >= win->x + CHROME_TOTAL_SIDE &&
|
|
mx < win->x + win->w - CHROME_TOTAL_SIDE) {
|
|
|
|
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) {
|
|
if (i != ctx->popup.menuIdx || ctx->popup.depth > 0) {
|
|
openPopupAtMenu(ctx, win, i);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
} else if ((buttons & 1) && !(prevBtn & 1)) {
|
|
closeAllPopups(ctx);
|
|
}
|
|
} else if ((buttons & 1) && !(prevBtn & 1)) {
|
|
closeAllPopups(ctx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Handle left button press
|
|
if ((buttons & 1) && !(prevBtn & 1)) {
|
|
handleMouseButton(ctx, mx, my, buttons);
|
|
}
|
|
|
|
// Handle right button press — context menus
|
|
if ((buttons & 2) && !(prevBtn & 2)) {
|
|
int32_t hitPart;
|
|
int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);
|
|
|
|
if (hitIdx >= 0 && hitPart == 0) {
|
|
WindowT *win = ctx->stack.windows[hitIdx];
|
|
|
|
// Raise and focus if not already
|
|
if (hitIdx != ctx->stack.focusedIdx) {
|
|
wmRaiseWindow(&ctx->stack, &ctx->dirty, hitIdx);
|
|
hitIdx = ctx->stack.count - 1;
|
|
wmSetFocus(&ctx->stack, &ctx->dirty, hitIdx);
|
|
win = ctx->stack.windows[hitIdx];
|
|
}
|
|
|
|
// Check widget context menu first
|
|
MenuT *ctxMenu = NULL;
|
|
|
|
if (win->widgetRoot) {
|
|
int32_t relX = mx - win->x - win->contentX;
|
|
int32_t relY = my - win->y - win->contentY;
|
|
WidgetT *hit = widgetHitTest(win->widgetRoot, relX, relY);
|
|
|
|
// Walk up the tree to find a context menu
|
|
while (hit && !hit->contextMenu) {
|
|
hit = hit->parent;
|
|
}
|
|
|
|
if (hit) {
|
|
ctxMenu = hit->contextMenu;
|
|
}
|
|
}
|
|
|
|
// Fall back to window context menu
|
|
if (!ctxMenu) {
|
|
ctxMenu = win->contextMenu;
|
|
}
|
|
|
|
if (ctxMenu) {
|
|
openContextMenu(ctx, win, ctxMenu, mx, my);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// drawPopupLevel — draw one popup menu (bevel + items)
|
|
// ============================================================
|
|
|
|
static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, const MenuT *menu, int32_t px, int32_t py, int32_t pw, int32_t ph, int32_t hoverItem, const RectT *clipTo) {
|
|
RectT popRect = { px, py, pw, ph };
|
|
RectT popIsect;
|
|
|
|
if (!rectIntersect(clipTo, &popRect, &popIsect)) {
|
|
return;
|
|
}
|
|
|
|
// Detect if menu has check/radio items (for left margin)
|
|
bool hasCheck = false;
|
|
|
|
for (int32_t k = 0; k < menu->itemCount; k++) {
|
|
if (menu->items[k].type == MenuItemCheckE || menu->items[k].type == MenuItemRadioE) {
|
|
hasCheck = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
int32_t checkMargin = hasCheck ? MENU_CHECK_WIDTH : 0;
|
|
|
|
// 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, px, py, pw, ph, &popBevel);
|
|
|
|
// Draw menu items
|
|
int32_t itemY = py + 2;
|
|
|
|
for (int32_t k = 0; k < menu->itemCount; k++) {
|
|
const MenuItemT *item = &menu->items[k];
|
|
|
|
if (item->separator) {
|
|
drawHLine(d, ops, px + 2, itemY + ctx->font.charHeight / 2, pw - 4, ctx->colors.windowShadow);
|
|
itemY += ctx->font.charHeight;
|
|
continue;
|
|
}
|
|
|
|
uint32_t bg = ctx->colors.menuBg;
|
|
uint32_t fg = ctx->colors.menuFg;
|
|
|
|
if (k == hoverItem) {
|
|
bg = ctx->colors.menuHighlightBg;
|
|
fg = ctx->colors.menuHighlightFg;
|
|
}
|
|
|
|
rectFill(d, ops, px + 2, itemY, pw - 4, ctx->font.charHeight, bg);
|
|
drawTextAccel(d, ops, &ctx->font, px + CHROME_TITLE_PAD + 2 + checkMargin, itemY, item->label, fg, bg, true);
|
|
|
|
// Draw check/radio indicator
|
|
if (item->checked) {
|
|
int32_t cy = itemY + ctx->font.charHeight / 2;
|
|
int32_t cx = px + 2 + MENU_CHECK_WIDTH / 2;
|
|
|
|
if (item->type == MenuItemCheckE) {
|
|
// Checkmark: small tick shape
|
|
drawVLine(d, ops, cx - 2, cy - 1, 2, fg);
|
|
drawVLine(d, ops, cx - 1, cy, 2, fg);
|
|
drawVLine(d, ops, cx, cy + 1, 2, fg);
|
|
drawVLine(d, ops, cx + 1, cy, 2, fg);
|
|
drawVLine(d, ops, cx + 2, cy - 1, 2, fg);
|
|
drawVLine(d, ops, cx + 3, cy - 2, 2, fg);
|
|
} else if (item->type == MenuItemRadioE) {
|
|
// Filled diamond bullet (5x5)
|
|
drawHLine(d, ops, cx, cy - 2, 1, fg);
|
|
drawHLine(d, ops, cx - 1, cy - 1, 3, fg);
|
|
drawHLine(d, ops, cx - 2, cy, 5, fg);
|
|
drawHLine(d, ops, cx - 1, cy + 1, 3, fg);
|
|
drawHLine(d, ops, cx, cy + 2, 1, fg);
|
|
}
|
|
}
|
|
|
|
// Draw submenu arrow indicator
|
|
if (item->subMenu) {
|
|
int32_t arrowX = px + pw - SUBMENU_ARROW_WIDTH - 2;
|
|
int32_t arrowY = itemY + ctx->font.charHeight / 2;
|
|
|
|
for (int32_t row = -3; row <= 3; row++) {
|
|
int32_t len = 4 - (row < 0 ? -row : row);
|
|
|
|
if (len > 0) {
|
|
drawHLine(d, ops, arrowX + (row < 0 ? 3 + row : 3 - row), arrowY + row, len, fg);
|
|
}
|
|
}
|
|
}
|
|
|
|
itemY += ctx->font.charHeight;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxAddAccel
|
|
// ============================================================
|
|
|
|
void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId) {
|
|
if (!table || table->count >= MAX_ACCEL_ENTRIES) {
|
|
return;
|
|
}
|
|
|
|
AccelEntryT *e = &table->entries[table->count++];
|
|
e->key = key;
|
|
e->modifiers = modifiers;
|
|
e->cmdId = cmdId;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxClipboardCopy
|
|
// ============================================================
|
|
|
|
void dvxClipboardCopy(const char *text, int32_t len) {
|
|
clipboardCopy(text, len);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxClipboardGet
|
|
// ============================================================
|
|
|
|
const char *dvxClipboardGet(int32_t *outLen) {
|
|
return clipboardGet(outLen);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxCreateAccelTable
|
|
// ============================================================
|
|
|
|
AccelTableT *dvxCreateAccelTable(void) {
|
|
AccelTableT *table = (AccelTableT *)calloc(1, sizeof(AccelTableT));
|
|
return table;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxFreeAccelTable
|
|
// ============================================================
|
|
|
|
void dvxFreeAccelTable(AccelTableT *table) {
|
|
free(table);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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));
|
|
|
|
// Disable Ctrl+C/Break termination
|
|
signal(SIGINT, SIG_IGN);
|
|
|
|
// 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);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxMaximizeWindow
|
|
// ============================================================
|
|
|
|
void dvxMaximizeWindow(AppContextT *ctx, WindowT *win) {
|
|
wmMaximize(&ctx->stack, &ctx->dirty, &ctx->display, win);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dvxMinimizeWindow
|
|
// ============================================================
|
|
|
|
void dvxMinimizeWindow(AppContextT *ctx, WindowT *win) {
|
|
wmMinimize(&ctx->stack, &ctx->dirty, win);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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);
|
|
updateTooltip(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 key-pressed button (one frame of animation)
|
|
if (sKeyPressedBtn) {
|
|
if (sKeyPressedBtn->type == WidgetImageButtonE) {
|
|
sKeyPressedBtn->as.imageButton.pressed = false;
|
|
} else {
|
|
sKeyPressedBtn->as.button.pressed = false;
|
|
}
|
|
|
|
if (sKeyPressedBtn->onClick) {
|
|
sKeyPressedBtn->onClick(sKeyPressedBtn);
|
|
}
|
|
|
|
wgtInvalidate(sKeyPressedBtn);
|
|
sKeyPressedBtn = 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:
|
|
if (ctx->modalWindow != win) {
|
|
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) {
|
|
// Modal window gating: only the modal window receives clicks
|
|
if (ctx->modalWindow) {
|
|
int32_t hitPart;
|
|
int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);
|
|
|
|
if (hitIdx >= 0 && ctx->stack.windows[hitIdx] != ctx->modalWindow) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 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
|
|
{
|
|
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) {
|
|
openPopupAtMenu(ctx, win, i);
|
|
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 (not allowed for modal windows)
|
|
if (ctx->modalWindow != win) {
|
|
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);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// openContextMenu — open a context menu at a screen position
|
|
// ============================================================
|
|
|
|
static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY) {
|
|
if (!menu || menu->itemCount <= 0) {
|
|
return;
|
|
}
|
|
|
|
closeAllPopups(ctx);
|
|
closeSysMenu(ctx);
|
|
|
|
ctx->popup.active = true;
|
|
ctx->popup.isContextMenu = true;
|
|
ctx->popup.windowId = win->id;
|
|
ctx->popup.menuIdx = -1;
|
|
ctx->popup.menu = menu;
|
|
ctx->popup.hoverItem = -1;
|
|
ctx->popup.depth = 0;
|
|
|
|
calcPopupSize(ctx, menu, &ctx->popup.popupW, &ctx->popup.popupH);
|
|
|
|
// Position at mouse, clamped to screen
|
|
ctx->popup.popupX = screenX;
|
|
ctx->popup.popupY = screenY;
|
|
|
|
if (ctx->popup.popupX + ctx->popup.popupW > ctx->display.width) {
|
|
ctx->popup.popupX = ctx->display.width - ctx->popup.popupW;
|
|
}
|
|
|
|
if (ctx->popup.popupY + ctx->popup.popupH > ctx->display.height) {
|
|
ctx->popup.popupY = ctx->display.height - ctx->popup.popupH;
|
|
}
|
|
|
|
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
|
|
ctx->popup.popupW, ctx->popup.popupH);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// openPopupAtMenu — open top-level popup for a menu bar menu
|
|
// ============================================================
|
|
|
|
static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx) {
|
|
if (!win->menuBar || menuIdx < 0 || menuIdx >= win->menuBar->menuCount) {
|
|
return;
|
|
}
|
|
|
|
// Close any existing popup chain first
|
|
closeAllPopups(ctx);
|
|
|
|
MenuT *menu = &win->menuBar->menus[menuIdx];
|
|
|
|
ctx->popup.active = true;
|
|
ctx->popup.isContextMenu = false;
|
|
ctx->popup.windowId = win->id;
|
|
ctx->popup.menuIdx = menuIdx;
|
|
ctx->popup.menu = menu;
|
|
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;
|
|
ctx->popup.depth = 0;
|
|
|
|
calcPopupSize(ctx, menu, &ctx->popup.popupW, &ctx->popup.popupH);
|
|
|
|
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
|
|
ctx->popup.popupW, ctx->popup.popupH);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// openSubMenu — open submenu for the currently hovered item
|
|
// ============================================================
|
|
|
|
static void openSubMenu(AppContextT *ctx) {
|
|
if (!ctx->popup.active || !ctx->popup.menu) {
|
|
return;
|
|
}
|
|
|
|
int32_t idx = ctx->popup.hoverItem;
|
|
|
|
if (idx < 0 || idx >= ctx->popup.menu->itemCount) {
|
|
return;
|
|
}
|
|
|
|
MenuItemT *item = &ctx->popup.menu->items[idx];
|
|
|
|
if (!item->subMenu) {
|
|
return;
|
|
}
|
|
|
|
if (ctx->popup.depth >= MAX_SUBMENU_DEPTH) {
|
|
return;
|
|
}
|
|
|
|
// Push current state to parent stack
|
|
PopupLevelT *pl = &ctx->popup.parentStack[ctx->popup.depth];
|
|
pl->menu = ctx->popup.menu;
|
|
pl->menuIdx = ctx->popup.menuIdx;
|
|
pl->popupX = ctx->popup.popupX;
|
|
pl->popupY = ctx->popup.popupY;
|
|
pl->popupW = ctx->popup.popupW;
|
|
pl->popupH = ctx->popup.popupH;
|
|
pl->hoverItem = ctx->popup.hoverItem;
|
|
ctx->popup.depth++;
|
|
|
|
// Open submenu at right edge of current popup, aligned with hovered item
|
|
ctx->popup.menu = item->subMenu;
|
|
ctx->popup.popupX = pl->popupX + pl->popupW - 2;
|
|
ctx->popup.popupY = pl->popupY + 2 + idx * ctx->font.charHeight;
|
|
ctx->popup.hoverItem = -1;
|
|
|
|
calcPopupSize(ctx, ctx->popup.menu, &ctx->popup.popupW, &ctx->popup.popupH);
|
|
|
|
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
|
|
ctx->popup.popupW, ctx->popup.popupH);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// openSysMenu
|
|
// ============================================================
|
|
|
|
static void openSysMenu(AppContextT *ctx, WindowT *win) {
|
|
// Close any existing popup menus first
|
|
closeAllPopups(ctx);
|
|
|
|
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 — not available on modal windows
|
|
item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
|
|
strncpy(item->label, "Mi&nimize", MAX_MENU_LABEL - 1);
|
|
item->cmd = SysMenuMinimizeE;
|
|
item->separator = false;
|
|
item->enabled = !win->modal;
|
|
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);
|
|
|
|
int32_t dirtyY = 0;
|
|
int32_t dirtyH = 0;
|
|
|
|
if (wgtAnsiTermRepaint(w, &dirtyY, &dirtyH) > 0) {
|
|
win->contentDirty = true;
|
|
|
|
// Dirty only the affected rows (in screen coords) instead of
|
|
// the entire window. This dramatically reduces compositor and
|
|
// LFB flush work for cursor blink and single-row updates.
|
|
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
|
|
int32_t rectX = win->x + win->contentX;
|
|
int32_t rectY = win->y + win->contentY + dirtyY - scrollY;
|
|
int32_t rectW = win->contentW;
|
|
dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH);
|
|
}
|
|
}
|
|
|
|
for (WidgetT *child = w->firstChild; child; child = child->nextSibling) {
|
|
pollAnsiTermWidgetsWalk(ctx, child, win);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// pollKeyboard
|
|
// ============================================================
|
|
|
|
static void pollKeyboard(AppContextT *ctx) {
|
|
__dpmi_regs r;
|
|
|
|
// Read shift state once per poll (INT 16h, AH=12h)
|
|
memset(&r, 0, sizeof(r));
|
|
r.x.ax = 0x1200;
|
|
__dpmi_int(0x16, &r);
|
|
int32_t shiftFlags = r.x.ax & 0xFF;
|
|
ctx->keyModifiers = shiftFlags;
|
|
bool shiftHeld = (shiftFlags & 0x03) != 0; // left or right shift
|
|
|
|
// Process buffered keys
|
|
while (1) {
|
|
// Check if key is available (INT 16h, enhanced function 11h)
|
|
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)
|
|
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;
|
|
}
|
|
|
|
// Alt+Tab / Shift+Alt+Tab — cycle windows
|
|
// Alt+Tab: scancode=0xA5, ascii=0x00
|
|
if (ascii == 0 && scancode == 0xA5) {
|
|
if (ctx->stack.count > 1) {
|
|
if (shiftHeld) {
|
|
// Shift+Alt+Tab — focus the bottom window, raise it
|
|
wmRaiseWindow(&ctx->stack, &ctx->dirty, 0);
|
|
wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1);
|
|
} else {
|
|
// Alt+Tab — send top window to bottom, focus new top
|
|
WindowT *top = ctx->stack.windows[ctx->stack.count - 1];
|
|
dirtyListAdd(&ctx->dirty, top->x, top->y, top->w, top->h);
|
|
|
|
// Shift all windows up
|
|
for (int32_t i = ctx->stack.count - 1; i > 0; i--) {
|
|
ctx->stack.windows[i] = ctx->stack.windows[i - 1];
|
|
}
|
|
|
|
ctx->stack.windows[0] = top;
|
|
top->focused = false;
|
|
|
|
// Focus the new top window
|
|
wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1);
|
|
dirtyListAdd(&ctx->dirty, ctx->stack.windows[ctx->stack.count - 1]->x,
|
|
ctx->stack.windows[ctx->stack.count - 1]->y,
|
|
ctx->stack.windows[ctx->stack.count - 1]->w,
|
|
ctx->stack.windows[ctx->stack.count - 1]->h);
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Alt+F4 — close focused window
|
|
if (ascii == 0 && scancode == 0x6B) {
|
|
if (ctx->stack.focusedIdx >= 0) {
|
|
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
|
|
|
|
if (win->onClose) {
|
|
win->onClose(win);
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// F10 — activate menu bar
|
|
if (ascii == 0 && scancode == 0x44) {
|
|
if (ctx->stack.focusedIdx >= 0) {
|
|
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
|
|
|
|
if (win->menuBar && win->menuBar->menuCount > 0) {
|
|
if (ctx->popup.active) {
|
|
closeAllPopups(ctx);
|
|
} else {
|
|
dispatchAccelKey(ctx, win->menuBar->menus[0].accelKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// 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) {
|
|
MenuT *curMenu = ctx->popup.menu;
|
|
|
|
// Up arrow
|
|
if (scancode == 0x48) {
|
|
if (curMenu && curMenu->itemCount > 0) {
|
|
int32_t idx = ctx->popup.hoverItem;
|
|
|
|
for (int32_t tries = 0; tries < curMenu->itemCount; tries++) {
|
|
idx--;
|
|
|
|
if (idx < 0) {
|
|
idx = curMenu->itemCount - 1;
|
|
}
|
|
|
|
if (!curMenu->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) {
|
|
if (curMenu && curMenu->itemCount > 0) {
|
|
int32_t idx = ctx->popup.hoverItem;
|
|
|
|
for (int32_t tries = 0; tries < curMenu->itemCount; tries++) {
|
|
idx++;
|
|
|
|
if (idx >= curMenu->itemCount) {
|
|
idx = 0;
|
|
}
|
|
|
|
if (!curMenu->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 — close submenu, or switch to previous top-level menu
|
|
if (scancode == 0x4B) {
|
|
if (ctx->popup.depth > 0) {
|
|
closePopupLevel(ctx);
|
|
} else {
|
|
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;
|
|
}
|
|
|
|
openPopupAtMenu(ctx, win, newIdx);
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Right arrow — open submenu, or switch to next top-level menu
|
|
if (scancode == 0x4D) {
|
|
// If hovered item has a submenu, open it
|
|
if (curMenu && ctx->popup.hoverItem >= 0 && ctx->popup.hoverItem < curMenu->itemCount) {
|
|
MenuItemT *hItem = &curMenu->items[ctx->popup.hoverItem];
|
|
|
|
if (hItem->subMenu && hItem->enabled) {
|
|
openSubMenu(ctx);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Otherwise switch to next top-level menu
|
|
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;
|
|
}
|
|
|
|
openPopupAtMenu(ctx, win, newIdx);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Enter executes highlighted popup menu item (or opens submenu)
|
|
if (ctx->popup.active && ascii == 0x0D) {
|
|
MenuT *curMenu = ctx->popup.menu;
|
|
|
|
if (curMenu && ctx->popup.hoverItem >= 0 && ctx->popup.hoverItem < curMenu->itemCount) {
|
|
MenuItemT *item = &curMenu->items[ctx->popup.hoverItem];
|
|
|
|
if (item->subMenu && item->enabled) {
|
|
openSubMenu(ctx);
|
|
} else if (item->enabled && !item->separator) {
|
|
int32_t menuId = item->id;
|
|
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
|
closeAllPopups(ctx);
|
|
|
|
if (win && win->onMenu) {
|
|
win->onMenu(win, menuId);
|
|
}
|
|
}
|
|
} else {
|
|
closeAllPopups(ctx);
|
|
}
|
|
|
|
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;
|
|
MenuT *curMenu = ctx->popup.menu;
|
|
|
|
// Try matching an item in the current popup
|
|
if (curMenu) {
|
|
for (int32_t k = 0; k < curMenu->itemCount; k++) {
|
|
MenuItemT *item = &curMenu->items[k];
|
|
|
|
if (item->accelKey == lc && item->enabled && !item->separator) {
|
|
if (item->subMenu) {
|
|
ctx->popup.hoverItem = k;
|
|
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
|
|
openSubMenu(ctx);
|
|
} else {
|
|
int32_t menuId = item->id;
|
|
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
|
closeAllPopups(ctx);
|
|
|
|
if (win && win->onMenu) {
|
|
win->onMenu(win, menuId);
|
|
}
|
|
}
|
|
|
|
goto nextKey;
|
|
}
|
|
}
|
|
}
|
|
|
|
// No match in current popup — try switching to another top-level menu
|
|
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
|
|
|
if (win && win->menuBar) {
|
|
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
|
|
if (win->menuBar->menus[i].accelKey == lc && i != ctx->popup.menuIdx) {
|
|
openPopupAtMenu(ctx, win, i);
|
|
goto nextKey;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 one popup level (or all if at top level)
|
|
if (ctx->popup.active && ascii == 0x1B) {
|
|
closePopupLevel(ctx);
|
|
continue;
|
|
}
|
|
|
|
// Check accelerator table on focused window
|
|
if (ctx->stack.focusedIdx >= 0) {
|
|
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
|
|
int32_t key = ascii ? ascii : (scancode | 0x100);
|
|
|
|
if (checkAccelTable(ctx, win, key, shiftFlags)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Tab / Shift-Tab — cycle focus between widgets
|
|
// Tab: scancode=0x0F, ascii=0x09
|
|
// Shift-Tab: scancode=0x0F, ascii=0x00
|
|
if (scancode == 0x0F && (ascii == 0x09 || ascii == 0)) {
|
|
if (ctx->stack.focusedIdx >= 0) {
|
|
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
|
|
|
|
if (win->widgetRoot) {
|
|
// Find currently focused widget
|
|
WidgetT *current = NULL;
|
|
WidgetT *fstack[64];
|
|
int32_t ftop = 0;
|
|
fstack[ftop++] = win->widgetRoot;
|
|
|
|
while (ftop > 0) {
|
|
WidgetT *w = fstack[--ftop];
|
|
|
|
if (w->focused && widgetIsFocusable(w->type)) {
|
|
// Don't tab out of the terminal — it swallows Tab
|
|
if (w->type == WidgetAnsiTermE) {
|
|
current = NULL;
|
|
break;
|
|
}
|
|
|
|
current = w;
|
|
break;
|
|
}
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (c->visible && ftop < 64) {
|
|
fstack[ftop++] = c;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Terminal swallowed Tab — send to widget system instead
|
|
if (current == NULL) {
|
|
// Check if a terminal is focused
|
|
ftop = 0;
|
|
fstack[ftop++] = win->widgetRoot;
|
|
bool termFocused = false;
|
|
|
|
while (ftop > 0) {
|
|
WidgetT *w = fstack[--ftop];
|
|
|
|
if (w->focused && w->type == WidgetAnsiTermE) {
|
|
termFocused = true;
|
|
break;
|
|
}
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (c->visible && ftop < 64) {
|
|
fstack[ftop++] = c;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (termFocused) {
|
|
// Terminal has focus — send Tab to it
|
|
if (win->onKey) {
|
|
win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
WidgetT *next;
|
|
|
|
if (ascii == 0x09) {
|
|
next = widgetFindNextFocusable(win->widgetRoot, current);
|
|
} else {
|
|
next = widgetFindPrevFocusable(win->widgetRoot, current);
|
|
}
|
|
|
|
if (next) {
|
|
sOpenPopup = NULL;
|
|
if (sFocusedWidget) {
|
|
sFocusedWidget->focused = false;
|
|
}
|
|
sFocusedWidget = next;
|
|
next->focused = true;
|
|
|
|
// Scroll the widget into view if needed
|
|
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
|
|
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
|
|
int32_t virtX = next->x + scrollX;
|
|
int32_t virtY = next->y + scrollY;
|
|
|
|
if (win->vScroll) {
|
|
if (virtY < win->vScroll->value) {
|
|
win->vScroll->value = virtY;
|
|
} else if (virtY + next->h > win->vScroll->value + win->contentH) {
|
|
win->vScroll->value = virtY + next->h - win->contentH;
|
|
}
|
|
|
|
win->vScroll->value = clampInt(win->vScroll->value, win->vScroll->min, win->vScroll->max);
|
|
}
|
|
|
|
if (win->hScroll) {
|
|
if (virtX < win->hScroll->value) {
|
|
win->hScroll->value = virtX;
|
|
} else if (virtX + next->w > win->hScroll->value + win->contentW) {
|
|
win->hScroll->value = virtX + next->w - win->contentW;
|
|
}
|
|
|
|
win->hScroll->value = clampInt(win->hScroll->value, win->hScroll->min, win->hScroll->max);
|
|
}
|
|
|
|
wgtInvalidate(win->widgetRoot);
|
|
}
|
|
}
|
|
}
|
|
|
|
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), shiftFlags);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
// Active ListView column resize drag
|
|
else if (sResizeListView) {
|
|
newCursor = CURSOR_RESIZE_H;
|
|
}
|
|
// Active splitter drag
|
|
else if (sDragSplitter) {
|
|
newCursor = sDragSplitter->as.splitter.vertical ? CURSOR_RESIZE_H : 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;
|
|
}
|
|
} else if (hitIdx >= 0 && hitPart == 0) {
|
|
// Hovering over content area — check for ListView column border
|
|
WindowT *win = ctx->stack.windows[hitIdx];
|
|
|
|
if (win->widgetRoot) {
|
|
int32_t cx = mx - win->x - win->contentX;
|
|
int32_t cy = my - win->y - win->contentY;
|
|
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
|
|
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
|
|
int32_t vx = cx + scrollX;
|
|
int32_t vy = cy + scrollY;
|
|
|
|
WidgetT *hit = widgetHitTest(win->widgetRoot, vx, vy);
|
|
|
|
if (hit && hit->type == WidgetListViewE && widgetListViewColBorderHit(hit, vx, vy)) {
|
|
newCursor = CURSOR_RESIZE_H;
|
|
}
|
|
|
|
// Walk into splitters (NO_HIT_RECURSE stops widgetHitTest at the outermost one)
|
|
while (hit && hit->type == WidgetSplitterE) {
|
|
int32_t pos = hit->as.splitter.dividerPos;
|
|
bool onBar;
|
|
|
|
if (hit->as.splitter.vertical) {
|
|
int32_t barX = hit->x + pos;
|
|
onBar = (vx >= barX && vx < barX + SPLITTER_BAR_W);
|
|
} else {
|
|
int32_t barY = hit->y + pos;
|
|
onBar = (vy >= barY && vy < barY + SPLITTER_BAR_W);
|
|
}
|
|
|
|
if (onBar) {
|
|
newCursor = hit->as.splitter.vertical ? CURSOR_RESIZE_H : CURSOR_RESIZE_V;
|
|
break;
|
|
}
|
|
|
|
// Not on this splitter's bar — check children for nested splitters
|
|
WidgetT *inner = NULL;
|
|
|
|
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
|
|
if (c->visible && vx >= c->x && vx < c->x + c->w && vy >= c->y && vy < c->y + c->h) {
|
|
inner = c;
|
|
}
|
|
}
|
|
|
|
hit = inner;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If cursor shape changed, dirty the cursor area
|
|
if (newCursor != ctx->cursorId) {
|
|
dirtyCursorArea(ctx, mx, my);
|
|
ctx->cursorId = newCursor;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// updateTooltip — show/hide tooltip based on hover state
|
|
// ============================================================
|
|
|
|
static void updateTooltip(AppContextT *ctx) {
|
|
clock_t now = clock();
|
|
clock_t threshold = (clock_t)TOOLTIP_DELAY_MS * CLOCKS_PER_SEC / 1000;
|
|
int32_t mx = ctx->mouseX;
|
|
int32_t my = ctx->mouseY;
|
|
|
|
// Mouse moved or button pressed — hide tooltip and reset timer
|
|
if (mx != ctx->prevMouseX || my != ctx->prevMouseY || ctx->mouseButtons) {
|
|
if (ctx->tooltipText) {
|
|
// Dirty old tooltip area
|
|
dirtyListAdd(&ctx->dirty, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH);
|
|
ctx->tooltipText = NULL;
|
|
}
|
|
|
|
ctx->tooltipHoverStart = now;
|
|
return;
|
|
}
|
|
|
|
// Already showing a tooltip
|
|
if (ctx->tooltipText) {
|
|
return;
|
|
}
|
|
|
|
// Not enough time has passed
|
|
if ((now - ctx->tooltipHoverStart) < threshold) {
|
|
return;
|
|
}
|
|
|
|
// Don't show tooltips while popups/menus are active
|
|
if (ctx->popup.active || ctx->sysMenu.active) {
|
|
return;
|
|
}
|
|
|
|
// Find the widget under the cursor
|
|
int32_t hitPart;
|
|
int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);
|
|
|
|
if (hitIdx < 0 || hitPart != 0) {
|
|
return;
|
|
}
|
|
|
|
WindowT *win = ctx->stack.windows[hitIdx];
|
|
|
|
if (!win->widgetRoot) {
|
|
return;
|
|
}
|
|
|
|
int32_t cx = mx - win->x - win->contentX;
|
|
int32_t cy = my - win->y - win->contentY;
|
|
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
|
|
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
|
|
int32_t vx = cx + scrollX;
|
|
int32_t vy = cy + scrollY;
|
|
|
|
WidgetT *hit = widgetHitTest(win->widgetRoot, vx, vy);
|
|
|
|
// Walk into NO_HIT_RECURSE containers to find deepest child
|
|
while (hit && hit->wclass && (hit->wclass->flags & WCLASS_NO_HIT_RECURSE)) {
|
|
WidgetT *inner = NULL;
|
|
|
|
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
|
|
if (c->visible && vx >= c->x && vx < c->x + c->w && vy >= c->y && vy < c->y + c->h) {
|
|
inner = c;
|
|
}
|
|
}
|
|
|
|
if (!inner) {
|
|
break;
|
|
}
|
|
|
|
// If the inner child has a tooltip, use it; otherwise check if it's another container
|
|
if (inner->tooltip) {
|
|
hit = inner;
|
|
break;
|
|
}
|
|
|
|
if (inner->wclass && (inner->wclass->flags & WCLASS_NO_HIT_RECURSE)) {
|
|
hit = inner;
|
|
} else {
|
|
WidgetT *deep = widgetHitTest(inner, vx, vy);
|
|
hit = deep ? deep : inner;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!hit || !hit->tooltip) {
|
|
return;
|
|
}
|
|
|
|
// Show the tooltip
|
|
ctx->tooltipText = hit->tooltip;
|
|
|
|
int32_t tw = textWidth(&ctx->font, hit->tooltip) + TOOLTIP_PAD * 2;
|
|
int32_t th = ctx->font.charHeight + TOOLTIP_PAD * 2;
|
|
|
|
// Position below and right of cursor
|
|
ctx->tooltipX = mx + 12;
|
|
ctx->tooltipY = my + 16;
|
|
|
|
// Keep on screen
|
|
if (ctx->tooltipX + tw > ctx->display.width) {
|
|
ctx->tooltipX = ctx->display.width - tw;
|
|
}
|
|
|
|
if (ctx->tooltipY + th > ctx->display.height) {
|
|
ctx->tooltipY = my - th - 4;
|
|
}
|
|
|
|
ctx->tooltipW = tw;
|
|
ctx->tooltipH = th;
|
|
|
|
// Dirty the tooltip area
|
|
dirtyListAdd(&ctx->dirty, ctx->tooltipX, ctx->tooltipY, tw, th);
|
|
}
|