More events added and wired to BASIC.

This commit is contained in:
Scott Duensing 2026-04-01 17:53:16 -05:00
parent 1fb8e2a387
commit 4d4aedbc43
8 changed files with 724 additions and 23 deletions

View file

@ -73,6 +73,12 @@ static void onWidgetChange(WidgetT *w);
static void onWidgetClick(WidgetT *w);
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 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);
static void onWidgetScroll(WidgetT *w, int32_t delta);
static void parseFrmLine(const char *line, char *key, char *value);
static void rebuildListBoxItems(BasControlT *ctrl);
static const char *resolveTypeName(const char *typeName);
@ -341,6 +347,10 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
// ============================================================
bool basFormRtFireEvent(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName) {
return basFormRtFireEventArgs(rt, form, ctrlName, eventName, NULL, 0);
}
bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount) {
if (!rt || !form || !rt->vm || !rt->module) {
return false;
}
@ -354,7 +364,13 @@ bool basFormRtFireEvent(BasFormRtT *rt, BasFormT *form, const char *ctrlName, co
return false;
}
if (proc->isFunction || proc->paramCount > 0) {
if (proc->isFunction) {
return false;
}
// Strict parameter matching: the sub must declare exactly the
// number of parameters the event provides, or zero (no params).
if (proc->paramCount != 0 && proc->paramCount != argCount) {
return false;
}
@ -362,7 +378,13 @@ bool basFormRtFireEvent(BasFormRtT *rt, BasFormT *form, const char *ctrlName, co
rt->currentForm = form;
basVmSetCurrentForm(rt->vm, form);
bool ok = basVmCallSub(rt->vm, proc->codeAddr);
bool ok;
if (argCount > 0 && args) {
ok = basVmCallSubWithArgs(rt->vm, proc->codeAddr, args, argCount);
} else {
ok = basVmCallSub(rt->vm, proc->codeAddr);
}
rt->currentForm = prevForm;
basVmSetCurrentForm(rt->vm, prevForm);
@ -647,11 +669,17 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
// Re-derive pointer after arrput (may realloc)
current = &form->controls[form->controlCount - 1];
widget->userData = current;
widget->onClick = onWidgetClick;
widget->onDblClick = onWidgetDblClick;
widget->onChange = onWidgetChange;
widget->onFocus = onWidgetFocus;
widget->onBlur = onWidgetBlur;
widget->onClick = onWidgetClick;
widget->onDblClick = onWidgetDblClick;
widget->onChange = onWidgetChange;
widget->onFocus = onWidgetFocus;
widget->onBlur = onWidgetBlur;
widget->onKeyPress = onWidgetKeyPress;
widget->onKeyDown = onWidgetKeyDown;
widget->onMouseDown = onWidgetMouseDown;
widget->onMouseUp = onWidgetMouseUp;
widget->onMouseMove = onWidgetMouseMove;
widget->onScroll = onWidgetScroll;
}
// Track block type for End handling
@ -1447,6 +1475,127 @@ static void onWidgetFocus(WidgetT *w) {
}
// ============================================================
// onWidgetKeyPress / onWidgetKeyDown
// ============================================================
static void onWidgetKeyPress(WidgetT *w, int32_t keyAscii) {
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[1];
args[0] = basValLong(keyAscii);
basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, "KeyPress", args, 1);
}
}
static void onWidgetKeyDown(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, "KeyDown", args, 2);
}
}
// ============================================================
// onWidgetMouseDown / onWidgetMouseUp / onWidgetMouseMove
// ============================================================
static void onWidgetMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y) {
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[3];
args[0] = basValLong(button);
args[1] = basValLong(x);
args[2] = basValLong(y);
basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, "MouseDown", args, 3);
}
}
static void onWidgetMouseUp(WidgetT *w, int32_t button, int32_t x, int32_t y) {
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[3];
args[0] = basValLong(button);
args[1] = basValLong(x);
args[2] = basValLong(y);
basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, "MouseUp", args, 3);
}
}
static void onWidgetMouseMove(WidgetT *w, int32_t button, int32_t x, int32_t y) {
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[3];
args[0] = basValLong(button);
args[1] = basValLong(x);
args[2] = basValLong(y);
basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, "MouseMove", args, 3);
}
}
// ============================================================
// onWidgetScroll
// ============================================================
static void onWidgetScroll(WidgetT *w, int32_t delta) {
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[1];
args[0] = basValLong(delta);
basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, "Scroll", args, 1);
}
}
// ============================================================
// parseFrmLine
// ============================================================

