From 2c87f396d37278a14356517a834844ac32e2fca8 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Thu, 12 Mar 2026 20:44:05 -0500 Subject: [PATCH] Working on keyboard control of GUI. --- dvx/dvxApp.c | 70 ++++++++++++++++++++++------ dvx/dvxDraw.c | 59 ++++++++++++++++++++++++ dvx/dvxDraw.h | 3 ++ dvx/widgets/widgetAnsiTerm.c | 4 ++ dvx/widgets/widgetButton.c | 5 ++ dvx/widgets/widgetCheckbox.c | 12 +++-- dvx/widgets/widgetCore.c | 3 +- dvx/widgets/widgetDropdown.c | 4 ++ dvx/widgets/widgetEvent.c | 81 +++++++++++++++++++++++++++------ dvx/widgets/widgetImageButton.c | 6 +++ dvx/widgets/widgetInternal.h | 1 + dvx/widgets/widgetListBox.c | 4 ++ dvx/widgets/widgetRadio.c | 12 +++-- dvx/widgets/widgetSlider.c | 4 ++ dvx/widgets/widgetTabControl.c | 4 ++ dvx/widgets/widgetTreeView.c | 5 ++ 16 files changed, 239 insertions(+), 38 deletions(-) diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index e4a7a6e..e088044 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -38,8 +38,8 @@ 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; +// 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 @@ -323,7 +323,7 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) { widgetClearFocus(win->widgetRoot); target->focused = true; target->as.button.pressed = true; - sAccelPressedBtn = target; + sKeyPressedBtn = target; wgtInvalidate(target); return true; @@ -344,9 +344,8 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) { case WidgetImageButtonE: widgetClearFocus(win->widgetRoot); target->focused = true; - if (target->onClick) { - target->onClick(target); - } + target->as.imageButton.pressed = true; + sKeyPressedBtn = target; wgtInvalidate(target); return true; @@ -859,16 +858,20 @@ 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); + // 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; } - wgtInvalidate(sAccelPressedBtn); - sAccelPressedBtn = NULL; + if (sKeyPressedBtn->onClick) { + sKeyPressedBtn->onClick(sKeyPressedBtn); + } + + wgtInvalidate(sKeyPressedBtn); + sKeyPressedBtn = NULL; } ctx->prevMouseX = ctx->mouseX; @@ -1942,6 +1945,45 @@ static void pollKeyboard(AppContextT *ctx) { sOpenPopup = NULL; widgetClearFocus(win->widgetRoot); 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; + } + + if (win->vScroll->value < win->vScroll->min) { + win->vScroll->value = win->vScroll->min; + } + + if (win->vScroll->value > win->vScroll->max) { + win->vScroll->value = 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; + } + + if (win->hScroll->value < win->hScroll->min) { + win->hScroll->value = win->hScroll->min; + } + + if (win->hScroll->value > win->hScroll->max) { + win->hScroll->value = win->hScroll->max; + } + } + wgtInvalidate(win->widgetRoot); } } diff --git a/dvx/dvxDraw.c b/dvx/dvxDraw.c index 0e24744..0b29720 100644 --- a/dvx/dvxDraw.c +++ b/dvx/dvxDraw.c @@ -10,6 +10,7 @@ char accelParse(const char *text); static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *w, int32_t *h); +void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color); 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); static void spanCopy16(uint8_t *dst, const uint8_t *src, int32_t count); @@ -262,6 +263,64 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 } +// ============================================================ +// drawFocusRect +// ============================================================ + +void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color) { + int32_t bpp = ops->bytesPerPixel; + int32_t pitch = d->pitch; + + int32_t clipX1 = d->clipX; + int32_t clipX2 = d->clipX + d->clipW; + int32_t clipY1 = d->clipY; + int32_t clipY2 = d->clipY + d->clipH; + + int32_t x2 = x + w - 1; + int32_t y2 = y + h - 1; + + // Top edge + if (y >= clipY1 && y < clipY2) { + for (int32_t px = x; px <= x2; px += 2) { + if (px >= clipX1 && px < clipX2) { + putPixel(d->backBuf + y * pitch + px * bpp, color, bpp); + } + } + } + + // Bottom edge + if (y2 >= clipY1 && y2 < clipY2 && y2 != y) { + int32_t parity = (y2 - y) & 1; + + for (int32_t px = x + parity; px <= x2; px += 2) { + if (px >= clipX1 && px < clipX2) { + putPixel(d->backBuf + y2 * pitch + px * bpp, color, bpp); + } + } + } + + // Left edge (skip corners already drawn) + if (x >= clipX1 && x < clipX2) { + for (int32_t py = y + 2; py < y2; py += 2) { + if (py >= clipY1 && py < clipY2) { + putPixel(d->backBuf + py * pitch + x * bpp, color, bpp); + } + } + } + + // Right edge (skip corners already drawn) + if (x2 >= clipX1 && x2 < clipX2 && x2 != x) { + int32_t parity = (x2 - x) & 1; + + for (int32_t py = y + 2 - parity; py < y2; py += 2) { + if (py >= clipY1 && py < clipY2) { + putPixel(d->backBuf + py * pitch + x2 * bpp, color, bpp); + } + } + } +} + + // ============================================================ // drawHLine // ============================================================ diff --git a/dvx/dvxDraw.h b/dvx/dvxDraw.h index 0be84cf..73bc68e 100644 --- a/dvx/dvxDraw.h +++ b/dvx/dvxDraw.h @@ -39,6 +39,9 @@ int32_t textWidthAccel(const BitmapFontT *font, const char *text); // 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); +// Dotted focus rectangle (every other pixel) +void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color); + // Horizontal line void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color); diff --git a/dvx/widgets/widgetAnsiTerm.c b/dvx/widgets/widgetAnsiTerm.c index 5bfc913..18e2456 100644 --- a/dvx/widgets/widgetAnsiTerm.c +++ b/dvx/widgets/widgetAnsiTerm.c @@ -1595,4 +1595,8 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit drawBevel(d, ops, sbX, thumbY, sbW, thumbH, &btnBevel); } + + if (w->focused) { + drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, colors->contentFg); + } } diff --git a/dvx/widgets/widgetButton.c b/dvx/widgets/widgetButton.c index 87dde6e..b00d798 100644 --- a/dvx/widgets/widgetButton.c +++ b/dvx/widgets/widgetButton.c @@ -68,4 +68,9 @@ void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma w->as.button.text, w->enabled ? fg : colors->windowShadow, bgFace, true); + + if (w->focused) { + int32_t off = w->as.button.pressed ? 1 : 0; + drawFocusRect(d, ops, w->x + 3 + off, w->y + 3 + off, w->w - 6, w->h - 6, fg); + } } diff --git a/dvx/widgets/widgetCheckbox.c b/dvx/widgets/widgetCheckbox.c index cfec267..d0808fc 100644 --- a/dvx/widgets/widgetCheckbox.c +++ b/dvx/widgets/widgetCheckbox.c @@ -74,8 +74,12 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } // Draw label - 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); + int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP; + int32_t labelY = w->y + (w->h - font->charHeight) / 2; + drawTextAccel(d, ops, font, labelX, labelY, w->as.checkbox.text, fg, bg, false); + + if (w->focused) { + int32_t labelW = textWidthAccel(font, w->as.checkbox.text); + drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg); + } } diff --git a/dvx/widgets/widgetCore.c b/dvx/widgets/widgetCore.c index ba6dace..ab267b8 100644 --- a/dvx/widgets/widgetCore.c +++ b/dvx/widgets/widgetCore.c @@ -395,7 +395,8 @@ bool widgetIsFocusable(WidgetTypeE type) { type == WidgetDropdownE || type == WidgetCheckboxE || type == WidgetRadioE || type == WidgetButtonE || type == WidgetSliderE || type == WidgetListBoxE || - type == WidgetTreeViewE || type == WidgetAnsiTermE; + type == WidgetTreeViewE || type == WidgetAnsiTermE || + type == WidgetTabControlE; } diff --git a/dvx/widgets/widgetDropdown.c b/dvx/widgets/widgetDropdown.c index 860067a..5289cf3 100644 --- a/dvx/widgets/widgetDropdown.c +++ b/dvx/widgets/widgetDropdown.c @@ -134,6 +134,10 @@ void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, colors->contentFg); } + + if (w->focused) { + drawFocusRect(d, ops, w->x + 3, w->y + 3, textAreaW - 6, w->h - 6, fg); + } } diff --git a/dvx/widgets/widgetEvent.c b/dvx/widgets/widgetEvent.c index a97f092..4d7657b 100644 --- a/dvx/widgets/widgetEvent.c +++ b/dvx/widgets/widgetEvent.c @@ -121,7 +121,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { while (top > 0) { WidgetT *w = stack[--top]; - if (w->focused && (w->type == WidgetTextInputE || w->type == WidgetComboBoxE || w->type == WidgetDropdownE || w->type == WidgetAnsiTermE || w->type == WidgetTreeViewE || w->type == WidgetListBoxE || w->type == WidgetButtonE || w->type == WidgetImageButtonE || w->type == WidgetCheckboxE || w->type == WidgetRadioE || w->type == WidgetSliderE)) { + if (w->focused && (w->type == WidgetTextInputE || w->type == WidgetComboBoxE || w->type == WidgetDropdownE || w->type == WidgetAnsiTermE || w->type == WidgetTreeViewE || w->type == WidgetListBoxE || w->type == WidgetButtonE || w->type == WidgetImageButtonE || w->type == WidgetCheckboxE || w->type == WidgetRadioE || w->type == WidgetSliderE || w->type == WidgetTabControlE)) { focus = w; break; } @@ -160,13 +160,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { if (focus->type == WidgetButtonE) { if (key == ' ' || key == 0x0D) { focus->as.button.pressed = true; - wgtInvalidate(focus); - focus->as.button.pressed = false; - - if (focus->onClick) { - focus->onClick(focus); - } - + sKeyPressedBtn = focus; wgtInvalidate(focus); } @@ -177,13 +171,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { if (focus->type == WidgetImageButtonE) { if (key == ' ' || key == 0x0D) { focus->as.imageButton.pressed = true; - wgtInvalidate(focus); - focus->as.imageButton.pressed = false; - - if (focus->onClick) { - focus->onClick(focus); - } - + sKeyPressedBtn = focus; wgtInvalidate(focus); } @@ -330,6 +318,59 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { return; } + // Handle tab control keyboard navigation + if (focus->type == WidgetTabControlE) { + int32_t tabCount = 0; + + for (WidgetT *c = focus->firstChild; c; c = c->nextSibling) { + if (c->type == WidgetTabPageE) { + tabCount++; + } + } + + if (tabCount > 1) { + int32_t active = focus->as.tabControl.activeTab; + + if (key == (0x4D | 0x100)) { + // Right — next tab (wrap) + active = (active + 1) % tabCount; + } else if (key == (0x4B | 0x100)) { + // Left — previous tab (wrap) + active = (active - 1 + tabCount) % tabCount; + } else if (key == (0x47 | 0x100)) { + // Home — first tab + active = 0; + } else if (key == (0x4F | 0x100)) { + // End — last tab + active = tabCount - 1; + } else { + return; + } + + if (active != focus->as.tabControl.activeTab) { + if (sOpenPopup) { + if (sOpenPopup->type == WidgetDropdownE) { + sOpenPopup->as.dropdown.open = false; + } else if (sOpenPopup->type == WidgetComboBoxE) { + sOpenPopup->as.comboBox.open = false; + } + + sOpenPopup = NULL; + } + + focus->as.tabControl.activeTab = active; + + if (focus->onChange) { + focus->onChange(focus); + } + + wgtInvalidate(focus); + } + } + + return; + } + // Handle dropdown keyboard navigation if (focus->type == WidgetDropdownE) { if (focus->as.dropdown.open) { @@ -878,6 +919,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { } if (hit->type == WidgetTabControlE && hit->enabled) { + hit->focused = true; widgetTabControlOnMouse(hit, root, vx, vy); } @@ -984,5 +1026,14 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) { RectT fullRect = {0, 0, win->contentW, win->contentH}; win->onPaint(win, &fullRect); win->contentDirty = true; + + // Dirty the window content area on screen so compositor redraws it + if (win->widgetRoot) { + AppContextT *ctx = (AppContextT *)win->widgetRoot->userData; + + if (ctx) { + dvxInvalidateWindow(ctx, win); + } + } } } diff --git a/dvx/widgets/widgetImageButton.c b/dvx/widgets/widgetImageButton.c index a920855..cb2c552 100644 --- a/dvx/widgets/widgetImageButton.c +++ b/dvx/widgets/widgetImageButton.c @@ -97,4 +97,10 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const w->as.imageButton.data, w->as.imageButton.imgPitch, 0, 0, w->as.imageButton.imgW, w->as.imageButton.imgH); } + + if (w->focused) { + uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; + int32_t off = w->as.imageButton.pressed ? 1 : 0; + drawFocusRect(d, ops, w->x + 3 + off, w->y + 3 + off, w->w - 6, w->h - 6, fg); + } } diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index 4949ce2..63f1ad0 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -45,6 +45,7 @@ // ============================================================ extern bool sDebugLayout; +extern WidgetT *sKeyPressedBtn; extern WidgetT *sOpenPopup; extern WidgetT *sPressedButton; extern WidgetT *sDragSlider; diff --git a/dvx/widgets/widgetListBox.c b/dvx/widgets/widgetListBox.c index 85d0cb1..78211ad 100644 --- a/dvx/widgets/widgetListBox.c +++ b/dvx/widgets/widgetListBox.c @@ -394,4 +394,8 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm drawBevel(d, ops, sbX, sbY + LISTBOX_SB_W + thumbPos, LISTBOX_SB_W, thumbSize, &btnBevel); } } + + if (w->focused) { + drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg); + } } diff --git a/dvx/widgets/widgetRadio.c b/dvx/widgets/widgetRadio.c index b2d3484..3a9329c 100644 --- a/dvx/widgets/widgetRadio.c +++ b/dvx/widgets/widgetRadio.c @@ -95,8 +95,12 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap CHECKBOX_BOX_SIZE - 6, CHECKBOX_BOX_SIZE - 6, fg); } - 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); + int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP; + int32_t labelY = w->y + (w->h - font->charHeight) / 2; + drawTextAccel(d, ops, font, labelX, labelY, w->as.radio.text, fg, bg, false); + + if (w->focused) { + int32_t labelW = textWidthAccel(font, w->as.radio.text); + drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg); + } } diff --git a/dvx/widgets/widgetSlider.c b/dvx/widgets/widgetSlider.c index 875c4b6..ee67bd8 100644 --- a/dvx/widgets/widgetSlider.c +++ b/dvx/widgets/widgetSlider.c @@ -197,4 +197,8 @@ void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma // Center tick on thumb drawVLine(d, ops, thumbX + SLIDER_THUMB_W / 2, w->y + 3, w->h - 6, fg); } + + if (w->focused) { + drawFocusRect(d, ops, w->x, w->y, w->w, w->h, fg); + } } diff --git a/dvx/widgets/widgetTabControl.c b/dvx/widgets/widgetTabControl.c index 2fbb6de..b7823ca 100644 --- a/dvx/widgets/widgetTabControl.c +++ b/dvx/widgets/widgetTabControl.c @@ -242,6 +242,10 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B drawTextAccel(d, ops, font, tabX + TAB_PAD_H, labelY, c->as.tabPage.title, colors->contentFg, tabFace, true); + if (isActive && w->focused) { + drawFocusRect(d, ops, tabX + 3, ty + 3, tw - 6, th - 4, colors->contentFg); + } + tabX += tw; tabIdx++; } diff --git a/dvx/widgets/widgetTreeView.c b/dvx/widgets/widgetTreeView.c index 42e1c68..1ccf484 100644 --- a/dvx/widgets/widgetTreeView.c +++ b/dvx/widgets/widgetTreeView.c @@ -1135,4 +1135,9 @@ void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit if (needHSb) { drawTreeHScrollbar(w, d, ops, colors, totalW, innerW, needVSb); } + + if (w->focused) { + uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; + drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg); + } }