Working on keyboard control of GUI.

This commit is contained in:
Scott Duensing 2026-03-12 20:44:05 -05:00
parent 1e5c085ef0
commit 2c87f396d3
16 changed files with 239 additions and 38 deletions

View file

@ -38,8 +38,8 @@ static void pollMouse(AppContextT *ctx);
static void refreshMinimizedIcons(AppContextT *ctx); static void refreshMinimizedIcons(AppContextT *ctx);
static void updateCursorShape(AppContextT *ctx); static void updateCursorShape(AppContextT *ctx);
// Button pressed via accelerator key — separate from sPressedButton (mouse) // Button pressed via keyboard — shared with widgetEvent.c for Space/Enter
static WidgetT *sAccelPressedBtn = NULL; WidgetT *sKeyPressedBtn = NULL;
// Alt+key scan code to ASCII lookup table (indexed by scan code) // 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 // 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); widgetClearFocus(win->widgetRoot);
target->focused = true; target->focused = true;
target->as.button.pressed = true; target->as.button.pressed = true;
sAccelPressedBtn = target; sKeyPressedBtn = target;
wgtInvalidate(target); wgtInvalidate(target);
return true; return true;
@ -344,9 +344,8 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
case WidgetImageButtonE: case WidgetImageButtonE:
widgetClearFocus(win->widgetRoot); widgetClearFocus(win->widgetRoot);
target->focused = true; target->focused = true;
if (target->onClick) { target->as.imageButton.pressed = true;
target->onClick(target); sKeyPressedBtn = target;
}
wgtInvalidate(target); wgtInvalidate(target);
return true; return true;
@ -859,16 +858,20 @@ bool dvxUpdate(AppContextT *ctx) {
__dpmi_yield(); __dpmi_yield();
} }
// After compositing, release accel-pressed button (one frame of animation) // After compositing, release key-pressed button (one frame of animation)
if (sAccelPressedBtn) { if (sKeyPressedBtn) {
sAccelPressedBtn->as.button.pressed = false; if (sKeyPressedBtn->type == WidgetImageButtonE) {
sKeyPressedBtn->as.imageButton.pressed = false;
if (sAccelPressedBtn->onClick) { } else {
sAccelPressedBtn->onClick(sAccelPressedBtn); sKeyPressedBtn->as.button.pressed = false;
} }
wgtInvalidate(sAccelPressedBtn); if (sKeyPressedBtn->onClick) {
sAccelPressedBtn = NULL; sKeyPressedBtn->onClick(sKeyPressedBtn);
}
wgtInvalidate(sKeyPressedBtn);
sKeyPressedBtn = NULL;
} }
ctx->prevMouseX = ctx->mouseX; ctx->prevMouseX = ctx->mouseX;
@ -1942,6 +1945,45 @@ static void pollKeyboard(AppContextT *ctx) {
sOpenPopup = NULL; sOpenPopup = NULL;
widgetClearFocus(win->widgetRoot); widgetClearFocus(win->widgetRoot);
next->focused = true; 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); wgtInvalidate(win->widgetRoot);
} }
} }

View file

@ -10,6 +10,7 @@
char accelParse(const char *text); 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 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 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 spanCopy8(uint8_t *dst, const uint8_t *src, int32_t count);
static void spanCopy16(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 // drawHLine
// ============================================================ // ============================================================

View file

@ -39,6 +39,9 @@ int32_t textWidthAccel(const BitmapFontT *font, const char *text);
// andMask/xorData are arrays of uint16_t, one per row // 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); 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 // Horizontal line
void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color); void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color);

View file

@ -1595,4 +1595,8 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
drawBevel(d, ops, sbX, thumbY, sbW, thumbH, &btnBevel); 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);
}
} }

View file

@ -68,4 +68,9 @@ void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
w->as.button.text, w->as.button.text,
w->enabled ? fg : colors->windowShadow, w->enabled ? fg : colors->windowShadow,
bgFace, true); 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);
}
} }

View file

@ -74,8 +74,12 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
} }
// Draw label // Draw label
drawTextAccel(d, ops, font, int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP;
w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP, int32_t labelY = w->y + (w->h - font->charHeight) / 2;
w->y + (w->h - font->charHeight) / 2, drawTextAccel(d, ops, font, labelX, labelY, w->as.checkbox.text, fg, bg, false);
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);
}
} }

View file