View file

@ -111,6 +111,7 @@ int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags);
// ---- Event dispatch ----
bool basFormRtFireEvent(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName);
bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount);
// ---- Form file loading ----

View file

@ -2432,24 +2432,64 @@ static void onEvtDropdownChange(WidgetT *w) {
int32_t objIdx = wgtDropdownGetSelected(sObjDropdown);
int32_t evtIdx = wgtDropdownGetSelected(sEvtDropdown);
if (objIdx < 0 || objIdx >= (int32_t)arrlen(sObjItems) || evtIdx < 0) {
if (objIdx < 0 || objIdx >= (int32_t)arrlen(sObjItems) ||
evtIdx < 0 || evtIdx >= (int32_t)arrlen(sEvtItems)) {
return;
}
const char *selObj = sObjItems[objIdx];
const char *selEvt = sEvtItems[evtIdx];
// Find the proc matching the selected object + event
int32_t matchIdx = 0;
int32_t procCount = (int32_t)arrlen(sProcTable);
// Strip brackets if present (unimplemented event)
char evtName[64];
if (selEvt[0] == '[') {
snprintf(evtName, sizeof(evtName), "%s", selEvt + 1);
int32_t len = (int32_t)strlen(evtName);
if (len > 0 && evtName[len - 1] == ']') {
evtName[len - 1] = '\0';
}
} else {
snprintf(evtName, sizeof(evtName), "%s", selEvt);
}
// Search for an existing proc matching object + event
int32_t procCount = (int32_t)arrlen(sProcTable);
for (int32_t i = 0; i < procCount; i++) {
if (strcasecmp(sProcTable[i].objName, selObj) == 0) {
if (matchIdx == evtIdx) {
showProc(i);
return;
}
if (strcasecmp(sProcTable[i].objName, selObj) == 0 &&
strcasecmp(sProcTable[i].evtName, evtName) == 0) {
showProc(i);
return;
}
}
matchIdx++;
// Not found -- create a new sub skeleton
char subName[128];
snprintf(subName, sizeof(subName), "%s_%s", selObj, evtName);
char skeleton[256];
snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName);
arrput(sProcBufs, strdup(skeleton));
// Update form code if editing a form
if (sDesigner.form) {
free(sDesigner.form->code);
sDesigner.form->code = strdup(getFullSource());
sDesigner.form->dirty = true;
}
updateDropdowns();
// Show the new proc (last in the list)
procCount = (int32_t)arrlen(sProcTable);
for (int32_t i = 0; i < procCount; i++) {
if (strcasecmp(sProcTable[i].objName, selObj) == 0 &&
strcasecmp(sProcTable[i].evtName, evtName) == 0) {
showProc(i);
return;
}
}
}
@ -2513,6 +2553,25 @@ static void onImmediateChange(WidgetT *w) {
//
// Update the Event dropdown when the Object selection changes.
// Common events available on all controls
static const char *sCommonEvents[] = {
"Click", "DblClick", "Change", "GotFocus", "LostFocus",
"KeyPress", "KeyDown",
"MouseDown", "MouseUp", "MouseMove", "Scroll",
NULL
};
// Form-specific events
static const char *sFormEvents[] = {
"Load", "Unload", "Resize", "Activate", "Deactivate",
"KeyPress", "KeyDown",
"MouseDown", "MouseUp", "MouseMove",
NULL
};
// Buffer for event dropdown labels (with [] for unimplemented)
static char sEvtLabelBufs[64][32];
static void onObjDropdownChange(WidgetT *w) {
(void)w;
@ -2528,17 +2587,157 @@ static void onObjDropdownChange(WidgetT *w) {
const char *selObj = sObjItems[objIdx];
// Build event list for the selected object
// Collect which events already have code
arrsetlen(sEvtItems, 0);
int32_t procCount = (int32_t)arrlen(sProcTable);
const char **existingEvts = NULL; // stb_ds temp array
for (int32_t i = 0; i < procCount; i++) {
if (strcasecmp(sProcTable[i].objName, selObj) == 0) {
arrput(sEvtItems, sProcTable[i].evtName);
arrput(existingEvts, sProcTable[i].evtName);
}
}
// Determine which event list to use
const char **availEvents = sCommonEvents;
if (strcasecmp(selObj, "(General)") == 0) {
// (General) has no standard events -- just show existing procs
for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) {
arrput(sEvtItems, existingEvts[i]);
}
arrfree(existingEvts);
int32_t evtCount = (int32_t)arrlen(sEvtItems);
wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount);
if (evtCount > 0) {
wgtDropdownSetSelected(sEvtDropdown, 0);
}
return;
}
// Check if this is a form name
bool isForm = false;
if (sDesigner.form && strcasecmp(selObj, sDesigner.form->name) == 0) {
isForm = true;
availEvents = sFormEvents;
}
// Get widget-specific events from the interface
const WgtIfaceT *iface = NULL;
if (!isForm && sDesigner.form) {
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) {
if (strcasecmp(sDesigner.form->controls[i].name, selObj) == 0) {
const char *wgtName = wgtFindByBasName(sDesigner.form->controls[i].typeName);
if (wgtName) {
iface = wgtGetIface(wgtName);
}
break;
}
}
}
// Build the event list: standard events + widget-specific events
int32_t labelIdx = 0;
// Add standard events (common or form)
for (int32_t i = 0; availEvents[i]; i++) {
bool hasCode = false;
for (int32_t j = 0; j < (int32_t)arrlen(existingEvts); j++) {
if (strcasecmp(existingEvts[j], availEvents[i]) == 0) {
hasCode = true;
break;
}
}
if (labelIdx < 64) {
if (hasCode) {
snprintf(sEvtLabelBufs[labelIdx], 32, "%s", availEvents[i]);
} else {
snprintf(sEvtLabelBufs[labelIdx], 32, "[%s]", availEvents[i]);
}
arrput(sEvtItems, sEvtLabelBufs[labelIdx]);
labelIdx++;
}
}
// Add widget-specific events
if (iface) {
for (int32_t i = 0; i < iface->eventCount; i++) {
const char *evtName = iface->events[i].name;
// Skip if already in the standard list
bool alreadyListed = false;
for (int32_t j = 0; availEvents[j]; j++) {
if (strcasecmp(availEvents[j], evtName) == 0) {
alreadyListed = true;
break;
}
}
if (alreadyListed) {
continue;
}
bool hasCode = false;
for (int32_t j = 0; j < (int32_t)arrlen(existingEvts); j++) {
if (strcasecmp(existingEvts[j], evtName) == 0) {
hasCode = true;
break;
}
}
if (labelIdx < 64) {
if (hasCode) {
snprintf(sEvtLabelBufs[labelIdx], 32, "%s", evtName);
} else {
snprintf(sEvtLabelBufs[labelIdx], 32, "[%s]", evtName);
}
arrput(sEvtItems, sEvtLabelBufs[labelIdx]);
labelIdx++;
}
}
}
// Add any existing events not in the standard/widget list (custom subs)
for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) {
bool alreadyListed = false;
int32_t evtCount = (int32_t)arrlen(sEvtItems);
for (int32_t j = 0; j < evtCount; j++) {
const char *label = sEvtItems[j];
// Strip brackets for comparison
if (label[0] == '[') { label++; }
if (strncasecmp(label, existingEvts[i], strlen(existingEvts[i])) == 0) {
alreadyListed = true;
break;
}
}
if (!alreadyListed && labelIdx < 64) {
snprintf(sEvtLabelBufs[labelIdx], 32, "%s", existingEvts[i]);
arrput(sEvtItems, sEvtLabelBufs[labelIdx]);
labelIdx++;
}
}
arrfree(existingEvts);
int32_t evtCount = (int32_t)arrlen(sEvtItems);
wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount);
@ -3320,7 +3519,178 @@ static void parseProcs(const char *source) {
}
// saveCurProc -- save editor contents back to the current buffer
// extractNewProcs -- scan a buffer for Sub/Function declarations that
// don't belong (e.g. user typed a new Sub in the General section).
// Extracts them into new sProcBufs entries and removes them from the
// source buffer. Returns a new buffer (caller frees) or NULL if no
// extraction was needed.
static char *extractNewProcs(const char *buf) {
if (!buf || !buf[0]) {
return NULL;
}
// Scan for Sub/Function at the start of a line
bool found = false;
const char *pos = buf;
while (*pos) {
const char *trimmed = pos;
while (*trimmed == ' ' || *trimmed == '\t') {
trimmed++;
}
bool isSub = (strncasecmp(trimmed, "SUB ", 4) == 0);
bool isFunc = (strncasecmp(trimmed, "FUNCTION ", 9) == 0);
if (isSub || isFunc) {
found = true;
break;
}
while (*pos && *pos != '\n') { pos++; }
if (*pos == '\n') { pos++; }
}
if (!found) {
return NULL;
}
// Build the remaining text (before the first proc) and extract procs
int32_t bufLen = (int32_t)strlen(buf);
char *remaining = (char *)malloc(bufLen + 1);
if (!remaining) {
return NULL;
}
int32_t remPos = 0;
pos = buf;
while (*pos) {
const char *lineStart = pos;
const char *trimmed = pos;
while (*trimmed == ' ' || *trimmed == '\t') {
trimmed++;
}
bool isSub = (strncasecmp(trimmed, "SUB ", 4) == 0);
bool isFunc = (strncasecmp(trimmed, "FUNCTION ", 9) == 0);
if (isSub || isFunc) {
// Extract procedure name for duplicate check
const char *np = trimmed + (isSub ? 4 : 9);
while (*np == ' ' || *np == '\t') { np++; }
char newName[128];
int32_t nn = 0;
while (*np && *np != '(' && *np != ' ' && *np != '\t' && *np != '\n' && nn < 127) {
newName[nn++] = *np++;
}
newName[nn] = '\0';
// Check for duplicate against existing proc buffers
bool isDuplicate = false;
for (int32_t p = 0; p < (int32_t)arrlen(sProcBufs); p++) {
if (!sProcBufs[p]) { continue; }
const char *ep = sProcBufs[p];
while (*ep == ' ' || *ep == '\t') { ep++; }
if (strncasecmp(ep, "SUB ", 4) == 0) { ep += 4; }
else if (strncasecmp(ep, "FUNCTION ", 9) == 0) { ep += 9; }
while (*ep == ' ' || *ep == '\t') { ep++; }
char existName[128];
int32_t en = 0;
while (*ep && *ep != '(' && *ep != ' ' && *ep != '\t' && *ep != '\n' && en < 127) {
existName[en++] = *ep++;
}
existName[en] = '\0';
if (strcasecmp(newName, existName) == 0) {
isDuplicate = true;
break;
}
}
// Find End Sub / End Function
const char *endTag = isSub ? "END SUB" : "END FUNCTION";
int32_t endTagLen = isSub ? 7 : 12;
const char *scan = pos;
while (*scan && *scan != '\n') { scan++; }
if (*scan == '\n') { scan++; }
while (*scan) {
const char *sl = scan;
while (*sl == ' ' || *sl == '\t') { sl++; }
if (strncasecmp(sl, endTag, endTagLen) == 0) {
while (*scan && *scan != '\n') { scan++; }
if (*scan == '\n') { scan++; }
break;
}
while (*scan && *scan != '\n') { scan++; }
if (*scan == '\n') { scan++; }
}
if (isDuplicate) {
// Leave it in the General section -- compiler will report the error
while (*pos && *pos != '\n') {
remaining[remPos++] = *pos++;
}
if (*pos == '\n') {
remaining[remPos++] = *pos++;
}
} else {
// Extract this procedure into a new buffer
int32_t procLen = (int32_t)(scan - lineStart);
char *procBuf = (char *)malloc(procLen + 1);
if (procBuf) {
memcpy(procBuf, lineStart, procLen);
procBuf[procLen] = '\0';
arrput(sProcBufs, procBuf);
}
pos = scan;
}
continue;
}
// Copy non-proc lines to remaining
while (*pos && *pos != '\n') {
remaining[remPos++] = *pos++;
}
if (*pos == '\n') {
remaining[remPos++] = *pos++;
}
}
remaining[remPos] = '\0';
return remaining;
}
// saveCurProc -- save editor contents back to the current buffer.
// If the user typed a new Sub/Function in the General section,
// it's automatically extracted into its own procedure buffer.
static void saveCurProc(void) {
if (!sEditor) {
@ -3334,8 +3704,17 @@ static void saveCurProc(void) {
}
if (sCurProcIdx == -1) {
// General section -- check for embedded proc declarations
char *cleaned = extractNewProcs(edText);
free(sGeneralBuf);
sGeneralBuf = strdup(edText);
sGeneralBuf = cleaned ? cleaned : strdup(edText);
if (cleaned) {
// Update editor to show the cleaned General section
wgtSetText(sEditor, sGeneralBuf);
updateDropdowns();
}
} else if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcBufs)) {
free(sProcBufs[sCurProcIdx]);
sProcBufs[sCurProcIdx] = strdup(edText);
@ -3560,7 +3939,20 @@ static void updateDropdowns(void) {
lineNum++;
}
// Build unique object names for the Object dropdown
// Build object names for the Object dropdown.
// Always include "(General)" and the form name (if editing a form).
// Then add control names from the designer, plus any from existing procs.
arrput(sObjItems, "(General)");
if (sDesigner.form) {
arrput(sObjItems, sDesigner.form->name);
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) {
arrput(sObjItems, sDesigner.form->controls[i].name);
}
}
// Add any objects from existing procs not already in the list
int32_t procCount = (int32_t)arrlen(sProcTable);
for (int32_t i = 0; i < procCount; i++) {

View file

@ -105,6 +105,71 @@ bool basVmCallSub(BasVmT *vm, int32_t codeAddr) {
}
// ============================================================
// basVmCallSubWithArgs
// ============================================================
bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, int32_t argCount) {
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));
// Set arguments as locals (parameter 0 = local 0, etc.)
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;
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;
}
if (result != BAS_VM_OK) {
vm->pc = savedPc;
vm->callDepth = savedCallDepth;
vm->running = savedRunning;
return false;
}
}
vm->pc = savedPc;
vm->running = savedRunning;
return true;
}
// ============================================================
// basVmCreate
// ============================================================

