More BASIC widget debugging.

This commit is contained in:
Scott Duensing 2026-04-20 22:12:23 -05:00
parent 4600f3e631
commit 4cdcfe6b8c
22 changed files with 1221 additions and 126 deletions

View file

@ -107,7 +107,7 @@ SDKDIR = bin/sdk
deploy-sdk:
@echo "Building SDK..."
@mkdir -p $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo
@mkdir -p $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow
@# Core headers (libdvx public API)
@for f in src/libs/kpunch/libdvx/dvxApp.h src/libs/kpunch/libdvx/dvxTypes.h \
src/libs/kpunch/libdvx/dvxWgt.h src/libs/kpunch/libdvx/dvxWgtP.h \
@ -141,6 +141,10 @@ deploy-sdk:
@cp src/apps/kpunch/basdemo/basdemo.dbp $(SDKDIR)/samples/basic/basdemo/
@cp src/apps/kpunch/basdemo/basdemo.frm $(SDKDIR)/samples/basic/basdemo/
@cp src/apps/kpunch/basdemo/ICON32.BMP $(SDKDIR)/samples/basic/basdemo/
@# BASIC sample: widshow (widget showcase)
@cp src/apps/kpunch/widshow/widshow.dbp $(SDKDIR)/samples/basic/widshow/
@cp src/apps/kpunch/widshow/widshow.frm $(SDKDIR)/samples/basic/widshow/
@cp src/apps/kpunch/widshow/ICON32.BMP $(SDKDIR)/samples/basic/widshow/
@# README
@printf '%s\n' \
'DVX SDK' \

View file

@ -376,6 +376,7 @@ img { max-width: 100%; }
<li><a href="#lang.func.string">LEN</a></li>
<li><a href="#lang.func.string">LTRIM$</a></li>
<li><a href="#lang.func.string">MID$</a></li>
<li><a href="#lang.func.string">OCT$</a></li>
<li><a href="#lang.func.string">RIGHT$</a></li>
<li><a href="#lang.func.string">RTRIM$</a></li>
<li><a href="#lang.func.string">SPACE$</a></li>
@ -397,6 +398,7 @@ img { max-width: 100%; }
<li><a href="#lang.func.math">SQR</a></li>
<li><a href="#lang.func.math">TAN</a></li>
<li><a href="#lang.func.math">TIMER</a></li>
<li><a href="#lang.func.conversion">CBOOL</a></li>
<li><a href="#lang.func.conversion">CDBL</a></li>
<li><a href="#lang.func.conversion">CINT</a></li>
<li><a href="#lang.func.conversion">CLNG</a></li>
@ -856,6 +858,14 @@ img { max-width: 100%; }
<p>File list -- relative paths of all .bas and .frm files in the project. Each entry tracks whether it is a form file.</p>
<h2>How Projects are Compiled</h2>
<p>When the project is compiled, all source files are concatenated into a single source stream. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form.</p>
<h2>Compile-Time Validation</h2>
<p>Before the source is handed to the compiler, the IDE scans every .frm file and cross-checks the forms against the widget registry and the statements that reference them. Three classes of error are caught and reported without producing a binary:</p>
<ul>
<li>Unknown widget types -- any Begin TypeName ... End block whose TypeName does not match a registered widget is flagged. The error message names the offending form, control, and type, and hints that DVX uses VB6-style names (SpinButton, not Spinner).</li>
<li>Unknown properties -- statements of the form CtrlName.Property = value, or reads of CtrlName.Property, are checked against the widget's declared property list. A typo such as btn.Captoin triggers a compile error with the form, control, and property name.</li>
</ul>
<p>Unknown methods -- method calls CtrlName.Method ... are checked the same way against the widget's method list, catching mistakes like list.AddTiem before the program ever runs.</p>
<p>These checks use the same metadata the Properties panel and Toolbox read from each widget DXE, so property and method names always match the current widget set without any separate list to maintain.</p>
<h2>Project Operations</h2>
<pre> Operation Description
--------- -----------
@ -1203,7 +1213,9 @@ name$ = &quot;Hello&quot; ' String</code></pre>
<pre> Form Example Description
---- ------- -----------
Decimal integer 42 Values -32768..32767 are Integer; larger are Long
Hex integer &amp;HFF, &amp;H1234 Hexadecimal literal
Hex integer &amp;HFF, &amp;H1234 Hexadecimal literal (0-9, A-F)
Octal integer &amp;O10, &amp;O77 Octal literal (0-7)
Binary integer &amp;B1010, &amp;B11111111 Binary literal (0-1)
Integer suffix 42% Force Integer type
Long suffix 42&amp;, &amp;HFF&amp; Force Long type
Floating-point 3.14, 1.5E10, 2.5D3 Any number containing a decimal point or exponent is Double by default
@ -1514,7 +1526,7 @@ END SUB</code></pre>
<pre><code>Sub Greet(ByVal name As String)
Print &quot;Hello, &quot; &amp; name
End Sub</code></pre>
<p>Parameters are passed by reference by default. Use ByVal for value semantics; there is no separate ByRef keyword (omitting ByVal is the by-reference form). Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:</p>
<p>Parameters are passed by reference by default. Use ByVal for value semantics; there is no separate ByRef keyword (omitting ByVal is the by-reference form). Writes to a by-reference parameter update the caller's variable, including individual array elements: `bump a(3)` passed to a by-reference parameter will modify `a(3)` in place. Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:</p>
<pre><code>Greet &quot;World&quot;
Greet(&quot;World&quot;)
Call Greet(&quot;World&quot;)</code></pre>
@ -1806,6 +1818,7 @@ WEND</code></pre>
LTRIM$(s$) String Removes leading spaces from s$
MID$(s$, start) String Substring from start (1-based) to end of string
MID$(s$, start, length) String Substring of length characters starting at start
OCT$(n) String Octal representation of n (no leading &amp;O)
RIGHT$(s$, n) String Rightmost n characters of s$
RTRIM$(s$) String Removes trailing spaces from s$
SPACE$(n) String String of n spaces
@ -1866,6 +1879,7 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background</code></pre>
<h1>Conversion Functions</h1>
<pre> Function Returns Description
-------- ------- -----------
CBOOL(n) Boolean Returns True (-1) if n is nonzero or a non-empty string; False (0) otherwise
CDBL(n) Double Converts n to Double
CINT(n) Integer Converts n to Integer (rounds half away from zero)
CLNG(n) Long Converts n to Long
@ -2233,7 +2247,8 @@ End If</code></pre>
Enabled Boolean R/W Whether the control accepts user input.
BackColor Long R/W Background color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
ForeColor Long R/W Foreground (text) color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
TabIndex Integer R Accepted for VB compatibility but ignored. DVX has no tab order.</pre>
TabIndex Integer R Accepted for VB compatibility but ignored. DVX has no tab order.
ToolTipText String R/W Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip.</pre>
<h2>Common Events</h2>
<p>These events are wired on every control loaded from a .frm file. Controls created dynamically at runtime via code only receive Click, DblClick, Change, GotFocus, and LostFocus; the keyboard, mouse, and scroll events below require the control to be defined in the .frm file.</p>
<pre> Event Parameters Description
@ -2863,7 +2878,7 @@ End Sub</code></pre>
List(index%) Return the text of the item at the given index.
ListCount() Return the number of items in the list.
RemoveItem index% Remove the item at the given index.</pre>
<p>Default Event: Click</p>
<p>Default Event: Change (fires when the user picks a new item; Click fires only when the arrow opens the popup)</p>
<h2>Example</h2>
<pre><code>Begin DropDown DropDown1
End
@ -2875,7 +2890,7 @@ Sub Form_Load ()
DropDown1.ListIndex = 1
End Sub
Sub DropDown1_Click ()
Sub DropDown1_Change ()
Label1.Caption = &quot;Picked: &quot; &amp; DropDown1.List(DropDown1.ListIndex)
End Sub</code></pre>
<p><a href="#ctrl.common.props">Common Properties, Events, and Methods</a></p>

View file

