More event work, code cleanup.

This commit is contained in:
Scott Duensing 2026-04-02 21:13:42 -05:00
parent 88746ec2ba
commit 6d75e4996a
13 changed files with 937 additions and 498 deletions

View file

@ -66,7 +66,9 @@ static void freeListBoxItems(BasControlT *ctrl);
static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled);
static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled);
static ListBoxItemsT *getListBoxItems(BasControlT *ctrl);
static void onFormActivate(WindowT *win);
static void onFormClose(WindowT *win);
static void onFormDeactivate(WindowT *win);
static void onFormResize(WindowT *win, int32_t newW, int32_t newH);
static void onWidgetBlur(WidgetT *w);
static void onWidgetChange(WidgetT *w);
@ -75,6 +77,7 @@ static void onWidgetDblClick(WidgetT *w);
static void onWidgetFocus(WidgetT *w);
static void onWidgetKeyPress(WidgetT *w, int32_t keyAscii);
static void onWidgetKeyDown(WidgetT *w, int32_t keyCode, int32_t shift);
static void onWidgetKeyUp(WidgetT *w, int32_t keyCode, int32_t shift);
static void onWidgetMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y);
static void onWidgetMouseUp(WidgetT *w, int32_t button, int32_t x, int32_t y);
static void onWidgetMouseMove(WidgetT *w, int32_t button, int32_t x, int32_t y);
@ -392,6 +395,57 @@ bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName
}
// ============================================================
// basFormRtFireEventWithCancel -- fire an event that has a Cancel
// parameter (first arg, Integer). Returns true if Cancel was set
// to non-zero by the event handler.
static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName) {
if (!rt || !form || !rt->vm || !rt->module) {
return false;
}
char handlerName[MAX_EVENT_NAME_LEN];
snprintf(handlerName, sizeof(handlerName), "%s_%s", ctrlName, eventName);
const BasProcEntryT *proc = basModuleFindProc(rt->module, handlerName);
if (!proc || proc->isFunction) {
return false;
}
// Must accept 0 or 1 parameter
if (proc->paramCount != 0 && proc->paramCount != 1) {
return false;
}
BasFormT *prevForm = rt->currentForm;
rt->currentForm = form;
basVmSetCurrentForm(rt->vm, form);
bool cancelled = false;
if (proc->paramCount == 1) {
BasValueT args[1];
args[0] = basValLong(0); // Cancel = 0 (don't cancel)
BasValueT outArgs[1];
memset(outArgs, 0, sizeof(outArgs));
if (basVmCallSubWithArgsOut(rt->vm, proc->codeAddr, args, 1, outArgs, 1)) {
cancelled = (basValToNumber(outArgs[0]) != 0);
basValRelease(&outArgs[0]);
}
} else {
basVmCallSub(rt->vm, proc->codeAddr);
}
rt->currentForm = prevForm;
basVmSetCurrentForm(rt->vm, prevForm);
return cancelled;
}
// ============================================================
// basFormRtGetProp
// ============================================================
@ -515,6 +569,8 @@ void *basFormRtLoadForm(void *ctx, const char *formName) {
snprintf(form->name, BAS_MAX_CTRL_NAME, "%s", formName);
win->onClose = onFormClose;
win->onResize = onFormResize;
win->onFocus = onFormActivate;
win->onBlur = onFormDeactivate;
form->window = win;
form->root = root;
form->contentBox = NULL; // created lazily after Layout property is known
@ -679,6 +735,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
widget->onBlur = onWidgetBlur;
widget->onKeyPress = onWidgetKeyPress;
widget->onKeyDown = onWidgetKeyDown;
widget->onKeyUp = onWidgetKeyUp;
widget->onMouseDown = onWidgetMouseDown;
widget->onMouseUp = onWidgetMouseUp;
widget->onMouseMove = onWidgetMouseMove;
@ -1296,9 +1353,6 @@ static ListBoxItemsT *getListBoxItems(BasControlT *ctrl) {
// Form_Unload event and stops the VM so the program exits.
static void onFormClose(WindowT *win) {
// Find which form owns this window
// The window's userData stores nothing useful, so we search
// by window pointer. We get the form runtime from sFormRt.
if (!sFormRt) {
return;
}
@ -1307,6 +1361,11 @@ static void onFormClose(WindowT *win) {
BasFormT *form = &sFormRt->forms[i];
if (form->window == win) {
// QueryUnload: if Cancel is set, abort the close
if (basFormRtFireEventWithCancel(sFormRt, form, form->name, "QueryUnload")) {
return;
}
basFormRtFireEvent(sFormRt, form, form->name, "Unload");
// Free control resources
@ -1367,6 +1426,42 @@ static void onFormResize(WindowT *win, int32_t newW, int32_t newH) {
}
// ============================================================
// onFormActivate / onFormDeactivate
// ============================================================
static void onFormActivate(WindowT *win) {
if (!sFormRt) {
return;
}
for (int32_t i = 0; i < sFormRt->formCount; i++) {
BasFormT *form = &sFormRt->forms[i];
if (form->window == win) {
basFormRtFireEvent(sFormRt, form, form->name, "Activate");
return;
}
}
}
static void onFormDeactivate(WindowT *win) {
if (!sFormRt) {
return;
}
for (int32_t i = 0; i < sFormRt->formCount; i++) {
BasFormT *form = &sFormRt->forms[i];
if (form->window == win) {
basFormRtFireEvent(sFormRt, form, form->name, "Deactivate");
return;
}
}
}
// ============================================================
// onWidgetBlur
// ============================================================
@ -1408,7 +1503,9 @@ static void onWidgetChange(WidgetT *w) {
}
if (rt) {
basFormRtFireEvent(rt, ctrl->form, ctrl->name, "Change");
// Timer widgets fire "Timer" event, everything else fires "Change"
const char *evtName = (strcasecmp(ctrl->typeName, "Timer") == 0) ? "Timer" : "Change";
basFormRtFireEvent(rt, ctrl->form, ctrl->name, evtName);
}
}
@ -1521,6 +1618,28 @@ static void onWidgetKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
}
// ============================================================
// onWidgetKeyUp
// ============================================================
static void onWidgetKeyUp(WidgetT *w, int32_t keyCode, int32_t shift) {
BasControlT *ctrl = (BasControlT *)w->userData;
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
if (rt) {
BasValueT args[2];
args[0] = basValLong(keyCode);
args[1] = basValLong(shift);
basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, "KeyUp", args, 2);
}
}
// ============================================================
// onWidgetMouseDown / onWidgetMouseUp / onWidgetMouseMove
// ============================================================

File diff suppressed because it is too large Load diff

View file

@ -70,27 +70,16 @@ bool basVmCallSub(BasVmT *vm, int32_t codeAddr) {
vm->pc = codeAddr;
vm->running = true;
// Step until the SUB returns (callDepth drops back)
int32_t steps = 0;
// Run until the SUB returns (callDepth drops back).
// No step limit -- event handlers must run to completion.
while (vm->running && vm->callDepth > savedCallDepth) {
if (vm->stepLimit > 0 && steps >= vm->stepLimit) {
// Unwind the call and restore state
vm->callDepth = savedCallDepth;
vm->pc = savedPc;
vm->running = savedRunning;
return false;
}
BasVmResultE result = basVmStep(vm);
steps++;
if (result == BAS_VM_HALTED) {
break;
}
if (result != BAS_VM_OK) {
// Runtime error in the event handler
vm->pc = savedPc;
vm->callDepth = savedCallDepth;
vm->running = savedRunning;
@ -98,7 +87,6 @@ bool basVmCallSub(BasVmT *vm, int32_t codeAddr) {
}
}
// Restore VM state
vm->pc = savedPc;
vm->running = savedRunning;
return true;
@ -139,18 +127,8 @@ bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, i
vm->pc = codeAddr;
vm->running = true;
int32_t steps = 0;
while (vm->running && vm->callDepth > savedCallDepth) {
if (vm->stepLimit > 0 && steps >= vm->stepLimit) {
vm->callDepth = savedCallDepth;
vm->pc = savedPc;
vm->running = savedRunning;
return false;
}
BasVmResultE result = basVmStep(vm);
steps++;
if (result == BAS_VM_HALTED) {
break;
@ -170,6 +148,71 @@ bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, i
}
// ============================================================
// basVmCallSubWithArgsOut
// ============================================================
bool basVmCallSubWithArgsOut(BasVmT *vm, int32_t codeAddr, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outCount) {
if (!vm || !vm->module) {
return false;
}
if (codeAddr < 0 || codeAddr >= vm->module->codeLen) {
return false;
}
if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE - 1) {
return false;
}
int32_t savedPc = vm->pc;
int32_t savedCallDepth = vm->callDepth;
bool savedRunning = vm->running;
BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
frame->returnPc = savedPc;
frame->localCount = BAS_VM_MAX_LOCALS;
memset(frame->locals, 0, sizeof(frame->locals));
for (int32_t i = 0; i < argCount && i < BAS_VM_MAX_LOCALS; i++) {
frame->locals[i] = basValCopy(args[i]);
}
vm->pc = codeAddr;
vm->running = true;
while (vm->running && vm->callDepth > savedCallDepth) {
BasVmResultE result = basVmStep(vm);
if (result == BAS_VM_HALTED) {
break;
}
if (result != BAS_VM_OK) {
vm->pc = savedPc;
vm->callDepth = savedCallDepth;
vm->running = savedRunning;
return false;
}
}
// Read back modified locals before restoring state.
// The frame at savedCallDepth still has the data even
// though callDepth was decremented by RET.
if (outArgs && outCount > 0) {
BasCallFrameT *doneFrame = &vm->callStack[savedCallDepth];
for (int32_t i = 0; i < outCount && i < BAS_VM_MAX_LOCALS; i++) {
outArgs[i] = basValCopy(doneFrame->locals[i]);
}
}
vm->pc = savedPc;
vm->running = savedRunning;
return true;
}
// ============================================================
// basVmCreate
// ============================================================

View file

@ -364,4 +364,9 @@ const char *basVmGetError(const BasVmT *vm);
bool basVmCallSub(BasVmT *vm, int32_t codeAddr);
bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, int32_t argCount);
// Call a SUB and read back modified argument values.
// outArgs receives copies of the locals after the SUB returns.
// outCount specifies how many args to read back.
bool basVmCallSubWithArgsOut(BasVmT *vm, int32_t codeAddr, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outCount);
#endif // DVXBASIC_VM_H

View file

@ -2273,6 +2273,167 @@ int main(void) {
printf("\n");
}
// ============================================================
// Coverage: Bare sub call (no CALL keyword, no parens)
// ============================================================
runProgram("Bare sub call",
"Sub Greet ()\n"
" PRINT \"hello\"\n"
"End Sub\n"
"\n"
"Greet\n"
);
// ============================================================
// Coverage: Bare sub call with forward reference
// ============================================================
runProgram("Bare sub call forward ref",
"DoWork\n"
"\n"
"Sub DoWork ()\n"
" PRINT \"worked\"\n"
"End Sub\n"
);
// ============================================================
// Coverage: Unresolved forward reference error
// ============================================================
{
printf("=== Unresolved forward reference ===\n");
const char *src = "NeverDefined\n";
int32_t len = (int32_t)strlen(src);
BasParserT parser;
basParserInit(&parser, src, len);
if (!basParse(&parser)) {
printf("Correctly caught: %s\n", parser.error);
} else {
printf("ERROR: should have failed\n");
}
basParserFree(&parser);
printf("\n");
}
// ============================================================
// Coverage: END statement terminates (distinct from HALT)
// ============================================================
runProgram("END statement",
"PRINT \"before\"\n"
"END\n"
"PRINT \"after\"\n"
);
// ============================================================
// Coverage: Nested UDT field store
// ============================================================
runProgram("Nested UDT store and load",
"TYPE InnerT\n"
" val AS INTEGER\n"
"END TYPE\n"
"\n"
"TYPE OuterT\n"
" child AS InnerT\n"
" name AS STRING\n"
"END TYPE\n"
"\n"
"DIM o AS OuterT\n"
"o.name = \"test\"\n"
"o.child.val = 42\n"
"PRINT o.name\n"
"PRINT o.child.val\n"
);
// ============================================================
// Coverage: Array of UDT field store
// ============================================================
runProgram("Array of UDT field store",
"TYPE PointT\n"
" x AS INTEGER\n"
" y AS INTEGER\n"
"END TYPE\n"
"\n"
"DIM pts(5) AS PointT\n"
"pts(1).x = 10\n"
"pts(1).y = 20\n"
"pts(3).x = 30\n"
"pts(3).y = 40\n"
"PRINT pts(1).x; pts(1).y\n"
"PRINT pts(3).x; pts(3).y\n"
);
// ============================================================
// Coverage: VB operator precedence (^ binds tighter than unary -)
// ============================================================
runProgram("Exponent precedence",
"PRINT -2 ^ 2\n" // -(2^2) = -4
"PRINT (-2) ^ 2\n" // (-2)^2 = 4
"PRINT 3 ^ 2 + 1\n" // 9 + 1 = 10
);
// ============================================================
// Coverage: Me.Show compiles in a Sub
// ============================================================
{
printf("=== Me.Show in Sub ===\n");
const char *src =
"Sub Form1_Load ()\n"
" Me.Show\n"
" Me.Caption = \"Hello\"\n"
" Me.Hide\n"
"End Sub\n";
int32_t len = (int32_t)strlen(src);
BasParserT parser;
basParserInit(&parser, src, len);
if (!basParse(&parser)) {
printf("COMPILE ERROR: %s\n", parser.error);
} else {
printf("OK\n");
}
basParserFree(&parser);
printf("\n");
}
// ============================================================
// Coverage: OPTION EXPLICIT with valid declaration
// ============================================================
runProgram("OPTION EXPLICIT valid",
"OPTION EXPLICIT\n"
"DIM x AS INTEGER\n"
"x = 42\n"
"PRINT x\n"
);
// ============================================================
// Coverage: STATIC variable retains value
// ============================================================
runProgram("STATIC in sub",
"Sub Counter ()\n"
" STATIC n AS INTEGER\n"
" n = n + 1\n"
" PRINT n\n"
"End Sub\n"
"\n"
"Counter\n"
"Counter\n"
"Counter\n"
);
printf("All tests complete.\n");
return 0;
}

View file

@ -3005,6 +3005,20 @@ static void pollKeyboard(AppContextT *ctx) {
continue;
nextKey:;
}
// Key-up events: dispatch to the focused window's onKeyUp callback
PlatformKeyEventT upEvt;
while (platformKeyUpRead(&upEvt)) {
if (ctx->stack.focusedIdx >= 0) {
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
int32_t mod = platformKeyboardGetModifiers();
if (win->onKeyUp) {
win->onKeyUp(win, upEvt.scancode | 0x100, mod);
}
}
}
}
@ -4182,6 +4196,7 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_
memset(ctx, 0, sizeof(*ctx));
platformInit();
platformKeyUpInit();
// Enumerate available video modes BEFORE setting one. Some VBE
// BIOSes return a stale or truncated mode list once a graphics
@ -4784,6 +4799,8 @@ int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path) {
// ============================================================
void dvxShutdown(AppContextT *ctx) {
platformKeyUpShutdown();
// Destroy all remaining windows
while (ctx->stack.count > 0) {
wmDestroyWindow(&ctx->stack, ctx->stack.windows[ctx->stack.count - 1]);

View file

@ -560,6 +560,7 @@ typedef struct WindowT {
void *userData;
void (*onPaint)(struct WindowT *win, RectT *dirtyArea);
void (*onKey)(struct WindowT *win, int32_t key, int32_t mod);
void (*onKeyUp)(struct WindowT *win, int32_t scancode, int32_t mod);
void (*onMouse)(struct WindowT *win, int32_t x, int32_t y, int32_t buttons);
void (*onResize)(struct WindowT *win, int32_t newW, int32_t newH);
void (*onClose)(struct WindowT *win);

View file

@ -175,6 +175,7 @@ void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font);
void widgetManageScrollbars(WindowT *win, AppContextT *ctx);
void widgetOnKey(WindowT *win, int32_t key, int32_t mod);
void widgetOnKeyUp(WindowT *win, int32_t scancode, int32_t mod);
void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
void widgetOnPaint(WindowT *win, RectT *dirtyArea);
void widgetOnResize(WindowT *win, int32_t newW, int32_t newH);

View file

@ -177,6 +177,19 @@ int32_t platformKeyboardGetModifiers(void);
// them unambiguously.
bool platformKeyboardRead(PlatformKeyEventT *evt);
// Non-blocking read of the next key-up event. Returns true if a
// key release was detected. On DOS this requires an INT 9 hook to
// detect break codes (scan code with bit 7 set). On Linux this
// uses SDL_KEYUP events.
bool platformKeyUpRead(PlatformKeyEventT *evt);
// Install/remove the INT 9 hook for key-up detection. On DOS this
// chains the hardware keyboard interrupt. On Linux this is a no-op
// (SDL provides key-up events natively). Call Init before using
// platformKeyUpRead, and Shutdown before exit.
void platformKeyUpInit(void);
void platformKeyUpShutdown(void);
// Translate an Alt+key scancode to its corresponding ASCII character.
// When Alt is held, DOS doesn't provide the ASCII value -- only the
// scancode. This function contains a lookup table mapping scancodes

View file

@ -1430,6 +1430,87 @@ bool platformKeyboardRead(PlatformKeyEventT *evt) {
}
// ============================================================
// Key-up detection via INT 9 hook
// ============================================================
//
// The BIOS keyboard interrupt (INT 16h) only reports key presses.
// To detect key releases we chain INT 9 (the hardware keyboard IRQ)
// and read the scan code directly from port 0x60. Break codes have
// bit 7 set. We queue them in a small ring buffer that
// platformKeyUpRead drains.
#define KEYUP_BUF_SIZE 16
static PlatformKeyEventT sKeyUpBuf[KEYUP_BUF_SIZE];
static volatile int32_t sKeyUpHead = 0;
static volatile int32_t sKeyUpTail = 0;
static _go32_dpmi_seginfo sOldInt9;
static _go32_dpmi_seginfo sNewInt9;
static bool sKeyUpInstalled = false;
// INT 9 handler: reads port 0x60 and queues break codes.
// DJGPP chains to the original handler automatically when using
// _go32_dpmi_chain_protected_mode_interrupt_vector.
static void int9Handler(void) {
uint8_t scan = inportb(0x60);
// Break code: bit 7 set and not the 0xE0 prefix
if ((scan & 0x80) && scan != 0xE0) {
int32_t next = (sKeyUpHead + 1) % KEYUP_BUF_SIZE;
if (next != sKeyUpTail) {
sKeyUpBuf[sKeyUpHead].scancode = scan & 0x7F;
sKeyUpBuf[sKeyUpHead].ascii = 0;
sKeyUpHead = next;
}
}
// Original handler is called automatically by DJGPP's chain wrapper
}
void platformKeyUpInit(void) {
if (sKeyUpInstalled) {
return;
}
_go32_dpmi_get_protected_mode_interrupt_vector(9, &sOldInt9);
sNewInt9.pm_offset = (unsigned long)int9Handler;
sNewInt9.pm_selector = _go32_my_cs();
// Chain: our handler runs first, then DJGPP automatically
// calls the original handler via an IRET wrapper.
_go32_dpmi_chain_protected_mode_interrupt_vector(9, &sNewInt9);
sKeyUpInstalled = true;
}
void platformKeyUpShutdown(void) {
if (!sKeyUpInstalled) {
return;
}
_go32_dpmi_set_protected_mode_interrupt_vector(9, &sOldInt9);
sKeyUpInstalled = false;
}
bool platformKeyUpRead(PlatformKeyEventT *evt) {
if (sKeyUpTail == sKeyUpHead) {
return false;
}
*evt = sKeyUpBuf[sKeyUpTail];
sKeyUpTail = (sKeyUpTail + 1) % KEYUP_BUF_SIZE;
return true;
}
// ============================================================
// platformLineEnding
// ============================================================
@ -2364,6 +2445,9 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformInstallCrashHandler)
DXE_EXPORT(platformKeyboardGetModifiers)
DXE_EXPORT(platformKeyboardRead)
DXE_EXPORT(platformKeyUpInit)
DXE_EXPORT(platformKeyUpRead)
DXE_EXPORT(platformKeyUpShutdown)
DXE_EXPORT(platformLineEnding)
DXE_EXPORT(platformLogCrashDetail)
DXE_EXPORT(platformMkdirRecursive)

View file

@ -197,6 +197,39 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
}
// ============================================================
// widgetOnKeyUp
// ============================================================
void widgetOnKeyUp(WindowT *win, int32_t scancode, int32_t mod) {
WidgetT *root = win->widgetRoot;
if (!root) {
return;
}
WidgetT *focus = sFocusedWidget;
if (!focus || !focus->focused || focus->window != win) {
return;
}
if (!focus->enabled) {
return;
}
if (focus->onKeyUp) {
AppContextT *ctx = (AppContextT *)root->userData;
int32_t prevAppId = ctx->currentAppId;
ctx->currentAppId = win->appId;
focus->onKeyUp(focus, scancode, mod);
ctx->currentAppId = prevAppId;
}
}
// ============================================================
// widgetOnMouse
// ============================================================

View file

@ -307,6 +307,7 @@ WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) {
win->onPaint = widgetOnPaint;
win->onMouse = widgetOnMouse;
win->onKey = widgetOnKey;
win->onKeyUp = widgetOnKeyUp;
win->onResize = widgetOnResize;
return root;

View file

@ -166,6 +166,15 @@ void wgtTimerStop(WidgetT *w) {
}
void wgtTimerSetEnabled(WidgetT *w, bool enabled) {
if (enabled) {
wgtTimerStart(w);
} else {
wgtTimerStop(w);
}
}
void wgtUpdateTimers(void) {
clock_t now = clock();
@ -220,7 +229,7 @@ static const struct {
};
static const WgtPropDescT sProps[] = {
{ "Enabled", WGT_IFACE_BOOL, (void *)wgtTimerIsRunning, NULL },
{ "Enabled", WGT_IFACE_BOOL, (void *)wgtTimerIsRunning, (void *)wgtTimerSetEnabled },
{ "Interval", WGT_IFACE_INT, NULL, (void *)wgtTimerSetInterval }
};