View file

@ -361,5 +361,6 @@ const char *basVmGetError(const BasVmT *vm);
// the previous execution state. Returns true if the SUB was called
// and returned normally, false on error or if the VM was not idle.
bool basVmCallSub(BasVmT *vm, int32_t codeAddr);
bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, int32_t argCount);
#endif // DVXBASIC_VM_H

View file

@ -1297,6 +1297,14 @@ static void dispatchEvents(AppContextT *ctx) {
win = ctx->stack.windows[hitIdx];
}
// Dispatch right-click to the widget system for MouseDown events
if (win->onMouse) {
int32_t relX = mx - win->x - win->contentX;
int32_t relY = my - win->y - win->contentY;
win->onMouse(win, relX, relY, buttons);
}
// Then check for context menus
MenuT *ctxMenu = NULL;
if (win->widgetRoot) {
@ -1338,7 +1346,7 @@ static void dispatchEvents(AppContextT *ctx) {
// Mouse movement in content area -- send to focused window
if ((mx != ctx->prevMouseX || my != ctx->prevMouseY) &&
ctx->stack.focusedIdx >= 0 && (buttons & MOUSE_LEFT)) {
ctx->stack.focusedIdx >= 0) {
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
if (win->onMouse) {
@ -1396,6 +1404,15 @@ static void dispatchEvents(AppContextT *ctx) {
}
}
// Fire onScroll callback on the focused widget
if (win->widgetRoot) {
WidgetT *focus = wgtGetFocused();
if (focus && focus->onScroll) {
focus->onScroll(focus, ctx->mouseWheel * ctx->wheelDirection);
}
}
ScrollbarT *sb = !consumed ? (win->vScroll ? win->vScroll : win->hScroll) : NULL;
if (sb) {

View file

@ -264,6 +264,13 @@ typedef struct WidgetT {
void (*onChange)(struct WidgetT *w);
void (*onFocus)(struct WidgetT *w);
void (*onBlur)(struct WidgetT *w);
void (*onKeyPress)(struct WidgetT *w, int32_t keyAscii);
void (*onKeyDown)(struct WidgetT *w, int32_t keyCode, int32_t shift);
void (*onKeyUp)(struct WidgetT *w, int32_t keyCode, int32_t shift);
void (*onMouseDown)(struct WidgetT *w, int32_t button, int32_t x, int32_t y);
void (*onMouseUp)(struct WidgetT *w, int32_t button, int32_t x, int32_t y);
void (*onMouseMove)(struct WidgetT *w, int32_t button, int32_t x, int32_t y);
void (*onScroll)(struct WidgetT *w, int32_t delta);
} WidgetT;

View file

@ -26,6 +26,11 @@
// button again and re-open the popup in the same event.
WidgetT *sClosedPopup = NULL;
// Mouse state for tracking button transitions and movement
static int32_t sPrevMouseButtons = 0;
static int32_t sPrevMouseX = -1;
static int32_t sPrevMouseY = -1;
// ============================================================
// widgetManageScrollbars
@ -179,6 +184,15 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
wclsOnKey(focus, key, mod);
// Fire user callbacks after the widget's internal handler
if (key >= 32 && key < 127 && focus->onKeyPress) {
focus->onKeyPress(focus, key);
}
if (focus->onKeyDown) {
focus->onKeyDown(focus, key, mod);
}
ctx->currentAppId = prevAppId;
}
@ -351,6 +365,61 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
}
}
// Fire mouse event callbacks
if (hit->enabled) {
int32_t relX = vx - hit->x;
int32_t relY = vy - hit->y;
// MouseDown: button just pressed
if ((buttons & MOUSE_LEFT) && !(sPrevMouseButtons & MOUSE_LEFT)) {
if (hit->onMouseDown) {
hit->onMouseDown(hit, 1, relX, relY);
}
}
if ((buttons & MOUSE_RIGHT) && !(sPrevMouseButtons & MOUSE_RIGHT)) {
if (hit->onMouseDown) {
hit->onMouseDown(hit, 2, relX, relY);
}
}
if ((buttons & MOUSE_MIDDLE) && !(sPrevMouseButtons & MOUSE_MIDDLE)) {
if (hit->onMouseDown) {
hit->onMouseDown(hit, 3, relX, relY);
}
}
// MouseUp: button just released
if (!(buttons & MOUSE_LEFT) && (sPrevMouseButtons & MOUSE_LEFT)) {
if (hit->onMouseUp) {
hit->onMouseUp(hit, 1, relX, relY);
}
}
if (!(buttons & MOUSE_RIGHT) && (sPrevMouseButtons & MOUSE_RIGHT)) {
if (hit->onMouseUp) {
hit->onMouseUp(hit, 2, relX, relY);
}
}
if (!(buttons & MOUSE_MIDDLE) && (sPrevMouseButtons & MOUSE_MIDDLE)) {
if (hit->onMouseUp) {
hit->onMouseUp(hit, 3, relX, relY);
}
}
// MouseMove: position changed
if (vx != sPrevMouseX || vy != sPrevMouseY) {
if (hit->onMouseMove) {
hit->onMouseMove(hit, buttons, relX, relY);
}
}
}
sPrevMouseButtons = buttons;
sPrevMouseX = vx;
sPrevMouseY = vy;
// Update the cached focus pointer for O(1) access in widgetOnKey
if (hit->focused) {
sFocusedWidget = hit;