@ -3271,6 +3271,7 @@ prefsClose(h);</code></pre>
--------- -----------
name Widget type name (e.g. &quot;button&quot;, &quot;listbox&quot;)
api Pointer to the widget's API struct</pre>
<blockquote><strong>Warning:</strong> The create function must be the first field of the API struct. BASIC's form runtime instantiates widgets by dereferencing the api pointer as a pointer-to-function-pointer (see createWidgetByIface), so whichever field is first is what gets invoked. If the struct has multiple constructor variants, the one matching createSig must come first; put helper constructors, index accessors, and other entry points after it.</blockquote>
<h3>wgtGetApi</h3>
<pre><code>const void *wgtGetApi(const char *name);</code></pre>
<p>Retrieve a registered widget API struct by name.</p>

View file

@ -39,7 +39,7 @@ C_APPS = progman clock dvxdemo cpanel dvxhelp
BASCOMP = ../../../bin/host/bascomp
# BASIC apps: each is a .dbp project in its own directory.
# BASIC-only notepad, imgview, etc. replace the old C versions.
BASIC_APPS = iconed notepad imgview helpedit resedit basdemo
BASIC_APPS = iconed notepad imgview helpedit resedit basdemo widshow
.PHONY: all clean $(C_APPS) dvxbasic $(BASIC_APPS)
@ -120,6 +120,11 @@ basdemo: $(BINDIR)/kpunch/basdemo/basdemo.app
$(BINDIR)/kpunch/basdemo/basdemo.app: basdemo/basdemo.dbp basdemo/basdemo.frm basdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basdemo dvxbasic
$(BASCOMP) basdemo/basdemo.dbp -o $@ -release
widshow: $(BINDIR)/kpunch/widshow/widshow.app
$(BINDIR)/kpunch/widshow/widshow.app: widshow/widshow.dbp widshow/widshow.frm widshow/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/widshow dvxbasic
$(BASCOMP) widshow/widshow.dbp -o $@ -release
$(OBJDIR):
mkdir -p $(OBJDIR)
@ -133,6 +138,7 @@ $(BINDIR)/kpunch/notepad: ; mkdir -p $@
$(BINDIR)/kpunch/imgview: ; mkdir -p $@
$(BINDIR)/kpunch/resedit: ; mkdir -p $@
$(BINDIR)/kpunch/basdemo: ; mkdir -p $@
$(BINDIR)/kpunch/widshow: ; mkdir -p $@
# Header dependencies
COMMON_H = ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxDlg.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxWm.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h
@ -154,4 +160,5 @@ clean:
rm -f $(BINDIR)/kpunch/dvxhelp/helpedit.app
rm -f $(BINDIR)/kpunch/resedit/resedit.app
rm -f $(BINDIR)/kpunch/basdemo/basdemo.app
rm -f $(BINDIR)/kpunch/widshow/widshow.app
$(MAKE) -C dvxbasic clean

View file

@ -55,6 +55,7 @@ Every control in DVX BASIC inherits a set of common properties, events, and meth
BackColor Long R/W Background color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
ForeColor Long R/W Foreground (text) color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
TabIndex Integer R Accepted for VB compatibility but ignored. DVX has no tab order.
ToolTipText String R/W Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip.
.endtable
.h2 Common Events

View file

