From 14eca6fcd296f1c940409aceded0350d66f24216 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Wed, 11 Mar 2026 22:42:09 -0500 Subject: [PATCH] Working on keyboard control of the GUI. --- dvx/dvxApp.c | 902 ++++++++++++++++++++++++++++++++- dvx/dvxApp.h | 2 + dvx/dvxDraw.c | 136 +++++ dvx/dvxDraw.h | 10 + dvx/dvxTypes.h | 56 ++ dvx/dvxWidget.h | 1 + dvx/dvxWm.c | 8 +- dvx/widgets/widgetBox.c | 7 +- dvx/widgets/widgetButton.c | 13 +- dvx/widgets/widgetCheckbox.c | 11 +- dvx/widgets/widgetCore.c | 113 +++++ dvx/widgets/widgetInternal.h | 4 + dvx/widgets/widgetLabel.c | 9 +- dvx/widgets/widgetOps.c | 4 + dvx/widgets/widgetRadio.c | 11 +- dvx/widgets/widgetTabControl.c | 11 +- dvxdemo/demo.c | 353 +++++++++---- 17 files changed, 1507 insertions(+), 144 deletions(-) diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 44a0fc0..3f3c34a 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -7,23 +7,30 @@ #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); @@ -31,6 +38,39 @@ 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 @@ -141,9 +181,9 @@ static void compositeAndFlush(AppContextT *ctx) { rectFill(d, ops, ctx->popup.popupX + 2, itemY, ctx->popup.popupW - 4, ctx->font.charHeight, bg); - drawText(d, ops, &ctx->font, - ctx->popup.popupX + CHROME_TITLE_PAD + 2, itemY, - item->label, fg, bg, true); + drawTextAccel(d, ops, &ctx->font, + ctx->popup.popupX + CHROME_TITLE_PAD + 2, itemY, + item->label, fg, bg, true); itemY += ctx->font.charHeight; } @@ -155,10 +195,52 @@ static void compositeAndFlush(AppContextT *ctx) { } } - // 4. Draw cursor + // 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); - // 5. Flush this dirty rect to LFB + // 6. Flush this dirty rect to LFB flushRect(d, dr); } @@ -179,6 +261,171 @@ static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y) { } +// ============================================================ +// 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: + { + // 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 // ============================================================ @@ -228,6 +475,44 @@ static void dispatchEvents(AppContextT *ctx) { 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 @@ -555,6 +840,18 @@ bool dvxUpdate(AppContextT *ctx) { __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; @@ -595,6 +892,79 @@ int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path) { } +// ============================================================ +// 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 // ============================================================ @@ -655,13 +1025,14 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t wmDragBegin(&ctx->stack, hitIdx, mx, my); break; - case 2: // close button (double-click to close) + 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); @@ -671,6 +1042,7 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t } else { ctx->lastCloseClickTime = now; ctx->lastCloseClickId = win->id; + openSysMenu(ctx, win); } } break; @@ -707,7 +1079,7 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t int32_t maxW = 0; for (int32_t k = 0; k < menu->itemCount; k++) { - int32_t itemW = textWidth(&ctx->font, menu->items[k].label); + int32_t itemW = textWidthAccel(&ctx->font, menu->items[k].label); if (itemW > maxW) { maxW = itemW; @@ -822,6 +1194,107 @@ static void initMouse(AppContextT *ctx) { } +// ============================================================ +// 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 // ============================================================ @@ -864,10 +1337,10 @@ static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win) static void pollKeyboard(AppContextT *ctx) { __dpmi_regs r; - // Check if key is available (INT 16h, function 01h) + // Check if key is available (INT 16h, enhanced function 11h) while (1) { memset(&r, 0, sizeof(r)); - r.x.ax = 0x0100; + r.x.ax = 0x1100; __dpmi_int(0x16, &r); // Zero flag set = no key available @@ -875,14 +1348,418 @@ static void pollKeyboard(AppContextT *ctx) { break; } - // Read the key (INT 16h, function 00h) + // Read the key (INT 16h, enhanced function 10h) memset(&r, 0, sizeof(r)); - r.x.ax = 0x0000; + 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) { + if (sOpenPopup->type == WidgetDropdownE) { + sOpenPopup->as.dropdown.open = false; + } else if (sOpenPopup->type == WidgetComboBoxE) { + sOpenPopup->as.comboBox.open = false; + } + + wgtInvalidate(sOpenPopup); + sOpenPopup = NULL; + 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]; @@ -891,6 +1768,9 @@ static void pollKeyboard(AppContextT *ctx) { win->onKey(win, ascii ? ascii : (scancode | 0x100), 0); } } + + continue; +nextKey:; } } diff --git a/dvx/dvxApp.h b/dvx/dvxApp.h index c891718..20c0661 100644 --- a/dvx/dvxApp.h +++ b/dvx/dvxApp.h @@ -22,6 +22,8 @@ typedef struct AppContextT { BitmapFontT font; ColorSchemeT colors; PopupStateT popup; + SysMenuStateT sysMenu; + KbMoveResizeT kbMoveResize; CursorT cursors[5]; // indexed by CURSOR_xxx int32_t cursorId; // active cursor shape uint32_t cursorFg; // pre-packed cursor colors diff --git a/dvx/dvxDraw.c b/dvx/dvxDraw.c index 3febdc5..0e24744 100644 --- a/dvx/dvxDraw.c +++ b/dvx/dvxDraw.c @@ -8,6 +8,7 @@ // Prototypes // ============================================================ +char accelParse(const char *text); static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *w, int32_t *h); static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp); static void spanCopy8(uint8_t *dst, const uint8_t *src, int32_t count); @@ -18,6 +19,53 @@ static void spanFill16(uint8_t *dst, uint32_t color, int32_t count); static void spanFill32(uint8_t *dst, uint32_t color, int32_t count); +// ============================================================ +// accelParse +// ============================================================ + +char accelParse(const char *text) { + if (!text) { + return 0; + } + + while (*text) { + if (*text == '&') { + text++; + + if (*text == '&') { + // Escaped && — literal &, not an accelerator + text++; + continue; + } + + if (*text && *text != '&') { + char ch = *text; + + if (ch >= 'A' && ch <= 'Z') { + return (char)(ch + 32); + } + + if (ch >= 'a' && ch <= 'z') { + return ch; + } + + if (ch >= '0' && ch <= '9') { + return ch; + } + + return ch; + } + + break; + } + + text++; + } + + return 0; +} + + // ============================================================ // clipRect // ============================================================ @@ -353,6 +401,58 @@ void drawText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t } +// ============================================================ +// drawTextAccel +// ============================================================ + +void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque) { + int32_t cw = font->charWidth; + int32_t clipX2 = d->clipX + d->clipW; + + while (*text) { + if (__builtin_expect(x >= clipX2, 0)) { + break; + } + + if (*text == '&') { + text++; + + if (*text == '&') { + // Escaped && — draw literal & + if (x + cw > d->clipX) { + drawChar(d, ops, font, x, y, '&', fg, bg, opaque); + } + + x += cw; + text++; + continue; + } + + if (*text) { + // Accelerator character — draw it then underline + if (x + cw > d->clipX) { + drawChar(d, ops, font, x, y, *text, fg, bg, opaque); + drawHLine(d, ops, x, y + font->charHeight - 1, cw, fg); + } + + x += cw; + text++; + continue; + } + + break; + } + + if (x + cw > d->clipX) { + drawChar(d, ops, font, x, y, *text, fg, bg, opaque); + } + + x += cw; + text++; + } +} + + // ============================================================ // drawVLine // ============================================================ @@ -640,3 +740,39 @@ int32_t textWidth(const BitmapFontT *font, const char *text) { return w; } + + +// ============================================================ +// textWidthAccel +// ============================================================ + +int32_t textWidthAccel(const BitmapFontT *font, const char *text) { + int32_t w = 0; + + while (*text) { + if (*text == '&') { + text++; + + if (*text == '&') { + // Escaped && — counts as one character + w += font->charWidth; + text++; + continue; + } + + if (*text) { + // Accelerator character — counts as one character, & is skipped + w += font->charWidth; + text++; + continue; + } + + break; + } + + w += font->charWidth; + text++; + } + + return w; +} diff --git a/dvx/dvxDraw.h b/dvx/dvxDraw.h index 2615ecd..0be84cf 100644 --- a/dvx/dvxDraw.h +++ b/dvx/dvxDraw.h @@ -25,6 +25,16 @@ void drawText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t // Measure text width in pixels int32_t textWidth(const BitmapFontT *font, const char *text); +// Parse accelerator key from text with & markers (e.g. "E&xit" -> 'x') +// Returns the lowercase accelerator character, or 0 if none found. +char accelParse(const char *text); + +// Draw text with & accelerator processing (underlines the accelerator character) +void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque); + +// Measure text width in pixels, skipping & markers +int32_t textWidthAccel(const BitmapFontT *font, const char *text); + // Draw a 1-bit bitmap with mask (for cursors, icons) // andMask/xorData are arrays of uint16_t, one per row void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const uint16_t *andMask, const uint16_t *xorData, uint32_t fgColor, uint32_t bgColor); diff --git a/dvx/dvxTypes.h b/dvx/dvxTypes.h index 03a4ab2..130e8d7 100644 --- a/dvx/dvxTypes.h +++ b/dvx/dvxTypes.h @@ -153,6 +153,7 @@ typedef struct { bool separator; // true = this is a separator line, not a clickable item bool enabled; bool checked; + char accelKey; // lowercase accelerator character, 0 if none } MenuItemT; typedef struct { @@ -161,6 +162,7 @@ typedef struct { int32_t itemCount; int32_t barX; // computed position on menu bar int32_t barW; // computed width on menu bar + char accelKey; // lowercase accelerator character, 0 if none } MenuT; typedef struct { @@ -312,6 +314,60 @@ typedef struct { int32_t hoverItem; // which item is highlighted (-1 = none) } PopupStateT; +// ============================================================ +// System menu (control menu / close box menu) +// ============================================================ + +typedef enum { + SysMenuRestoreE = 1, + SysMenuMoveE = 2, + SysMenuSizeE = 3, + SysMenuMinimizeE = 4, + SysMenuMaximizeE = 5, + SysMenuCloseE = 6 +} SysMenuCmdE; + +#define SYS_MENU_MAX_ITEMS 8 + +typedef struct { + char label[MAX_MENU_LABEL]; + int32_t cmd; + bool separator; + bool enabled; + char accelKey; +} SysMenuItemT; + +typedef struct { + bool active; + int32_t windowId; + int32_t popupX; + int32_t popupY; + int32_t popupW; + int32_t popupH; + int32_t hoverItem; + SysMenuItemT items[SYS_MENU_MAX_ITEMS]; + int32_t itemCount; +} SysMenuStateT; + +// ============================================================ +// Keyboard move/resize mode +// ============================================================ + +typedef enum { + KbModeNoneE = 0, + KbModeMoveE = 1, + KbModeResizeE = 2 +} KbModeE; + +typedef struct { + KbModeE mode; + int32_t windowId; + int32_t origX; + int32_t origY; + int32_t origW; + int32_t origH; +} KbMoveResizeT; + // ============================================================ // Utility macros // ============================================================ diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 0a7310a..51a7ab7 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -142,6 +142,7 @@ typedef struct WidgetT { bool visible; bool enabled; bool focused; + char accelKey; // lowercase accelerator character, 0 if none // User data and callbacks void *userData; diff --git a/dvx/dvxWm.c b/dvx/dvxWm.c index 692a9a1..184312e 100644 --- a/dvx/dvxWm.c +++ b/dvx/dvxWm.c @@ -67,7 +67,7 @@ static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font) { for (int32_t i = 0; i < win->menuBar->menuCount; i++) { MenuT *menu = &win->menuBar->menus[i]; - int32_t labelW = (int32_t)strlen(menu->label) * font->charWidth + CHROME_TITLE_PAD * 2; + int32_t labelW = textWidthAccel(font, menu->label) + CHROME_TITLE_PAD * 2; menu->barX = x; menu->barW = labelW; @@ -193,8 +193,8 @@ static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fon int32_t textX = win->x + menu->barX + CHROME_TITLE_PAD; int32_t textY = barY + (barH - font->charHeight) / 2; - drawText(d, ops, font, textX, textY, menu->label, - colors->menuFg, colors->menuBg, true); + drawTextAccel(d, ops, font, textX, textY, menu->label, + colors->menuFg, colors->menuBg, true); } // Draw bottom separator line @@ -567,6 +567,7 @@ MenuT *wmAddMenu(MenuBarT *bar, const char *label) { memset(menu, 0, sizeof(*menu)); strncpy(menu->label, label, MAX_MENU_LABEL - 1); menu->label[MAX_MENU_LABEL - 1] = '\0'; + menu->accelKey = accelParse(label); bar->menuCount++; return menu; @@ -608,6 +609,7 @@ void wmAddMenuItem(MenuT *menu, const char *label, int32_t id) { item->separator = false; item->enabled = true; item->checked = false; + item->accelKey = accelParse(label); menu->itemCount++; } diff --git a/dvx/widgets/widgetBox.c b/dvx/widgets/widgetBox.c index b5ab52f..29b73cc 100644 --- a/dvx/widgets/widgetBox.c +++ b/dvx/widgets/widgetBox.c @@ -49,14 +49,14 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap // Draw title centered vertically on the top border line if (w->as.frame.title && w->as.frame.title[0]) { - int32_t titleW = (int32_t)strlen(w->as.frame.title) * font->charWidth; + int32_t titleW = textWidthAccel(font, w->as.frame.title); int32_t titleX = w->x + DEFAULT_PADDING + fb; int32_t titleY = boxY + (fb - font->charHeight) / 2; rectFill(d, ops, titleX - 2, titleY, titleW + 4, font->charHeight, bg); - drawText(d, ops, font, titleX, titleY, - w->as.frame.title, fg, bg, true); + drawTextAccel(d, ops, font, titleX, titleY, + w->as.frame.title, fg, bg, true); } } @@ -72,6 +72,7 @@ WidgetT *wgtFrame(WidgetT *parent, const char *title) { w->as.frame.title = title; w->as.frame.style = FrameInE; w->as.frame.color = 0; + w->accelKey = accelParse(title); } return w; diff --git a/dvx/widgets/widgetButton.c b/dvx/widgets/widgetButton.c index e0a2c8a..87dde6e 100644 --- a/dvx/widgets/widgetButton.c +++ b/dvx/widgets/widgetButton.c @@ -13,6 +13,7 @@ WidgetT *wgtButton(WidgetT *parent, const char *text) { if (w) { w->as.button.text = text; w->as.button.pressed = false; + w->accelKey = accelParse(text); } return w; @@ -24,7 +25,7 @@ WidgetT *wgtButton(WidgetT *parent, const char *text) { // ============================================================ void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) { - w->calcMinW = (int32_t)strlen(w->as.button.text) * font->charWidth + BUTTON_PAD_H * 2; + w->calcMinW = textWidthAccel(font, w->as.button.text) + BUTTON_PAD_H * 2; w->calcMinH = font->charHeight + BUTTON_PAD_V * 2; } @@ -54,7 +55,7 @@ void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma bevel.width = 2; drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); - int32_t textW = (int32_t)strlen(w->as.button.text) * font->charWidth; + int32_t textW = textWidthAccel(font, w->as.button.text); int32_t textX = w->x + (w->w - textW) / 2; int32_t textY = w->y + (w->h - font->charHeight) / 2; @@ -63,8 +64,8 @@ void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma textY++; } - drawText(d, ops, font, textX, textY, - w->as.button.text, - w->enabled ? fg : colors->windowShadow, - bgFace, true); + drawTextAccel(d, ops, font, textX, textY, + w->as.button.text, + w->enabled ? fg : colors->windowShadow, + bgFace, true); } diff --git a/dvx/widgets/widgetCheckbox.c b/dvx/widgets/widgetCheckbox.c index 8aa1092..cfec267 100644 --- a/dvx/widgets/widgetCheckbox.c +++ b/dvx/widgets/widgetCheckbox.c @@ -13,6 +13,7 @@ WidgetT *wgtCheckbox(WidgetT *parent, const char *text) { if (w) { w->as.checkbox.text = text; w->as.checkbox.checked = false; + w->accelKey = accelParse(text); } return w; @@ -25,7 +26,7 @@ WidgetT *wgtCheckbox(WidgetT *parent, const char *text) { void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font) { w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP + - (int32_t)strlen(w->as.checkbox.text) * font->charWidth; + textWidthAccel(font, w->as.checkbox.text); w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight); } @@ -73,8 +74,8 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } // Draw label - drawText(d, ops, font, - w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP, - w->y + (w->h - font->charHeight) / 2, - w->as.checkbox.text, fg, bg, false); + drawTextAccel(d, ops, font, + w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP, + w->y + (w->h - font->charHeight) / 2, + w->as.checkbox.text, fg, bg, false); } diff --git a/dvx/widgets/widgetCore.c b/dvx/widgets/widgetCore.c index 051829a..8fff078 100644 --- a/dvx/widgets/widgetCore.c +++ b/dvx/widgets/widgetCore.c @@ -57,6 +57,23 @@ WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type) { } +// ============================================================ +// widgetClearFocus +// ============================================================ + +void widgetClearFocus(WidgetT *root) { + if (!root) { + return; + } + + root->focused = false; + + for (WidgetT *c = root->firstChild; c; c = c->nextSibling) { + widgetClearFocus(c); + } +} + + // ============================================================ // widgetCountVisibleChildren // ============================================================ @@ -170,6 +187,89 @@ void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t conten } +// ============================================================ +// widgetFindByAccel +// ============================================================ + +WidgetT *widgetFindByAccel(WidgetT *root, char key) { + if (!root || !root->enabled) { + return NULL; + } + + // Invisible tab pages: match the page itself (for tab switching) + // but don't recurse into children (their accels shouldn't be active) + if (!root->visible) { + if (root->type == WidgetTabPageE && root->accelKey == key) { + return root; + } + + return NULL; + } + + if (root->accelKey == key) { + return root; + } + + for (WidgetT *c = root->firstChild; c; c = c->nextSibling) { + WidgetT *found = widgetFindByAccel(c, key); + + if (found) { + return found; + } + } + + return NULL; +} + + +// ============================================================ +// widgetFindNextFocusable +// ============================================================ +// +// Depth-first walk of the widget tree. Returns the first focusable +// widget found after 'after'. If 'after' is NULL, returns the first +// focusable widget. Wraps around to the beginning if needed. + +static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfter) { + if (!w->visible || !w->enabled) { + return NULL; + } + + if (after == NULL) { + *pastAfter = true; + } + + if (w == after) { + *pastAfter = true; + } else if (*pastAfter && widgetIsFocusable(w->type)) { + return w; + } + + for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { + WidgetT *found = findNextFocusableImpl(c, after, pastAfter); + + if (found) { + return found; + } + } + + return NULL; +} + +WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after) { + bool pastAfter = false; + WidgetT *found = findNextFocusableImpl(root, after, &pastAfter); + + if (found) { + return found; + } + + // Wrap around — search from the beginning + pastAfter = true; + return findNextFocusableImpl(root, NULL, &pastAfter); +} + + // ============================================================ // widgetFrameBorderWidth // ============================================================ @@ -220,6 +320,19 @@ WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y) { } +// ============================================================ +// widgetIsFocusable +// ============================================================ + +bool widgetIsFocusable(WidgetTypeE type) { + return type == WidgetTextInputE || type == WidgetComboBoxE || + type == WidgetDropdownE || type == WidgetCheckboxE || + type == WidgetRadioE || type == WidgetButtonE || + type == WidgetSliderE || type == WidgetListBoxE || + type == WidgetTreeViewE || type == WidgetAnsiTermE; +} + + // ============================================================ // widgetIsBoxContainer // ============================================================ diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index eaf78fe..c85d91d 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -57,11 +57,15 @@ extern int32_t sDragOffset; void widgetAddChild(WidgetT *parent, WidgetT *child); WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type); +void widgetClearFocus(WidgetT *root); int32_t widgetCountVisibleChildren(const WidgetT *w); void widgetDestroyChildren(WidgetT *w); void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH); +WidgetT *widgetFindByAccel(WidgetT *root, char key); +WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after); int32_t widgetFrameBorderWidth(const WidgetT *w); WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y); +bool widgetIsFocusable(WidgetTypeE type); bool widgetIsBoxContainer(WidgetTypeE type); bool widgetIsHorizContainer(WidgetTypeE type); void widgetRemoveChild(WidgetT *parent, WidgetT *child); diff --git a/dvx/widgets/widgetLabel.c b/dvx/widgets/widgetLabel.c index 270a7b2..e68a19c 100644 --- a/dvx/widgets/widgetLabel.c +++ b/dvx/widgets/widgetLabel.c @@ -12,6 +12,7 @@ WidgetT *wgtLabel(WidgetT *parent, const char *text) { if (w) { w->as.label.text = text; + w->accelKey = accelParse(text); } return w; @@ -23,8 +24,8 @@ WidgetT *wgtLabel(WidgetT *parent, const char *text) { // ============================================================ void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font) { - w->calcMinW = (int32_t)strlen(w->as.label.text) * font->charWidth; - w->calcMinH = font->charHeight; + w->calcMinW = textWidthAccel(font, w->as.label.text); + w->calcMinH = font->charHeight + 1; } @@ -36,6 +37,6 @@ void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; - drawText(d, ops, font, w->x, w->y + (w->h - font->charHeight) / 2, - w->as.label.text, fg, bg, false); + drawTextAccel(d, ops, font, w->x, w->y + (w->h - font->charHeight) / 2, + w->as.label.text, fg, bg, false); } diff --git a/dvx/widgets/widgetOps.c b/dvx/widgets/widgetOps.c index d9bc78b..4d7cf79 100644 --- a/dvx/widgets/widgetOps.c +++ b/dvx/widgets/widgetOps.c @@ -396,18 +396,22 @@ void wgtSetText(WidgetT *w, const char *text) { switch (w->type) { case WidgetLabelE: w->as.label.text = text; + w->accelKey = accelParse(text); break; case WidgetButtonE: w->as.button.text = text; + w->accelKey = accelParse(text); break; case WidgetCheckboxE: w->as.checkbox.text = text; + w->accelKey = accelParse(text); break; case WidgetRadioE: w->as.radio.text = text; + w->accelKey = accelParse(text); break; case WidgetTextInputE: diff --git a/dvx/widgets/widgetRadio.c b/dvx/widgets/widgetRadio.c index ab65188..b2d3484 100644 --- a/dvx/widgets/widgetRadio.c +++ b/dvx/widgets/widgetRadio.c @@ -12,6 +12,7 @@ WidgetT *wgtRadio(WidgetT *parent, const char *text) { if (w) { w->as.radio.text = text; + w->accelKey = accelParse(text); // Auto-assign index based on position in parent int32_t idx = 0; @@ -50,7 +51,7 @@ WidgetT *wgtRadioGroup(WidgetT *parent) { void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) { w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP + - (int32_t)strlen(w->as.radio.text) * font->charWidth; + textWidthAccel(font, w->as.radio.text); w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight); } @@ -94,8 +95,8 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap CHECKBOX_BOX_SIZE - 6, CHECKBOX_BOX_SIZE - 6, fg); } - drawText(d, ops, font, - w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP, - w->y + (w->h - font->charHeight) / 2, - w->as.radio.text, fg, bg, false); + drawTextAccel(d, ops, font, + w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP, + w->y + (w->h - font->charHeight) / 2, + w->as.radio.text, fg, bg, false); } diff --git a/dvx/widgets/widgetTabControl.c b/dvx/widgets/widgetTabControl.c index f8ca926..6937b19 100644 --- a/dvx/widgets/widgetTabControl.c +++ b/dvx/widgets/widgetTabControl.c @@ -54,6 +54,7 @@ WidgetT *wgtTabPage(WidgetT *parent, const char *title) { if (w) { w->as.tabPage.title = title; + w->accelKey = accelParse(title); } return w; @@ -79,7 +80,7 @@ void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font) { maxPageW = DVX_MAX(maxPageW, c->calcMinW); maxPageH = DVX_MAX(maxPageH, c->calcMinH); - int32_t labelW = (int32_t)strlen(c->as.tabPage.title) * font->charWidth + TAB_PAD_H * 2; + int32_t labelW = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2; tabHeaderW += labelW; } @@ -145,7 +146,7 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy continue; } - int32_t tw = (int32_t)strlen(c->as.tabPage.title) * font->charWidth + TAB_PAD_H * 2; + int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2; if (vx >= tabX && vx < tabX + tw) { if (tabIdx != hit->as.tabControl.activeTab) { @@ -190,7 +191,7 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B continue; } - int32_t tw = (int32_t)strlen(c->as.tabPage.title) * font->charWidth + TAB_PAD_H * 2; + int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2; bool isActive = (tabIdx == w->as.tabControl.activeTab); int32_t ty = isActive ? w->y : w->y + 2; int32_t th = isActive ? tabH + 2 : tabH; @@ -227,8 +228,8 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B labelY++; } - drawText(d, ops, font, tabX + TAB_PAD_H, labelY, - c->as.tabPage.title, colors->contentFg, tabFace, true); + drawTextAccel(d, ops, font, tabX + TAB_PAD_H, labelY, + c->as.tabPage.title, colors->contentFg, tabFace, true); tabX += tw; tabIdx++; diff --git a/dvxdemo/demo.c b/dvxdemo/demo.c index 89c3353..c9ed53d 100644 --- a/dvxdemo/demo.c +++ b/dvxdemo/demo.c @@ -14,21 +14,35 @@ #define CMD_FILE_OPEN 101 #define CMD_FILE_SAVE 102 #define CMD_FILE_EXIT 103 -#define CMD_HELP_ABOUT 200 +#define CMD_EDIT_CUT 200 +#define CMD_EDIT_COPY 201 +#define CMD_EDIT_PASTE 202 +#define CMD_VIEW_TERM 300 +#define CMD_VIEW_CTRL 301 +#define CMD_HELP_ABOUT 400 // ============================================================ // Prototypes // ============================================================ static void onCloseCb(WindowT *win); +static void onCloseMainCb(WindowT *win); static void onMenuCb(WindowT *win, int32_t menuId); static void onOkClick(WidgetT *w); static void onPaintColor(WindowT *win, RectT *dirtyArea); -static void onToolbarClick(WidgetT *w); static void onPaintPattern(WindowT *win, RectT *dirtyArea); static void onPaintText(WindowT *win, RectT *dirtyArea); -static void setupWidgetDemo2(AppContextT *ctx); -static void setupWindows(AppContextT *ctx); +static void onToolbarClick(WidgetT *w); +static void setupControlsWindow(AppContextT *ctx); +static void setupMainWindow(AppContextT *ctx); +static void setupTerminalWindow(AppContextT *ctx); +static void setupWidgetDemo(AppContextT *ctx); + +// ============================================================ +// Globals +// ============================================================ + +static AppContextT *sCtx = NULL; // ============================================================ @@ -44,6 +58,19 @@ static void onCloseCb(WindowT *win) { } +// ============================================================ +// onCloseMainCb +// ============================================================ + +static void onCloseMainCb(WindowT *win) { + AppContextT *ctx = (AppContextT *)win->userData; + + if (ctx) { + dvxQuit(ctx); + } +} + + // ============================================================ // onMenuCb // ============================================================ @@ -58,8 +85,15 @@ static void onMenuCb(WindowT *win, int32_t menuId) { } break; + case CMD_VIEW_TERM: + setupTerminalWindow(ctx); + break; + + case CMD_VIEW_CTRL: + setupControlsWindow(ctx); + break; + case CMD_HELP_ABOUT: - // Could create an about dialog window here break; } } @@ -70,7 +104,6 @@ static void onMenuCb(WindowT *win, int32_t menuId) { // ============================================================ static void onOkClick(WidgetT *w) { - // Find the status label and update it WidgetT *root = w; while (root->parent) { @@ -86,26 +119,6 @@ static void onOkClick(WidgetT *w) { } -// ============================================================ -// onToolbarClick -// ============================================================ - -static void onToolbarClick(WidgetT *w) { - WidgetT *root = w; - - while (root->parent) { - root = root->parent; - } - - WidgetT *status = wgtFind(root, "advStatus"); - - if (status) { - wgtSetText(status, wgtGetText(w)); - wgtInvalidate(status); - } -} - - // ============================================================ // onPaintColor // ============================================================ @@ -148,7 +161,7 @@ static void onPaintPattern(WindowT *win, RectT *dirtyArea) { const DisplayT *d = dvxGetDisplay(ctx); int32_t bpp = d->format.bytesPerPixel; - int32_t sq = 16; // square size + int32_t sq = 16; uint32_t c1 = packColor(d, 255, 255, 255); uint32_t c2 = packColor(d, 0, 0, 180); @@ -187,7 +200,6 @@ static void onPaintText(WindowT *win, RectT *dirtyArea) { const BitmapFontT *font = dvxGetFont(ctx); int32_t bpp = d->format.bytesPerPixel; - // Fill white background uint32_t bg = packColor(d, 255, 255, 255); uint32_t fg = packColor(d, 0, 0, 0); @@ -195,7 +207,6 @@ static void onPaintText(WindowT *win, RectT *dirtyArea) { ops->spanFill(win->contentBuf + y * win->contentPitch, bg, win->contentW); } - // Draw text lines directly into the content buffer static const char *lines[] = { "DV/X GUI Compositor", "", @@ -207,13 +218,11 @@ static void onPaintText(WindowT *win, RectT *dirtyArea) { " - VESA VBE 2.0+ LFB", " - Dirty-rect compositing", " - Beveled Motif-style chrome", - " - Draggable windows", - " - Resizable windows", - " - Menu bars", + " - Draggable/resizable windows", + " - Menu bars with accelerators", " - Scrollbars", " - Widget system", - "", - "Press ESC to exit.", + " - ANSI terminal emulator", NULL }; @@ -272,13 +281,33 @@ static void onPaintText(WindowT *win, RectT *dirtyArea) { // ============================================================ -// setupWidgetDemo2 +// onToolbarClick +// ============================================================ + +static void onToolbarClick(WidgetT *w) { + WidgetT *root = w; + + while (root->parent) { + root = root->parent; + } + + WidgetT *status = wgtFind(root, "advStatus"); + + if (status) { + wgtSetText(status, wgtGetText(w)); + wgtInvalidate(status); + } +} + + +// ============================================================ +// setupControlsWindow — advanced widgets with tabs // ============================================================ static const char *colorItems[] = {"Red", "Green", "Blue", "Yellow", "Cyan", "Magenta"}; static const char *sizeItems[] = {"Small", "Medium", "Large", "Extra Large"}; -static void setupWidgetDemo2(AppContextT *ctx) { +static void setupControlsWindow(AppContextT *ctx) { WindowT *win = dvxCreateWindow(ctx, "Advanced Widgets", 380, 50, 320, 400, true); if (!win) { @@ -294,31 +323,31 @@ static void setupWidgetDemo2(AppContextT *ctx) { WidgetT *tabs = wgtTabControl(root); // --- Tab 1: Controls --- - WidgetT *page1 = wgtTabPage(tabs, "Controls"); + WidgetT *page1 = wgtTabPage(tabs, "&Controls"); WidgetT *ddRow = wgtHBox(page1); - wgtLabel(ddRow, "Color:"); + wgtLabel(ddRow, "Co&lor:"); WidgetT *dd = wgtDropdown(ddRow); wgtDropdownSetItems(dd, colorItems, 6); wgtDropdownSetSelected(dd, 0); WidgetT *cbRow = wgtHBox(page1); - wgtLabel(cbRow, "Size:"); + wgtLabel(cbRow, "Si&ze:"); WidgetT *cb = wgtComboBox(cbRow, 32); wgtComboBoxSetItems(cb, sizeItems, 4); wgtComboBoxSetSelected(cb, 1); wgtHSeparator(page1); - wgtLabel(page1, "Progress:"); + wgtLabel(page1, "&Progress:"); WidgetT *pb = wgtProgressBar(page1); wgtProgressBarSetValue(pb, 65); - wgtLabel(page1, "Volume:"); + wgtLabel(page1, "&Volume:"); wgtSlider(page1, 0, 100); // --- Tab 2: Tree --- - WidgetT *page2 = wgtTabPage(tabs, "Tree"); + WidgetT *page2 = wgtTabPage(tabs, "&Tree"); WidgetT *tree = wgtTreeView(page2); WidgetT *docs = wgtTreeItem(tree, "Documents"); @@ -338,12 +367,12 @@ static void setupWidgetDemo2(AppContextT *ctx) { wgtTreeItem(config, "palette.dat"); // --- Tab 3: Toolbar --- - WidgetT *page3 = wgtTabPage(tabs, "Toolbar"); + WidgetT *page3 = wgtTabPage(tabs, "Tool&bar"); WidgetT *tb = wgtToolbar(page3); - WidgetT *btnNew = wgtButton(tb, "New"); - WidgetT *btnOpen = wgtButton(tb, "Open"); - WidgetT *btnSave = wgtButton(tb, "Save"); + WidgetT *btnNew = wgtButton(tb, "&New"); + WidgetT *btnOpen = wgtButton(tb, "&Open"); + WidgetT *btnSave = wgtButton(tb, "&Save"); btnNew->onClick = onToolbarClick; btnOpen->onClick = onToolbarClick; btnSave->onClick = onToolbarClick; @@ -362,43 +391,57 @@ static void setupWidgetDemo2(AppContextT *ctx) { // ============================================================ -// setupWindows +// setupMainWindow — info window + paint demos // ============================================================ -static void setupWindows(AppContextT *ctx) { +static void setupMainWindow(AppContextT *ctx) { // Window 1: Text information window with menu bar WindowT *win1 = dvxCreateWindow(ctx, "DV/X Information", 50, 40, 340, 350, true); if (win1) { win1->userData = ctx; win1->onPaint = onPaintText; - win1->onClose = onCloseCb; + win1->onClose = onCloseMainCb; win1->onMenu = onMenuCb; MenuBarT *bar = wmAddMenuBar(win1); if (bar) { - MenuT *fileMenu = wmAddMenu(bar, "File"); + MenuT *fileMenu = wmAddMenu(bar, "&File"); if (fileMenu) { - wmAddMenuItem(fileMenu, "New", CMD_FILE_NEW); - wmAddMenuItem(fileMenu, "Open...", CMD_FILE_OPEN); - wmAddMenuItem(fileMenu, "Save", CMD_FILE_SAVE); + wmAddMenuItem(fileMenu, "&New", CMD_FILE_NEW); + wmAddMenuItem(fileMenu, "&Open...", CMD_FILE_OPEN); + wmAddMenuItem(fileMenu, "&Save", CMD_FILE_SAVE); wmAddMenuSeparator(fileMenu); - wmAddMenuItem(fileMenu, "Exit", CMD_FILE_EXIT); + wmAddMenuItem(fileMenu, "E&xit", CMD_FILE_EXIT); } - MenuT *helpMenu = wmAddMenu(bar, "Help"); + MenuT *editMenu = wmAddMenu(bar, "&Edit"); + + if (editMenu) { + wmAddMenuItem(editMenu, "Cu&t", CMD_EDIT_CUT); + wmAddMenuItem(editMenu, "&Copy", CMD_EDIT_COPY); + wmAddMenuItem(editMenu, "&Paste", CMD_EDIT_PASTE); + } + + MenuT *viewMenu = wmAddMenu(bar, "&View"); + + if (viewMenu) { + wmAddMenuItem(viewMenu, "&Terminal", CMD_VIEW_TERM); + wmAddMenuItem(viewMenu, "&Controls", CMD_VIEW_CTRL); + } + + MenuT *helpMenu = wmAddMenu(bar, "&Help"); if (helpMenu) { - wmAddMenuItem(helpMenu, "About...", CMD_HELP_ABOUT); + wmAddMenuItem(helpMenu, "&About...", CMD_HELP_ABOUT); } } wmUpdateContentRect(win1); wmReallocContentBuf(win1, &ctx->display); - // Paint initial content RectT fullRect = {0, 0, win1->contentW, win1->contentH}; win1->onPaint(win1, &fullRect); } @@ -431,54 +474,156 @@ static void setupWindows(AppContextT *ctx) { RectT fullRect = {0, 0, win3->contentW, win3->contentH}; win3->onPaint(win3, &fullRect); } +} - // Window 4: Widget demo - WindowT *win4 = dvxCreateWindow(ctx, "Widget Demo", 80, 200, 280, 320, true); - if (win4) { - win4->userData = ctx; - win4->onClose = onCloseCb; +// ============================================================ +// setupTerminalWindow — ANSI terminal widget demo +// ============================================================ - WidgetT *root = wgtInitWindow(ctx, win4); +static void setupTerminalWindow(AppContextT *ctx) { + WindowT *win = dvxCreateWindow(ctx, "ANSI Terminal", 60, 60, 660, 420, true); - // Status label at top - WidgetT *status = wgtLabel(root, "Ready."); - strncpy(status->name, "status", MAX_WIDGET_NAME); - - wgtHSeparator(root); - - // Frame with text input - WidgetT *frame = wgtFrame(root, "User Input"); - WidgetT *row1 = wgtHBox(frame); - wgtLabel(row1, "Name:"); - wgtTextInput(row1, 64); - - wgtHSeparator(root); - - // Checkboxes - wgtCheckbox(root, "Enable feature A"); - wgtCheckbox(root, "Enable feature B"); - - wgtHSeparator(root); - - // Radio buttons - WidgetT *rg = wgtRadioGroup(root); - wgtRadio(rg, "Option 1"); - wgtRadio(rg, "Option 2"); - wgtRadio(rg, "Option 3"); - - wgtHSeparator(root); - - // Button row at bottom - WidgetT *btnRow = wgtHBox(root); - btnRow->align = AlignEndE; - WidgetT *okBtn = wgtButton(btnRow, "OK"); - okBtn->onClick = onOkClick; - wgtButton(btnRow, "Cancel"); - - // Layout and paint now that widget tree is complete - wgtInvalidate(root); + if (!win) { + return; } + + win->userData = ctx; + win->onClose = onCloseCb; + + WidgetT *root = wgtInitWindow(ctx, win); + WidgetT *term = wgtAnsiTerm(root, 80, 25); + + term->weight = 100; + wgtAnsiTermSetScrollback(term, 500); + + // Feed some ANSI content to demonstrate the terminal + static const uint8_t ansiDemo[] = + "\x1B[2J" // clear screen + "\x1B[1;34m========================================\r\n" + " DV/X ANSI Terminal Emulator\r\n" + "========================================\x1B[0m\r\n" + "\r\n" + "\x1B[1mBold text\x1B[0m, " + "\x1B[4mUnderlined\x1B[0m, " + "\x1B[7mReverse\x1B[0m, " + "\x1B[5mBlinking\x1B[0m\r\n" + "\r\n" + "Standard colors:\r\n" + " \x1B[30m\x1B[47m Black \x1B[0m" + " \x1B[31m Red \x1B[0m" + " \x1B[32m Green \x1B[0m" + " \x1B[33m Yellow \x1B[0m" + " \x1B[34m Blue \x1B[0m" + " \x1B[35m Magenta \x1B[0m" + " \x1B[36m Cyan \x1B[0m" + " \x1B[37m White \x1B[0m\r\n" + "\r\n" + "Bright colors:\r\n" + " \x1B[1;30m\x1B[47m DkGray \x1B[0m" + " \x1B[1;31m LtRed \x1B[0m" + " \x1B[1;32m LtGreen \x1B[0m" + " \x1B[1;33m LtYellow \x1B[0m" + " \x1B[1;34m LtBlue \x1B[0m" + " \x1B[1;35m LtMagenta \x1B[0m" + " \x1B[1;36m LtCyan \x1B[0m" + " \x1B[1;37m BrWhite \x1B[0m\r\n" + "\r\n" + "Background colors:\r\n" + " \x1B[40m\x1B[37m Black \x1B[0m" + " \x1B[41m\x1B[37m Red \x1B[0m" + " \x1B[42m\x1B[30m Green \x1B[0m" + " \x1B[43m\x1B[30m Yellow \x1B[0m" + " \x1B[44m\x1B[37m Blue \x1B[0m" + " \x1B[45m\x1B[37m Magenta \x1B[0m" + " \x1B[46m\x1B[30m Cyan \x1B[0m" + " \x1B[47m\x1B[30m White \x1B[0m\r\n" + "\r\n" + "CP437 graphics: " + "\x01\x02\x03\x04\x05\x06" // smileys, hearts, diamonds, clubs, spades + " \xB0\xB1\xB2\xDB" // shade blocks + " \xC4\xC5\xB3\xDA\xBF\xC0\xD9" // box drawing + "\r\n" + "\r\n" + "\x1B[1;32mTerminal ready.\x1B[0m " + "\x1B[36m(Not connected to any host)\x1B[0m\r\n"; + + wgtAnsiTermWrite(term, ansiDemo, (int32_t)(sizeof(ansiDemo) - 1)); + + WidgetT *sb = wgtStatusBar(root); + wgtLabel(sb, "80x25 [Local]"); + + dvxFitWindow(ctx, win); + wgtInvalidate(root); +} + + +// ============================================================ +// setupWidgetDemo — form with accelerators +// ============================================================ + +static void setupWidgetDemo(AppContextT *ctx) { + WindowT *win = dvxCreateWindow(ctx, "Widget Demo", 80, 200, 280, 360, true); + + if (!win) { + return; + } + + win->userData = ctx; + win->onClose = onCloseCb; + + WidgetT *root = wgtInitWindow(ctx, win); + + // Status label at top + WidgetT *status = wgtLabel(root, "Ready."); + strncpy(status->name, "status", MAX_WIDGET_NAME); + + wgtHSeparator(root); + + // Frame with text input + WidgetT *frame = wgtFrame(root, "&User Input"); + WidgetT *row1 = wgtHBox(frame); + wgtLabel(row1, "&Name:"); + wgtTextInput(row1, 64); + + WidgetT *row2 = wgtHBox(frame); + wgtLabel(row2, "A&ddress:"); + wgtTextInput(row2, 64); + + wgtHSeparator(root); + + // Checkboxes + wgtCheckbox(root, "Enable feature &A"); + wgtCheckbox(root, "Enable feature &B"); + + wgtHSeparator(root); + + // Radio buttons + WidgetT *rg = wgtRadioGroup(root); + wgtRadio(rg, "Option &1"); + wgtRadio(rg, "Option &2"); + wgtRadio(rg, "Option &3"); + + wgtHSeparator(root); + + // List box + static const char *listItems[] = {"Alpha", "Beta", "Gamma", "Delta", "Epsilon"}; + WidgetT *listRow = wgtHBox(root); + wgtLabel(listRow, "&Items:"); + WidgetT *lb = wgtListBox(listRow); + wgtListBoxSetItems(lb, listItems, 5); + lb->weight = 100; + + wgtHSeparator(root); + + // Button row at bottom + WidgetT *btnRow = wgtHBox(root); + btnRow->align = AlignEndE; + WidgetT *okBtn = wgtButton(btnRow, "&OK"); + okBtn->onClick = onOkClick; + wgtButton(btnRow, "&Cancel"); + + wgtInvalidate(root); } @@ -497,8 +642,12 @@ int main(void) { return 1; } - setupWindows(&ctx); - setupWidgetDemo2(&ctx); + sCtx = &ctx; + + setupMainWindow(&ctx); + setupWidgetDemo(&ctx); + setupControlsWindow(&ctx); + setupTerminalWindow(&ctx); dvxRun(&ctx);