@ -395,7 +395,8 @@ bool widgetIsFocusable(WidgetTypeE type) {
type == WidgetDropdownE || type == WidgetCheckboxE || type == WidgetDropdownE || type == WidgetCheckboxE ||
type == WidgetRadioE || type == WidgetButtonE || type == WidgetRadioE || type == WidgetButtonE ||
type == WidgetSliderE || type == WidgetListBoxE || type == WidgetSliderE || type == WidgetListBoxE ||
type == WidgetTreeViewE || type == WidgetAnsiTermE; type == WidgetTreeViewE || type == WidgetAnsiTermE ||
type == WidgetTabControlE;
} }

View file

@ -134,6 +134,10 @@ void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
for (int32_t i = 0; i < 4; i++) { for (int32_t i = 0; i < 4; i++) {
drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, colors->contentFg); 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);
}
} }

View file

@ -121,7 +121,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
while (top > 0) { while (top > 0) {
WidgetT *w = stack[--top]; 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; focus = w;
break; break;
} }
@ -160,13 +160,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
if (focus->type == WidgetButtonE) { if (focus->type == WidgetButtonE) {
if (key == ' ' || key == 0x0D) { if (key == ' ' || key == 0x0D) {
focus->as.button.pressed = true; focus->as.button.pressed = true;
wgtInvalidate(focus); sKeyPressedBtn = focus;
focus->as.button.pressed = false;
if (focus->onClick) {
focus->onClick(focus);
}
wgtInvalidate(focus); wgtInvalidate(focus);
} }
@ -177,13 +171,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
if (focus->type == WidgetImageButtonE) { if (focus->type == WidgetImageButtonE) {
if (key == ' ' || key == 0x0D) { if (key == ' ' || key == 0x0D) {
focus->as.imageButton.pressed = true; focus->as.imageButton.pressed = true;
wgtInvalidate(focus); sKeyPressedBtn = focus;
focus->as.imageButton.pressed = false;
if (focus->onClick) {
focus->onClick(focus);
}
wgtInvalidate(focus); wgtInvalidate(focus);
} }
@ -330,6 +318,59 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
return; 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 // Handle dropdown keyboard navigation
if (focus->type == WidgetDropdownE) { if (focus->type == WidgetDropdownE) {
if (focus->as.dropdown.open) { 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) { if (hit->type == WidgetTabControlE && hit->enabled) {
hit->focused = true;
widgetTabControlOnMouse(hit, root, vx, vy); 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}; RectT fullRect = {0, 0, win->contentW, win->contentH};
win->onPaint(win, &fullRect); win->onPaint(win, &fullRect);
win->contentDirty = true; 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);
}
}
} }
} }

View file

@ -97,4 +97,10 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
w->as.imageButton.data, w->as.imageButton.imgPitch, w->as.imageButton.data, w->as.imageButton.imgPitch,
0, 0, w->as.imageButton.imgW, w->as.imageButton.imgH); 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);
}
} }

View file

@ -45,6 +45,7 @@
// ============================================================ // ============================================================
extern bool sDebugLayout; extern bool sDebugLayout;
extern WidgetT *sKeyPressedBtn;
extern WidgetT *sOpenPopup; extern WidgetT *sOpenPopup;
extern WidgetT *sPressedButton; extern WidgetT *sPressedButton;
extern WidgetT *sDragSlider; extern WidgetT *sDragSlider;

View file

@ -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); 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);
}
} }

View file

@ -95,8 +95,12 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
CHECKBOX_BOX_SIZE - 6, CHECKBOX_BOX_SIZE - 6, fg); CHECKBOX_BOX_SIZE - 6, CHECKBOX_BOX_SIZE - 6, fg);
} }
drawTextAccel(d, ops, font, int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP;
w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP, int32_t labelY = w->y + (w->h - font->charHeight) / 2;
w->y + (w->h - font->charHeight) / 2, drawTextAccel(d, ops, font, labelX, labelY, w->as.radio.text, fg, bg, false);
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);
}
} }

View file

@ -197,4 +197,8 @@ void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
// Center tick on thumb // Center tick on thumb
drawVLine(d, ops, thumbX + SLIDER_THUMB_W / 2, w->y + 3, w->h - 6, fg); 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);
}
} }

View file

@ -242,6 +242,10 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
drawTextAccel(d, ops, font, tabX + TAB_PAD_H, labelY, drawTextAccel(d, ops, font, tabX + TAB_PAD_H, labelY,
c->as.tabPage.title, colors->contentFg, tabFace, true); 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; tabX += tw;
tabIdx++; tabIdx++;
} }

View file

@ -1135,4 +1135,9 @@ void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
if (needHSb) { if (needHSb) {
drawTreeHScrollbar(w, d, ops, colors, totalW, innerW, needVSb); 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);
}
} }