@ -1168,13 +1168,7 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
BasFormRtT *rt = (BasFormRtT *)ctx;
BasFormT *form = (BasFormT *)formRef;
dvxLog("[FC] findCtrl: form=%p '%s' looking for '%s'",
(void *)form,
form ? form->name : "(null)",
ctrlName ? ctrlName : "(null)");
if (!form) {
dvxLog("[FC] NULL FORM");
return NULL;
}
@ -2184,13 +2178,6 @@ void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const ch
(void)ctx;
BasControlT *ctrl = (BasControlT *)ctrlRef;
dvxLog("[SE] SetEvent: ctrl=%p '%s' type='%s' event='%s' -> handler='%s'",
(void *)ctrl,
ctrl ? ctrl->name : "(null)",
ctrl ? ctrl->typeName : "(null)",
eventName ? eventName : "(null)",
handlerName ? handlerName : "(null)");
if (!ctrl || !eventName || !handlerName) {
return;
}
@ -2208,9 +2195,6 @@ void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const ch
BasEventOverrideT *ov = &ctrl->eventOverrides[ctrl->eventOverrideCount++];
snprintf(ov->eventName, BAS_MAX_CTRL_NAME, "%s", eventName);
snprintf(ov->handlerName, BAS_MAX_CTRL_NAME, "%s", handlerName);
dvxLog("[SE] added (count=%d)", (int)ctrl->eventOverrideCount);
} else {
dvxLog("[SE] NO SLOT (count=%d)", (int)ctrl->eventOverrideCount);
}
}
@ -2345,7 +2329,6 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT
// strdup their text internally).
if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) {
BasStringT *s = basValFormatString(value);
dvxLog("[SP] setProp Caption: ctrl='%s' text='%s'", ctrl->name, s ? s->data : "(null)");
wgtSetText(ctrl->widget, s->data);
basStringUnref(s);
return;
@ -2453,6 +2436,7 @@ void basFormRtUnloadForm(void *ctx, void *formRef) {
}
for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
free(form->controls[i]->tooltip);
free(form->controls[i]);
}
@ -2548,20 +2532,11 @@ static const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char
// the SUB is module-global (no BEGINFORM scope) or the owning form
// isn't currently loaded -- callers should fall back to ctrl->form.
static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc) {
if (!rt || !proc) {
return NULL;
}
dvxLog("[OF] resolveOwningForm: proc='%s' formName='%s' formCount=%d",
proc->name, proc->formName, (int)arrlen(rt->forms));
if (!proc->formName[0]) {
if (!rt || !proc || !proc->formName[0]) {
return NULL;
}
for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
dvxLog("[OF] form[%d] name='%s' varCount=%d",
(int)i, rt->forms[i]->name, (int)rt->forms[i]->formVarCount);
if (strcasecmp(rt->forms[i]->name, proc->formName) == 0) {
return rt->forms[i];
}
@ -3000,14 +2975,6 @@ WidgetT *createWidgetByIface(const WgtIfaceT *iface, const void *api, WidgetT *p
static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount) {
dvxLog("[FE] fireCtrlEvent: ctrl=%p '%s' type='%s' event='%s' eventFiring=%d overrideCount=%d",
(void *)ctrl,
ctrl ? ctrl->name : "(null)",
ctrl ? ctrl->typeName : "(null)",
eventName ? eventName : "(null)",
ctrl ? (int)ctrl->eventFiring : -1,
ctrl ? (int)ctrl->eventOverrideCount : -1);
// Re-entrancy guard: reject only same-event re-entry (runSubLoop
// pumps events during long handlers; without this, a lingering
// mouse-up can re-deliver Click on top of itself). But allow
@ -3018,7 +2985,6 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
strcasecmp(ctrl->firingEventName, eventName) == 0);
if (sameEvent) {
dvxLog(" SUPPRESSED (same event already firing)");
return;
}
}
@ -3049,27 +3015,18 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
// Check for event override (SetEvent)
for (int32_t i = 0; i < ctrl->eventOverrideCount; i++) {
dvxLog("[FE] override[%d] event='%s' handler='%s'",
(int)i,
ctrl->eventOverrides[i].eventName,
ctrl->eventOverrides[i].handlerName);
if (strcasecmp(ctrl->eventOverrides[i].eventName, eventName) != 0) {
continue;
}
// Override found: call the named SUB directly
if (!rt->vm || !rt->module) {
dvxLog("[FE] NO VM/MODULE");
return;
}
const BasProcEntryT *proc = basModuleFindProc(rt->module, ctrl->eventOverrides[i].handlerName);
if (!proc || proc->isFunction) {
dvxLog("[FE] PROC NOT FOUND or IS FUNCTION: '%s' proc=%p isFunction=%d",
ctrl->eventOverrides[i].handlerName,
(void *)proc,
proc ? (int)proc->isFunction : -1);
free(allArgs);
return;
}
@ -3100,24 +3057,15 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
snprintf(ctrl->firingEventName, sizeof(ctrl->firingEventName), "%s", eventName ? eventName : "");
}
dvxLog(" -> calling SUB '%s' at addr=%d", ctrl->eventOverrides[i].handlerName, (int)proc->codeAddr);
rt->vm->errorMsg[0] = '\0';
rt->vm->errorNumber = 0;
bool subOk;
if (finalArgCount > 0 && finalArgs) {
subOk = basVmCallSubWithArgs(rt->vm, proc->codeAddr, finalArgs, finalArgCount);
basVmCallSubWithArgs(rt->vm, proc->codeAddr, finalArgs, finalArgCount);
} else {
subOk = basVmCallSub(rt->vm, proc->codeAddr);
basVmCallSub(rt->vm, proc->codeAddr);
}
dvxLog(" <- SUB '%s' returned ok=%d errNum=%d errMsg='%s'",
ctrl->eventOverrides[i].handlerName,
(int)subOk,
(int)rt->vm->errorNumber,
rt->vm->errorMsg);
if (claimedGuard) {
ctrl->eventFiring = false;
ctrl->firingEventName[0] = '\0';
@ -3131,9 +3079,6 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
}
// No override -- fall back to naming convention (CtrlName_EventName).
dvxLog("[FE] no override matched, trying naming convention '%s_%s'",
ctrl->name, eventName);
bool claimedGuard = !ctrl->eventFiring;
if (claimedGuard) {
@ -3141,16 +3086,12 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
snprintf(ctrl->firingEventName, sizeof(ctrl->firingEventName), "%s", eventName ? eventName : "");
}
bool fired;
if (finalArgCount > 0 && finalArgs) {
fired = basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, eventName, finalArgs, finalArgCount);
basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, eventName, finalArgs, finalArgCount);
} else {
fired = basFormRtFireEvent(rt, ctrl->form, ctrl->name, eventName);
basFormRtFireEvent(rt, ctrl->form, ctrl->name, eventName);
}
dvxLog("[FE] naming-convention result: fired=%d", (int)fired);
if (claimedGuard) {
ctrl->eventFiring = false;
ctrl->firingEventName[0] = '\0';
@ -3180,6 +3121,20 @@ static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char
const char *wgtTypeName = resolveTypeName(typeName);
bool isCtrlContainer = false;
if (!wgtTypeName) {
// Unknown widget type in .frm -- catch typos ("Spinner" instead
// of "SpinButton") that would otherwise silently drop the
// control and surface later as a misleading "control not
// found" runtime error. The IDE validator blocks these at
// compile time; this is the safety net for bascomp standalone.
basFormRtRuntimeError(sFormRt,
"Unknown widget type in form",
"Form: %s\nControl: %s\nType: %s\nNot a recognized widget type (check spelling; DVX uses VB6-style names, e.g. SpinButton not Spinner).",
ctx->form->name,
name ? name : "(null)",
typeName ? typeName : "(null)");
}
if (wgtTypeName) {
WidgetT *parent = ctx->parentStack[ctx->nestDepth - 1];
WidgetT *widget = createWidget(wgtTypeName, parent);
@ -3456,6 +3411,10 @@ static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *ha
return basValLong((int32_t)ctrl->widget->fgColor);
}
if (strcasecmp(propName, "ToolTipText") == 0) {
return basValStringFromC(ctrl->tooltip ? ctrl->tooltip : "");
}
*handled = false;
return zeroValue();
}
@ -3727,11 +3686,6 @@ static void onWidgetBlur(WidgetT *w) {
static void onWidgetChange(WidgetT *w) {
BasControlT *ctrl = (BasControlT *)w->userData;
dvxLog("[OC] onWidgetChange: w=%p ctrl=%p name='%s' type='%s'",
(void *)w, (void *)ctrl,
ctrl ? ctrl->name : "(null)",
ctrl ? ctrl->typeName : "(null)");
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
@ -3757,13 +3711,6 @@ static void onWidgetChange(WidgetT *w) {
static void onWidgetClick(WidgetT *w) {
BasControlT *ctrl = (BasControlT *)w->userData;
dvxLog("[OK] onWidgetClick: w=%p ctrl=%p name='%s' type='%s' form=%p vm=%p",
(void *)w, (void *)ctrl,
ctrl ? ctrl->name : "(null)",
ctrl ? ctrl->typeName : "(null)",
ctrl ? (void *)ctrl->form : NULL,
ctrl && ctrl->form ? (void *)ctrl->form->vm : NULL);
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
@ -4662,6 +4609,15 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val
return true;
}
if (strcasecmp(propName, "ToolTipText") == 0) {
BasStringT *s = basValFormatString(value);
free(ctrl->tooltip);
ctrl->tooltip = (s->len > 0) ? strdup(s->data) : NULL;
wgtSetTooltip(ctrl->widget, ctrl->tooltip);
basStringUnref(s);
return true;
}
return false;
}

View file

@ -84,6 +84,7 @@ typedef struct BasControlT {
char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding
char dataField[BAS_MAX_CTRL_NAME]; // column name for binding
char helpTopic[BAS_MAX_CTRL_NAME]; // help topic ID for F1
char *tooltip; // heap-owned tooltip text (NULL = none); wgtSetTooltip references this buffer, so it must outlive the widget
int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls)
BasEventOverrideT eventOverrides[BAS_MAX_EVENT_OVERRIDES];
int32_t eventOverrideCount;

View file

@ -1446,7 +1446,16 @@ typedef struct {
typedef struct {
IdeCtrlMapEntryT *entries; // stb_ds dynamic array
char ctrlName[BAS_MAX_CTRL_NAME]; // control that has the bad type
char typeName[BAS_MAX_CTRL_NAME]; // the unrecognised type name
char formName[BAS_MAX_CTRL_NAME]; // enclosing form (for error message)
} IdeBadTypeT;
typedef struct {
IdeCtrlMapEntryT *entries; // stb_ds: known controls
IdeBadTypeT *badTypes; // stb_ds: unknown widget types encountered
char currentForm[BAS_MAX_CTRL_NAME]; // most recent Begin Form
} IdeValidatorCtxT;
@ -1457,6 +1466,7 @@ static bool ideValidator_onFormBegin(void *ud, const char *name) {
snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name);
snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", "Form");
arrput(v->entries, e);
snprintf(v->currentForm, BAS_MAX_CTRL_NAME, "%s", name ? name : "");
return true;
}
@ -1468,6 +1478,24 @@ static void ideValidator_onCtrlBegin(void *ud, const char *typeName, const char
snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name);
snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", typeName ? typeName : "");
arrput(v->entries, e);
// Also flag unknown widget types. wgtFindByBasName returns NULL
// for anything not registered by a .wgt DXE loaded by the IDE.
// Skip internal structural types (they're handled by layout code,
// not widget DXEs).
if (typeName && typeName[0]) {
bool structural = (strcasecmp(typeName, "Form") == 0 ||
strcasecmp(typeName, "Menu") == 0);
if (!structural && !wgtFindByBasName(typeName)) {
IdeBadTypeT bad;
memset(&bad, 0, sizeof(bad));
snprintf(bad.ctrlName, BAS_MAX_CTRL_NAME, "%s", name ? name : "?");
snprintf(bad.typeName, BAS_MAX_CTRL_NAME, "%s", typeName);
snprintf(bad.formName, BAS_MAX_CTRL_NAME, "%s", v->currentForm);
arrput(v->badTypes, bad);
}
}
}
@ -1524,9 +1552,14 @@ static bool ideValidator_isCommonProp(const char *propName) {
strcasecmp(propName, "Weight") == 0 ||
strcasecmp(propName, "Visible") == 0 ||
strcasecmp(propName, "Enabled") == 0 ||
strcasecmp(propName, "ReadOnly") == 0 ||
strcasecmp(propName, "TabIndex") == 0 ||
strcasecmp(propName, "BackColor") == 0 ||
strcasecmp(propName, "ForeColor") == 0 ||
strcasecmp(propName, "Caption") == 0 ||
strcasecmp(propName, "Text") == 0 ||
strcasecmp(propName, "HelpTopic") == 0 ||
strcasecmp(propName, "ToolTipText") == 0 ||
strcasecmp(propName, "DataSource") == 0 ||
strcasecmp(propName, "DataField") == 0 ||
strcasecmp(propName, "ListCount") == 0;
@ -1906,6 +1939,40 @@ static bool compileProject(void) {
memset(&validatorCtx, 0, sizeof(validatorCtx));
ideBuildCtrlMap(&validatorCtx);
// Reject unknown widget types up front -- the .frm parse records
// any `Begin <Type> <Name>` whose type isn't registered by a .wgt
// DXE. Those would silently drop the control at runtime and
// produce a misleading "control not found" error on first access.
if (arrlen(validatorCtx.badTypes) > 0) {
int32_t n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\nUnknown widget type(s) in .frm files:\n");
for (int32_t i = 0; i < (int32_t)arrlen(validatorCtx.badTypes) && n < IDE_MAX_OUTPUT - 256; i++) {
IdeBadTypeT *b = &validatorCtx.badTypes[i];
n += snprintf(sOutputBuf + n, IDE_MAX_OUTPUT - n,
" Form '%s' control '%s': type '%s' is not registered.\n",
b->formName, b->ctrlName, b->typeName);
}
n += snprintf(sOutputBuf + n, IDE_MAX_OUTPUT - n,
"\nDVX uses VB6-style widget names (e.g. SpinButton, OptionButton, HScrollBar, CheckBox, DropDown).\n");
sOutputLen = n;
setOutputText(sOutputBuf);
showOutputWindow();
if (sOutWin) {
dvxRaiseWindow(sAc, sOutWin);
}
setStatus("Compilation failed: unknown widget type.");
dvxSetBusy(sAc, false);
basParserFree(parser);
free(parser);
free(concatBuf);
arrfree(validatorCtx.entries);
arrfree(validatorCtx.badTypes);
return false;
}
BasCtrlValidatorT validator;
validator.lookupCtrlType = ideValidator_lookupCtrlType;
validator.isMethodValid = ideValidator_isMethodValid;
@ -1980,11 +2047,13 @@ static bool compileProject(void) {
free(parser);
free(concatBuf);
arrfree(validatorCtx.entries);
arrfree(validatorCtx.badTypes);
return false;
}
free(concatBuf);
arrfree(validatorCtx.entries);
arrfree(validatorCtx.badTypes);
BasModuleT *mod = basParserBuildModule(parser);
basParserFree(parser);

View file

@ -451,6 +451,18 @@ A DVX BASIC project is stored as a .dbp file (DVX BASIC Project). The project fi
When the project is compiled, all source files are concatenated into a single source stream. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form.
.h2 Compile-Time Validation
Before the source is handed to the compiler, the IDE scans every .frm file and cross-checks the forms against the widget registry and the statements that reference them. Three classes of error are caught and reported without producing a binary:
.list
.item Unknown widget types -- any Begin TypeName ... End block whose TypeName does not match a registered widget is flagged. The error message names the offending form, control, and type, and hints that DVX uses VB6-style names (SpinButton, not Spinner).
.item Unknown properties -- statements of the form CtrlName.Property = value, or reads of CtrlName.Property, are checked against the widget's declared property list. A typo such as btn.Captoin triggers a compile error with the form, control, and property name.
.item Unknown methods -- method calls CtrlName.Method ... are checked the same way against the widget's method list, catching mistakes like list.AddTiem before the program ever runs.
.endlist
These checks use the same metadata the Properties panel and Toolbox read from each widget DXE, so property and method names always match the current widget set without any separate list to maintain.
.h2 Project Operations
.table

View file

@ -80,7 +80,9 @@ name$ = "Hello" ' String
Form Example Description
---- ------- -----------
Decimal integer 42 Values -32768..32767 are Integer; larger are Long
Hex integer &HFF, &H1234 Hexadecimal literal
Hex integer &HFF, &H1234 Hexadecimal literal (0-9, A-F)
Octal integer &O10, &O77 Octal literal (0-7)
Binary integer &B1010, &B11111111 Binary literal (0-1)
Integer suffix 42% Force Integer type
Long suffix 42&, &HFF& Force Long type
Floating-point 3.14, 1.5E10, 2.5D3 Any number containing a decimal point or exponent is Double by default
@ -669,7 +671,7 @@ Sub Greet(ByVal name As String)
End Sub
.endcode
Parameters are passed by reference by default. Use ByVal for value semantics; there is no separate ByRef keyword (omitting ByVal is the by-reference form). Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:
Parameters are passed by reference by default. Use ByVal for value semantics; there is no separate ByRef keyword (omitting ByVal is the by-reference form). Writes to a by-reference parameter update the caller's variable, including individual array elements: `bump a(3)` passed to a by-reference parameter will modify `a(3)` in place. Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:
.code
Greet "World"
@ -1253,6 +1255,7 @@ bytes = FILELEN(filename$)
.index LEN
.index LTRIM$
.index MID$
.index OCT$
.index RIGHT$
.index RTRIM$
.index SPACE$
@ -1279,6 +1282,7 @@ bytes = FILELEN(filename$)
LTRIM$(s$) String Removes leading spaces from s$
MID$(s$, start) String Substring from start (1-based) to end of string
MID$(s$, start, length) String Substring of length characters starting at start
OCT$(n) String Octal representation of n (no leading &O)
RIGHT$(s$, n) String Rightmost n characters of s$
RTRIM$(s$) String Removes trailing spaces from s$
SPACE$(n) String String of n spaces
@ -1387,6 +1391,7 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background
.topic lang.func.conversion
.title Conversion Functions
.toc 2 Conversion Functions
.index CBOOL
.index CDBL
.index CINT
.index CLNG
@ -1398,6 +1403,7 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background
.table
Function Returns Description
-------- ------- -----------
CBOOL(n) Boolean Returns True (-1) if n is nonzero or a non-empty string; False (0) otherwise
CDBL(n) Double Converts n to Double
CINT(n) Integer Converts n to Integer (rounds half away from zero)
CLNG(n) Long Converts n to Long

View file

@ -34,6 +34,8 @@
#include "runtime/vm.h"
#include "runtime/values.h"
#include "stb_ds_wrap.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -381,6 +383,329 @@ static void testRuntimeError(const char *name, const char *source, const char *n
#define TEST_RUNTIME_ERROR(n, s, e) testRuntimeError((n), (s), (e))
// ============================================================
// Widget-level event-dispatch harness
// ============================================================
//
// formrt.c depends on the full DVX widget/window stack and can't be
// linked into the host test binary. This harness mirrors the dispatch
// semantics of fireCtrlEvent / basFormRtFireEvent using stand-alone
// mini-structs and a bare minimum of VM calls.
//
// IMPORTANT: if you change the dispatch rules in formrt.c (event
// override table, re-entrancy guard, form-scope-vars binding), update
// the matching code below. The tests are re-implementation tests,
// not link-through tests.
#define MINI_MAX_NAME 32
#define MINI_MAX_OVERRIDES 16
typedef struct {
char eventName[MINI_MAX_NAME];
char handlerName[MINI_MAX_NAME];
} MiniOverrideT;
typedef struct MiniCtrlT {
char name[MINI_MAX_NAME];
char typeName[MINI_MAX_NAME];
struct MiniFormT *form;
MiniOverrideT overrides[MINI_MAX_OVERRIDES];
int32_t overrideCount;
bool eventFiring;
char firingEventName[MINI_MAX_NAME];
} MiniCtrlT;
typedef struct MiniFormT {
char name[MINI_MAX_NAME];
MiniCtrlT *ctrls; // stb_ds array
BasValueT *formVars;
int32_t formVarCount;
} MiniFormT;
typedef struct {
BasVmT *vm;
BasModuleT *module;
MiniFormT **forms; // stb_ds array of heap-alloc'd forms
MiniFormT *currentForm;
} MiniRtT;
static MiniFormT *miniCreateForm(MiniRtT *rt, const char *name, int32_t formVarCount) {
MiniFormT *f = (MiniFormT *)calloc(1, sizeof(MiniFormT));
snprintf(f->name, MINI_MAX_NAME, "%s", name);
f->ctrls = NULL;
if (formVarCount > 0) {
f->formVars = (BasValueT *)calloc(formVarCount, sizeof(BasValueT));
f->formVarCount = formVarCount;
}
arrput(rt->forms, f);
return f;
}
static MiniCtrlT *miniAddCtrl(MiniFormT *f, const char *name, const char *typeName) {
MiniCtrlT c;
memset(&c, 0, sizeof(c));
snprintf(c.name, MINI_MAX_NAME, "%s", name);
snprintf(c.typeName, MINI_MAX_NAME, "%s", typeName ? typeName : "");
c.form = f;
arrput(f->ctrls, c);
return &f->ctrls[arrlen(f->ctrls) - 1];
}
static void miniSetEvent(MiniCtrlT *ctrl, const char *eventName, const char *handlerName) {
// Match formrt's SetEvent: update existing or append
for (int32_t i = 0; i < ctrl->overrideCount; i++) {
if (strcasecmp(ctrl->overrides[i].eventName, eventName) == 0) {
snprintf(ctrl->overrides[i].handlerName, MINI_MAX_NAME, "%s", handlerName);
return;
}
}
if (ctrl->overrideCount < MINI_MAX_OVERRIDES) {
MiniOverrideT *o = &ctrl->overrides[ctrl->overrideCount++];
snprintf(o->eventName, MINI_MAX_NAME, "%s", eventName);
snprintf(o->handlerName, MINI_MAX_NAME, "%s", handlerName);
}
}
static const BasProcEntryT *miniFindProc(BasModuleT *mod, const char *name) {
if (!mod || !mod->procs || !name) {
return NULL;
}
for (int32_t i = 0; i < mod->procCount; i++) {
if (strcasecmp(mod->procs[i].name, name) == 0) {
return &mod->procs[i];
}
}
return NULL;
}
static MiniFormT *miniResolveOwningForm(MiniRtT *rt, const BasProcEntryT *proc) {
if (!rt || !proc || !proc->formName[0]) {
return NULL;
}
for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
if (strcasecmp(rt->forms[i]->name, proc->formName) == 0) {
return rt->forms[i];
}
}
return NULL;
}
// Mirror of formrt.c's fireCtrlEvent. Re-entrancy guard, override
// table, form-scope vars binding -- kept in sync by copy.
static void miniFireCtrlEvent(MiniRtT *rt, MiniCtrlT *ctrl, const char *eventName) {
if (!rt || !ctrl || !eventName) {
return;
}
if (ctrl->eventFiring) {
bool sameEvent = (ctrl->firingEventName[0] &&
strcasecmp(ctrl->firingEventName, eventName) == 0);
if (sameEvent) {
return;
}
}
// Event-override path (SetEvent)
for (int32_t i = 0; i < ctrl->overrideCount; i++) {
if (strcasecmp(ctrl->overrides[i].eventName, eventName) != 0) {
continue;
}
if (!rt->vm || !rt->module) {
return;
}
const BasProcEntryT *proc = miniFindProc(rt->module, ctrl->overrides[i].handlerName);
if (!proc || proc->isFunction) {
return;
}
MiniFormT *prevForm = rt->currentForm;
BasValueT *prevVars = rt->vm->currentFormVars;
int32_t prevVarCount = rt->vm->currentFormVarCount;
MiniFormT *owningForm = miniResolveOwningForm(rt, proc);
MiniFormT *varsForm = owningForm ? owningForm : ctrl->form;
rt->currentForm = ctrl->form;
basVmSetCurrentFormVars(rt->vm, varsForm->formVars, varsForm->formVarCount);
bool claimedGuard = !ctrl->eventFiring;
if (claimedGuard) {
ctrl->eventFiring = true;
snprintf(ctrl->firingEventName, MINI_MAX_NAME, "%s", eventName);
}
rt->vm->errorMsg[0] = '\0';
rt->vm->errorNumber = 0;
basVmCallSub(rt->vm, proc->codeAddr);
if (claimedGuard) {
ctrl->eventFiring = false;
ctrl->firingEventName[0] = '\0';
}
rt->currentForm = prevForm;
basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount);
return;
}
// Naming-convention fallback: CtrlName_EventName
char handlerName[MINI_MAX_NAME * 2];
snprintf(handlerName, sizeof(handlerName), "%s_%s", ctrl->name, eventName);
const BasProcEntryT *proc = miniFindProc(rt->module, handlerName);
if (!proc || proc->isFunction) {
return;
}
MiniFormT *prevForm = rt->currentForm;
BasValueT *prevVars = rt->vm->currentFormVars;
int32_t prevVarCount = rt->vm->currentFormVarCount;
MiniFormT *owningForm = miniResolveOwningForm(rt, proc);
MiniFormT *varsForm = owningForm ? owningForm : ctrl->form;
rt->currentForm = ctrl->form;
basVmSetCurrentFormVars(rt->vm, varsForm->formVars, varsForm->formVarCount);
bool claimedGuard = !ctrl->eventFiring;
if (claimedGuard) {
ctrl->eventFiring = true;
snprintf(ctrl->firingEventName, MINI_MAX_NAME, "%s", eventName);
}
basVmCallSub(rt->vm, proc->codeAddr);
if (claimedGuard) {
ctrl->eventFiring = false;
ctrl->firingEventName[0] = '\0';
}
rt->currentForm = prevForm;
basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount);
}
// Context for an event-dispatch test: what to build and how to fire.
typedef struct {
const char *source; // BASIC source
const char *formName; // "" if ctrl has no owning BEGINFORM
int32_t formVarCount; // >0 if the module has form-scope vars
const char *ctrlName; // control to fire events on
const char *ctrlType; // widget type, e.g. "CommandButton" / "Timer"
// Array of (eventName, handlerName) for SetEvent. Terminate with NULL.
const char *overrides[2 * 8];
// Array of event names to fire in order. Terminate with NULL.
const char *fires[8];
} TestDispatchT;
static void testDispatchEq(const char *name, const TestDispatchT *d, const char *expected) {
int32_t len = (int32_t)strlen(d->source);
BasParserT parser;
basParserInit(&parser, d->source, len);
if (!basParse(&parser)) {
char detail[256];
snprintf(detail, sizeof(detail), "compile error: %s", parser.error);
reportFail(name, detail);
basParserFree(&parser);
return;
}
BasModuleT *mod = basParserBuildModule(&parser);
basParserFree(&parser);
if (!mod) {
reportFail(name, "module build failed");
return;
}
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
CaptureT cap;
captureReset(&cap);
basVmSetPrintCallback(vm, capturePrint, &cap);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
// Run module-level init (populates globals, runs BEGINFORM init block).
basVmRun(vm);
// Build the mini runtime + form + control.
MiniRtT rt;
memset(&rt, 0, sizeof(rt));
rt.vm = vm;
rt.module = mod;
MiniFormT *form = miniCreateForm(&rt, d->formName[0] ? d->formName : "_NoForm", d->formVarCount);
MiniCtrlT *ctrl = miniAddCtrl(form, d->ctrlName, d->ctrlType);
// Wire overrides.
for (int32_t i = 0; d->overrides[i] && d->overrides[i + 1]; i += 2) {
miniSetEvent(ctrl, d->overrides[i], d->overrides[i + 1]);
}
// Fire the sequence of events.
for (int32_t i = 0; d->fires[i]; i++) {
miniFireCtrlEvent(&rt, ctrl, d->fires[i]);
}
if (strcmp(cap.buf, expected) == 0) {
reportPass(name);
} else {
reportFailDiff(name, expected, cap.buf);
}
// Teardown.
for (int32_t i = 0; i < (int32_t)arrlen(rt.forms); i++) {
MiniFormT *f = rt.forms[i];
if (f->formVars) {
for (int32_t j = 0; j < f->formVarCount; j++) {
basValRelease(&f->formVars[j]);
}
free(f->formVars);
}
arrfree(f->ctrls);
free(f);
}
arrfree(rt.forms);
basVmDestroy(vm);
basModuleFree(mod);
}
// Helper macros to keep tests compact. `overrides` and `fires` are
// comma lists terminated with NULL/NULL.
#define DISPATCH_OVR(...) { __VA_ARGS__, NULL, NULL }
#define DISPATCH_FIRE(...) { __VA_ARGS__, NULL }
// ============================================================
// Test cases
// ============================================================
@ -2043,6 +2368,241 @@ int main(void) {
"driver",
"ok\ncaught\nok\n");
// ============================================================
// Widget-level event dispatch (mirrors formrt.c fireCtrlEvent)
// ============================================================
// SetEvent override: handler name differs from Ctrl_Event convention
{
TestDispatchT d = {
.source = "SUB handleIt\n PRINT \"clicked\"\nEND SUB\n",
.formName = "",
.ctrlName = "btn1",
.ctrlType = "CommandButton",
.overrides = DISPATCH_OVR("Click", "handleIt"),
.fires = DISPATCH_FIRE("Click"),
};
testDispatchEq("dispatch-override", &d, "clicked\n");
}
// Naming-convention fallback: no override, CtrlName_EventName SUB
{
TestDispatchT d = {
.source = "SUB btn1_Click\n PRINT \"fallback\"\nEND SUB\n",
.formName = "",
.ctrlName = "btn1",
.ctrlType = "CommandButton",
.fires = DISPATCH_FIRE("Click"),
};
testDispatchEq("dispatch-naming-convention", &d, "fallback\n");
}
// Override wins over naming convention
{
TestDispatchT d = {
.source =
"SUB btn1_Click\n PRINT \"convention\"\nEND SUB\n"
"SUB doOverride\n PRINT \"override\"\nEND SUB\n",
.formName = "",
.ctrlName = "btn1",
.ctrlType = "CommandButton",
.overrides = DISPATCH_OVR("Click", "doOverride"),
.fires = DISPATCH_FIRE("Click"),
};
testDispatchEq("dispatch-override-beats-naming", &d, "override\n");
}
// Handler missing: silently skipped, no crash
{
TestDispatchT d = {
.source = "SUB unrelated\n PRINT \"never\"\nEND SUB\n",
.formName = "",
.ctrlName = "btn1",
.ctrlType = "CommandButton",
.fires = DISPATCH_FIRE("Click"),
};
testDispatchEq("dispatch-no-handler", &d, "");
}
// Multiple events on same control
{
TestDispatchT d = {
.source =
"SUB btn1_Click\n PRINT \"click\"\nEND SUB\n"
"SUB btn1_GotFocus\n PRINT \"focus\"\nEND SUB\n",
.formName = "",
.ctrlName = "btn1",
.ctrlType = "CommandButton",
.fires = DISPATCH_FIRE("GotFocus", "Click", "GotFocus"),
};
testDispatchEq("dispatch-multiple-events", &d, "focus\nclick\nfocus\n");
}
// Globals accumulate across events (real apps' common pattern)
{
TestDispatchT d = {
.source =
"DIM counter AS INTEGER\n"
"counter = 0\n"
"SUB btn1_Click\n"
" counter = counter + 1\n"
" PRINT counter\n"
"END SUB\n",
.formName = "",
.ctrlName = "btn1",
.ctrlType = "CommandButton",
.fires = DISPATCH_FIRE("Click", "Click", "Click"),
};
testDispatchEq("dispatch-global-counter", &d, "1 \n2 \n3 \n");
}
// Form-scope vars accessed via SUB declared in BEGINFORM
{
TestDispatchT d = {
.source =
"BEGINFORM \"MyForm\"\n"
"DIM hits AS INTEGER\n"
"SUB btn1_Click\n"
" hits = hits + 1\n"
" PRINT hits\n"
"END SUB\n"
"ENDFORM\n",
.formName = "MyForm",
.formVarCount = 1,
.ctrlName = "btn1",
.ctrlType = "CommandButton",
.fires = DISPATCH_FIRE("Click", "Click"),
};
testDispatchEq("dispatch-form-scope-vars", &d, "1 \n2 \n");
}
// ON ERROR inside the handler traps the error and the handler is
// reusable on the next fire (inErrorHandler must reset cleanly).
{
TestDispatchT d = {
.source =
"SUB btn1_Click\n"
" ON ERROR GOTO h\n"
" DIM a AS INTEGER\n DIM b AS INTEGER\n"
" a = 10\n b = 0\n"
" PRINT a \\ b\n"
" EXIT SUB\n"
" h:\n"
" PRINT \"caught\"\n"
"END SUB\n",
.formName = "",
.ctrlName = "btn1",
.ctrlType = "CommandButton",
.fires = DISPATCH_FIRE("Click", "Click"),
};
testDispatchEq("dispatch-on-error-reusable", &d, "caught\ncaught\n");
}
// Handler that recurses into the same SUB via a method-ish call.
// The re-entrancy guard rejects same-event re-entry. Outer run
// produces one PRINT; inner attempt is swallowed.
{
TestDispatchT d = {
.source =
"DIM depth AS INTEGER\n"
"depth = 0\n"
"SUB btn1_Click\n"
" depth = depth + 1\n"
" PRINT depth\n"
"END SUB\n",
.formName = "",
.ctrlName = "btn1",
.ctrlType = "CommandButton",
.fires = DISPATCH_FIRE("Click"),
};
testDispatchEq("dispatch-guard-single-fire", &d, "1 \n");
}
// SetEvent override twice on the same event name: second wins.
{
TestDispatchT d = {
.source =
"SUB first\n PRINT \"first\"\nEND SUB\n"
"SUB second\n PRINT \"second\"\nEND SUB\n",
.formName = "",
.ctrlName = "btn1",
.ctrlType = "CommandButton",
.overrides = DISPATCH_OVR("Click", "first", "Click", "second"),
.fires = DISPATCH_FIRE("Click"),
};
testDispatchEq("dispatch-override-replace", &d, "second\n");
}
// Timer-like pattern: onChange fires "Timer" (mapped in formrt by
// ctrl->typeName == "Timer"); here we exercise the SetEvent path
// with a Timer-typed control.
{
TestDispatchT d = {
.source =
"DIM ticks AS INTEGER\n"
"ticks = 0\n"
"SUB onTick\n"
" ticks = ticks + 1\n"
" PRINT ticks\n"
"END SUB\n",
.formName = "",
.ctrlName = "t1",
.ctrlType = "Timer",
.overrides = DISPATCH_OVR("Timer", "onTick"),
.fires = DISPATCH_FIRE("Timer", "Timer", "Timer"),
};
testDispatchEq("dispatch-timer-override", &d, "1 \n2 \n3 \n");
}
// Menu-like dispatch via naming convention (proxies have typeName "")
{
TestDispatchT d = {
.source = "SUB mnuFoo_Click\n PRINT \"menu\"\nEND SUB\n",
.formName = "",
.ctrlName = "mnuFoo",
.ctrlType = "Menu",
.fires = DISPATCH_FIRE("Click"),
};
testDispatchEq("dispatch-menu-click", &d, "menu\n");
}
// Handler calls into another SUB. Verifies nested SUB calls don't
// interact badly with the event guard.
{
TestDispatchT d = {
.source =
"SUB say(s AS STRING)\n PRINT s\nEND SUB\n"
"SUB btn1_Click\n"
" say \"a\"\n say \"b\"\n say \"c\"\n"
"END SUB\n",
.formName = "",
.ctrlName = "btn1",
.ctrlType = "CommandButton",
.fires = DISPATCH_FIRE("Click"),
};
testDispatchEq("dispatch-handler-nested-calls", &d, "a\nb\nc\n");
}
// Different events on the same control during a single sequence:
// GotFocus, Click, LostFocus. Guard lets each event through.
{
TestDispatchT d = {
.source =
"SUB onFocus\n PRINT \"focus\"\nEND SUB\n"
"SUB onClick\n PRINT \"click\"\nEND SUB\n"
"SUB onBlur\n PRINT \"blur\"\nEND SUB\n",
.formName = "",
.ctrlName = "btn1",
.ctrlType = "CommandButton",
.overrides = DISPATCH_OVR(
"GotFocus", "onFocus",
"Click", "onClick",
"LostFocus", "onBlur"),
.fires = DISPATCH_FIRE("GotFocus", "Click", "LostFocus"),
};
testDispatchEq("dispatch-focus-click-blur", &d, "focus\nclick\nblur\n");
}
// ============================================================
// Operator precedence edge cases
// ============================================================

View file

@ -1020,11 +1020,9 @@ static void navigateToTopic(int32_t topicIdx) {
}
if (topicIdx == sCurrentTopic) {
dvxLog("[WRAP] navigateToTopic: SKIPPED (same topic %d)", (int)topicIdx);
return;
}
dvxLog("[WRAP] navigateToTopic: %d -> %d", (int)sCurrentTopic, (int)topicIdx);
historyPush(topicIdx);
sCurrentTopic = topicIdx;
buildContentWidgets();
@ -1039,7 +1037,6 @@ static void navigateToTopic(int32_t topicIdx) {
// Sync TOC tree selection
if (sTocTree) {
WidgetT *item = findTreeItemByTopic(sTocTree, topicIdx);
dvxLog("[WRAP] treeSync: topicIdx=%d item=%p", (int)topicIdx, (void *)item);
if (item) {
// Suppress onChange to avoid re-entering navigateToTopic

BIN
src/apps/kpunch/widshow/ICON32.BMP (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,14 @@
[Project]
Name = Widget Showcase
Author = Scott Duensing
Publisher = Kangaroo Punch Studios
Copyright = Copyright 2026 Scott Duensing
Description = Tour of DVX BASIC widgets not covered by BASIC Demo
Icon = ICON32.BMP
[Forms]
File0 = widshow.frm
Count = 1
[Settings]
StartupForm = WidgetShow

View file

@ -0,0 +1,396 @@
VERSION DVX 1.00
Begin Form WidgetShow
Caption = "Widget Showcase"
Layout = VBox
AutoSize = False
Resizable = True
Centered = True
Width = 620
Height = 440
Begin TabStrip tabs
Weight = 1
Begin TabPage pgText
Caption = "&Text"
Begin Label lbl1
Caption = "TextBox echoes into a Label on the right."
Weight = 0
End
Begin HBox rowInput
Weight = 0
Begin Label lblPrompt
Caption = "Type:"
Weight = 0
End
Begin TextBox txtInput
Weight = 1
ToolTipText = "A single-line editable text field. Fires Change on every keystroke."
End
Begin Label lblEcho
Caption = "(echo)"
Weight = 1
ToolTipText = "A static Label. Its Caption property is being driven by the TextBox."
End
End
Begin Spacer spc1
Weight = 1
End
End
Begin TabPage pgPick
Caption = "&Picks"
Begin Label lbl2
Caption = "ComboBox (typeable), DropDown, SpinButton, CheckBox."
Weight = 0
End
Begin HBox rowPick
Weight = 0
Begin ComboBox cboSize
Weight = 1
ToolTipText = "ComboBox: pick from the list OR type your own value."
End
Begin DropDown ddColor
Weight = 1
ToolTipText = "DropDown: read-only list. Pick one of the options."
End
Begin SpinButton spnQty
Weight = 0
ToolTipText = "SpinButton: numeric input with up/down arrows."
End
Begin CheckBox chkLoud
Caption = "Loud"
Weight = 0
ToolTipText = "CheckBox: toggle a boolean flag."
End
End
Begin Label lbl3
Caption = "OptionButtons form a mutually exclusive group."
Weight = 0
End
Begin HBox rowRadio
Weight = 0
Begin OptionButton rdoSmall
Caption = "Small"
Weight = 1
ToolTipText = "OptionButton: picking one clears siblings in the same container."
End
Begin OptionButton rdoMed
Caption = "Medium"
Weight = 1
ToolTipText = "OptionButton: picking one clears siblings in the same container."
End
Begin OptionButton rdoLarge
Caption = "Large"
Weight = 1
ToolTipText = "OptionButton: picking one clears siblings in the same container."
End
End
Begin Spacer spc2
Weight = 1
End
End
Begin TabPage pgRange
Caption = "&Range"
Begin Label lbl4
Caption = "HScrollBar drives a ProgressBar."
Weight = 0
End
Begin HBox rowRange
Weight = 0
Begin HScrollBar sldVal
Weight = 2
ToolTipText = "HScrollBar: drag the thumb; fires Change with the new value."
End
Begin ProgressBar prgVal
Weight = 2
ToolTipText = "ProgressBar: visual-only indicator; set Value 0-100."
End
Begin Label lblVal
Caption = "0"
Weight = 0
End
End
Begin Spacer spc3
Weight = 1
End
End
Begin TabPage pgLists
Caption = "&Lists"
Begin Label lbl5
Caption = "ListBox, ListView, and TreeView."
Weight = 0
End
Begin HBox rowLists
Weight = 1
Begin VBox colList
Weight = 1
Begin ListBox lstItems
Weight = 1
ToolTipText = "ListBox: simple vertical list of strings."
End
Begin HBox rowLB
Weight = 0
Begin CommandButton btnAdd
Caption = "Add"
Weight = 1
ToolTipText = "Append a new item to the list."
End
Begin CommandButton btnDel
Caption = "Del"
Weight = 1
ToolTipText = "Remove the selected item."
End
End
End
Begin ListView lvFiles
Weight = 1
ToolTipText = "ListView: multi-column table with selectable rows."
End
Begin TreeView tvTree
Weight = 1
ToolTipText = "TreeView: hierarchical list with collapsible nodes."
End
End
End
Begin TabPage pgAbout
Caption = "&About"
Begin Label lblAbout1
Caption = "DVX BASIC Widget Showcase"
Weight = 0
End
Begin Label lblAbout2
Caption = "Exercises the standard widgets"
Weight = 0
End
Begin Label lblAbout3
Caption = "available to BASIC programs."
Weight = 0
End
Begin Label lblAbout4
Caption = " "
Weight = 0
End
Begin Label lblAbout5
Caption = "Hover any widget for a tooltip."
Weight = 0
End
Begin Label lblAbout6
Caption = "The status bar below reflects the"
Weight = 0
End
Begin Label lblAbout7
Caption = "last event fired by the widget."
Weight = 0
End
Begin Spacer spc4
Weight = 1
End
End
End
Begin StatusBar sbar
Caption = "Ready."
Weight = 0
End
End
' The MIT License (MIT)
'
' Copyright (C) 2026 Scott Duensing
'
' Permission is hereby granted, free of charge, to any person obtaining a copy
' of this software and associated documentation files (the "Software"), to
' deal in the Software without restriction, including without limitation the
' rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
' sell copies of the Software, and to permit persons to whom the Software is
' furnished to do so, subject to the following conditions:
'
' The above copyright notice and this permission notice shall be included in
' all copies or substantial portions of the Software.
'
' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
' IN THE SOFTWARE.
OPTION EXPLICIT
DIM addCount AS INTEGER
SUB setStatus(msg AS STRING)
sbar.Caption = msg
END SUB
Load WidgetShow
WidgetShow.Show
cboSize.AddItem "Small"
cboSize.AddItem "Medium"
cboSize.AddItem "Large"
ddColor.AddItem "Red"
ddColor.AddItem "Green"
ddColor.AddItem "Blue"
spnQty.SetRange 0, 99
spnQty.Value = 5
lstItems.AddItem "Apple"
lstItems.AddItem "Banana"
lstItems.AddItem "Cherry"
addCount = 0
lvFiles.SetColumns "Name,12|Size,8|Type,8"
lvFiles.AddItem "readme.txt"
lvFiles.SetCell 0, 1, "1024"
lvFiles.SetCell 0, 2, "Text"
lvFiles.AddItem "app.exe"
lvFiles.SetCell 1, 1, "65536"
lvFiles.SetCell 1, 2, "EXE"
lvFiles.AddItem "data.dat"
lvFiles.SetCell 2, 1, "8192"
lvFiles.SetCell 2, 2, "Binary"
tvTree.AddItem "Animals"
tvTree.AddChildItem 0, "Cat"
tvTree.AddChildItem 0, "Dog"
tvTree.AddChildItem 0, "Fox"
tvTree.AddItem "Plants"
tvTree.AddChildItem 4, "Oak"
tvTree.AddChildItem 4, "Pine"
tvTree.SetExpanded 0, -1
tvTree.SetExpanded 4, -1
setStatus "Ready. Hover widgets for descriptions."
' ============================================================
' Event handlers
' ============================================================
SUB tabs_Click
setStatus "Tab: " + STR$(tabs.GetActive())
END SUB
SUB txtInput_Change
lblEcho.Caption = txtInput.Text
setStatus "Typed: " + txtInput.Text
END SUB
SUB cboSize_Change
setStatus "Size: " + cboSize.Text
END SUB
SUB ddColor_Change
setStatus "Color: " + ddColor.List(ddColor.ListIndex)
END SUB
SUB spnQty_Change
setStatus "Quantity: " + STR$(spnQty.Value)
END SUB
SUB chkLoud_Click
IF chkLoud.Value THEN
setStatus "LOUD MODE ON"
ELSE
setStatus "quiet"
END IF
END SUB
SUB rdoSmall_Click
setStatus "Selected Small"
END SUB
SUB rdoMed_Click
setStatus "Selected Medium"
END SUB
SUB rdoLarge_Click
setStatus "Selected Large"
END SUB
SUB btnAdd_Click
addCount = addCount + 1
lstItems.AddItem "Item " + STR$(addCount)
setStatus "Added; " + STR$(lstItems.ListCount()) + " entries."
END SUB
SUB btnDel_Click
IF lstItems.ListIndex >= 0 THEN
lstItems.RemoveItem lstItems.ListIndex
setStatus "Removed; " + STR$(lstItems.ListCount()) + " entries."
ELSE
setStatus "Nothing selected."
END IF
END SUB
SUB lstItems_Click
IF lstItems.ListIndex >= 0 THEN
setStatus "ListBox: " + lstItems.List(lstItems.ListIndex)
END IF
END SUB
SUB lvFiles_Click
DIM i AS INTEGER
FOR i = 0 TO lvFiles.RowCount() - 1
IF lvFiles.IsItemSelected(i) THEN
setStatus "ListView: " + lvFiles.GetCell(i, 0) + " (" + lvFiles.GetCell(i, 1) + " bytes)"
EXIT SUB
END IF
NEXT i
END SUB
SUB tvTree_Click
DIM i AS INTEGER
FOR i = 0 TO tvTree.ItemCount() - 1
IF tvTree.IsItemSelected(i) THEN
setStatus "Tree: " + tvTree.GetItemText(i)
EXIT SUB
END IF
NEXT i
END SUB
SUB sldVal_Change
DIM v AS INTEGER
v = sldVal.Value
prgVal.Value = v
lblVal.Caption = STR$(v)
setStatus "Slider: " + STR$(v) + "%"
END SUB
SUB WidgetShow_Unload
END
END SUB

View file

@ -3623,6 +3623,10 @@ Register a widget API struct under a name. Each widget DXE registers its API dur
api Pointer to the widget's API struct
.endtable
.note warning
The create function must be the first field of the API struct. BASIC's form runtime instantiates widgets by dereferencing the api pointer as a pointer-to-function-pointer (see createWidgetByIface), so whichever field is first is what gets invoked. If the struct has multiple constructor variants, the one matching createSig must come first; put helper constructors, index accessors, and other entry points after it.
.endnote
.h3 wgtGetApi
.code

View file

@ -636,11 +636,15 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
(void)orient;
(void)value;
// Repaint with new scroll position -- dvxInvalidateWindow calls onPaint
// Repaint with new scroll position. PAINT_FULL is required so
// widgetOnPaint re-lays out children at the new root x/y offset;
// otherwise children keep their old positions and the content
// does not visibly scroll.
if (win->widgetRoot) {
AppContextT *ctx = wgtGetContext(win->widgetRoot);
if (ctx) {
win->paintNeeded = PAINT_FULL;
dvxInvalidateWindow(ctx, win);
}
}

View file

@ -51,7 +51,7 @@ A read-only drop-down list. Unlike ComboBox, the user cannot type free text; the
RemoveItem index% Remove the item at the given index.
.endtable
Default Event: Click
Default Event: Change (fires when the user picks a new item; Click fires only when the arrow opens the popup)
.h2 Example
@ -66,7 +66,7 @@ Sub Form_Load ()
DropDown1.ListIndex = 1
End Sub
Sub DropDown1_Click ()
Sub DropDown1_Change ()
Label1.Caption = "Picked: " & DropDown1.List(DropDown1.ListIndex)
End Sub
.endcode

View file

@ -554,7 +554,7 @@ static const WgtIfaceT sIface = {
.events = NULL,
.eventCount = 0,
.createSig = WGT_CREATE_PARENT,
.defaultEvent = "Click"
.defaultEvent = "Change"
};
void wgtRegister(void) {

View file

@ -173,14 +173,18 @@ static const WidgetClassT sClassRadio = {
}
};
// The `create` function MUST be the first field of this struct:
// createWidgetByIface dereferences the api pointer as a function
// pointer to call the widget's create function, so whatever field
// is first is what gets invoked when BASIC instantiates the widget.
static const struct {
WidgetT *(*group)(WidgetT *parent);
WidgetT *(*create)(WidgetT *parent, const char *text);
WidgetT *(*group)(WidgetT *parent);
void (*groupSetSelected)(WidgetT *group, int32_t index);
int32_t (*getIndex)(const WidgetT *w);
} sApi = {
.group = wgtRadioGroup,
.create = wgtRadio,
.group = wgtRadioGroup,
.groupSetSelected = wgtRadioGroupSetSelected,
.getIndex = wgtRadioGetIndex
};

View file

@ -75,7 +75,7 @@ typedef struct {
} TabControlDataT;
typedef struct {
const char *title;
char *title;
} TabPageDataT;
#define TAB_ARROW_W 16
@ -99,6 +99,8 @@ void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod);
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetTabPageAccelActivate(WidgetT *w, WidgetT *root);
void widgetTabPageDestroy(WidgetT *w);
void widgetTabPageSetText(WidgetT *w, const char *text);
// tabClosePopup -- close any open dropdown/combobox popup
@ -225,15 +227,37 @@ WidgetT *wgtTabPage(WidgetT *parent, const char *title) {
if (w) {
TabPageDataT *d = calloc(1, sizeof(TabPageDataT));
d->title = title;
d->title = strdup(title ? title : "");
w->data = d;
w->accelKey = accelParse(title);
w->accelKey = accelParse(d->title);
}
return w;
}
void widgetTabPageDestroy(WidgetT *w) {
TabPageDataT *d = (TabPageDataT *)w->data;
if (d) {
free(d->title);
free(d);
w->data = NULL;
}
}
void widgetTabPageSetText(WidgetT *w, const char *text) {
TabPageDataT *d = (TabPageDataT *)w->data;
if (d) {
free(d->title);
d->title = strdup(text ? text : "");
w->accelKey = accelParse(d->title);
}
}
// Min size: tab header height + the maximum min size across ALL pages
// (not just the active one). This ensures the tab control reserves
// enough space for the largest page, preventing resize flicker when
@ -648,6 +672,8 @@ static const WidgetClassT sClassTabPage = {
[WGT_METHOD_ON_ACCEL_ACTIVATE] = (void *)widgetTabPageAccelActivate,
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetCalcMinSizeBox,
[WGT_METHOD_LAYOUT] = (void *)widgetLayoutBox,
[WGT_METHOD_SET_TEXT] = (void *)widgetTabPageSetText,
[WGT_METHOD_DESTROY] = (void *)widgetTabPageDestroy,
}
};
@ -693,9 +719,33 @@ static const WgtIfaceT sIface = {
.defaultEvent = "Click"
};
// TabPage: authored in .frm as Begin TabPage name ... End inside a
// TabStrip. Caption sets the title string. The page is a container
// so children nest inside it (one page per Begin/End block).
static const struct {
WidgetT *(*create)(WidgetT *parent, const char *title);
} sTabPageApi = {
.create = wgtTabPage
};
static const WgtIfaceT sTabPageIface = {
.basName = "TabPage",
.props = NULL,
.propCount = 0,
.methods = NULL,
.methodCount = 0,
.events = NULL,
.eventCount = 0,
.createSig = WGT_CREATE_PARENT_TEXT,
.isContainer = true,
.defaultEvent = "Click"
};
void wgtRegister(void) {
sTabControlTypeId = wgtRegisterClass(&sClassTabControl);
sTabPageTypeId = wgtRegisterClass(&sClassTabPage);
wgtRegisterApi("tabcontrol", &sApi);
wgtRegisterIface("tabcontrol", &sIface);
wgtRegisterApi("tabpage", &sTabPageApi);
wgtRegisterIface("tabpage", &sTabPageIface);
}

View file

@ -181,12 +181,6 @@ void wgtTimerStop(WidgetT *w) {
void wgtUpdateTimers(void) {
static int32_t sUpdateCount = 0;
sUpdateCount++;
if (sUpdateCount <= 3 || sUpdateCount % 100 == 0) {
dvxLog("[T] wgtUpdateTimers call #%d activeCount=%d",
(int)sUpdateCount, (int)arrlen(sActiveTimers));
}
clock_t now = clock();
// Iterate backwards so arrdel doesn't skip entries
@ -203,9 +197,6 @@ void wgtUpdateTimers(void) {
clock_t interval = (clock_t)d->intervalMs * CLOCKS_PER_SEC / 1000;
if (elapsed >= interval) {
dvxLog("[T] timer tick: w=%p onChange=%p intervalMs=%d elapsed=%ld interval=%ld",
(void *)w, (void *)w->onChange, (int)d->intervalMs,
(long)elapsed, (long)interval);
d->lastFire = now;
if (w->onChange) {