From de60200f23f4b73fc8211c37e276d0aa58be4df6 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Wed, 15 Apr 2026 15:15:09 -0500 Subject: [PATCH] We can make binaries from BASIC! --- apps/cpanel/cpanel.c | 98 +- apps/dvxbasic/Makefile | 42 +- apps/dvxbasic/compiler/lexer.c | 29 +- apps/dvxbasic/compiler/lexer.h | 19 +- apps/dvxbasic/compiler/opcodes.h | 24 +- apps/dvxbasic/compiler/parser.c | 376 ++-- apps/dvxbasic/compiler/strip.c | 39 + apps/dvxbasic/compiler/strip.h | 18 + apps/dvxbasic/compiler/symtab.h | 2 + apps/dvxbasic/formrt/formcfm.c | 810 ++++++++ apps/dvxbasic/formrt/formcfm.h | 26 + apps/dvxbasic/formrt/formrt.c | 1726 ++++++++++++++++- apps/dvxbasic/formrt/formrt.h | 66 +- apps/dvxbasic/ide/ideDesigner.c | 80 +- apps/dvxbasic/ide/ideDesigner.h | 2 +- apps/dvxbasic/ide/ideMain.c | 419 ++-- apps/dvxbasic/runtime/serialize.c | 524 +++++ apps/dvxbasic/runtime/serialize.h | 41 + apps/dvxbasic/runtime/vm.c | 299 +-- apps/dvxbasic/runtime/vm.h | 43 +- apps/dvxbasic/samples/MULTI.DBP | 18 - apps/dvxbasic/samples/MULTI1.FRM | 10 - apps/dvxbasic/samples/clickme.bas | 29 - apps/dvxbasic/samples/clickme.frm | 12 - apps/dvxbasic/samples/formtest.bas | 8 - apps/dvxbasic/samples/hello.bas | 27 - apps/dvxbasic/samples/input.bas | 13 - apps/dvxbasic/stub/basstub.c | 217 +++ config/dvx.ini | 2 + core/dvxApp.c | 6 +- core/dvxApp.h | 3 +- core/dvxRes.h | 5 + core/dvxResource.c | 6 + core/dvxWgt.h | 5 + core/platform/dvxPlat.h | 5 + core/platform/dvxPlatformDos.c | 23 + docs/dvx_basic_reference.html | 171 +- sdk/include/basic/comm.bas | 128 ++ sdk/include/basic/commdlg.bas | 9 +- sdk/include/basic/help.bas | 15 + sdk/include/basic/sql.bas | 53 + sdk/samples/basic/commdlg.frm | 156 ++ sdk/samples/basic/dynform.bas | 81 + sdk/samples/basic/helpedit.frm | 205 ++ .../samples/basic/iconed}/ICON32.BMP | 0 sdk/samples/basic/sqltest.bas | 72 + shell/shellMain.c | 6 +- tools/dvxResWrite.h | 280 +++ tools/dvxres.c | 289 +-- widgets/ansiTerm/ansiterm.bhs | 25 +- widgets/ansiTerm/widgetAnsiTerm.c | 3 +- widgets/canvas/canvas.bhs | 36 +- widgets/canvas/widgetCanvas.c | 30 +- widgets/listBox/listbox.bhs | 1 + widgets/listBox/widgetListBox.c | 37 +- widgets/listView/listview.bhs | 44 +- widgets/listView/widgetListView.c | 77 +- widgets/tabControl/tabctrl.bhs | 8 +- widgets/tabControl/widgetTabControl.c | 12 +- widgets/textInput/textinpt.bhs | 51 +- widgets/textInput/widgetTextInput.c | 299 ++- widgets/treeView/treeview.bhs | 33 +- widgets/treeView/widgetTreeView.c | 150 +- 63 files changed, 6032 insertions(+), 1311 deletions(-) create mode 100644 apps/dvxbasic/compiler/strip.c create mode 100644 apps/dvxbasic/compiler/strip.h create mode 100644 apps/dvxbasic/formrt/formcfm.c create mode 100644 apps/dvxbasic/formrt/formcfm.h create mode 100644 apps/dvxbasic/runtime/serialize.c create mode 100644 apps/dvxbasic/runtime/serialize.h delete mode 100644 apps/dvxbasic/samples/MULTI.DBP delete mode 100644 apps/dvxbasic/samples/MULTI1.FRM delete mode 100644 apps/dvxbasic/samples/clickme.bas delete mode 100644 apps/dvxbasic/samples/clickme.frm delete mode 100644 apps/dvxbasic/samples/formtest.bas delete mode 100644 apps/dvxbasic/samples/hello.bas delete mode 100644 apps/dvxbasic/samples/input.bas create mode 100644 apps/dvxbasic/stub/basstub.c create mode 100644 sdk/include/basic/comm.bas create mode 100644 sdk/include/basic/help.bas create mode 100644 sdk/include/basic/sql.bas create mode 100644 sdk/samples/basic/commdlg.frm create mode 100644 sdk/samples/basic/dynform.bas create mode 100644 sdk/samples/basic/helpedit.frm rename {apps/dvxbasic/samples => sdk/samples/basic/iconed}/ICON32.BMP (100%) create mode 100644 sdk/samples/basic/sqltest.bas create mode 100644 tools/dvxResWrite.h diff --git a/apps/cpanel/cpanel.c b/apps/cpanel/cpanel.c index 5db729f..6d96e2d 100644 --- a/apps/cpanel/cpanel.c +++ b/apps/cpanel/cpanel.c @@ -1,7 +1,7 @@ // ctrlpanel.c -- DVX Control Panel // // System configuration app with four tabs: -// Mouse -- scroll direction, double-click speed, acceleration +// Mouse -- scroll direction, double-click speed, acceleration, cursor speed // Colors -- all 18 system colors, theme load/save // Desktop -- wallpaper image (stretch mode) // Video -- resolution and color depth @@ -94,6 +94,7 @@ static uint8_t sSavedColorRgb[ColorCountE][3]; static int32_t sSavedWheelDir; static int32_t sSavedDblClick; static int32_t sSavedAccel; +static int32_t sSavedSpeed; static int32_t sSavedVideoW; static int32_t sSavedVideoH; static int32_t sSavedVideoBpp; @@ -104,6 +105,8 @@ static WidgetT *sWheelDrop = NULL; static WidgetT *sDblClickSldr = NULL; static WidgetT *sDblClickLbl = NULL; static WidgetT *sAccelDrop = NULL; +static WidgetT *sSpeedSldr = NULL; +static WidgetT *sSpeedLbl = NULL; static WidgetT *sDblTestLbl = NULL; // Colors tab widgets @@ -147,6 +150,9 @@ static void buildVideoTab(WidgetT *page); static int32_t mapAccelName(const char *name); static const char *mapAccelValue(int32_t val); static void onAccelChange(WidgetT *w); +static void onSpeedSlider(WidgetT *w); +static void updateSpeedLabel(void); +static void applyMouseConfig(void); static void onApplyTheme(WidgetT *w); static void onBrowseTheme(WidgetT *w); static void onResetColors(WidgetT *w); @@ -373,6 +379,26 @@ static void buildMouseTab(WidgetT *page) { wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL); + // Speed (mickey-to-pixel ratio) + wgtLabel(page, "Cursor Speed:"); + + WidgetT *speedRow = wgtHBox(page); + speedRow->spacing = wgtPixels(CP_SPACING); + + wgtLabel(speedRow, "Slow"); + sSpeedSldr = wgtSlider(speedRow, 2, 32); + sSpeedSldr->weight = 100; + sSpeedSldr->onChange = onSpeedSlider; + + int32_t speed = prefsGetInt(sPrefs, "mouse", "speed", 8); + wgtSliderSetValue(sSpeedSldr, speed); + wgtLabel(speedRow, "Fast "); + sSpeedLbl = wgtLabel(speedRow, ""); + sSpeedLbl->prefW = wgtPixels(CP_DBLCLICK_LBL_W); + updateSpeedLabel(); + + wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL); + // Double-click test area WidgetT *testRow = wgtHBox(page); testRow->spacing = wgtPixels(CP_SPACING); @@ -469,28 +495,15 @@ static const char *mapAccelValue(int32_t idx) { // ============================================================ static void onWheelChange(WidgetT *w) { - int32_t sel = wgtDropdownGetSelected(w); - int32_t dir = (sel == 1) ? -1 : 1; - int32_t dbl = wgtSliderGetValue(sDblClickSldr); - - const char *accelName = mapAccelValue(wgtDropdownGetSelected(sAccelDrop)); - int32_t accelVal = mapAccelName(accelName); - - dvxSetMouseConfig(sAc, dir, dbl, accelVal); + (void)w; + applyMouseConfig(); } static void onDblClickSlider(WidgetT *w) { (void)w; updateDblClickLabel(); - - int32_t dir = (wgtDropdownGetSelected(sWheelDrop) == 1) ? -1 : 1; - int32_t dbl = wgtSliderGetValue(sDblClickSldr); - - const char *accelName = mapAccelValue(wgtDropdownGetSelected(sAccelDrop)); - int32_t accelVal = mapAccelName(accelName); - - dvxSetMouseConfig(sAc, dir, dbl, accelVal); + applyMouseConfig(); } @@ -506,13 +519,15 @@ static void onDblClickTest(WidgetT *w) { static void onAccelChange(WidgetT *w) { - int32_t dir = (wgtDropdownGetSelected(sWheelDrop) == 1) ? -1 : 1; - int32_t dbl = wgtSliderGetValue(sDblClickSldr); + (void)w; + applyMouseConfig(); +} - const char *accelName = mapAccelValue(wgtDropdownGetSelected(w)); - int32_t accelVal = mapAccelName(accelName); - dvxSetMouseConfig(sAc, dir, dbl, accelVal); +static void onSpeedSlider(WidgetT *w) { + (void)w; + updateSpeedLabel(); + applyMouseConfig(); } @@ -808,6 +823,7 @@ static void onOk(WidgetT *w) { prefsSetString(sPrefs, "mouse", "wheel", wheelSel == 1 ? "reversed" : "normal"); prefsSetInt(sPrefs, "mouse", "doubleclick", wgtSliderGetValue(sDblClickSldr)); prefsSetString(sPrefs, "mouse", "acceleration", mapAccelValue(wgtDropdownGetSelected(sAccelDrop))); + prefsSetInt(sPrefs, "mouse", "speed", wgtSliderGetValue(sSpeedSldr)); // Save colors to INI for (int32_t i = 0; i < ColorCountE; i++) { @@ -881,6 +897,7 @@ static void saveSnapshot(void) { sSavedWheelDir = sAc->wheelDirection; sSavedDblClick = (int32_t)(sAc->dblClickTicks * 1000 / CLOCKS_PER_SEC); sSavedAccel = wgtDropdownGetSelected(sAccelDrop); + sSavedSpeed = wgtSliderGetValue(sSpeedSldr); sSavedVideoW = sAc->display.width; sSavedVideoH = sAc->display.height; sSavedVideoBpp = sAc->display.format.bitsPerPixel; @@ -900,7 +917,8 @@ static void restoreSnapshot(void) { // Restore mouse const char *accelName = mapAccelValue(sSavedAccel); int32_t accelVal = mapAccelName(accelName); - dvxSetMouseConfig(sAc, sSavedWheelDir, sSavedDblClick, accelVal); + int32_t speedVal = 34 - sSavedSpeed; + dvxSetMouseConfig(sAc, sSavedWheelDir, sSavedDblClick, accelVal, speedVal); // Restore video mode if changed if (sAc->display.width != sSavedVideoW || @@ -1067,6 +1085,40 @@ static void updateDblClickLabel(void) { } +// ============================================================ +// updateSpeedLabel +// ============================================================ + +static void updateSpeedLabel(void) { + static char buf[32]; + int32_t val = wgtSliderGetValue(sSpeedSldr); + snprintf(buf, sizeof(buf), "%ld", (long)val); + wgtSetText(sSpeedLbl, buf); +} + + +// ============================================================ +// applyMouseConfig -- gather current widget values and apply +// ============================================================ + +static void applyMouseConfig(void) { + int32_t dir = (wgtDropdownGetSelected(sWheelDrop) == 1) ? -1 : 1; + int32_t dbl = wgtSliderGetValue(sDblClickSldr); + + const char *accelName = mapAccelValue(wgtDropdownGetSelected(sAccelDrop)); + int32_t accelVal = mapAccelName(accelName); + + // Slider is inverted: low value = slow (high mickeys), high value = fast (low mickeys) + // Slider range 2..32. Mickey ratio = 34 - sliderValue, so: + // slider 2 -> 32 mickeys/8px (slowest) + // slider 8 -> 26 (near default) + // slider 32 -> 2 mickeys/8px (fastest) + int32_t speedVal = 34 - wgtSliderGetValue(sSpeedSldr); + + dvxSetMouseConfig(sAc, dir, dbl, accelVal, speedVal); +} + + // ============================================================ // updateSwatch // ============================================================ diff --git a/apps/dvxbasic/Makefile b/apps/dvxbasic/Makefile index 04ca477..9c1398b 100644 --- a/apps/dvxbasic/Makefile +++ b/apps/dvxbasic/Makefile @@ -19,22 +19,23 @@ LIBSDIR = ../../bin/libs APPDIR = ../../bin/apps/kpunch/dvxbasic DVXRES = ../../bin/host/dvxres -# Runtime library objects (VM + values) -RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o +# Runtime library objects (VM + values + form runtime + serialization + compiled forms) +RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/formrt.o $(OBJDIR)/serialize.o $(OBJDIR)/formcfm.o RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt RT_TARGET = $(RT_TARGETDIR)/basrt.lib # Compiler objects (only needed by the IDE) -COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/symtab.o - -# Form runtime objects (bridge between BASIC and DVX widgets) -FORMRT_OBJS = $(OBJDIR)/formrt.o +COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/symtab.o $(OBJDIR)/strip.o # IDE app objects IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideMenuEditor.o $(OBJDIR)/ideProject.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o -APP_OBJS = $(IDE_OBJS) $(FORMRT_OBJS) +APP_OBJS = $(IDE_OBJS) APP_TARGET = $(APPDIR)/dvxbasic.app +# Standalone stub (embedded as IDE resource) +STUB_OBJS = $(OBJDIR)/basstub.o +STUB_TARGET = $(OBJDIR)/basstub.app + # Native test programs (host gcc, not cross-compiled) HOSTCC = gcc HOSTCFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -I. -I../../core @@ -53,7 +54,7 @@ TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/co .PHONY: all clean tests -all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(APP_TARGET) +all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) @@ -77,17 +78,34 @@ $(RT_TARGET): $(RT_OBJS) | $(RT_TARGETDIR) $(RT_TARGETDIR)/basrt.dep: ../../config/basrt.dep | $(RT_TARGETDIR) sed 's/$$/\r/' $< > $@ -# IDE app DXE (compiler linked in, runtime from basrt.lib) -$(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) dvxbasic.res | $(APPDIR) +# Standalone stub DXE (embedded as resource in IDE app) +$(STUB_TARGET): $(STUB_OBJS) | $(APPDIR) + $(DXE3GEN) -o $@ -U $(STUB_OBJS) + +# IDE app DXE (compiler linked in, runtime from basrt.lib, stub embedded) +$(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) $(STUB_TARGET) dvxbasic.res | $(APPDIR) $(DXE3GEN) -o $@ -U $(COMP_OBJS) $(APP_OBJS) $(DVXRES) build $@ dvxbasic.res + $(DVXRES) add $@ STUB binary @$(STUB_TARGET) # Object files $(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/symtab.h compiler/opcodes.h runtime/values.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< -$(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h compiler/codegen.h runtime/vm.h | $(OBJDIR) +$(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h compiler/opcodes.h runtime/vm.h | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/formcfm.o: formrt/formcfm.c formrt/formcfm.h formrt/formrt.h | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/serialize.o: runtime/serialize.c runtime/serialize.h runtime/vm.h | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/strip.o: compiler/strip.c compiler/strip.h compiler/opcodes.h runtime/vm.h | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/basstub.o: stub/basstub.c runtime/vm.h runtime/serialize.h formrt/formrt.h formrt/formcfm.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< $(OBJDIR)/ideDesigner.o: ide/ideDesigner.c ide/ideDesigner.h | $(OBJDIR) @@ -140,5 +158,5 @@ $(BINDIR): mkdir -p $(BINDIR) clean: - rm -rf $(RT_OBJS) $(COMP_OBJS) $(FORMRT_OBJS) $(IDE_OBJS) $(RT_TARGET) $(APP_TARGET) $(RT_TARGETDIR)/basrt.dep $(RT_TARGETDIR) $(OBJDIR)/basrt_init.o + rm -rf $(RT_OBJS) $(COMP_OBJS) $(IDE_OBJS) $(STUB_OBJS) $(RT_TARGET) $(APP_TARGET) $(STUB_TARGET) $(RT_TARGETDIR)/basrt.dep $(RT_TARGETDIR) $(OBJDIR)/basrt_init.o rm -f $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) diff --git a/apps/dvxbasic/compiler/lexer.c b/apps/dvxbasic/compiler/lexer.c index c214462..fcd825b 100644 --- a/apps/dvxbasic/compiler/lexer.c +++ b/apps/dvxbasic/compiler/lexer.c @@ -34,8 +34,10 @@ static const KeywordEntryT sKeywords[] = { { "CASE", TOK_CASE }, { "CHDIR", TOK_CHDIR }, { "CHDRIVE", TOK_CHDRIVE }, - { "CLOSE", TOK_CLOSE }, - { "CURDIR", TOK_CURDIR }, + { "CLOSE", TOK_CLOSE }, + { "CREATECONTROL", TOK_CREATECONTROL }, + { "CREATEFORM", TOK_CREATEFORM }, + { "CURDIR", TOK_CURDIR }, { "CURDIR$", TOK_CURDIR }, { "CONST", TOK_CONST }, { "DATA", TOK_DATA }, @@ -95,8 +97,10 @@ static const KeywordEntryT sKeywords[] = { { "NAME", TOK_NAME }, { "NEXT", TOK_NEXT }, { "NOT", TOK_NOT }, + { "NOTHING", TOK_NOTHING }, { "ON", TOK_ON }, { "OPEN", TOK_OPEN }, + { "OPTIONAL", TOK_OPTIONAL }, { "OPTION", TOK_OPTION }, { "OR", TOK_OR }, { "OUTPUT", TOK_OUTPUT }, @@ -106,8 +110,9 @@ static const KeywordEntryT sKeywords[] = { { "RANDOM", TOK_RANDOM }, { "RANDOMIZE", TOK_RANDOMIZE }, { "READ", TOK_READ }, - { "REDIM", TOK_REDIM }, - { "REM", TOK_REM }, + { "REDIM", TOK_REDIM }, + { "REM", TOK_REM }, + { "REMOVECONTROL", TOK_REMOVECONTROL }, { "RESTORE", TOK_RESTORE }, { "RESUME", TOK_RESUME }, { "RETURN", TOK_RETURN }, @@ -116,24 +121,12 @@ static const KeywordEntryT sKeywords[] = { { "SELECT", TOK_SELECT }, { "SET", TOK_SET }, { "SETATTR", TOK_SETATTR }, + { "SETEVENT", TOK_SETEVENT }, { "SHARED", TOK_SHARED }, { "SHELL", TOK_SHELL }, { "SHOW", TOK_SHOW }, { "SINGLE", TOK_SINGLE }, { "SLEEP", TOK_SLEEP }, - { "SQLAFFECTED", TOK_SQLAFFECTED }, - { "SQLCLOSE", TOK_SQLCLOSE }, - { "SQLEOF", TOK_SQLEOF }, - { "SQLERROR", TOK_SQLERROR }, - { "SQLEXEC", TOK_SQLEXEC }, - { "SQLFIELD", TOK_SQLFIELD }, - { "SQLFIELDCOUNT", TOK_SQLFIELDCOUNT }, - { "SQLFIELDDBL", TOK_SQLFIELDDBL }, - { "SQLFIELDINT", TOK_SQLFIELDINT }, - { "SQLFREERESULT", TOK_SQLFREERESULT }, - { "SQLNEXT", TOK_SQLNEXT }, - { "SQLOPEN", TOK_SQLOPEN }, - { "SQLQUERY", TOK_SQLQUERY }, { "STATIC", TOK_STATIC }, { "STEP", TOK_STEP }, { "STRING", TOK_STRING_KW }, @@ -703,7 +696,7 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) { // If it's a keyword and has no suffix, return the keyword token. // String-returning builtins (SQLError$, SQLField$) also match with $. - if (kwType != TOK_IDENT && (baseLen == idx || kwType == TOK_SQLERROR || kwType == TOK_SQLFIELD || kwType == TOK_INPUTBOX)) { + if (kwType != TOK_IDENT && (baseLen == idx || kwType == TOK_INPUTBOX)) { return kwType; } diff --git a/apps/dvxbasic/compiler/lexer.h b/apps/dvxbasic/compiler/lexer.h index d76b625..082dd7b 100644 --- a/apps/dvxbasic/compiler/lexer.h +++ b/apps/dvxbasic/compiler/lexer.h @@ -66,6 +66,8 @@ typedef enum { TOK_CASE, TOK_CLOSE, TOK_CONST, + TOK_CREATECONTROL, + TOK_CREATEFORM, TOK_DATA, TOK_DECLARE, TOK_DEF, @@ -112,8 +114,10 @@ typedef enum { TOK_MSGBOX, TOK_NEXT, TOK_NOT, + TOK_NOTHING, TOK_ON, TOK_OPEN, + TOK_OPTIONAL, TOK_OPTION, TOK_OR, TOK_OUTPUT, @@ -124,12 +128,14 @@ typedef enum { TOK_READ, TOK_REDIM, TOK_REM, + TOK_REMOVECONTROL, TOK_RESTORE, TOK_RESUME, TOK_RETURN, TOK_SEEK, TOK_SELECT, TOK_SET, + TOK_SETEVENT, TOK_SHARED, TOK_SHELL, TOK_SHOW, @@ -137,19 +143,6 @@ typedef enum { TOK_SLEEP, TOK_INIREAD, TOK_INIWRITE, - TOK_SQLCLOSE, - TOK_SQLEOF, - TOK_SQLERROR, - TOK_SQLEXEC, - TOK_SQLFIELD, // SQLField$(rs, col) or SQLField$(rs, "name") - TOK_SQLFIELDCOUNT, - TOK_SQLFIELDINT, - TOK_SQLFIELDDBL, - TOK_SQLFREERESULT, - TOK_SQLNEXT, - TOK_SQLOPEN, - TOK_SQLQUERY, - TOK_SQLAFFECTED, TOK_STATIC, TOK_STEP, TOK_STRING_KW, diff --git a/apps/dvxbasic/compiler/opcodes.h b/apps/dvxbasic/compiler/opcodes.h index 5cfccb7..df72326 100644 --- a/apps/dvxbasic/compiler/opcodes.h +++ b/apps/dvxbasic/compiler/opcodes.h @@ -197,6 +197,10 @@ #define OP_LOAD_FORM_VAR 0x8F // [uint16 idx] push currentFormVars[idx] #define OP_STORE_FORM_VAR 0x9B // [uint16 idx] pop, store to currentFormVars[idx] #define OP_PUSH_FORM_ADDR 0x9C // [uint16 idx] push ¤tFormVars[idx] (ByRef) +#define OP_CREATE_CTRL_EX 0x9D // pop parentRef, pop name, pop type, pop formRef, push ctrlRef +#define OP_CREATE_FORM 0xF0 // pop height, pop width, pop nameStr, push formRef +#define OP_SET_EVENT 0xF1 // pop handlerNameStr, pop eventNameStr, pop ctrlRef +#define OP_REMOVE_CTRL 0xF2 // pop ctrlNameStr, pop formRef // ============================================================ // Array / misc @@ -313,26 +317,6 @@ #define OP_CALL_EXTERN 0xCD // [uint16 libNameIdx] [uint16 funcNameIdx] [uint8 argc] [uint8 retType] -// ============================================================ -// SQL database operations -// ============================================================ - -#define OP_SQL_OPEN 0xCE // pop path, push db handle (int) -#define OP_SQL_CLOSE 0xCF // pop db -#define OP_SQL_EXEC 0xD0 // pop sql, pop db, push bool -#define OP_SQL_ERROR 0xD1 // pop db, push error string -#define OP_SQL_QUERY 0xD2 // pop sql, pop db, push cursor handle (int) -#define OP_SQL_NEXT 0xD3 // pop rs, push bool -#define OP_SQL_EOF 0xD4 // pop rs, push bool -#define OP_SQL_FIELD_COUNT 0xD5 // pop rs, push int -#define OP_SQL_FIELD_NAME 0xD6 // pop col, pop rs, push string -#define OP_SQL_FIELD_TEXT 0xD7 // pop col, pop rs, push string -#define OP_SQL_FIELD_BYNAME 0xD8 // pop name, pop rs, push string -#define OP_SQL_FIELD_INT 0xD9 // pop col, pop rs, push int -#define OP_SQL_FIELD_DBL 0xDA // pop col, pop rs, push double -#define OP_SQL_FREE_RESULT 0xDB // pop rs -#define OP_SQL_AFFECTED 0xDC // pop db, push int - // App object #define OP_APP_PATH 0xDD // push App.Path string #define OP_APP_CONFIG 0xDE // push App.Config string diff --git a/apps/dvxbasic/compiler/parser.c b/apps/dvxbasic/compiler/parser.c index fb1a9e5..d87c7d9 100644 --- a/apps/dvxbasic/compiler/parser.c +++ b/apps/dvxbasic/compiler/parser.c @@ -180,7 +180,9 @@ static void parseRedim(BasParserT *p); static void parseRestore(BasParserT *p); static void parseResume(BasParserT *p); static void parseRmDir(BasParserT *p); +static void parseRemoveControl(BasParserT *p); static void parseSeek(BasParserT *p); +static void parseSetEvent(BasParserT *p); static void parseSetAttr(BasParserT *p); static void parseSelectCase(BasParserT *p); static void parseShell(BasParserT *p); @@ -717,13 +719,52 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) { return; } - if (argc != sym->paramCount) { + // Determine minimum required arguments + int32_t minArgs = sym->requiredParams; + if (minArgs == 0 && sym->paramCount > 0) { + // No optional params declared -- all are required + bool hasOptional = false; + for (int32_t i = 0; i < sym->paramCount && i < BAS_MAX_PARAMS; i++) { + if (sym->paramOptional[i]) { + hasOptional = true; + break; + } + } + if (!hasOptional) { + minArgs = sym->paramCount; + } + } + + if (argc < minArgs || argc > sym->paramCount) { char buf[256]; - snprintf(buf, sizeof(buf), "Function '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc); + if (minArgs == sym->paramCount) { + snprintf(buf, sizeof(buf), "Function '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc); + } else { + snprintf(buf, sizeof(buf), "Function '%s' expects %d to %d arguments, got %d", sym->name, (int)minArgs, (int)sym->paramCount, (int)argc); + } error(p, buf); return; } + // Push default zero-values for omitted optional parameters + for (int32_t i = argc; i < sym->paramCount; i++) { + uint8_t pdt = sym->paramTypes[i]; + if (pdt == BAS_TYPE_STRING) { + uint16_t emptyIdx = basAddConstant(&p->cg, "", 0); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, emptyIdx); + } else if (pdt == BAS_TYPE_OBJECT) { + // Nothing -- push NULL object + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 0); + } else { + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 0); + } + } + + argc = sym->paramCount; + // External library function: emit OP_CALL_EXTERN if (sym->isExtern) { basEmit8(&p->cg, OP_CALL_EXTERN); @@ -1280,6 +1321,49 @@ static void parsePrimary(BasParserT *p) { return; } + // Nothing -- null object reference + if (tt == TOK_NOTHING) { + advance(p); + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 0); + return; + } + + // CreateForm(name$, width%, height%) -- create a form in code + if (tt == TOK_CREATEFORM) { + advance(p); + expect(p, TOK_LPAREN); + parseExpression(p); // name + expect(p, TOK_COMMA); + parseExpression(p); // width + expect(p, TOK_COMMA); + parseExpression(p); // height + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_CREATE_FORM); + return; + } + + // CreateControl(form, typeName$, ctrlName$ [, parent]) -- create a control + if (tt == TOK_CREATECONTROL) { + advance(p); + expect(p, TOK_LPAREN); + parseExpression(p); // form ref + expect(p, TOK_COMMA); + parseExpression(p); // type name + expect(p, TOK_COMMA); + parseExpression(p); // control name + if (match(p, TOK_COMMA)) { + // Optional parent parameter + parseExpression(p); // parent ctrl ref + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_CREATE_CTRL_EX); + } else { + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_CREATE_CTRL); + } + return; + } + // EOF(#channel) -- file end-of-file test if (tt == TOK_EOF_KW) { advance(p); @@ -1415,15 +1499,6 @@ static void parsePrimary(BasParserT *p) { } // SQL expression functions -- all require parentheses - if (tt == TOK_SQLOPEN) { - advance(p); - expect(p, TOK_LPAREN); - parseExpression(p); - expect(p, TOK_RPAREN); - basEmit8(&p->cg, OP_SQL_OPEN); - return; - } - // IniRead$(file, section, key, default) if (tt == TOK_INIREAD) { advance(p); @@ -1440,115 +1515,6 @@ static void parsePrimary(BasParserT *p) { return; } - if (tt == TOK_SQLERROR) { - advance(p); - expect(p, TOK_LPAREN); - parseExpression(p); - expect(p, TOK_RPAREN); - basEmit8(&p->cg, OP_SQL_ERROR); - return; - } - - if (tt == TOK_SQLQUERY) { - advance(p); - expect(p, TOK_LPAREN); - parseExpression(p); // db - expect(p, TOK_COMMA); - parseExpression(p); // sql - expect(p, TOK_RPAREN); - basEmit8(&p->cg, OP_SQL_QUERY); - return; - } - - if (tt == TOK_SQLEOF) { - advance(p); - expect(p, TOK_LPAREN); - parseExpression(p); - expect(p, TOK_RPAREN); - basEmit8(&p->cg, OP_SQL_EOF); - return; - } - - if (tt == TOK_SQLFIELDCOUNT) { - advance(p); - expect(p, TOK_LPAREN); - parseExpression(p); - expect(p, TOK_RPAREN); - basEmit8(&p->cg, OP_SQL_FIELD_COUNT); - return; - } - - if (tt == TOK_SQLFIELD) { - // SQLField$(rs, col) or SQLField$(rs, "name") - // If second arg is a string, use OP_SQL_FIELD_BYNAME; else OP_SQL_FIELD_TEXT - advance(p); - expect(p, TOK_LPAREN); - parseExpression(p); // rs - expect(p, TOK_COMMA); - parseExpression(p); // col or name - expect(p, TOK_RPAREN); - // Runtime will determine type — use BYNAME if string, TEXT if int. - // For simplicity, always use BYNAME (works with both int index and string name - // since the C function handles both patterns). Actually we need separate opcodes. - // Use a simple heuristic: if the arg is a string literal, use BYNAME. - // For now, default to BYNAME which is more flexible. - basEmit8(&p->cg, OP_SQL_FIELD_BYNAME); - return; - } - - if (tt == TOK_SQLFIELDINT) { - advance(p); - expect(p, TOK_LPAREN); - parseExpression(p); // rs - expect(p, TOK_COMMA); - parseExpression(p); // col - expect(p, TOK_RPAREN); - basEmit8(&p->cg, OP_SQL_FIELD_INT); - return; - } - - if (tt == TOK_SQLFIELDDBL) { - advance(p); - expect(p, TOK_LPAREN); - parseExpression(p); // rs - expect(p, TOK_COMMA); - parseExpression(p); // col - expect(p, TOK_RPAREN); - basEmit8(&p->cg, OP_SQL_FIELD_DBL); - return; - } - - if (tt == TOK_SQLAFFECTED) { - advance(p); - expect(p, TOK_LPAREN); - parseExpression(p); - expect(p, TOK_RPAREN); - basEmit8(&p->cg, OP_SQL_AFFECTED); - return; - } - - if (tt == TOK_SQLEXEC) { - // SQLExec as expression: SQLExec(db, sql) returns bool - advance(p); - expect(p, TOK_LPAREN); - parseExpression(p); // db - expect(p, TOK_COMMA); - parseExpression(p); // sql - expect(p, TOK_RPAREN); - basEmit8(&p->cg, OP_SQL_EXEC); - return; - } - - if (tt == TOK_SQLNEXT) { - // SQLNext as expression: SQLNext(rs) returns bool - advance(p); - expect(p, TOK_LPAREN); - parseExpression(p); - expect(p, TOK_RPAREN); - basEmit8(&p->cg, OP_SQL_NEXT); - return; - } - // SHELL("command") -- as function expression if (tt == TOK_SHELL) { advance(p); @@ -3410,9 +3376,12 @@ static void parseFunction(BasParserT *p) { basSymTabAllocSlot(&p->sym); // Parse parameter list - int32_t paramCount = 0; + int32_t paramCount = 0; + int32_t requiredCount = 0; + bool seenOptional = false; uint8_t paramTypes[BAS_MAX_PARAMS]; bool paramByVal[BAS_MAX_PARAMS]; + bool paramOptional[BAS_MAX_PARAMS]; if (match(p, TOK_LPAREN)) { while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) { @@ -3420,6 +3389,15 @@ static void parseFunction(BasParserT *p) { expect(p, TOK_COMMA); } + bool optional = false; + if (match(p, TOK_OPTIONAL)) { + optional = true; + seenOptional = true; + } else if (seenOptional) { + error(p, "Required parameter cannot follow Optional parameter"); + return; + } + bool byVal = false; if (match(p, TOK_BYVAL)) { byVal = true; @@ -3450,9 +3428,15 @@ static void parseFunction(BasParserT *p) { paramSym->isDefined = true; if (paramCount < BAS_MAX_PARAMS) { - paramTypes[paramCount] = pdt; - paramByVal[paramCount] = byVal; + paramTypes[paramCount] = pdt; + paramByVal[paramCount] = byVal; + paramOptional[paramCount] = optional; } + + if (!optional) { + requiredCount = paramCount + 1; + } + paramCount++; } expect(p, TOK_RPAREN); @@ -3485,13 +3469,15 @@ static void parseFunction(BasParserT *p) { return; } - funcSym->codeAddr = funcAddr; - funcSym->isDefined = true; - funcSym->paramCount = paramCount; - funcSym->scope = SCOPE_GLOBAL; + funcSym->codeAddr = funcAddr; + funcSym->isDefined = true; + funcSym->paramCount = paramCount; + funcSym->requiredParams = requiredCount; + funcSym->scope = SCOPE_GLOBAL; for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) { - funcSym->paramTypes[i] = paramTypes[i]; - funcSym->paramByVal[i] = paramByVal[i]; + funcSym->paramTypes[i] = paramTypes[i]; + funcSym->paramByVal[i] = paramByVal[i]; + funcSym->paramOptional[i] = paramOptional[i]; } // Backpatch any forward-reference calls to this function @@ -5102,6 +5088,48 @@ static void parseStatement(BasParserT *p) { basEmit8(&p->cg, OP_DO_EVENTS); break; + case TOK_CREATEFORM: + // CreateForm used as statement (discard return value) + parsePrimary(p); + basEmit8(&p->cg, OP_POP); + break; + + case TOK_CREATECONTROL: + // CreateControl used as statement (discard return value) + parsePrimary(p); + basEmit8(&p->cg, OP_POP); + break; + + case TOK_SETEVENT: + parseSetEvent(p); + break; + + case TOK_REMOVECONTROL: + parseRemoveControl(p); + break; + + case TOK_SET: + // SET var = expr (object assignment) + advance(p); // consume SET + if (!check(p, TOK_IDENT)) { + errorExpected(p, "variable name"); + break; + } + { + char varName[BAS_MAX_TOKEN_LEN]; + strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + expect(p, TOK_EQ); + parseExpression(p); + BasSymbolT *varSym = ensureVariable(p, varName); + if (varSym) { + varSym->dataType = BAS_TYPE_OBJECT; + emitStore(p, varSym); + } + } + break; + case TOK_LOAD: // Load FormName (identifier, not string) advance(p); @@ -5174,12 +5202,6 @@ static void parseStatement(BasParserT *p) { break; // SQL statement forms (no return value) - case TOK_SQLCLOSE: - advance(p); - parseExpression(p); // db - basEmit8(&p->cg, OP_SQL_CLOSE); - break; - case TOK_INIWRITE: // IniWrite file, section, key, value advance(p); @@ -5193,30 +5215,6 @@ static void parseStatement(BasParserT *p) { basEmit8(&p->cg, OP_INI_WRITE); break; - case TOK_SQLEXEC: - // SQLExec db, sql (statement form, discard result) - advance(p); - parseExpression(p); // db - expect(p, TOK_COMMA); - parseExpression(p); // sql - basEmit8(&p->cg, OP_SQL_EXEC); - basEmit8(&p->cg, OP_POP); // discard bool result - break; - - case TOK_SQLNEXT: - // SQLNext rs (statement form, discard result) - advance(p); - parseExpression(p); // rs - basEmit8(&p->cg, OP_SQL_NEXT); - basEmit8(&p->cg, OP_POP); // discard bool result - break; - - case TOK_SQLFREERESULT: - advance(p); - parseExpression(p); // rs - basEmit8(&p->cg, OP_SQL_FREE_RESULT); - break; - case TOK_ME: { // Me.Show / Me.Hide / Me.CtrlName.Property = expr advance(p); // consume Me @@ -5456,9 +5454,12 @@ static void parseSub(BasParserT *p) { exitListInit(&exitSubList); // Parse parameter list - int32_t paramCount = 0; + int32_t paramCount = 0; + int32_t requiredCount = 0; + bool seenOptional = false; uint8_t paramTypes[BAS_MAX_PARAMS]; bool paramByVal[BAS_MAX_PARAMS]; + bool paramOptional[BAS_MAX_PARAMS]; if (match(p, TOK_LPAREN)) { while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) { @@ -5466,6 +5467,15 @@ static void parseSub(BasParserT *p) { expect(p, TOK_COMMA); } + bool optional = false; + if (match(p, TOK_OPTIONAL)) { + optional = true; + seenOptional = true; + } else if (seenOptional) { + error(p, "Required parameter cannot follow Optional parameter"); + return; + } + bool byVal = false; if (match(p, TOK_BYVAL)) { byVal = true; @@ -5496,9 +5506,15 @@ static void parseSub(BasParserT *p) { paramSym->isDefined = true; if (paramCount < BAS_MAX_PARAMS) { - paramTypes[paramCount] = pdt; - paramByVal[paramCount] = byVal; + paramTypes[paramCount] = pdt; + paramByVal[paramCount] = byVal; + paramOptional[paramCount] = optional; } + + if (!optional) { + requiredCount = paramCount + 1; + } + paramCount++; } expect(p, TOK_RPAREN); @@ -5522,13 +5538,15 @@ static void parseSub(BasParserT *p) { return; } - subSym->codeAddr = subAddr; - subSym->isDefined = true; - subSym->paramCount = paramCount; - subSym->scope = SCOPE_GLOBAL; + subSym->codeAddr = subAddr; + subSym->isDefined = true; + subSym->paramCount = paramCount; + subSym->requiredParams = requiredCount; + subSym->scope = SCOPE_GLOBAL; for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) { - subSym->paramTypes[i] = paramTypes[i]; - subSym->paramByVal[i] = paramByVal[i]; + subSym->paramTypes[i] = paramTypes[i]; + subSym->paramByVal[i] = paramByVal[i]; + subSym->paramOptional[i] = paramOptional[i]; } // Backpatch any forward-reference calls to this sub @@ -5761,6 +5779,28 @@ static void parsePut(BasParserT *p) { } +static void parseRemoveControl(BasParserT *p) { + // REMOVECONTROL formRef, ctrlName$ + advance(p); // consume REMOVECONTROL + parseExpression(p); // form reference + expect(p, TOK_COMMA); + parseExpression(p); // control name string + basEmit8(&p->cg, OP_REMOVE_CTRL); +} + + +static void parseSetEvent(BasParserT *p) { + // SETEVENT ctrlRef, eventName$, handlerName$ + advance(p); // consume SETEVENT + parseExpression(p); // control reference + expect(p, TOK_COMMA); + parseExpression(p); // event name string + expect(p, TOK_COMMA); + parseExpression(p); // handler name string + basEmit8(&p->cg, OP_SET_EVENT); +} + + static void parseSeek(BasParserT *p) { // SEEK #channel, position advance(p); // consume SEEK diff --git a/apps/dvxbasic/compiler/strip.c b/apps/dvxbasic/compiler/strip.c new file mode 100644 index 0000000..85d5a7e --- /dev/null +++ b/apps/dvxbasic/compiler/strip.c @@ -0,0 +1,39 @@ +// strip.c -- Release build stripping +// +// Removes debug information from a compiled module: +// - Clears debug variable info (names, scopes, types) +// - Clears debug UDT definitions +// +// Procedure names are preserved because the form runtime uses +// them for event dispatch (ControlName_EventName convention). +// +// OP_LINE removal is deferred to a future version (requires +// bytecode compaction and offset rewriting). + +#include "strip.h" + +#include +#include + + +void basStripModule(BasModuleT *mod) { + if (!mod) { + return; + } + + // Clear debug variable info + free(mod->debugVars); + mod->debugVars = NULL; + mod->debugVarCount = 0; + + // Clear debug UDT definitions + if (mod->debugUdtDefs) { + for (int32_t i = 0; i < mod->debugUdtDefCount; i++) { + free(mod->debugUdtDefs[i].fields); + } + + free(mod->debugUdtDefs); + mod->debugUdtDefs = NULL; + mod->debugUdtDefCount = 0; + } +} diff --git a/apps/dvxbasic/compiler/strip.h b/apps/dvxbasic/compiler/strip.h new file mode 100644 index 0000000..bc14155 --- /dev/null +++ b/apps/dvxbasic/compiler/strip.h @@ -0,0 +1,18 @@ +// strip.h -- Release build stripping +// +// Removes debug information from a compiled module to prevent +// decompilation. Clears procedure names, debug variable info, +// and debug UDT definitions. + +#ifndef DVXBASIC_STRIP_H +#define DVXBASIC_STRIP_H + +#include "../runtime/vm.h" + +// Strip debug info from a module for release builds: +// - Clear all procedure names +// - Clear debug variable info +// - Clear debug UDT definitions +void basStripModule(BasModuleT *mod); + +#endif // DVXBASIC_STRIP_H diff --git a/apps/dvxbasic/compiler/symtab.h b/apps/dvxbasic/compiler/symtab.h index 38023d0..03baa9c 100644 --- a/apps/dvxbasic/compiler/symtab.h +++ b/apps/dvxbasic/compiler/symtab.h @@ -72,8 +72,10 @@ typedef struct { // For SUB/FUNCTION: parameter info int32_t paramCount; + int32_t requiredParams; // count of non-optional params uint8_t paramTypes[BAS_MAX_PARAMS]; bool paramByVal[BAS_MAX_PARAMS]; + bool paramOptional[BAS_MAX_PARAMS]; // true = OPTIONAL parameter // Forward-reference backpatch list (code addresses to patch when defined) int32_t *patchAddrs; // stb_ds dynamic array diff --git a/apps/dvxbasic/formrt/formcfm.c b/apps/dvxbasic/formrt/formcfm.c new file mode 100644 index 0000000..5ba7232 --- /dev/null +++ b/apps/dvxbasic/formrt/formcfm.c @@ -0,0 +1,810 @@ +// formcfm.c -- Compiled form format implementation +// +// Serializes the layout portion of a .frm file (the Begin/End block +// structure) into a compact binary format. The BASIC code that follows +// the form layout is NOT included -- it's already compiled into the +// bytecode module. +// +// Binary format: +// magic 4 bytes "DCFM" +// version uint16 1 +// formName len-prefixed string +// caption len-prefixed string +// layout len-prefixed string ("VBox", "HBox", etc.) +// width int16 +// height int16 +// left int16 +// top int16 +// flags uint8 (bit 0=resizable, 1=centered, 2=autoSize) +// controlCount uint16 +// menuItemCount uint16 +// +// Controls[controlCount]: +// typeName len-prefixed string +// ctrlName len-prefixed string +// parentIndex int16 (-1 = form root) +// arrayIndex int16 (-1 = not array) +// propertyCount uint16 +// properties[]: +// key len-prefixed string +// valueType uint8 (0=string, 1=int, 2=bool) +// value type-dependent +// +// MenuItems[menuItemCount]: +// name len-prefixed string +// caption len-prefixed string +// level uint8 +// flags uint8 (bit 0=checked, 1=radioCheck, 2=enabled) + +#include "formcfm.h" +#include "../compiler/opcodes.h" +#include "thirdparty/stb_ds_wrap.h" + +#include +#include +#include +#include +#include + +// ============================================================ +// Write helpers (same pattern as serialize.c) +// ============================================================ + +typedef struct { + uint8_t *buf; + int32_t len; + int32_t cap; +} CfmBufT; + + +static void cbufInit(CfmBufT *b) { + b->cap = 2048; + b->buf = (uint8_t *)malloc(b->cap); + b->len = 0; +} + + +static void cbufGrow(CfmBufT *b, int32_t need) { + while (b->len + need > b->cap) { + b->cap *= 2; + } + + b->buf = (uint8_t *)realloc(b->buf, b->cap); +} + + +static void cbufWrite(CfmBufT *b, const void *data, int32_t len) { + cbufGrow(b, len); + memcpy(b->buf + b->len, data, len); + b->len += len; +} + + +static void cbufU8(CfmBufT *b, uint8_t v) { + cbufWrite(b, &v, 1); +} + + +static void cbufU16(CfmBufT *b, uint16_t v) { + cbufWrite(b, &v, 2); +} + + +static void cbufI16(CfmBufT *b, int16_t v) { + cbufWrite(b, &v, 2); +} + + +static void cbufStr(CfmBufT *b, const char *s) { + uint16_t len = s ? (uint16_t)strlen(s) : 0; + cbufU16(b, len); + + if (len > 0) { + cbufWrite(b, s, len); + } +} + + +// ============================================================ +// Read helpers +// ============================================================ + +typedef struct { + const uint8_t *data; + int32_t len; + int32_t pos; +} CfmReaderT; + + +static bool crOk(const CfmReaderT *r, int32_t need) { + return r->pos + need <= r->len; +} + + +static uint8_t crU8(CfmReaderT *r) { + return crOk(r, 1) ? r->data[r->pos++] : 0; +} + + +static uint16_t crU16(CfmReaderT *r) { + if (!crOk(r, 2)) { + return 0; + } + + uint16_t v; + memcpy(&v, r->data + r->pos, 2); + r->pos += 2; + return v; +} + + +static int16_t crI16(CfmReaderT *r) { + if (!crOk(r, 2)) { + return 0; + } + + int16_t v; + memcpy(&v, r->data + r->pos, 2); + r->pos += 2; + return v; +} + + +static int32_t crI32(CfmReaderT *r) { + if (!crOk(r, 4)) { + return 0; + } + + int32_t v; + memcpy(&v, r->data + r->pos, 4); + r->pos += 4; + return v; +} + + +static char *crStr(CfmReaderT *r) { + uint16_t len = crU16(r); + + if (len == 0 || !crOk(r, len)) { + char *s = (char *)malloc(1); + s[0] = '\0'; + return s; + } + + char *s = (char *)malloc(len + 1); + memcpy(s, r->data + r->pos, len); + s[len] = '\0'; + r->pos += len; + return s; +} + + +// ============================================================ +// .frm text parser for compilation +// ============================================================ + +#define MAX_LINE 512 + +typedef struct { + char typeName[BAS_MAX_CTRL_NAME]; + char ctrlName[BAS_MAX_CTRL_NAME]; + int16_t parentIndex; + int16_t arrayIndex; + + // Properties + struct { + char key[64]; + uint8_t valueType; // 0=string, 1=int, 2=bool + char strVal[256]; + int32_t intVal; + } props[32]; + + int32_t propCount; +} CfmControlT; + + +typedef struct { + char name[BAS_MAX_CTRL_NAME]; + char caption[256]; + uint8_t level; + uint8_t flags; // bit 0=checked, 1=radioCheck, 2=enabled +} CfmMenuItemT; + + +static void parseFrmKeyVal(const char *line, char *key, char *value) { + key[0] = '\0'; + value[0] = '\0'; + + while (*line == ' ' || *line == '\t') { + line++; + } + + int32_t ki = 0; + + while (*line && *line != ' ' && *line != '\t' && *line != '=' && ki < 63) { + key[ki++] = *line++; + } + + key[ki] = '\0'; + + while (*line == ' ' || *line == '\t') { + line++; + } + + if (*line == '=') { + line++; + } + + while (*line == ' ' || *line == '\t') { + line++; + } + + // Value: strip surrounding quotes if present + int32_t vi = 0; + bool inQ = false; + + if (*line == '"') { + inQ = true; + line++; + } + + while (*line && vi < 255) { + if (inQ && *line == '"') { + break; + } + + value[vi++] = *line++; + } + + value[vi] = '\0'; +} + + +// ============================================================ +// basFormCompile +// ============================================================ + +uint8_t *basFormCompile(const char *frmSource, int32_t frmLen, int32_t *outLen) { + if (!frmSource || frmLen <= 0 || !outLen) { + return NULL; + } + + // Parse the .frm text to extract form layout + char formName[BAS_MAX_CTRL_NAME] = ""; + char caption[256] = ""; + char layout[BAS_MAX_CTRL_NAME] = "VBox"; + int16_t width = 0; + int16_t height = 0; + int16_t left = 0; + int16_t top = 0; + bool resizable = true; + bool centered = true; + bool autoSize = true; + + CfmControlT *controls = NULL; + int32_t controlCount = 0; + int32_t controlCap = 32; + controls = (CfmControlT *)calloc(controlCap, sizeof(CfmControlT)); + + CfmMenuItemT *menuItems = NULL; + int32_t menuCount = 0; + int32_t menuCap = 16; + menuItems = (CfmMenuItemT *)calloc(menuCap, sizeof(CfmMenuItemT)); + + // Parent index stack for nested containers + int16_t parentStack[BAS_MAX_FRM_NESTING]; + int32_t nestDepth = 0; + int32_t menuNestDepth = 0; + bool inForm = false; + bool inMenu = false; + int32_t currentCtrl = -1; + + const char *pos = frmSource; + const char *end = frmSource + frmLen; + + while (pos < end) { + const char *lineStart = pos; + + while (pos < end && *pos != '\n' && *pos != '\r') { + pos++; + } + + int32_t lineLen = (int32_t)(pos - lineStart); + + if (pos < end && *pos == '\r') { + pos++; + } + + if (pos < end && *pos == '\n') { + pos++; + } + + char line[MAX_LINE]; + + if (lineLen >= MAX_LINE) { + lineLen = MAX_LINE - 1; + } + + memcpy(line, lineStart, lineLen); + line[lineLen] = '\0'; + + char *trimmed = line; + + while (*trimmed == ' ' || *trimmed == '\t') { + trimmed++; + } + + if (*trimmed == '\0' || *trimmed == '\'') { + continue; + } + + // Stop at code section (first SUB/FUNCTION/DIM/executable statement after End Form) + if (!inForm && formName[0]) { + break; + } + + // VERSION line -- skip + if (strncasecmp(trimmed, "VERSION ", 8) == 0) { + continue; + } + + // Begin block + if (strncasecmp(trimmed, "Begin ", 6) == 0) { + char *rest = trimmed + 6; + char typeName[BAS_MAX_CTRL_NAME]; + char ctrlName[BAS_MAX_CTRL_NAME]; + + int32_t ti = 0; + + while (*rest && *rest != ' ' && *rest != '\t' && ti < BAS_MAX_CTRL_NAME - 1) { + typeName[ti++] = *rest++; + } + + typeName[ti] = '\0'; + + while (*rest == ' ' || *rest == '\t') { + rest++; + } + + int32_t ci = 0; + + while (*rest && *rest != ' ' && *rest != '\t' && *rest != '\r' && *rest != '\n' && ci < BAS_MAX_CTRL_NAME - 1) { + ctrlName[ci++] = *rest++; + } + + ctrlName[ci] = '\0'; + + if (strcasecmp(typeName, "Form") == 0) { + snprintf(formName, BAS_MAX_CTRL_NAME, "%s", ctrlName); + snprintf(caption, sizeof(caption), "%s", ctrlName); + inForm = true; + nestDepth = 0; + + for (int32_t i = 0; i < BAS_MAX_FRM_NESTING; i++) { + parentStack[i] = -1; + } + } else if (strcasecmp(typeName, "Menu") == 0 && inForm) { + if (menuCount >= menuCap) { + menuCap *= 2; + menuItems = (CfmMenuItemT *)realloc(menuItems, menuCap * sizeof(CfmMenuItemT)); + } + + CfmMenuItemT *mi = &menuItems[menuCount++]; + memset(mi, 0, sizeof(CfmMenuItemT)); + snprintf(mi->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + mi->level = (uint8_t)menuNestDepth; + mi->flags = 0x04; // enabled by default + inMenu = true; + menuNestDepth++; + currentCtrl = -1; + } else if (inForm) { + if (controlCount >= controlCap) { + controlCap *= 2; + controls = (CfmControlT *)realloc(controls, controlCap * sizeof(CfmControlT)); + } + + CfmControlT *ctrl = &controls[controlCount]; + memset(ctrl, 0, sizeof(CfmControlT)); + snprintf(ctrl->typeName, BAS_MAX_CTRL_NAME, "%s", typeName); + snprintf(ctrl->ctrlName, BAS_MAX_CTRL_NAME, "%s", ctrlName); + ctrl->parentIndex = (nestDepth > 0) ? parentStack[nestDepth - 1] : -1; + ctrl->arrayIndex = -1; + + currentCtrl = controlCount; + controlCount++; + + // Check if this is a container -- push onto parent stack + const char *wgtName = wgtFindByBasName(typeName); + + if (wgtName) { + const WgtIfaceT *iface = wgtGetIface(wgtName); + + if (iface && iface->isContainer) { + if (nestDepth < BAS_MAX_FRM_NESTING) { + parentStack[nestDepth] = (int16_t)currentCtrl; + } + + nestDepth++; + } + } + } + + continue; + } + + // End block + if (strncasecmp(trimmed, "End", 3) == 0 && (trimmed[3] == '\0' || trimmed[3] == ' ' || trimmed[3] == '\t' || trimmed[3] == '\r' || trimmed[3] == '\n')) { + if (inMenu) { + menuNestDepth--; + + if (menuNestDepth <= 0) { + inMenu = false; + menuNestDepth = 0; + } + } else if (nestDepth > 0) { + nestDepth--; + } else { + // End Form + inForm = false; + } + + currentCtrl = -1; + continue; + } + + // Property assignment inside a block + if (inForm) { + char key[64]; + char value[256]; + parseFrmKeyVal(trimmed, key, value); + + if (!key[0]) { + continue; + } + + // Form-level properties + if (currentCtrl < 0 && !inMenu) { + if (strcasecmp(key, "Caption") == 0) { + snprintf(caption, sizeof(caption), "%s", value); + } else if (strcasecmp(key, "Layout") == 0) { + snprintf(layout, BAS_MAX_CTRL_NAME, "%s", value); + } else if (strcasecmp(key, "Width") == 0) { + width = (int16_t)atoi(value); + } else if (strcasecmp(key, "Height") == 0) { + height = (int16_t)atoi(value); + } else if (strcasecmp(key, "Left") == 0) { + left = (int16_t)atoi(value); + } else if (strcasecmp(key, "Top") == 0) { + top = (int16_t)atoi(value); + } else if (strcasecmp(key, "Resizable") == 0) { + resizable = (strcasecmp(value, "True") == 0); + } else if (strcasecmp(key, "Centered") == 0) { + centered = (strcasecmp(value, "True") == 0); + } else if (strcasecmp(key, "AutoSize") == 0) { + autoSize = (strcasecmp(value, "True") == 0); + } + } else if (inMenu && menuCount > 0) { + CfmMenuItemT *mi = &menuItems[menuCount - 1]; + + if (strcasecmp(key, "Caption") == 0) { + snprintf(mi->caption, sizeof(mi->caption), "%s", value); + } else if (strcasecmp(key, "Checked") == 0) { + if (strcasecmp(value, "True") == 0) { + mi->flags |= 0x01; + } + } else if (strcasecmp(key, "Enabled") == 0) { + if (strcasecmp(value, "False") == 0) { + mi->flags &= ~0x04; + } + } + } else if (currentCtrl >= 0 && currentCtrl < controlCount) { + CfmControlT *ctrl = &controls[currentCtrl]; + + // Index property for control arrays + if (strcasecmp(key, "Index") == 0) { + ctrl->arrayIndex = (int16_t)atoi(value); + continue; + } + + if (ctrl->propCount < 32) { + int32_t pi = ctrl->propCount++; + snprintf(ctrl->props[pi].key, 64, "%s", key); + + // Detect value type + if (strcasecmp(value, "True") == 0) { + ctrl->props[pi].valueType = 2; + ctrl->props[pi].intVal = 1; + } else if (strcasecmp(value, "False") == 0) { + ctrl->props[pi].valueType = 2; + ctrl->props[pi].intVal = 0; + } else if (value[0] == '-' || isdigit((unsigned char)value[0])) { + ctrl->props[pi].valueType = 1; + ctrl->props[pi].intVal = atoi(value); + } else { + ctrl->props[pi].valueType = 0; + snprintf(ctrl->props[pi].strVal, 256, "%s", value); + } + } + } + } + } + + // Serialize to binary + CfmBufT b; + cbufInit(&b); + + cbufWrite(&b, "DCFM", 4); + cbufU16(&b, 1); // version + cbufStr(&b, formName); + cbufStr(&b, caption); + cbufStr(&b, layout); + cbufI16(&b, width); + cbufI16(&b, height); + cbufI16(&b, left); + cbufI16(&b, top); + + uint8_t flags = 0; + if (resizable) { flags |= 0x01; } + if (centered) { flags |= 0x02; } + if (autoSize) { flags |= 0x04; } + cbufU8(&b, flags); + + cbufU16(&b, (uint16_t)controlCount); + cbufU16(&b, (uint16_t)menuCount); + + for (int32_t i = 0; i < controlCount; i++) { + CfmControlT *ctrl = &controls[i]; + cbufStr(&b, ctrl->typeName); + cbufStr(&b, ctrl->ctrlName); + cbufI16(&b, ctrl->parentIndex); + cbufI16(&b, ctrl->arrayIndex); + cbufU16(&b, (uint16_t)ctrl->propCount); + + for (int32_t j = 0; j < ctrl->propCount; j++) { + cbufStr(&b, ctrl->props[j].key); + cbufU8(&b, ctrl->props[j].valueType); + + switch (ctrl->props[j].valueType) { + case 0: // string + cbufStr(&b, ctrl->props[j].strVal); + break; + + case 1: // int + cbufWrite(&b, &ctrl->props[j].intVal, 4); + break; + + case 2: // bool + cbufU8(&b, ctrl->props[j].intVal ? 1 : 0); + break; + } + } + } + + for (int32_t i = 0; i < menuCount; i++) { + CfmMenuItemT *mi = &menuItems[i]; + cbufStr(&b, mi->name); + cbufStr(&b, mi->caption); + cbufU8(&b, mi->level); + cbufU8(&b, mi->flags); + } + + free(controls); + free(menuItems); + + *outLen = b.len; + return b.buf; +} + + +// ============================================================ +// basFormLoadCompiled +// ============================================================ + +BasFormT *basFormLoadCompiled(BasFormRtT *rt, const uint8_t *data, int32_t dataLen) { + if (!rt || !data || dataLen < 10) { + return NULL; + } + + CfmReaderT r = { data, dataLen, 0 }; + + // Check magic + if (data[0] != 'D' || data[1] != 'C' || data[2] != 'F' || data[3] != 'M') { + return NULL; + } + + r.pos = 4; + + uint16_t version = crU16(&r); + + if (version != 1) { + return NULL; + } + + char *formName = crStr(&r); + char *caption = crStr(&r); + char *layout = crStr(&r); + int16_t width = crI16(&r); + int16_t height = crI16(&r); + int16_t left = crI16(&r); + int16_t top = crI16(&r); + uint8_t flags = crU8(&r); + bool resizable = (flags & 0x01) != 0; + bool centered = (flags & 0x02) != 0; + bool autoSize = (flags & 0x04) != 0; + uint16_t controlCount = crU16(&r); + uint16_t menuCount = crU16(&r); + + // Create the form + WidgetT *root; + WidgetT *contentBox; + WindowT *win = basFormRtCreateFormWindow(rt->ctx, caption, layout, resizable, centered, autoSize, width, height, left, top, &root, &contentBox); + + if (!win) { + free(formName); + free(caption); + free(layout); + return NULL; + } + + BasFormT *form = (BasFormT *)calloc(1, sizeof(BasFormT)); + snprintf(form->name, BAS_MAX_CTRL_NAME, "%s", formName); + snprintf(form->frmLayout, sizeof(form->frmLayout), "%s", layout); + form->frmWidth = width; + form->frmHeight = height; + form->frmLeft = left; + form->frmTop = top; + form->frmResizable = resizable; + form->frmCentered = centered; + form->frmAutoSize = autoSize; + form->window = win; + form->root = root; + form->contentBox = contentBox; + form->ctx = rt->ctx; + form->vm = rt->vm; + form->module = rt->module; + + // Synthetic control for form-level property access + memset(&form->formCtrl, 0, sizeof(form->formCtrl)); + snprintf(form->formCtrl.name, BAS_MAX_CTRL_NAME, "%s", formName); + form->formCtrl.widget = root; + form->formCtrl.form = form; + + // Window callbacks + win->onClose = NULL; // stub sets these after + win->onResize = NULL; + win->onFocus = NULL; + win->onBlur = NULL; + + // Create controls + for (uint16_t i = 0; i < controlCount; i++) { + char *typeName = crStr(&r); + char *ctrlName = crStr(&r); + int16_t parentIndex = crI16(&r); + int16_t arrayIndex = crI16(&r); + uint16_t propCount = crU16(&r); + + // Determine parent widget + WidgetT *parent = contentBox; + + if (parentIndex >= 0 && parentIndex < (int16_t)i) { + // Find the parent control's widget + BasControlT *parentCtrl = form->controls[parentIndex]; + parent = parentCtrl->widget; + } + + // Resolve widget type and create + const char *wgtTypeName = wgtFindByBasName(typeName); + WidgetT *widget = NULL; + + if (wgtTypeName) { + widget = createWidget(wgtTypeName, parent); + } + + if (!widget) { + // Skip properties + for (uint16_t j = 0; j < propCount; j++) { + free(crStr(&r)); // key + uint8_t vt = crU8(&r); + if (vt == 0) { free(crStr(&r)); } + else if (vt == 1) { crI32(&r); } + else { crU8(&r); } + } + + free(typeName); + free(ctrlName); + continue; + } + + wgtSetName(widget, ctrlName); + + // Create control entry + BasControlT *ctrl = (BasControlT *)calloc(1, sizeof(BasControlT)); + snprintf(ctrl->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + snprintf(ctrl->typeName, BAS_MAX_CTRL_NAME, "%s", typeName); + ctrl->index = arrayIndex; + ctrl->widget = widget; + ctrl->form = form; + ctrl->iface = wgtGetIface(wgtTypeName); + arrput(form->controls, ctrl); + + widget->userData = ctrl; + + // Apply properties + for (uint16_t j = 0; j < propCount; j++) { + char *key = crStr(&r); + uint8_t vt = crU8(&r); + BasValueT val; + memset(&val, 0, sizeof(val)); + + switch (vt) { + case 0: { // string + char *sv = crStr(&r); + val = basValStringFromC(sv); + free(sv); + break; + } + + case 1: // int + val = basValLong(crI32(&r)); + break; + + case 2: // bool + val = basValBool(crU8(&r) != 0); + break; + } + + basFormRtSetProp(rt, ctrl, key, val); + basValRelease(&val); + free(key); + } + + free(typeName); + free(ctrlName); + } + + // TODO: menu items (skip for now, read and discard) + for (uint16_t i = 0; i < menuCount; i++) { + free(crStr(&r)); // name + free(crStr(&r)); // caption + crU8(&r); // level + crU8(&r); // flags + } + + // Add form to runtime + arrput(rt->forms, form); + + free(formName); + free(caption); + free(layout); + + return form; +} + + +bool basFormGetCompiledName(const uint8_t *data, int32_t dataLen, char *nameBuf, int32_t bufSize) { + if (!data || dataLen < 10 || !nameBuf || bufSize < 2) { + return false; + } + + if (data[0] != 'D' || data[1] != 'C' || data[2] != 'F' || data[3] != 'M') { + return false; + } + + // Skip magic (4) + version (2) = offset 6 + // Form name is the first length-prefixed string + if (dataLen < 8) { + return false; + } + + uint16_t nameLen; + memcpy(&nameLen, data + 6, 2); + + if (nameLen == 0 || 8 + nameLen > dataLen) { + return false; + } + + int32_t copyLen = (nameLen < bufSize - 1) ? nameLen : bufSize - 1; + memcpy(nameBuf, data + 8, copyLen); + nameBuf[copyLen] = '\0'; + return true; +} diff --git a/apps/dvxbasic/formrt/formcfm.h b/apps/dvxbasic/formrt/formcfm.h new file mode 100644 index 0000000..f140e40 --- /dev/null +++ b/apps/dvxbasic/formrt/formcfm.h @@ -0,0 +1,26 @@ +// formcfm.h -- Compiled form format +// +// Binary serialization of .frm layout data for standalone apps. +// Widget types and layout stored as strings for stability across +// widget set changes. No BASIC source code is included. + +#ifndef DVXBASIC_FORMCFM_H +#define DVXBASIC_FORMCFM_H + +#include "formrt.h" + +#include + +// Serialize a .frm source string to compiled form binary. +// Returns a malloc'd buffer (caller frees) and sets *outLen. +uint8_t *basFormCompile(const char *frmSource, int32_t frmLen, int32_t *outLen); + +// Load a compiled form binary into the form runtime. +// Creates the form window and all controls. +BasFormT *basFormLoadCompiled(BasFormRtT *rt, const uint8_t *data, int32_t dataLen); + +// Extract the form name from a compiled form binary without loading it. +// Writes to nameBuf (up to bufSize-1 chars). Returns true on success. +bool basFormGetCompiledName(const uint8_t *data, int32_t dataLen, char *nameBuf, int32_t bufSize); + +#endif // DVXBASIC_FORMCFM_H diff --git a/apps/dvxbasic/formrt/formrt.c b/apps/dvxbasic/formrt/formrt.c index 0c88078..5d825a2 100644 --- a/apps/dvxbasic/formrt/formrt.c +++ b/apps/dvxbasic/formrt/formrt.c @@ -5,17 +5,18 @@ // registered by .wgt DXE files. No hardcoded control types. #include "formrt.h" -#include "../ide/ideDesigner.h" -#include "../compiler/codegen.h" +#include "formcfm.h" #include "../compiler/opcodes.h" #include "dvxDlg.h" #include "dvxWm.h" #include "box/box.h" +#include "ansiTerm/ansiTerm.h" #include "dataCtrl/dataCtrl.h" #include "dbGrid/dbGrid.h" #include "thirdparty/stb_ds_wrap.h" #include +#include #include #include #include @@ -33,6 +34,100 @@ // Module-level form runtime pointer for onFormClose callback static BasFormRtT *sFormRt = NULL; +// ============================================================ +// basModuleFindProc -- find a SUB/FUNCTION by name in the module +// ============================================================ + +static const BasProcEntryT *basModuleFindProc(const 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; +} + + +// ============================================================ +// basFormRtCreateContentBox -- create layout container for a form +// ============================================================ + +WidgetT *basFormRtCreateContentBox(WidgetT *root, const char *layout) { + if (!layout || !layout[0] || strcasecmp(layout, "VBox") == 0) { + return root; + } + + const char *wgtName = wgtFindByBasName(layout); + + if (wgtName) { + const WgtIfaceT *iface = wgtGetIface(wgtName); + + if (iface && iface->isContainer && iface->createSig == WGT_CREATE_PARENT) { + const void *api = wgtGetApi(wgtName); + + if (api) { + WidgetT *(*createFn)(WidgetT *) = *(WidgetT *(*const *)(WidgetT *))api; + WidgetT *box = createFn(root); + box->weight = 100; + return box; + } + } + } + + return root; +} + + +// ============================================================ +// basFormRtCreateFormWindow -- create a DVX window for a form +// ============================================================ + +WindowT *basFormRtCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox) { + int32_t defW = (width > 0) ? width : DEFAULT_FORM_W; + int32_t defH = (height > 0) ? height : DEFAULT_FORM_H; + + WindowT *win = dvxCreateWindowCentered(ctx, title, defW, defH, resizable); + + if (!win) { + return NULL; + } + + win->visible = false; + + WidgetT *root = wgtInitWindow(ctx, win); + + if (!root) { + dvxDestroyWindow(ctx, win); + return NULL; + } + + WidgetT *contentBox = basFormRtCreateContentBox(root, layout); + + if (autoSize) { + dvxFitWindow(ctx, win); + } else if (width > 0 && height > 0) { + dvxResizeWindow(ctx, win, width, height); + } + + if (centered) { + win->x = (ctx->display.width - win->w) / 2; + win->y = (ctx->display.height - win->h) / 2; + } else if (left > 0 || top > 0) { + win->x = left; + win->y = top; + } + + *outRoot = root; + *outContentBox = contentBox; + return win; +} + + // ============================================================ // Prototypes // ============================================================ @@ -40,7 +135,7 @@ static BasFormRtT *sFormRt = NULL; static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char *title, const char *defaultText); static void onFormMenu(WindowT *win, int32_t menuId); static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc); -static WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent); +WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent); static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled); static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled); static void onFormActivate(WindowT *win); @@ -76,19 +171,23 @@ void basFormRtBindVm(BasFormRtT *rt) { BasUiCallbacksT ui; memset(&ui, 0, sizeof(ui)); - ui.getProp = basFormRtGetProp; - ui.setProp = basFormRtSetProp; - ui.callMethod = basFormRtCallMethod; - ui.createCtrl = basFormRtCreateCtrl; - ui.findCtrl = basFormRtFindCtrl; - ui.findCtrlIdx = basFormRtFindCtrlIdx; - ui.loadForm = basFormRtLoadForm; - ui.unloadForm = basFormRtUnloadForm; - ui.showForm = basFormRtShowForm; - ui.hideForm = basFormRtHideForm; - ui.msgBox = basFormRtMsgBox; - ui.inputBox = basFormRtInputBox; - ui.ctx = rt; + ui.getProp = basFormRtGetProp; + ui.setProp = basFormRtSetProp; + ui.callMethod = basFormRtCallMethod; + ui.createCtrl = basFormRtCreateCtrl; + ui.findCtrl = basFormRtFindCtrl; + ui.findCtrlIdx = basFormRtFindCtrlIdx; + ui.loadForm = basFormRtLoadForm; + ui.unloadForm = basFormRtUnloadForm; + ui.showForm = basFormRtShowForm; + ui.hideForm = basFormRtHideForm; + ui.msgBox = basFormRtMsgBox; + ui.inputBox = basFormRtInputBox; + ui.createForm = basFormRtCreateForm; + ui.createCtrlEx = basFormRtCreateCtrlEx; + ui.setEvent = basFormRtSetEvent; + ui.removeCtrl = basFormRtRemoveCtrl; + ui.ctx = rt; basVmSetUiCallbacks(rt->vm, &ui); } @@ -222,6 +321,49 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, basStringUnref(s); } return zeroValue(); + + case WGT_SIG_STR_BOOL_BOOL: { + if (argc >= 3) { + BasStringT *s = basValFormatString(args[0]); + bool v = ((bool (*)(WidgetT *, const char *, bool, bool))m->fn)(w, s->data, basValIsTruthy(args[1]), basValIsTruthy(args[2])); + basStringUnref(s); + return basValBool(v); + } + return basValBool(false); + } + + case WGT_SIG_STR_STR_BOOL: { + if (argc >= 3) { + BasStringT *s1 = basValFormatString(args[0]); + BasStringT *s2 = basValFormatString(args[1]); + int32_t v = ((int32_t (*)(WidgetT *, const char *, const char *, bool))m->fn)(w, s1->data, s2->data, basValIsTruthy(args[2])); + basStringUnref(s1); + basStringUnref(s2); + return basValLong(v); + } + return zeroValue(); + } + + case WGT_SIG_RET_STR: { + const char *s = ((const char *(*)(const WidgetT *))m->fn)(w); + return basValStringFromC(s ? s : ""); + } + + case WGT_SIG_STR_INT: + if (argc >= 2) { + BasStringT *s = basValFormatString(args[0]); + ((void (*)(WidgetT *, const char *, int32_t))m->fn)(w, s->data, (int32_t)basValToNumber(args[1])); + basStringUnref(s); + } + return zeroValue(); + + case WGT_SIG_INT_STR: + if (argc >= 2) { + BasStringT *s = basValFormatString(args[1]); + ((void (*)(WidgetT *, int32_t, const char *))m->fn)(w, (int32_t)basValToNumber(args[0]), s->data); + basStringUnref(s); + } + return zeroValue(); } } } @@ -252,6 +394,106 @@ BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module) { } +// ============================================================ +// basFormRtLoadAllForms -- load all cached forms and show startup +// ============================================================ + +void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName) { + if (!rt) { + return; + } + + // Load .frm text cache entries (skip if already loaded) + for (int32_t i = 0; i < rt->frmCacheCount; i++) { + basFormRtLoadForm(rt, rt->frmCache[i].formName); + } + + // Load compiled form cache entries (skip if already loaded) + for (int32_t i = 0; i < rt->cfmCacheCount; i++) { + basFormRtLoadForm(rt, rt->cfmCache[i].formName); + } + + // Show the startup form (named or first) + int32_t formCount = (int32_t)arrlen(rt->forms); + + if (formCount > 0) { + BasFormT *startup = rt->forms[0]; + + if (startupFormName && startupFormName[0]) { + for (int32_t i = 0; i < formCount; i++) { + if (strcasecmp(rt->forms[i]->name, startupFormName) == 0) { + startup = rt->forms[i]; + break; + } + } + } + + basFormRtShowForm(rt, startup, false); + } +} + + +// ============================================================ +// basFormRtEventLoop -- VB-style event pump until all forms close +// ============================================================ + +void basFormRtEventLoop(BasFormRtT *rt) { + if (!rt || !rt->ctx || !rt->vm) { + return; + } + + while (rt->ctx->running && arrlen(rt->forms) > 0) { + if (!dvxUpdate(rt->ctx)) { + break; + } + } +} + + +// ============================================================ +// basFormRtRunSimple -- non-debug execution for standalone apps +// ============================================================ + +#define STUB_STEP_SLICE 10000 + +void basFormRtRunSimple(BasFormRtT *rt) { + if (!rt || !rt->vm) { + dvxLog("basFormRtRunSimple: no rt or vm"); + return; + } + + BasVmT *vm = rt->vm; + basVmSetStepLimit(vm, STUB_STEP_SLICE); + + BasVmResultE result; + + do { + result = basVmRun(vm); + + if (result == BAS_VM_STEP_LIMIT) { + if (!dvxUpdate(rt->ctx)) { + break; + } + } else if (result == BAS_VM_YIELDED) { + // DoEvents returned, continue + } else if (result == BAS_VM_ERROR) { + const char *errMsg = basVmGetError(vm); + char buf[512]; + snprintf(buf, sizeof(buf), "Runtime error:\n%s", errMsg ? errMsg : "Unknown error"); + dvxMessageBox(rt->ctx, "Error", buf, 0); + break; + } else { + break; + } + } while (1); + + // VB-style event loop: keep alive while forms are open + if (result == BAS_VM_HALTED) { + basFormRtEventLoop(rt); + } +} + + // ============================================================ // basFormRtCreateCtrl // ============================================================ @@ -299,7 +541,7 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const ctrl->form = form; ctrl->iface = wgtGetIface(wgtTypeName); arrput(form->controls, ctrl); - form->controlCount = (int32_t)arrlen(form->controls); + // Wire up event callbacks widget->userData = ctrl; @@ -314,6 +556,224 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const } +// ============================================================ +// basFormRtCreateCtrlEx -- create control with explicit parent +// ============================================================ + +void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, const char *ctrlName, void *parentRef) { + (void)ctx; + BasFormT *form = (BasFormT *)formRef; + + if (!form) { + return NULL; + } + + const char *wgtTypeName = resolveTypeName(typeName); + + if (!wgtTypeName) { + return NULL; + } + + // Determine parent widget: if parentRef is a control, use its widget; + // otherwise fall back to the form's content box. + WidgetT *parent = NULL; + + if (parentRef) { + BasControlT *parentCtrl = (BasControlT *)parentRef; + parent = parentCtrl->widget; + } + + if (!parent) { + parent = form->contentBox ? form->contentBox : form->root; + } + + if (!parent) { + return NULL; + } + + WidgetT *widget = createWidget(wgtTypeName, parent); + + if (!widget) { + return NULL; + } + + wgtSetName(widget, ctrlName); + + BasControlT *ctrl = (BasControlT *)calloc(1, sizeof(BasControlT)); + + if (!ctrl) { + return NULL; + } + + ctrl->index = -1; + snprintf(ctrl->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + ctrl->widget = widget; + ctrl->form = form; + ctrl->iface = wgtGetIface(wgtTypeName); + arrput(form->controls, ctrl); + + + widget->userData = ctrl; + widget->onClick = onWidgetClick; + widget->onDblClick = onWidgetDblClick; + widget->onChange = onWidgetChange; + widget->onFocus = onWidgetFocus; + widget->onBlur = onWidgetBlur; + widget->onValidate = onWidgetValidate; + + return ctrl; +} + + +// ============================================================ +// basFormRtCreateForm -- create a form programmatically +// ============================================================ + +void *basFormRtCreateForm(void *ctx, const char *formName, int32_t width, int32_t height) { + BasFormRtT *rt = (BasFormRtT *)ctx; + + if (!rt || !formName) { + return NULL; + } + + // Check if form already exists + for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { + if (strcasecmp(rt->forms[i]->name, formName) == 0) { + return rt->forms[i]; + } + } + + if (width <= 0) { + width = DEFAULT_FORM_W; + } + + if (height <= 0) { + height = DEFAULT_FORM_H; + } + + WidgetT *root; + WidgetT *contentBox; + WindowT *win = basFormRtCreateFormWindow(rt->ctx, formName, "VBox", true, true, false, width, height, 0, 0, &root, &contentBox); + + if (!win) { + return NULL; + } + + BasFormT *form = (BasFormT *)calloc(1, sizeof(BasFormT)); + arrput(rt->forms, form); + + snprintf(form->name, BAS_MAX_CTRL_NAME, "%s", formName); + snprintf(form->frmLayout, sizeof(form->frmLayout), "VBox"); + form->frmWidth = width; + form->frmHeight = height; + form->frmResizable = true; + form->frmCentered = true; + win->onClose = onFormClose; + win->onResize = onFormResize; + win->onFocus = onFormActivate; + win->onBlur = onFormDeactivate; + form->window = win; + form->root = root; + form->contentBox = contentBox; + form->ctx = rt->ctx; + form->vm = rt->vm; + form->module = rt->module; + + // Synthetic control for form-level property access + memset(&form->formCtrl, 0, sizeof(form->formCtrl)); + snprintf(form->formCtrl.name, BAS_MAX_CTRL_NAME, "%s", formName); + form->formCtrl.widget = root; + form->formCtrl.form = form; + + // Allocate per-form variable storage from module metadata + if (rt->module && rt->module->formVarInfo) { + for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) { + if (strcasecmp(rt->module->formVarInfo[j].formName, formName) == 0) { + int32_t vc = rt->module->formVarInfo[j].varCount; + + if (vc > 0) { + form->formVars = (BasValueT *)calloc(vc, sizeof(BasValueT)); + form->formVarCount = vc; + } + + int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr; + + if (initAddr >= 0 && rt->vm) { + basVmSetCurrentForm(rt->vm, form); + basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount); + basVmCallSub(rt->vm, initAddr); + basVmSetCurrentForm(rt->vm, NULL); + basVmSetCurrentFormVars(rt->vm, NULL, 0); + } + + break; + } + } + } + + return form; +} + + +// ============================================================ +// basFormRtRemoveCtrl -- remove a control from a form +// ============================================================ + +void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName) { + (void)ctx; + BasFormT *form = (BasFormT *)formRef; + + if (!form || !ctrlName) { + return; + } + + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { + if (strcasecmp(form->controls[i]->name, ctrlName) == 0) { + BasControlT *ctrl = form->controls[i]; + + // Destroy the widget + if (ctrl->widget) { + wgtDestroy(ctrl->widget); + } + + free(ctrl); + arrdel(form->controls, i); + + return; + } + } +} + + +// ============================================================ +// basFormRtSetEvent -- override event handler for a control +// ============================================================ + +void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName) { + (void)ctx; + BasControlT *ctrl = (BasControlT *)ctrlRef; + + if (!ctrl || !eventName || !handlerName) { + return; + } + + // Check for existing override on this event and update it + for (int32_t i = 0; i < ctrl->eventOverrideCount; i++) { + if (strcasecmp(ctrl->eventOverrides[i].eventName, eventName) == 0) { + snprintf(ctrl->eventOverrides[i].handlerName, BAS_MAX_CTRL_NAME, "%s", handlerName); + return; + } + } + + // Add new override + if (ctrl->eventOverrideCount < BAS_MAX_EVENT_OVERRIDES) { + BasEventOverrideT *ov = &ctrl->eventOverrides[ctrl->eventOverrideCount++]; + snprintf(ov->eventName, BAS_MAX_CTRL_NAME, "%s", eventName); + snprintf(ov->handlerName, BAS_MAX_CTRL_NAME, "%s", handlerName); + } +} + + // ============================================================ // basFormRtDestroy // ============================================================ @@ -327,7 +787,7 @@ void basFormRtDestroy(BasFormRtT *rt) { sFormRt = NULL; } - for (int32_t i = 0; i < rt->formCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { BasFormT *form = rt->forms[i]; if (form->formVars) { @@ -338,7 +798,7 @@ void basFormRtDestroy(BasFormRtT *rt) { free(form->formVars); } - for (int32_t j = 0; j < form->controlCount; j++) { + for (int32_t j = 0; j < (int32_t)arrlen(form->controls); j++) { free(form->controls[j]); } @@ -388,7 +848,7 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) { } // Search controls on the current form - for (int32_t i = 0; i < form->controlCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { if (strcasecmp(form->controls[i]->name, ctrlName) == 0) { return form->controls[i]; } @@ -411,7 +871,7 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) { // Search other loaded forms by name (for cross-form property access) if (rt) { - for (int32_t i = 0; i < rt->formCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { if (strcasecmp(rt->forms[i]->name, ctrlName) == 0) { return &rt->forms[i]->formCtrl; } @@ -434,7 +894,7 @@ void *basFormRtFindCtrlIdx(void *ctx, void *formRef, const char *ctrlName, int32 return NULL; } - for (int32_t i = 0; i < form->controlCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { if (form->controls[i]->index == index && strcasecmp(form->controls[i]->name, ctrlName) == 0) { return form->controls[i]; } @@ -703,6 +1163,120 @@ static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char * } +// ============================================================ +// basFormRtRegisterFrm -- cache .frm source text for lazy loading +// ============================================================ + +void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *source, int32_t sourceLen) { + if (!rt || !formName || !source || sourceLen <= 0) { + return; + } + + BasFrmCacheT entry; + snprintf(entry.formName, BAS_MAX_CTRL_NAME, "%s", formName); + entry.frmSource = (char *)malloc(sourceLen + 1); + entry.frmSourceLen = sourceLen; + memcpy(entry.frmSource, source, sourceLen); + entry.frmSource[sourceLen] = '\0'; + arrput(rt->frmCache, entry); + rt->frmCacheCount = (int32_t)arrlen(rt->frmCache); +} + + +// ============================================================ +// basFormRtRegisterCfm -- cache compiled form binary for lazy loading +// ============================================================ + +void basFormRtRegisterCfm(BasFormRtT *rt, const char *formName, const uint8_t *data, int32_t dataLen) { + if (!rt || !formName || !data || dataLen <= 0) { + return; + } + + BasCfmCacheT entry; + snprintf(entry.formName, BAS_MAX_CTRL_NAME, "%s", formName); + entry.data = (uint8_t *)malloc(dataLen); + entry.dataLen = dataLen; + memcpy(entry.data, data, dataLen); + arrput(rt->cfmCache, entry); + rt->cfmCacheCount = (int32_t)arrlen(rt->cfmCache); +} + + +// ============================================================ +// basFormRtLoadCfm -- load form from compiled binary via formcfm +// ============================================================ + +static void *basFormRtLoadCfm(BasFormRtT *rt, const uint8_t *data, int32_t dataLen) { + dvxLog("basFormRtLoadCfm: loading compiled form (%ld bytes)", (long)dataLen); + BasFormT *form = basFormLoadCompiled(rt, data, dataLen); + + if (!form || !form->window) { + dvxLog("basFormRtLoadCfm: basFormLoadCompiled failed (form=%p)", (void *)form); + return form; + } + + dvxLog("basFormRtLoadCfm: form '%s' created, %ld controls", form->name, (long)arrlen(form->controls)); + + // Set window callbacks (same as basFormRtLoadForm) + form->window->onClose = onFormClose; + form->window->onResize = onFormResize; + form->window->onFocus = onFormActivate; + form->window->onBlur = onFormDeactivate; + + // Wire event callbacks on all controls + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { + BasControlT *ctrl = form->controls[i]; + + if (ctrl->widget) { + ctrl->widget->onClick = onWidgetClick; + ctrl->widget->onDblClick = onWidgetDblClick; + ctrl->widget->onChange = onWidgetChange; + ctrl->widget->onFocus = onWidgetFocus; + ctrl->widget->onBlur = onWidgetBlur; + ctrl->widget->onValidate = onWidgetValidate; + ctrl->widget->onKeyPress = onWidgetKeyPress; + ctrl->widget->onKeyDown = onWidgetKeyDown; + ctrl->widget->onKeyUp = onWidgetKeyUp; + ctrl->widget->onMouseDown = onWidgetMouseDown; + ctrl->widget->onMouseUp = onWidgetMouseUp; + ctrl->widget->onMouseMove = onWidgetMouseMove; + ctrl->widget->onScroll = onWidgetScroll; + } + } + + // Allocate per-form variable storage + if (rt->module && rt->module->formVarInfo) { + for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) { + if (strcasecmp(rt->module->formVarInfo[j].formName, form->name) == 0) { + int32_t vc = rt->module->formVarInfo[j].varCount; + + if (vc > 0) { + form->formVars = (BasValueT *)calloc(vc, sizeof(BasValueT)); + form->formVarCount = vc; + } + + int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr; + + if (initAddr >= 0 && rt->vm) { + basVmSetCurrentForm(rt->vm, form); + basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount); + basVmCallSub(rt->vm, initAddr); + basVmSetCurrentForm(rt->vm, NULL); + basVmSetCurrentFormVars(rt->vm, NULL, 0); + } + + break; + } + } + } + + // Fire Form_Load event + basFormRtFireEvent(rt, form, form->name, "Load"); + + return form; +} + + // ============================================================ // basFormRtLoadForm // ============================================================ @@ -711,7 +1285,7 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { BasFormRtT *rt = (BasFormRtT *)ctx; // Check if form already exists - for (int32_t i = 0; i < rt->formCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { if (strcasecmp(rt->forms[i]->name, formName) == 0) { return rt->forms[i]; } @@ -720,15 +1294,31 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { // Check the .frm cache for reload after unload for (int32_t i = 0; i < rt->frmCacheCount; i++) { if (strcasecmp(rt->frmCache[i].formName, formName) == 0) { - // Re-parse the cached .frm source (creates window, controls, fires Load) - return basFormRtLoadFrm(rt, rt->frmCache[i].frmSource, rt->frmCache[i].frmSourceLen); + // Save source and remove from cache BEFORE calling basFormRtLoadFrm, + // because basFormRtLoadFrm internally calls basFormRtLoadForm (for the + // "Begin Form" line) which would find this cache entry again and recurse. + char *src = rt->frmCache[i].frmSource; + int32_t srcLen = rt->frmCache[i].frmSourceLen; + arrdel(rt->frmCache, i); + rt->frmCacheCount = (int32_t)arrlen(rt->frmCache); + + BasFormT *form = basFormRtLoadFrm(rt, src, srcLen); + free(src); + return form; + } + } + + // Check the compiled form cache (standalone apps) + for (int32_t i = 0; i < rt->cfmCacheCount; i++) { + if (strcasecmp(rt->cfmCache[i].formName, formName) == 0) { + return basFormRtLoadCfm(rt, rt->cfmCache[i].data, rt->cfmCache[i].dataLen); } } // No cache entry — create a bare form (first-time load without .frm file) WidgetT *root; WidgetT *bareContentBox; - WindowT *win = dsgnCreateFormWindow(rt->ctx, formName, "VBox", false, true, false, DEFAULT_FORM_W, DEFAULT_FORM_H, 0, 0, &root, &bareContentBox); + WindowT *win = basFormRtCreateFormWindow(rt->ctx, formName, "VBox", false, true, false, DEFAULT_FORM_W, DEFAULT_FORM_H, 0, 0, &root, &bareContentBox); if (!win) { return NULL; @@ -736,7 +1326,7 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { BasFormT *form = (BasFormT *)calloc(1, sizeof(BasFormT)); arrput(rt->forms, form); - rt->formCount = (int32_t)arrlen(rt->forms); + snprintf(form->name, BAS_MAX_CTRL_NAME, "%s", formName); snprintf(form->frmLayout, sizeof(form->frmLayout), "VBox"); win->onClose = onFormClose; @@ -799,11 +1389,11 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen BasFormT *form = NULL; BasControlT *current = NULL; - WidgetT *parentStack[DSGN_MAX_FRM_NESTING]; + WidgetT *parentStack[BAS_MAX_FRM_NESTING]; int32_t nestDepth = 0; // Track Begin/End blocks: true = container (Form/Frame), false = control - bool isContainer[DSGN_MAX_FRM_NESTING]; + bool isContainer[BAS_MAX_FRM_NESTING]; int32_t blockDepth = 0; // Temporary menu item accumulation @@ -914,7 +1504,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen parentStack[0] = form->contentBox; current = NULL; - if (blockDepth < DSGN_MAX_FRM_NESTING) { + if (blockDepth < BAS_MAX_FRM_NESTING) { isContainer[blockDepth++] = true; } } else if (strcasecmp(typeName, "Menu") == 0 && form) { @@ -929,7 +1519,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen menuNestDepth++; inMenu = true; - if (blockDepth < DSGN_MAX_FRM_NESTING) { + if (blockDepth < BAS_MAX_FRM_NESTING) { isContainer[blockDepth++] = false; } @@ -937,7 +1527,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen } else if (form && nestDepth > 0) { // Create the content box on first control if not yet done if (!form->contentBox && form->root) { - form->contentBox = dsgnCreateContentBox(form->root, form->frmLayout); + form->contentBox = basFormRtCreateContentBox(form->root, form->frmLayout); parentStack[0] = form->contentBox; } WidgetT *parent = parentStack[nestDepth - 1]; @@ -970,7 +1560,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen ctrlEntry->form = form; ctrlEntry->iface = wgtGetIface(wgtTypeName); arrput(form->controls, ctrlEntry); - form->controlCount = (int32_t)arrlen(form->controls); + current = ctrlEntry; widget->userData = current; @@ -993,17 +1583,17 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen const WgtIfaceT *ctrlIface = wgtGetIface(wgtTypeName); bool isCtrlContainer = ctrlIface && ctrlIface->isContainer; - if (isCtrlContainer && nestDepth < DSGN_MAX_FRM_NESTING) { + if (isCtrlContainer && nestDepth < BAS_MAX_FRM_NESTING) { // Push the widget for now; the Layout property hasn't // been parsed yet. It will be applied when we see it // via containerLayout[] below. parentStack[nestDepth++] = widget; - if (blockDepth < DSGN_MAX_FRM_NESTING) { + if (blockDepth < BAS_MAX_FRM_NESTING) { isContainer[blockDepth++] = true; } } else { - if (blockDepth < DSGN_MAX_FRM_NESTING) { + if (blockDepth < BAS_MAX_FRM_NESTING) { isContainer[blockDepth++] = false; } } @@ -1096,7 +1686,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen int32_t tlen = (int32_t)strlen(text); if (tlen > 0 && text[tlen - 1] == '"') { text[tlen - 1] = '\0'; } if (strcasecmp(text, "VBox") != 0) { - parentStack[nestDepth - 1] = dsgnCreateContentBox(current->widget, text); + parentStack[nestDepth - 1] = basFormRtCreateContentBox(current->widget, text); } continue; } @@ -1159,7 +1749,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen if (form) { // Ensure content box exists even if form has no controls if (!form->contentBox && form->root) { - form->contentBox = dsgnCreateContentBox(form->root, form->frmLayout); + form->contentBox = basFormRtCreateContentBox(form->root, form->frmLayout); } // Build menu bar from accumulated menu items @@ -1224,7 +1814,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen // internal bitmap matches the layout size. Width/Height aren't // known until properties are parsed, so this must happen after // loading but before dvxFitWindow. - for (int32_t i = 0; i < form->controlCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { if (!form->controls[i]->widget) { continue; } @@ -1301,7 +1891,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen // Auto-refresh Data controls that have no MasterSource (masters/standalone). // Detail controls are refreshed automatically by the master-detail cascade // when the master's Reposition event fires. - for (int32_t i = 0; i < form->controlCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { BasControlT *dc = form->controls[i]; if (strcasecmp(dc->typeName, "Data") != 0 || !dc->widget) { @@ -1552,13 +2142,12 @@ void basFormRtUnloadForm(void *ctx, void *formRef) { form->formVarCount = 0; } - for (int32_t i = 0; i < form->controlCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { free(form->controls[i]); } arrfree(form->controls); - form->controls = NULL; - form->controlCount = 0; + form->controls = NULL; if (form->window) { if (rt->ctx->modalWindow == form->window) { @@ -1573,22 +2162,17 @@ void basFormRtUnloadForm(void *ctx, void *formRef) { int32_t idx = -1; - for (int32_t i = 0; i < rt->formCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { if (rt->forms[i] == form) { idx = i; break; } } - if (idx >= 0 && idx < rt->formCount) { + if (idx >= 0 && idx < (int32_t)arrlen(rt->forms)) { free(form); - rt->formCount--; - - if (idx < rt->formCount) { - rt->forms[idx] = rt->forms[rt->formCount]; - } - - rt->forms[rt->formCount] = NULL; + arrdel(rt->forms, idx); + } } @@ -1625,7 +2209,7 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas // For INT_INT types (e.g. PictureBox), hintW/hintH override the // default createArgs if non-zero (so the bitmap matches the .frm size). -static WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent) { +WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent) { const void *api = wgtGetApi(wgtTypeName); if (!api) { @@ -1805,7 +2389,7 @@ static void onFormMenu(WindowT *win, int32_t menuId) { return; } - for (int32_t i = 0; i < sFormRt->formCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) { BasFormT *form = sFormRt->forms[i]; if (form->window == win) { @@ -1834,7 +2418,7 @@ static void onFormClose(WindowT *win) { return; } - for (int32_t i = 0; i < sFormRt->formCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) { BasFormT *form = sFormRt->forms[i]; if (form->window == win) { @@ -1857,13 +2441,12 @@ static void onFormClose(WindowT *win) { } // Free control resources - for (int32_t j = 0; j < form->controlCount; j++) { + for (int32_t j = 0; j < (int32_t)arrlen(form->controls); j++) { free(form->controls[j]); } arrfree(form->controls); - form->controls = NULL; - form->controlCount = 0; + form->controls = NULL; // Destroy the window if (sFormRt->ctx->modalWindow == win) { @@ -1874,16 +2457,10 @@ static void onFormClose(WindowT *win) { // Remove from form list and free the form free(form); - sFormRt->formCount--; - - if (i < sFormRt->formCount) { - sFormRt->forms[i] = sFormRt->forms[sFormRt->formCount]; - } - - sFormRt->forms[sFormRt->formCount] = NULL; + arrdel(sFormRt->forms, i); // If no forms left, stop the VM - if (sFormRt->formCount == 0 && sFormRt->vm) { + if (arrlen(sFormRt->forms) == 0 && sFormRt->vm) { sFormRt->vm->running = false; } @@ -1905,7 +2482,7 @@ static void onFormResize(WindowT *win, int32_t newW, int32_t newH) { return; } - for (int32_t i = 0; i < sFormRt->formCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) { BasFormT *form = sFormRt->forms[i]; if (form->window == win) { @@ -1925,7 +2502,7 @@ static void onFormActivate(WindowT *win) { return; } - for (int32_t i = 0; i < sFormRt->formCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) { BasFormT *form = sFormRt->forms[i]; if (form->window == win) { @@ -1941,7 +2518,7 @@ static void onFormDeactivate(WindowT *win) { return; } - for (int32_t i = 0; i < sFormRt->formCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) { BasFormT *form = sFormRt->forms[i]; if (form->window == win) { @@ -1959,18 +2536,62 @@ static void onFormDeactivate(WindowT *win) { #define MAX_FIRE_ARGS 8 static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount) { + // Build final argument list (prepend Index for control arrays) + BasValueT allArgs[MAX_FIRE_ARGS]; + const BasValueT *finalArgs = args; + int32_t finalArgCount = argCount; + if (ctrl->index >= 0) { - // Control array element: prepend Index as first argument - BasValueT allArgs[MAX_FIRE_ARGS]; allArgs[0] = basValLong(ctrl->index); for (int32_t i = 0; i < argCount && i < MAX_FIRE_ARGS - 1; i++) { allArgs[i + 1] = args[i]; } - basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, eventName, allArgs, argCount + 1); - } else if (argCount > 0 && args) { - basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, eventName, args, argCount); + finalArgs = allArgs; + finalArgCount = argCount + 1; + } + + // Check for event override (SetEvent) + for (int32_t i = 0; i < ctrl->eventOverrideCount; i++) { + if (strcasecmp(ctrl->eventOverrides[i].eventName, eventName) != 0) { + continue; + } + + // Override found: call the named SUB directly + if (!rt->vm || !rt->module) { + return; + } + + const BasProcEntryT *proc = basModuleFindProc(rt->module, ctrl->eventOverrides[i].handlerName); + + if (!proc || proc->isFunction) { + return; + } + + BasFormT *prevForm = rt->currentForm; + BasValueT *prevVars = rt->vm->currentFormVars; + int32_t prevVarCount = rt->vm->currentFormVarCount; + + rt->currentForm = ctrl->form; + basVmSetCurrentForm(rt->vm, ctrl->form); + basVmSetCurrentFormVars(rt->vm, ctrl->form->formVars, ctrl->form->formVarCount); + + if (finalArgCount > 0 && finalArgs) { + basVmCallSubWithArgs(rt->vm, proc->codeAddr, finalArgs, finalArgCount); + } else { + basVmCallSub(rt->vm, proc->codeAddr); + } + + rt->currentForm = prevForm; + basVmSetCurrentForm(rt->vm, prevForm); + basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount); + return; + } + + // No override -- fall back to naming convention (CtrlName_EventName) + if (finalArgCount > 0 && finalArgs) { + basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, eventName, finalArgs, finalArgCount); } else { basFormRtFireEvent(rt, ctrl->form, ctrl->name, eventName); } @@ -2047,7 +2668,7 @@ static void onWidgetBlur(WidgetT *w) { // Write-back: if this control is data-bound, update the Data control's // cache and persist to the database if (ctrl->dataSource[0] && ctrl->dataField[0] && ctrl->widget && ctrl->form) { - for (int32_t i = 0; i < ctrl->form->controlCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(ctrl->form->controls); i++) { BasControlT *dc = ctrl->form->controls[i]; if (strcasecmp(dc->typeName, "Data") == 0 && @@ -2073,7 +2694,7 @@ static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl) { return; } - for (int32_t i = 0; i < form->controlCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { BasControlT *ctrl = form->controls[i]; if (strcasecmp(ctrl->typeName, "Data") != 0 || !ctrl->widget || !ctrl->iface) { @@ -2125,7 +2746,7 @@ static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl) { return; } - for (int32_t i = 0; i < form->controlCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { BasControlT *ctrl = form->controls[i]; if (!ctrl->dataSource[0] || strcasecmp(ctrl->dataSource, dataCtrl->name) != 0) { @@ -2712,3 +3333,954 @@ int32_t basPromptSave(const char *title) { } +// ============================================================ +// basExternResolve / basExternCall +// +// Shared extern call callbacks for DECLARE LIBRARY functions. +// Used by both the IDE and the standalone stub. +// ============================================================ + +void *basExternResolve(void *ctx, const char *libName, const char *funcName) { + (void)ctx; + (void)libName; + + char mangledName[256]; + snprintf(mangledName, sizeof(mangledName), "_%s", funcName); + + return dlsym(NULL, mangledName); +} + + +BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType) { + (void)ctx; + (void)libName; + (void)funcName; + + uint32_t nativeArgs[32]; + char *tempStrings[16]; + int32_t tempStringCount = 0; + int32_t nativeCount = 0; + + for (int32_t i = 0; i < argc && i < 16; i++) { + switch (args[i].type) { + case BAS_TYPE_STRING: { + BasStringT *s = basValFormatString(args[i]); + char *cstr = strdup(s->data); + basStringUnref(s); + nativeArgs[nativeCount++] = (uint32_t)(uintptr_t)cstr; + tempStrings[tempStringCount++] = cstr; + break; + } + + case BAS_TYPE_DOUBLE: + case BAS_TYPE_SINGLE: { + union { double d; uint32_t u[2]; } conv; + conv.d = args[i].dblVal; + nativeArgs[nativeCount++] = conv.u[0]; + nativeArgs[nativeCount++] = conv.u[1]; + break; + } + + default: { + nativeArgs[nativeCount++] = (uint32_t)(int32_t)basValToNumber(args[i]); + break; + } + } + } + + BasValueT result; + memset(&result, 0, sizeof(result)); + + if (retType == BAS_TYPE_DOUBLE || retType == BAS_TYPE_SINGLE) { + double dblResult = 0.0; + + __asm__ __volatile__( + "movl %%esp, %%ebx\n\t" + ::: "ebx" + ); + + for (int32_t i = nativeCount - 1; i >= 0; i--) { + uint32_t val = nativeArgs[i]; + __asm__ __volatile__("pushl %0" :: "r"(val)); + } + + __asm__ __volatile__( + "call *%1\n\t" + "fstpl %0\n\t" + "movl %%ebx, %%esp\n\t" + : "=m"(dblResult) + : "r"(funcPtr) + : "eax", "ecx", "edx", "memory" + ); + + result = basValDouble(dblResult); + } else if (retType == BAS_TYPE_STRING) { + uint32_t rawResult = 0; + + __asm__ __volatile__( + "movl %%esp, %%ebx\n\t" + ::: "ebx" + ); + + for (int32_t i = nativeCount - 1; i >= 0; i--) { + uint32_t val = nativeArgs[i]; + __asm__ __volatile__("pushl %0" :: "r"(val)); + } + + __asm__ __volatile__( + "call *%1\n\t" + "movl %%ebx, %%esp\n\t" + : "=a"(rawResult) + : "r"(funcPtr) + : "ecx", "edx", "memory" + ); + + const char *str = (const char *)(uintptr_t)rawResult; + result = basValStringFromC(str ? str : ""); + } else { + uint32_t rawResult = 0; + + __asm__ __volatile__( + "movl %%esp, %%ebx\n\t" + ::: "ebx" + ); + + for (int32_t i = nativeCount - 1; i >= 0; i--) { + uint32_t val = nativeArgs[i]; + __asm__ __volatile__("pushl %0" :: "r"(val)); + } + + __asm__ __volatile__( + "call *%1\n\t" + "movl %%ebx, %%esp\n\t" + : "=a"(rawResult) + : "r"(funcPtr) + : "ecx", "edx", "memory" + ); + + result = basValLong((int32_t)rawResult); + } + + for (int32_t i = 0; i < tempStringCount; i++) { + free(tempStrings[i]); + } + + return result; +} + + +// ============================================================ +// SQL extern wrappers (DECLARE LIBRARY "basrt") +// +// These bridge from the extern call convention to the +// BasSqlCallbacksT callbacks on the VM. +// ============================================================ + +int32_t SQLOpen(const char *path) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlOpen) { + return 0; + } + + return sFormRt->vm->sql.sqlOpen(path); +} + + +void SQLClose(int32_t db) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlClose) { + return; + } + + sFormRt->vm->sql.sqlClose(db); +} + + +int32_t SQLExec(int32_t db, const char *sql) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlExec) { + return 0; + } + + return sFormRt->vm->sql.sqlExec(db, sql) ? -1 : 0; +} + + +const char *SQLError(int32_t db) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlError) { + return ""; + } + + const char *err = sFormRt->vm->sql.sqlError(db); + return err ? err : ""; +} + + +int32_t SQLQuery(int32_t db, const char *sql) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlQuery) { + return 0; + } + + return sFormRt->vm->sql.sqlQuery(db, sql); +} + + +int32_t SQLNext(int32_t rs) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlNext) { + return 0; + } + + return sFormRt->vm->sql.sqlNext(rs) ? -1 : 0; +} + + +int32_t SQLEof(int32_t rs) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlEof) { + return -1; + } + + return sFormRt->vm->sql.sqlEof(rs) ? -1 : 0; +} + + +int32_t SQLFieldCount(int32_t rs) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlFieldCount) { + return 0; + } + + return sFormRt->vm->sql.sqlFieldCount(rs); +} + + +const char *SQLFieldName(int32_t rs, int32_t col) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlFieldName) { + return ""; + } + + const char *name = sFormRt->vm->sql.sqlFieldName(rs, col); + return name ? name : ""; +} + + +const char *SQLFieldText(int32_t rs, int32_t col) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlFieldText) { + return ""; + } + + const char *text = sFormRt->vm->sql.sqlFieldText(rs, col); + return text ? text : ""; +} + + +const char *SQLField(int32_t rs, const char *colName) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlFieldByName) { + return ""; + } + + const char *text = sFormRt->vm->sql.sqlFieldByName(rs, colName); + return text ? text : ""; +} + + +int32_t SQLFieldInt(int32_t rs, int32_t col) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlFieldInt) { + return 0; + } + + return sFormRt->vm->sql.sqlFieldInt(rs, col); +} + + +double SQLFieldDbl(int32_t rs, int32_t col) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlFieldDbl) { + return 0.0; + } + + return sFormRt->vm->sql.sqlFieldDbl(rs, col); +} + + +void SQLFreeResult(int32_t rs) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlFreeResult) { + return; + } + + sFormRt->vm->sql.sqlFreeResult(rs); +} + + +int32_t SQLAffected(int32_t db) { + if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlAffectedRows) { + return 0; + } + + return sFormRt->vm->sql.sqlAffectedRows(db); +} + + +// ============================================================ +// Serial communication extern wrappers (DECLARE LIBRARY "basrt") +// +// Two tiers: +// SerXxx -- raw UART I/O via rs232 library +// CommXxx -- packetized + optional encryption via secLink +// +// All functions resolved lazily via dlsym so the serial library +// is only required when comm functions are actually called. +// ============================================================ + + +// ============================================================ +// Raw serial API (rs232 function pointers) +// ============================================================ + +typedef struct { + int (*open)(int, int32_t, int, char, int, int); + int (*close)(int); + int (*read)(int, char *, int); + int (*write)(int, const char *, int); + int (*writeBuf)(int, const char *, int); + int (*getRxBuffered)(int); + int (*clearRxBuffer)(int); + int (*getUartType)(int); + int (*getBase)(int); + int (*getIrq)(int); + int (*setBase)(int, int); + int (*setIrq)(int, int); +} SerApiT; + +static SerApiT sSerApi; +static bool sSerApiResolved = false; + +// Per-COM raw serial state for terminal attach +#define SER_MAX_PORTS 4 +#define SER_RECV_BUF 4096 + +typedef struct { + bool attached; + WidgetT *term; + int32_t com; // 0-based COM port index +} SerAttachT; + +static SerAttachT sSerAttach[SER_MAX_PORTS]; + + +static bool serResolveApi(void) { + if (sSerApiResolved) { + return sSerApi.open != NULL; + } + + sSerApiResolved = true; + sSerApi.open = dlsym(NULL, "_rs232Open"); + sSerApi.close = dlsym(NULL, "_rs232Close"); + sSerApi.read = dlsym(NULL, "_rs232Read"); + sSerApi.write = dlsym(NULL, "_rs232Write"); + sSerApi.writeBuf = dlsym(NULL, "_rs232WriteBuf"); + sSerApi.getRxBuffered = dlsym(NULL, "_rs232GetRxBuffered"); + sSerApi.clearRxBuffer = dlsym(NULL, "_rs232ClearRxBuffer"); + sSerApi.getUartType = dlsym(NULL, "_rs232GetUartType"); + sSerApi.getBase = dlsym(NULL, "_rs232GetBase"); + sSerApi.getIrq = dlsym(NULL, "_rs232GetIrq"); + sSerApi.setBase = dlsym(NULL, "_rs232SetBase"); + sSerApi.setIrq = dlsym(NULL, "_rs232SetIrq"); + + return sSerApi.open != NULL; +} + + +int32_t SerGetUart(int32_t com) { + if (!serResolveApi() || !sSerApi.getUartType) { + return 0; + } + + return sSerApi.getUartType(com - 1); +} + + +void SerSetBase(int32_t com, int32_t base) { + if (!serResolveApi() || !sSerApi.setBase) { + return; + } + + sSerApi.setBase(com - 1, base); +} + + +void SerSetIrq(int32_t com, int32_t irq) { + if (!serResolveApi() || !sSerApi.setIrq) { + return; + } + + sSerApi.setIrq(com - 1, irq); +} + + +int32_t SerGetBase(int32_t com) { + if (!serResolveApi() || !sSerApi.getBase) { + return 0; + } + + return sSerApi.getBase(com - 1); +} + + +int32_t SerGetIrq(int32_t com) { + if (!serResolveApi() || !sSerApi.getIrq) { + return 0; + } + + return sSerApi.getIrq(com - 1); +} + + +int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake) { + if (!serResolveApi()) { + return 0; + } + + char parityChar = (parity && parity[0]) ? parity[0] : 'N'; + int rc = sSerApi.open(com - 1, baud, dataBits, parityChar, stopBits, handshake); + return (rc == 0) ? -1 : 0; +} + + +void SerClose(int32_t com) { + if (!serResolveApi()) { + return; + } + + // Detach terminal if attached + int32_t idx = com - 1; + + if (idx >= 0 && idx < SER_MAX_PORTS && sSerAttach[idx].attached) { + wgtAnsiTermSetComm(sSerAttach[idx].term, NULL, NULL, NULL); + sSerAttach[idx].attached = false; + sSerAttach[idx].term = NULL; + } + + sSerApi.close(com - 1); +} + + +int32_t SerWrite(int32_t com, const char *data) { + if (!serResolveApi() || !data) { + return 0; + } + + int32_t len = (int32_t)strlen(data); + + if (len == 0) { + return -1; + } + + int rc = sSerApi.writeBuf(com - 1, data, len); + return (rc >= 0) ? -1 : 0; +} + + +const char *SerRead(int32_t com) { + if (!serResolveApi()) { + return ""; + } + + static char buf[SER_RECV_BUF]; + int32_t n = sSerApi.read(com - 1, buf, (int)(sizeof(buf) - 1)); + + if (n <= 0) { + return ""; + } + + buf[n] = '\0'; + return buf; +} + + +int32_t SerAvailable(int32_t com) { + if (!serResolveApi() || !sSerApi.getRxBuffered) { + return 0; + } + + return sSerApi.getRxBuffered(com - 1); +} + + +void SerFlush(int32_t com) { + if (!serResolveApi() || !sSerApi.clearRxBuffer) { + return; + } + + sSerApi.clearRxBuffer(com - 1); +} + + +// Raw serial terminal callbacks +static int32_t serTermRead(void *ctx, uint8_t *buf, int32_t maxLen) { + SerAttachT *sa = (SerAttachT *)ctx; + + if (!sSerApi.read) { + return 0; + } + + return sSerApi.read(sa->com, (char *)buf, maxLen); +} + + +static int32_t serTermWrite(void *ctx, const uint8_t *data, int32_t len) { + SerAttachT *sa = (SerAttachT *)ctx; + + if (!sSerApi.writeBuf) { + return 0; + } + + int rc = sSerApi.writeBuf(sa->com, (const char *)data, len); + return (rc >= 0) ? len : 0; +} + + +// Idle callback for raw serial: polls all attached ports +static void serIdlePoll(void *ctx) { + (void)ctx; + + for (int32_t i = 0; i < SER_MAX_PORTS; i++) { + if (sSerAttach[i].attached && sSerAttach[i].term) { + wgtAnsiTermPoll(sSerAttach[i].term); + } + } +} + + +void SerAttach(int32_t com, const char *termCtrlName) { + if (!serResolveApi() || !sFormRt || !termCtrlName) { + return; + } + + int32_t idx = com - 1; + + if (idx < 0 || idx >= SER_MAX_PORTS) { + return; + } + + // Find the terminal widget by control name + WidgetT *termWidget = NULL; + + for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) { + BasFormT *form = sFormRt->forms[i]; + + for (int32_t j = 0; j < (int32_t)arrlen(form->controls); j++) { + if (strcasecmp(form->controls[j]->name, termCtrlName) == 0) { + termWidget = form->controls[j]->widget; + break; + } + } + + if (termWidget) { + break; + } + } + + if (!termWidget) { + return; + } + + sSerAttach[idx].com = idx; + sSerAttach[idx].term = termWidget; + sSerAttach[idx].attached = true; + + wgtAnsiTermSetComm(termWidget, &sSerAttach[idx], serTermRead, serTermWrite); + + if (sFormRt->ctx) { + sFormRt->ctx->idleCallback = serIdlePoll; + sFormRt->ctx->idleCtx = NULL; + } +} + + +void SerDetach(int32_t com) { + int32_t idx = com - 1; + + if (idx < 0 || idx >= SER_MAX_PORTS || !sSerAttach[idx].attached) { + return; + } + + wgtAnsiTermSetComm(sSerAttach[idx].term, NULL, NULL, NULL); + sSerAttach[idx].attached = false; + sSerAttach[idx].term = NULL; +} + + +// ============================================================ +// Packet/encrypted serial API (secLink function pointers) +// ============================================================ + +#define COMM_MAX_LINKS 4 +#define COMM_NUM_CHANNELS 128 +#define COMM_CHAN_BUF_SIZE 4096 + +// SecLink function pointers (resolved lazily) +typedef struct { + void *(*open)(int, int32_t, int, char, int, int, void (*)(void *, const uint8_t *, int, uint8_t), void *); + void (*close)(void *); + int (*handshake)(void *); + bool (*isReady)(void *); + int (*poll)(void *); + int (*send)(void *, const uint8_t *, int, uint8_t, bool, bool); + int (*sendBuf)(void *, const uint8_t *, int, uint8_t, bool); + int (*getPending)(void *); +} CommSecLinkApiT; + +static CommSecLinkApiT sCommApi; +static bool sCommApiResolved = false; + +// Per-channel receive ring buffer +typedef struct { + uint8_t data[COMM_CHAN_BUF_SIZE]; + int32_t head; + int32_t tail; +} CommChanBufT; + +typedef struct { + bool active; + void *link; // SecLinkT* + CommChanBufT channels[COMM_NUM_CHANNELS]; // per-channel receive buffers + WidgetT *attachedTerm; // AnsiTerm widget if attached + int32_t termChannel; // channel for terminal I/O + bool termEncrypt; // encrypt terminal traffic +} CommSlotT; + +static CommSlotT sCommSlots[COMM_MAX_LINKS]; + + +static bool commResolveApi(void) { + if (sCommApiResolved) { + return sCommApi.open != NULL; + } + + sCommApiResolved = true; + sCommApi.open = dlsym(NULL, "_secLinkOpen"); + sCommApi.close = dlsym(NULL, "_secLinkClose"); + sCommApi.handshake = dlsym(NULL, "_secLinkHandshake"); + sCommApi.isReady = dlsym(NULL, "_secLinkIsReady"); + sCommApi.poll = dlsym(NULL, "_secLinkPoll"); + sCommApi.send = dlsym(NULL, "_secLinkSend"); + sCommApi.sendBuf = dlsym(NULL, "_secLinkSendBuf"); + sCommApi.getPending = dlsym(NULL, "_secLinkGetPending"); + + return sCommApi.open != NULL; +} + + +static CommSlotT *commGetSlot(int32_t handle) { + if (handle < 1 || handle > COMM_MAX_LINKS) { + return NULL; + } + + CommSlotT *slot = &sCommSlots[handle - 1]; + return slot->active ? slot : NULL; +} + + +// SecLink receive callback: routes data into per-channel ring buffers +static void commOnRecv(void *ctx, const uint8_t *data, int len, uint8_t channel) { + CommSlotT *slot = (CommSlotT *)ctx; + CommChanBufT *cb = &slot->channels[channel & 0x7F]; + + for (int i = 0; i < len; i++) { + int32_t next = (cb->head + 1) % COMM_CHAN_BUF_SIZE; + + if (next == cb->tail) { + break; + } + + cb->data[cb->head] = data[i]; + cb->head = next; + } +} + + +// AnsiTerm read callback: drains the terminal channel's ring buffer +static int32_t commTermRead(void *ctx, uint8_t *buf, int32_t maxLen) { + CommSlotT *slot = (CommSlotT *)ctx; + CommChanBufT *cb = &slot->channels[slot->termChannel]; + int32_t count = 0; + + while (count < maxLen && cb->tail != cb->head) { + buf[count++] = cb->data[cb->tail]; + cb->tail = (cb->tail + 1) % COMM_CHAN_BUF_SIZE; + } + + return count; +} + + +// AnsiTerm write callback: sends keystrokes on the terminal channel +static int32_t commTermWrite(void *ctx, const uint8_t *data, int32_t len) { + CommSlotT *slot = (CommSlotT *)ctx; + + if (!slot->link || !sCommApi.send) { + return 0; + } + + int rc = sCommApi.send(slot->link, data, len, (uint8_t)slot->termChannel, slot->termEncrypt, false); + return (rc == 0) ? len : 0; +} + + +// Idle callback: polls all active secLink connections +static void commIdlePoll(void *ctx) { + (void)ctx; + + for (int32_t i = 0; i < COMM_MAX_LINKS; i++) { + if (sCommSlots[i].active && sCommSlots[i].link && sCommApi.poll) { + sCommApi.poll(sCommSlots[i].link); + } + } +} + + +int32_t CommOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake) { + if (!commResolveApi()) { + return 0; + } + + // Find free slot + int32_t handle = 0; + + for (int32_t i = 0; i < COMM_MAX_LINKS; i++) { + if (!sCommSlots[i].active) { + handle = i + 1; + break; + } + } + + if (handle == 0) { + return 0; + } + + char parityChar = 'N'; + + if (parity && parity[0]) { + parityChar = parity[0]; + } + + CommSlotT *slot = &sCommSlots[handle - 1]; + memset(slot, 0, sizeof(CommSlotT)); + + slot->link = sCommApi.open(com, baud, dataBits, parityChar, stopBits, handshake, commOnRecv, slot); + + if (!slot->link) { + return 0; + } + + slot->active = true; + return handle; +} + + +void CommClose(int32_t handle) { + CommSlotT *slot = commGetSlot(handle); + + if (!slot) { + return; + } + + if (slot->attachedTerm) { + wgtAnsiTermSetComm(slot->attachedTerm, NULL, NULL, NULL); + slot->attachedTerm = NULL; + } + + if (sCommApi.close && slot->link) { + sCommApi.close(slot->link); + } + + slot->link = NULL; + slot->active = false; +} + + +int32_t CommHandshake(int32_t handle) { + CommSlotT *slot = commGetSlot(handle); + + if (!slot || !sCommApi.handshake) { + return 0; + } + + return (sCommApi.handshake(slot->link) == 0) ? -1 : 0; +} + + +int32_t CommIsReady(int32_t handle) { + CommSlotT *slot = commGetSlot(handle); + + if (!slot || !sCommApi.isReady) { + return 0; + } + + return sCommApi.isReady(slot->link) ? -1 : 0; +} + + +int32_t CommSend(int32_t handle, const char *data, int32_t channel, int32_t encrypt) { + CommSlotT *slot = commGetSlot(handle); + + if (!slot || !data || !sCommApi.sendBuf) { + return 0; + } + + int32_t len = (int32_t)strlen(data); + + if (len == 0) { + return -1; + } + + int rc = sCommApi.sendBuf(slot->link, (const uint8_t *)data, len, (uint8_t)channel, encrypt != 0); + return (rc == 0) ? -1 : 0; +} + + +const char *CommRecv(int32_t handle, int32_t channel) { + CommSlotT *slot = commGetSlot(handle); + + if (!slot || channel < 0 || channel >= COMM_NUM_CHANNELS) { + return ""; + } + + CommChanBufT *cb = &slot->channels[channel]; + static char buf[COMM_CHAN_BUF_SIZE]; + int32_t count = 0; + + while (count < (int32_t)sizeof(buf) - 1 && cb->tail != cb->head) { + buf[count++] = cb->data[cb->tail]; + cb->tail = (cb->tail + 1) % COMM_CHAN_BUF_SIZE; + } + + buf[count] = '\0'; + return buf; +} + + +int32_t CommPoll(int32_t handle) { + CommSlotT *slot = commGetSlot(handle); + + if (!slot || !sCommApi.poll) { + return 0; + } + + return sCommApi.poll(slot->link); +} + + +int32_t CommPending(int32_t handle) { + CommSlotT *slot = commGetSlot(handle); + + if (!slot || !sCommApi.getPending) { + return 0; + } + + return sCommApi.getPending(slot->link); +} + + +void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt) { + CommSlotT *slot = commGetSlot(handle); + + if (!slot || !sFormRt || !termCtrlName) { + return; + } + + // Find the terminal widget by control name on any loaded form + WidgetT *termWidget = NULL; + + for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) { + BasFormT *form = sFormRt->forms[i]; + + for (int32_t j = 0; j < (int32_t)arrlen(form->controls); j++) { + if (strcasecmp(form->controls[j]->name, termCtrlName) == 0) { + termWidget = form->controls[j]->widget; + break; + } + } + + if (termWidget) { + break; + } + } + + if (!termWidget) { + return; + } + + slot->termChannel = channel; + slot->termEncrypt = (encrypt != 0); + + wgtAnsiTermSetComm(termWidget, slot, commTermRead, commTermWrite); + slot->attachedTerm = termWidget; + + // Set idle callback for automatic polling + if (sFormRt->ctx) { + sFormRt->ctx->idleCallback = commIdlePoll; + sFormRt->ctx->idleCtx = NULL; + } +} + + +void CommDetach(int32_t handle) { + CommSlotT *slot = commGetSlot(handle); + + if (!slot || !slot->attachedTerm) { + return; + } + + wgtAnsiTermSetComm(slot->attachedTerm, NULL, NULL, NULL); + slot->attachedTerm = NULL; +} + + +// ============================================================ +// Help system extern wrappers (DECLARE LIBRARY "basrt") +// ============================================================ + +// hlpcCompile function pointer (resolved lazily) +typedef int32_t (*HlpcCompileFnT)(const char **, int32_t, const char *, const char *, const char *, int32_t, void *, void *); + +static HlpcCompileFnT sHlpcCompile = NULL; +static bool sHlpcResolved = false; + +// shellLoadAppWithArgs function pointer (resolved lazily) +typedef int32_t (*ShellLoadAppFnT)(void *, const char *, const char *); + +static ShellLoadAppFnT sShellLoadApp = NULL; +static bool sShellLoadResolved = false; + + +int32_t HelpCompile(const char *inputFile, const char *outputFile) { + if (!sHlpcResolved) { + sHlpcResolved = true; + sHlpcCompile = dlsym(NULL, "_hlpcCompile"); + } + + if (!sHlpcCompile || !inputFile || !outputFile) { + return 0; + } + + const char *inputs[1]; + inputs[0] = inputFile; + + int32_t rc = sHlpcCompile(inputs, 1, outputFile, NULL, NULL, 1, NULL, NULL); + return (rc == 0) ? -1 : 0; +} + + +void HelpView(const char *hlpFile) { + if (!sShellLoadResolved) { + sShellLoadResolved = true; + sShellLoadApp = dlsym(NULL, "_shellLoadAppWithArgs"); + } + + if (!sShellLoadApp || !sFormRt || !sFormRt->ctx || !hlpFile) { + return; + } + + char viewerPath[260]; + snprintf(viewerPath, sizeof(viewerPath), "APPS%cKPUNCH%cDVXHELP%cDVXHELP.APP", DVX_PATH_SEP, DVX_PATH_SEP, DVX_PATH_SEP); + + sShellLoadApp(sFormRt->ctx, viewerPath, hlpFile); +} + + diff --git a/apps/dvxbasic/formrt/formrt.h b/apps/dvxbasic/formrt/formrt.h index 837dce3..e9c248d 100644 --- a/apps/dvxbasic/formrt/formrt.h +++ b/apps/dvxbasic/formrt/formrt.h @@ -26,7 +26,8 @@ typedef struct BasControlT BasControlT; // Limits // ============================================================ -#define BAS_MAX_CTRL_NAME 32 +#define BAS_MAX_CTRL_NAME 32 +#define BAS_MAX_FRM_NESTING 16 // ============================================================ // Menu ID to name mapping for event dispatch @@ -42,7 +43,14 @@ typedef struct { // Control instance (a widget on a form) // ============================================================ -#define BAS_MAX_TEXT_BUF 256 +#define BAS_MAX_TEXT_BUF 256 +#define BAS_MAX_EVENT_OVERRIDES 16 + +// Event handler override (SetEvent) +typedef struct { + char eventName[BAS_MAX_CTRL_NAME]; // e.g. "Click" + char handlerName[BAS_MAX_CTRL_NAME]; // e.g. "HandleOkClick" +} BasEventOverrideT; typedef struct BasControlT { char name[BAS_MAX_CTRL_NAME]; // VB control name (e.g. "Command1") @@ -56,6 +64,8 @@ typedef struct BasControlT { char dataField[BAS_MAX_CTRL_NAME]; // column name for binding char helpTopic[BAS_MAX_CTRL_NAME]; // help topic ID for F1 int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls) + BasEventOverrideT eventOverrides[BAS_MAX_EVENT_OVERRIDES]; + int32_t eventOverrideCount; } BasControlT; // ============================================================ @@ -69,7 +79,6 @@ typedef struct BasFormT { WidgetT *contentBox; // VBox/HBox for user controls AppContextT *ctx; // DVX app context BasControlT **controls; // stb_ds array of heap-allocated pointers - int32_t controlCount; BasVmT *vm; // VM for event dispatch BasModuleT *module; // compiled module (for SUB lookup) // Form-level properties (accumulated during .frm parsing) @@ -105,16 +114,24 @@ typedef struct { int32_t frmSourceLen; } BasFrmCacheT; +// Cached compiled form binary (for standalone apps) +typedef struct { + char formName[BAS_MAX_CTRL_NAME]; + uint8_t *data; // malloc'd binary data + int32_t dataLen; +} BasCfmCacheT; + typedef struct { AppContextT *ctx; // DVX app context BasVmT *vm; // shared VM instance BasModuleT *module; // compiled module BasFormT **forms; // stb_ds array of heap-allocated pointers - int32_t formCount; BasFormT *currentForm; // form currently dispatching events char helpFile[256]; // project help file path (for F1) BasFrmCacheT *frmCache; // stb_ds array of cached .frm sources int32_t frmCacheCount; + BasCfmCacheT *cfmCache; // stb_ds array of compiled form binaries + int32_t cfmCacheCount; } BasFormRtT; // ============================================================ @@ -124,6 +141,18 @@ typedef struct { // Initialize the form runtime with a DVX context and a compiled module. BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module); +// Load all cached forms (.frm text and compiled binaries) and show the +// startup form. Called before bytecode execution begins. +void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName); + +// VB-style event loop: pump DVX events until all forms are closed. +// Called after bytecode halts to keep the app alive for event handlers. +void basFormRtEventLoop(BasFormRtT *rt); + +// Simple non-debug execution: run bytecode in slices with dvxUpdate +// between slices, then enter the event loop. Used by the standalone stub. +void basFormRtRunSimple(BasFormRtT *rt); + // Destroy the form runtime and all forms/controls. void basFormRtDestroy(BasFormRtT *rt); @@ -144,6 +173,35 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal); void basFormRtHideForm(void *ctx, void *formRef); int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags); +// ---- Extern call callbacks (shared by IDE and stub) ---- + +void *basExternResolve(void *ctx, const char *libName, const char *funcName); +BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType); + +// ---- Form caching ---- + +// Register .frm source text for lazy loading when bytecode calls Load. +void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *source, int32_t sourceLen); + +// Register a compiled form binary for lazy loading when bytecode calls Load. +void basFormRtRegisterCfm(BasFormRtT *rt, const char *formName, const uint8_t *data, int32_t dataLen); + +// ---- Widget creation ---- + +WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent); + +// ---- Form window creation ---- + +WidgetT *basFormRtCreateContentBox(WidgetT *root, const char *layout); +WindowT *basFormRtCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox); + +// ---- Dynamic form/control API ---- + +void *basFormRtCreateForm(void *ctx, const char *formName, int32_t width, int32_t height); +void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, const char *ctrlName, void *parentRef); +void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName); +void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName); + // ---- Event dispatch ---- bool basFormRtFireEvent(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName); diff --git a/apps/dvxbasic/ide/ideDesigner.c b/apps/dvxbasic/ide/ideDesigner.c index 9b5053d..cf6ed9a 100644 --- a/apps/dvxbasic/ide/ideDesigner.c +++ b/apps/dvxbasic/ide/ideDesigner.c @@ -5,6 +5,7 @@ // handles and grid dots are drawn in the window's onPaint callback. #include "ideDesigner.h" +#include "../formrt/formrt.h" #include "dvxDraw.h" #include "dvxVideo.h" #include "dvxWm.h" @@ -116,80 +117,17 @@ WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) { // ============================================================ -// dsgnCreateFormWindow +// dsgnCreateFormWindow / dsgnCreateContentBox +// Thin wrappers around formrt functions for backward compatibility. // ============================================================ WidgetT *dsgnCreateContentBox(WidgetT *root, const char *layout) { - // wgtInitWindow creates a VBox root. If the requested layout is VBox - // (or empty/missing), reuse root directly to avoid double-nesting. - if (!layout || !layout[0] || strcasecmp(layout, "VBox") == 0) { - return root; - } - - // Look up the layout widget by BASIC name and create it dynamically - const char *wgtName = wgtFindByBasName(layout); - - if (wgtName) { - const WgtIfaceT *iface = wgtGetIface(wgtName); - - if (iface && iface->isContainer && iface->createSig == WGT_CREATE_PARENT) { - const void *api = wgtGetApi(wgtName); - - if (api) { - // All WGT_CREATE_PARENT APIs have create(parent) as the first function pointer - WidgetT *(*createFn)(WidgetT *) = *(WidgetT *(*const *)(WidgetT *))api; - WidgetT *box = createFn(root); - box->weight = 100; - return box; - } - } - } - - // Unknown layout — fall back to root VBox - return root; + return basFormRtCreateContentBox(root, layout); } WindowT *dsgnCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox) { - int32_t defW = (width > 0) ? width : 400; - int32_t defH = (height > 0) ? height : 300; - - WindowT *win = dvxCreateWindowCentered(ctx, title, defW, defH, resizable); - - if (!win) { - return NULL; - } - - win->visible = false; - - WidgetT *root = wgtInitWindow(ctx, win); - - if (!root) { - dvxDestroyWindow(ctx, win); - return NULL; - } - - WidgetT *contentBox = dsgnCreateContentBox(root, layout); - - // Apply sizing - if (autoSize) { - dvxFitWindow(ctx, win); - } else if (width > 0 && height > 0) { - dvxResizeWindow(ctx, win, width, height); - } - - // Apply positioning - if (centered) { - win->x = (ctx->display.width - win->w) / 2; - win->y = (ctx->display.height - win->h) / 2; - } else if (left > 0 || top > 0) { - win->x = left; - win->y = top; - } - - *outRoot = root; - *outContentBox = contentBox; - return win; + return basFormRtCreateFormWindow(ctx, title, layout, resizable, centered, autoSize, width, height, left, top, outRoot, outContentBox); } @@ -513,7 +451,7 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { bool inMenu = false; int32_t menuNestDepth = 0; int32_t blockDepth = 0; // Begin/End nesting depth (0 = form level) - bool blockIsContainer[DSGN_MAX_FRM_NESTING]; // whether each block is a container + bool blockIsContainer[BAS_MAX_FRM_NESTING]; // whether each block is a container // Parent name stack for nesting (index 0 = form level) char parentStack[8][DSGN_MAX_NAME]; @@ -614,7 +552,7 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { curCtrl = NULL; // not a control menuNestDepth++; inMenu = true; - if (blockDepth < DSGN_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = false; } + if (blockDepth < BAS_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = false; } blockDepth++; } else if (inForm) { DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT)); @@ -632,7 +570,7 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { arrput(form->controls, cp); curCtrl = form->controls[arrlen(form->controls) - 1]; bool isCtrl = dsgnIsContainer(typeName); - if (blockDepth < DSGN_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = isCtrl; } + if (blockDepth < BAS_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = isCtrl; } blockDepth++; // If this is a container, push onto parent stack @@ -659,7 +597,7 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { } } else { // If we're closing a container, pop the parent stack - if (blockDepth < DSGN_MAX_FRM_NESTING && blockIsContainer[blockDepth] && nestDepth > 0) { + if (blockDepth < BAS_MAX_FRM_NESTING && blockIsContainer[blockDepth] && nestDepth > 0) { nestDepth--; } diff --git a/apps/dvxbasic/ide/ideDesigner.h b/apps/dvxbasic/ide/ideDesigner.h index 84417f7..ca49de6 100644 --- a/apps/dvxbasic/ide/ideDesigner.h +++ b/apps/dvxbasic/ide/ideDesigner.h @@ -10,6 +10,7 @@ #include "dvxApp.h" #include "dvxWgt.h" #include "canvas/canvas.h" +#include "../formrt/formrt.h" #include "stb_ds_wrap.h" @@ -23,7 +24,6 @@ #define DSGN_MAX_NAME 32 #define DSGN_MAX_TEXT 256 #define DSGN_MAX_PROPS 32 -#define DSGN_MAX_FRM_NESTING 16 #define DSGN_GRID_SIZE 8 #define DSGN_HANDLE_SIZE 6 #define DSGN_MENU_ID_BASE 20000 // base ID for designer preview menu items diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index 90aabca..d7d2717 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -41,7 +41,11 @@ #include "ideProperties.h" #include "../compiler/parser.h" +#include "../compiler/strip.h" +#include "../runtime/serialize.h" #include "../formrt/formrt.h" +#include "../formrt/formcfm.h" +#include "dvxRes.h" #include "../../sql/dvxSql.h" #include "../runtime/vm.h" #include "../runtime/values.h" @@ -116,6 +120,7 @@ #define CMD_DEBUG_LAYOUT 156 #define CMD_OUTPUT_TO_LOG 159 #define CMD_RECENT_BASE 160 // 160-167 reserved for recent files +#define CMD_MAKE_EXE 170 #define CMD_RECENT_MAX 8 #define CMD_HELP_CONTENTS 157 #define CMD_HELP_API 158 @@ -147,6 +152,7 @@ int32_t appMain(DxeAppContextT *ctx); static void buildWindow(void); static void clearOutput(void); static int cmpStrPtrs(const void *a, const void *b); +static bool compileProject(void); static void compileAndRun(void); static void debugStartOrResume(int32_t cmd); static void toggleBreakpoint(void); @@ -190,6 +196,7 @@ static bool findInProject(const char *needle, bool caseSensitive); static void openFindDialog(bool showReplace); static void handleEditCmd(int32_t cmd); static void handleFileCmd(int32_t cmd); +static void makeExecutable(void); static void handleProjectCmd(int32_t cmd); static void handleRunCmd(int32_t cmd); static void handleViewCmd(int32_t cmd); @@ -226,8 +233,6 @@ static void onObjDropdownChange(WidgetT *w); static void printCallback(void *ctx, const char *text, bool newline); static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize); static bool doEventsCallback(void *ctx); -static void *resolveExternCallback(void *ctx, const char *libName, const char *funcName); -static BasValueT callExternCallback(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType); static void runCached(void); static void runModule(BasModuleT *mod); static void onEditorChange(WidgetT *w); @@ -909,6 +914,8 @@ static void buildWindow(void) { wmAddMenuItem(fileMenu, "&Save File\tCtrl+S", CMD_SAVE); wmAddMenuItem(fileMenu, "Save A&ll", CMD_SAVE_ALL); wmAddMenuSeparator(fileMenu); + wmAddMenuItem(fileMenu, "&Make Executable...", CMD_MAKE_EXE); + wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "&Remove File", CMD_PRJ_REMOVE); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT); @@ -1706,10 +1713,10 @@ static int cmpEvtPtrs(const void *a, const void *b) { // ============================================================ -// compileAndRun +// compileProject -- compile without running, returns true on success // ============================================================ -static void compileAndRun(void) { +static bool compileProject(void) { // Save all dirty files before compiling if Save on Run is enabled if (sWin && sWin->menuBar && wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN)) { if (sProject.activeFileIdx >= 0) { @@ -1759,7 +1766,7 @@ static void compileAndRun(void) { if (!concatBuf) { setStatus("Out of memory."); dvxSetBusy(sAc, false); - return; + return false; } int32_t pos = 0; @@ -1920,9 +1927,9 @@ static void compileAndRun(void) { src = getFullSource(); if (!src || *src == '\0') { - setStatus("No source code to run."); + setStatus("No source code to compile."); dvxSetBusy(sAc, false); - return; + return false; } srcLen = (int32_t)strlen(src); @@ -1935,7 +1942,7 @@ static void compileAndRun(void) { free(concatBuf); setStatus("Out of memory."); dvxSetBusy(sAc, false); - return; + return false; } basParserInit(parser, src, srcLen); @@ -2007,7 +2014,7 @@ static void compileAndRun(void) { basParserFree(parser); free(parser); free(concatBuf); - return; + return false; } free(concatBuf); @@ -2019,7 +2026,7 @@ static void compileAndRun(void) { if (!mod) { setStatus("Failed to build module."); dvxSetBusy(sAc, false); - return; + return false; } dvxSetBusy(sAc, false); @@ -2034,7 +2041,18 @@ static void compileAndRun(void) { // Update Object/Event dropdowns updateDropdowns(); - runModule(mod); + return true; +} + + +// ============================================================ +// compileAndRun +// ============================================================ + +static void compileAndRun(void) { + if (compileProject()) { + runModule(sCachedModule); + } } @@ -2190,8 +2208,8 @@ static void runModule(BasModuleT *mod) { // Set extern library callbacks (DECLARE LIBRARY support) BasExternCallbacksT extCb; - extCb.resolveExtern = resolveExternCallback; - extCb.callExtern = callExternCallback; + extCb.resolveExtern = basExternResolve; + extCb.callExtern = basExternCall; extCb.ctx = NULL; basVmSetExternCallbacks(vm, &extCb); @@ -2221,24 +2239,10 @@ static void runModule(BasModuleT *mod) { } } - // Load forms (AFTER breakpoints are set so init code breakpoints fire) + // Load forms from project files (AFTER breakpoints are set so + // init code breakpoints fire), then show the startup form. loadFrmFiles(formRt); - - // Auto-show the startup form (or first form if none specified). - if (formRt->formCount > 0) { - BasFormT *startupForm = formRt->forms[0]; - - if (sProject.startupForm[0]) { - for (int32_t i = 0; i < formRt->formCount; i++) { - if (strcasecmp(formRt->forms[i]->name, sProject.startupForm) == 0) { - startupForm = formRt->forms[i]; - break; - } - } - } - - basFormRtShowForm(formRt, startupForm, false); - } + basFormRtLoadAllForms(formRt, sProject.startupForm); // Run in slices of 10000 steps, yielding to DVX between slices basVmSetStepLimit(vm, IDE_STEP_SLICE); @@ -2308,11 +2312,11 @@ static void runModule(BasModuleT *mod) { // VB-style event loop: after module-level code finishes, // keep processing events as long as any form is loaded. // The program ends when all forms are unloaded (closed). - if (result == BAS_VM_HALTED && formRt->formCount > 0) { + if (result == BAS_VM_HALTED && (int32_t)arrlen(formRt->forms) > 0) { setStatus("Running (event loop)..."); sStopRequested = false; - while (sWin && sAc->running && formRt->formCount > 0 && !sStopRequested && !vm->ended) { + while (sWin && sAc->running && (int32_t)arrlen(formRt->forms) > 0 && !sStopRequested && !vm->ended) { if (sDbgState == DBG_PAUSED) { // Paused inside an event handler @@ -3943,6 +3947,192 @@ static void onClose(WindowT *win) { // onMenu // ============================================================ +// ============================================================ +// makeExecutable -- compile project into standalone .app file +// ============================================================ + +static void makeExecutable(void) { + if (sProject.projectPath[0] == '\0') { + setStatus("Save the project first."); + return; + } + + // Compile (always recompile to ensure latest code) + if (!compileProject()) { + return; + } + + // Ask for output path + FileFilterT filters[] = { + { "DVX Applications (*.app)", "*.app" }, + { "All Files (*.*)", "*.*" } + }; + char outPath[DVX_MAX_PATH]; + outPath[0] = '\0'; + + if (!dvxFileDialog(sAc, "Make Executable", FD_SAVE, NULL, filters, 2, outPath, sizeof(outPath))) { + return; + } + + // Ask debug or release + const char *modeItems[] = { "Debug (include error info)", "Release (stripped)" }; + int32_t modeChoice = 0; + + if (!dvxChoiceDialog(sAc, "Build Mode", "Select build mode:", modeItems, 2, 0, &modeChoice)) { + return; + } + + bool release = (modeChoice == 1); + + setStatus(release ? "Building release executable..." : "Building debug executable..."); + dvxSetBusy(sAc, true); + dvxUpdate(sAc); + + // Make a copy of the module for potential stripping + int32_t modLen = 0; + uint8_t *modData = basModuleSerialize(sCachedModule, &modLen); + + if (!modData) { + setStatus("Failed to serialize module."); + dvxSetBusy(sAc, false); + return; + } + + // For release, deserialize, strip, re-serialize + if (release) { + BasModuleT *modCopy = basModuleDeserialize(modData, modLen); + free(modData); + + if (!modCopy) { + setStatus("Failed to prepare release module."); + dvxSetBusy(sAc, false); + return; + } + + basStripModule(modCopy); + modData = basModuleSerialize(modCopy, &modLen); + basModuleFree(modCopy); + + if (!modData) { + setStatus("Failed to serialize stripped module."); + dvxSetBusy(sAc, false); + return; + } + } + + // Serialize debug info from the original (unstripped) module + int32_t dbgLen = 0; + uint8_t *dbgData = NULL; + + if (!release) { + dbgData = basDebugSerialize(sCachedModule, &dbgLen); + } + + // Extract stub from our own resources + DvxResHandleT *selfRes = dvxResOpen(sCtx->appPath); + + if (!selfRes) { + setStatus("Cannot read IDE resources."); + free(modData); + free(dbgData); + dvxSetBusy(sAc, false); + return; + } + + uint32_t stubSize = 0; + void *stubData = dvxResRead(selfRes, "STUB", &stubSize); + dvxResClose(selfRes); + + if (!stubData || stubSize == 0) { + setStatus("Stub not found in IDE resources."); + free(modData); + free(dbgData); + dvxSetBusy(sAc, false); + return; + } + + // Write stub to output file + FILE *outFile = fopen(outPath, "wb"); + + if (!outFile) { + setStatus("Cannot create output file."); + free(stubData); + free(modData); + free(dbgData); + dvxSetBusy(sAc, false); + return; + } + + fwrite(stubData, 1, stubSize, outFile); + fclose(outFile); + free(stubData); + + // Attach app name from project properties + const char *projName = sProject.name[0] ? sProject.name : "BASIC App"; + dvxResAppend(outPath, "APPNAME", DVX_RES_TEXT, projName, (uint32_t)strlen(projName) + 1); + + // Attach MODULE resource + dvxResAppend(outPath, "MODULE", DVX_RES_BINARY, modData, (uint32_t)modLen); + free(modData); + + // Attach DEBUG resource + if (dbgData) { + dvxResAppend(outPath, "DEBUG", DVX_RES_BINARY, dbgData, (uint32_t)dbgLen); + free(dbgData); + } + + // Compile and attach form resources + int32_t formIdx = 0; + + for (int32_t i = 0; i < sProject.fileCount; i++) { + if (!sProject.files[i].isForm) { + continue; + } + + char *frmSrc = NULL; + + if (sProject.files[i].buffer) { + frmSrc = strdup(sProject.files[i].buffer); + } else { + char fp[DVX_MAX_PATH]; + prjFullPath(&sProject, i, fp, sizeof(fp)); + FILE *ff = fopen(fp, "r"); + + if (ff) { + fseek(ff, 0, SEEK_END); + long sz = ftell(ff); + fseek(ff, 0, SEEK_SET); + frmSrc = (char *)malloc(sz + 1); + + if (frmSrc) { + size_t rd = fread(frmSrc, 1, sz, ff); + frmSrc[rd] = '\0'; + } + + fclose(ff); + } + } + + if (!frmSrc) { + continue; + } + + int32_t frmLen = (int32_t)strlen(frmSrc); + char resName[16]; + snprintf(resName, sizeof(resName), "FORM%ld", (long)formIdx); + dvxResAppend(outPath, resName, DVX_RES_BINARY, frmSrc, (uint32_t)frmLen); + free(frmSrc); + formIdx++; + } + + dvxSetBusy(sAc, false); + + char msg[512]; + snprintf(msg, sizeof(msg), "Created %s (%s)", outPath, release ? "release" : "debug"); + setStatus(msg); +} + + static void handleFileCmd(int32_t cmd) { switch (cmd) { case CMD_OPEN: @@ -3984,6 +4174,10 @@ static void handleFileCmd(int32_t cmd) { updateDirtyIndicators(); break; + case CMD_MAKE_EXE: + makeExecutable(); + break; + case CMD_EXIT: if (sWin) { onClose(sWin); @@ -5196,7 +5390,7 @@ static void helpQueryHandler(void *ctx) { // Running BASIC program: check if focused window belongs to a form if (sDbgFormRt && sDbgFormRt->helpFile[0] && focusWin) { - for (int32_t i = 0; i < sDbgFormRt->formCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(sDbgFormRt->forms); i++) { BasFormT *form = sDbgFormRt->forms[i]; if (form->window == focusWin) { @@ -5926,163 +6120,6 @@ static void printCallback(void *ctx, const char *text, bool newline) { } } -// ============================================================ -// Extern library callbacks (DECLARE LIBRARY support) -// ============================================================ - -// Resolve a native function by library and symbol name. -// Library name is ignored (all DXE exports are globally visible via -// RTLD_GLOBAL), but could be used to dlopen a specific library in -// the future. The underscore prefix is added automatically to match -// DJGPP's cdecl name mangling. -static void *resolveExternCallback(void *ctx, const char *libName, const char *funcName) { - (void)ctx; - (void)libName; - - // DJGPP adds underscore prefix to C symbols - char mangledName[256]; - snprintf(mangledName, sizeof(mangledName), "_%s", funcName); - - return dlsym(NULL, mangledName); -} - - -// Marshal a call from BASIC to a native C function. -// Converts BasValueT arguments to C types, calls the function using -// inline assembly (cdecl: push args right-to-left, caller cleans up), -// and converts the return value back to BasValueT. -// -// Supported types: Integer (int16_t passed as int32_t), Long (int32_t), -// Single/Double (double), String (const char *), Boolean (int32_t). -static BasValueT callExternCallback(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType) { - (void)ctx; - (void)libName; - (void)funcName; - - // Convert BASIC values to native C values and collect pointers - // to temporary C strings so we can free them after the call. - uint32_t nativeArgs[32]; - char *tempStrings[16]; - int32_t tempStringCount = 0; - int32_t nativeCount = 0; - - for (int32_t i = 0; i < argc && i < 16; i++) { - switch (args[i].type) { - case BAS_TYPE_STRING: { - BasStringT *s = basValFormatString(args[i]); - char *cstr = strdup(s->data); - basStringUnref(s); - nativeArgs[nativeCount++] = (uint32_t)(uintptr_t)cstr; - tempStrings[tempStringCount++] = cstr; - break; - } - - case BAS_TYPE_DOUBLE: - case BAS_TYPE_SINGLE: { - // Doubles are passed as 8 bytes (two 32-bit pushes on i386) - union { double d; uint32_t u[2]; } conv; - conv.d = args[i].dblVal; - nativeArgs[nativeCount++] = conv.u[0]; - nativeArgs[nativeCount++] = conv.u[1]; - break; - } - - default: { - // Integer, Long, Boolean — all fit in 32 bits - nativeArgs[nativeCount++] = (uint32_t)(int32_t)basValToNumber(args[i]); - break; - } - } - } - - // Call the function using cdecl convention. - // Push args right-to-left, call, read return value from eax (or FPU for double). - BasValueT result; - memset(&result, 0, sizeof(result)); - - if (retType == BAS_TYPE_DOUBLE || retType == BAS_TYPE_SINGLE) { - // Return value in st(0) - double dblResult = 0.0; - - // Push args right-to-left and call - __asm__ __volatile__( - "movl %%esp, %%ebx\n\t" // save stack pointer - ::: "ebx" - ); - - for (int32_t i = nativeCount - 1; i >= 0; i--) { - uint32_t val = nativeArgs[i]; - __asm__ __volatile__("pushl %0" :: "r"(val)); - } - - __asm__ __volatile__( - "call *%1\n\t" - "fstpl %0\n\t" - "movl %%ebx, %%esp\n\t" // restore stack pointer - : "=m"(dblResult) - : "r"(funcPtr) - : "eax", "ecx", "edx", "memory" - ); - - result = basValDouble(dblResult); - } else if (retType == BAS_TYPE_STRING) { - // Return value is const char * in eax - uint32_t rawResult = 0; - - __asm__ __volatile__( - "movl %%esp, %%ebx\n\t" - ::: "ebx" - ); - - for (int32_t i = nativeCount - 1; i >= 0; i--) { - uint32_t val = nativeArgs[i]; - __asm__ __volatile__("pushl %0" :: "r"(val)); - } - - __asm__ __volatile__( - "call *%1\n\t" - "movl %%ebx, %%esp\n\t" - : "=a"(rawResult) - : "r"(funcPtr) - : "ecx", "edx", "memory" - ); - - const char *str = (const char *)(uintptr_t)rawResult; - result = basValStringFromC(str ? str : ""); - } else { - // Integer/Long/Boolean — return in eax - uint32_t rawResult = 0; - - __asm__ __volatile__( - "movl %%esp, %%ebx\n\t" - ::: "ebx" - ); - - for (int32_t i = nativeCount - 1; i >= 0; i--) { - uint32_t val = nativeArgs[i]; - __asm__ __volatile__("pushl %0" :: "r"(val)); - } - - __asm__ __volatile__( - "call *%1\n\t" - "movl %%ebx, %%esp\n\t" - : "=a"(rawResult) - : "r"(funcPtr) - : "ecx", "edx", "memory" - ); - - result = basValLong((int32_t)rawResult); - } - - // Free temporary strings - for (int32_t i = 0; i < tempStringCount; i++) { - free(tempStrings[i]); - } - - return result; -} - - // ============================================================ // onFormWinMouse // ============================================================ @@ -7032,7 +7069,7 @@ static void debugSetBreakTitles(bool paused) { return; } - for (int32_t i = 0; i < sDbgFormRt->formCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(sDbgFormRt->forms); i++) { BasFormT *form = sDbgFormRt->forms[i]; if (!form->window) { diff --git a/apps/dvxbasic/runtime/serialize.c b/apps/dvxbasic/runtime/serialize.c new file mode 100644 index 0000000..6432930 --- /dev/null +++ b/apps/dvxbasic/runtime/serialize.c @@ -0,0 +1,524 @@ +// serialize.c -- DVX BASIC module serialization +// +// Binary format (all little-endian, no alignment padding): +// +// Header: +// magic 4 bytes "DBXM" +// version uint16 1 +// entryPoint int32 +// globalCount int32 +// codeLen int32 +// constCount int32 +// dataCount int32 +// procCount int32 +// +// code[] codeLen bytes of raw bytecode +// +// constants[] constCount entries, each: +// len uint16 + len bytes of string data +// +// dataPool[] dataCount entries, each: +// type uint8 + value (type-dependent) +// +// procs[] procCount entries, each: +// codeAddr int32 +// paramCount int32 +// localCount int32 +// returnType uint8 +// isFunction uint8 +// nameLen uint16 + nameLen bytes + +#include "serialize.h" +#include "../compiler/opcodes.h" + +#include +#include +#include + +// ============================================================ +// Write helpers +// ============================================================ + +typedef struct { + uint8_t *buf; + int32_t len; + int32_t cap; +} BufT; + + +static void bufInit(BufT *b) { + b->cap = 4096; + b->buf = (uint8_t *)malloc(b->cap); + b->len = 0; +} + + +static void bufGrow(BufT *b, int32_t need) { + while (b->len + need > b->cap) { + b->cap *= 2; + } + + b->buf = (uint8_t *)realloc(b->buf, b->cap); +} + + +static void bufWrite(BufT *b, const void *data, int32_t len) { + bufGrow(b, len); + memcpy(b->buf + b->len, data, len); + b->len += len; +} + + +static void bufWriteU8(BufT *b, uint8_t v) { + bufWrite(b, &v, 1); +} + + +static void bufWriteU16(BufT *b, uint16_t v) { + bufWrite(b, &v, 2); +} + + +static void bufWriteI32(BufT *b, int32_t v) { + bufWrite(b, &v, 4); +} + + +static void bufWriteStr(BufT *b, const char *s) { + uint16_t len = s ? (uint16_t)strlen(s) : 0; + bufWriteU16(b, len); + + if (len > 0) { + bufWrite(b, s, len); + } +} + + +// ============================================================ +// Read helpers +// ============================================================ + +typedef struct { + const uint8_t *data; + int32_t len; + int32_t pos; +} ReaderT; + + +static bool rOk(const ReaderT *r, int32_t need) { + return r->pos + need <= r->len; +} + + +static uint8_t rU8(ReaderT *r) { + if (!rOk(r, 1)) { + return 0; + } + + return r->data[r->pos++]; +} + + +static uint16_t rU16(ReaderT *r) { + if (!rOk(r, 2)) { + return 0; + } + + uint16_t v; + memcpy(&v, r->data + r->pos, 2); + r->pos += 2; + return v; +} + + +static int32_t rI32(ReaderT *r) { + if (!rOk(r, 4)) { + return 0; + } + + int32_t v; + memcpy(&v, r->data + r->pos, 4); + r->pos += 4; + return v; +} + + +static double rF64(ReaderT *r) { + if (!rOk(r, 8)) { + return 0.0; + } + + double v; + memcpy(&v, r->data + r->pos, 8); + r->pos += 8; + return v; +} + + +// Read a length-prefixed string into a malloc'd buffer. +static char *rStr(ReaderT *r) { + uint16_t len = rU16(r); + + if (len == 0 || !rOk(r, len)) { + char *s = (char *)malloc(1); + s[0] = '\0'; + return s; + } + + char *s = (char *)malloc(len + 1); + memcpy(s, r->data + r->pos, len); + s[len] = '\0'; + r->pos += len; + return s; +} + + +// ============================================================ +// basModuleSerialize +// ============================================================ + +uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) { + if (!mod || !outLen) { + return NULL; + } + + BufT b; + bufInit(&b); + + // Header + bufWrite(&b, "DBXM", 4); + bufWriteU16(&b, 1); // version + bufWriteI32(&b, mod->entryPoint); + bufWriteI32(&b, mod->globalCount); + bufWriteI32(&b, mod->codeLen); + bufWriteI32(&b, mod->constCount); + bufWriteI32(&b, mod->dataCount); + bufWriteI32(&b, mod->procCount); + + // Code + bufWrite(&b, mod->code, mod->codeLen); + + // String constant pool + for (int32_t i = 0; i < mod->constCount; i++) { + const char *s = mod->constants[i] ? mod->constants[i]->data : ""; + bufWriteStr(&b, s); + } + + // Data pool + for (int32_t i = 0; i < mod->dataCount; i++) { + const BasValueT *v = &mod->dataPool[i]; + bufWriteU8(&b, v->type); + + switch (v->type) { + case BAS_TYPE_INTEGER: + bufWriteI32(&b, v->intVal); + break; + + case BAS_TYPE_LONG: + bufWriteI32(&b, v->longVal); + break; + + case BAS_TYPE_SINGLE: { + float f = (float)v->dblVal; + bufWrite(&b, &f, 4); + break; + } + + case BAS_TYPE_DOUBLE: + bufWrite(&b, &v->dblVal, 8); + break; + + case BAS_TYPE_STRING: + bufWriteStr(&b, v->strVal ? v->strVal->data : ""); + break; + + case BAS_TYPE_BOOLEAN: + bufWriteU8(&b, v->intVal ? 1 : 0); + break; + + default: + bufWriteI32(&b, 0); + break; + } + } + + // Procedure table + for (int32_t i = 0; i < mod->procCount; i++) { + const BasProcEntryT *p = &mod->procs[i]; + bufWriteI32(&b, p->codeAddr); + bufWriteI32(&b, p->paramCount); + bufWriteI32(&b, p->localCount); + bufWriteU8(&b, p->returnType); + bufWriteU8(&b, p->isFunction ? 1 : 0); + bufWriteStr(&b, p->name); + } + + // Form variable info (runtime-essential for per-form variable allocation) + bufWriteI32(&b, mod->formVarInfoCount); + + for (int32_t i = 0; i < mod->formVarInfoCount; i++) { + const BasFormVarInfoT *fv = &mod->formVarInfo[i]; + bufWriteStr(&b, fv->formName); + bufWriteI32(&b, fv->varCount); + bufWriteI32(&b, fv->initCodeAddr); + bufWriteI32(&b, fv->initCodeLen); + } + + *outLen = b.len; + return b.buf; +} + + +// ============================================================ +// basModuleDeserialize +// ============================================================ + +BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) { + if (!data || dataLen < 30) { + return NULL; + } + + ReaderT r = { data, dataLen, 0 }; + + // Check magic + if (data[0] != 'D' || data[1] != 'B' || data[2] != 'X' || data[3] != 'M') { + return NULL; + } + + r.pos = 4; + + uint16_t version = rU16(&r); + + if (version != 1) { + return NULL; + } + + BasModuleT *mod = (BasModuleT *)calloc(1, sizeof(BasModuleT)); + + mod->entryPoint = rI32(&r); + mod->globalCount = rI32(&r); + mod->codeLen = rI32(&r); + mod->constCount = rI32(&r); + mod->dataCount = rI32(&r); + mod->procCount = rI32(&r); + + // Code + if (!rOk(&r, mod->codeLen)) { + free(mod); + return NULL; + } + + mod->code = (uint8_t *)malloc(mod->codeLen); + memcpy(mod->code, r.data + r.pos, mod->codeLen); + r.pos += mod->codeLen; + + // String constant pool + mod->constants = (BasStringT **)calloc(mod->constCount, sizeof(BasStringT *)); + + for (int32_t i = 0; i < mod->constCount; i++) { + char *s = rStr(&r); + mod->constants[i] = basStringNew(s, (int32_t)strlen(s)); + free(s); + } + + // Data pool + if (mod->dataCount > 0) { + mod->dataPool = (BasValueT *)calloc(mod->dataCount, sizeof(BasValueT)); + + for (int32_t i = 0; i < mod->dataCount; i++) { + uint8_t type = rU8(&r); + mod->dataPool[i].type = type; + + switch (type) { + case BAS_TYPE_INTEGER: + mod->dataPool[i].intVal = (int16_t)rI32(&r); + break; + + case BAS_TYPE_LONG: + mod->dataPool[i].longVal = rI32(&r); + break; + + case BAS_TYPE_SINGLE: { + float f; + + if (rOk(&r, 4)) { + memcpy(&f, r.data + r.pos, 4); + r.pos += 4; + } else { + f = 0.0f; + } + + mod->dataPool[i].dblVal = (double)f; + break; + } + + case BAS_TYPE_DOUBLE: + mod->dataPool[i].dblVal = rF64(&r); + break; + + case BAS_TYPE_STRING: { + char *s = rStr(&r); + mod->dataPool[i].strVal = basStringNew(s, (int32_t)strlen(s)); + free(s); + break; + } + + case BAS_TYPE_BOOLEAN: + mod->dataPool[i].intVal = rU8(&r) ? -1 : 0; + break; + + default: + rI32(&r); + break; + } + } + } + + // Procedure table + if (mod->procCount > 0) { + mod->procs = (BasProcEntryT *)calloc(mod->procCount, sizeof(BasProcEntryT)); + + for (int32_t i = 0; i < mod->procCount; i++) { + BasProcEntryT *p = &mod->procs[i]; + p->codeAddr = rI32(&r); + p->paramCount = rI32(&r); + p->localCount = rI32(&r); + p->returnType = rU8(&r); + p->isFunction = rU8(&r) != 0; + + char *name = rStr(&r); + snprintf(p->name, BAS_MAX_PROC_NAME, "%s", name); + free(name); + } + } + + // Form variable info + mod->formVarInfoCount = rI32(&r); + + if (mod->formVarInfoCount > 0) { + mod->formVarInfo = (BasFormVarInfoT *)calloc(mod->formVarInfoCount, sizeof(BasFormVarInfoT)); + + for (int32_t i = 0; i < mod->formVarInfoCount; i++) { + BasFormVarInfoT *fv = &mod->formVarInfo[i]; + char *fvName = rStr(&r); + snprintf(fv->formName, BAS_MAX_PROC_NAME, "%s", fvName); + free(fvName); + fv->varCount = rI32(&r); + fv->initCodeAddr = rI32(&r); + fv->initCodeLen = rI32(&r); + } + } + + return mod; +} + + +// ============================================================ +// basModuleFree +// ============================================================ + +void basModuleFree(BasModuleT *mod) { + if (!mod) { + return; + } + + free(mod->code); + + if (mod->constants) { + for (int32_t i = 0; i < mod->constCount; i++) { + basStringUnref(mod->constants[i]); + } + + free(mod->constants); + } + + if (mod->dataPool) { + for (int32_t i = 0; i < mod->dataCount; i++) { + basValRelease(&mod->dataPool[i]); + } + + free(mod->dataPool); + } + + free(mod->procs); + free(mod->debugVars); + + if (mod->debugUdtDefs) { + for (int32_t i = 0; i < mod->debugUdtDefCount; i++) { + free(mod->debugUdtDefs[i].fields); + } + + free(mod->debugUdtDefs); + } + + if (mod->formVarInfo) { + free(mod->formVarInfo); + } + + free(mod); +} + + +// ============================================================ +// basDebugSerialize +// ============================================================ + +uint8_t *basDebugSerialize(const BasModuleT *mod, int32_t *outLen) { + if (!mod || !outLen) { + return NULL; + } + + BufT b; + bufInit(&b); + + // Debug variables + bufWriteI32(&b, mod->debugVarCount); + + for (int32_t i = 0; i < mod->debugVarCount; i++) { + const BasDebugVarT *v = &mod->debugVars[i]; + bufWriteStr(&b, v->name); + bufWriteStr(&b, v->formName); + bufWriteU8(&b, v->scope); + bufWriteU8(&b, v->dataType); + bufWriteI32(&b, v->index); + bufWriteI32(&b, v->procIndex); + } + + *outLen = b.len; + return b.buf; +} + + +// ============================================================ +// basDebugDeserialize +// ============================================================ + +void basDebugDeserialize(BasModuleT *mod, const uint8_t *data, int32_t dataLen) { + if (!mod || !data || dataLen < 4) { + return; + } + + ReaderT r = { data, dataLen, 0 }; + + // Debug variables + mod->debugVarCount = rI32(&r); + + if (mod->debugVarCount > 0) { + mod->debugVars = (BasDebugVarT *)calloc(mod->debugVarCount, sizeof(BasDebugVarT)); + + for (int32_t i = 0; i < mod->debugVarCount; i++) { + BasDebugVarT *v = &mod->debugVars[i]; + char *name = rStr(&r); + char *formName = rStr(&r); + snprintf(v->name, BAS_MAX_PROC_NAME, "%s", name); + snprintf(v->formName, BAS_MAX_PROC_NAME, "%s", formName); + free(name); + free(formName); + v->scope = rU8(&r); + v->dataType = rU8(&r); + v->index = rI32(&r); + v->procIndex = rI32(&r); + } + } + +} diff --git a/apps/dvxbasic/runtime/serialize.h b/apps/dvxbasic/runtime/serialize.h new file mode 100644 index 0000000..49261ec --- /dev/null +++ b/apps/dvxbasic/runtime/serialize.h @@ -0,0 +1,41 @@ +// serialize.h -- DVX BASIC module serialization +// +// Serialize/deserialize a compiled BasModuleT to/from a byte buffer. +// Used by "Make Executable" (serialize) and the standalone stub +// (deserialize). The format is a flat binary with no alignment padding. + +#ifndef DVXBASIC_SERIALIZE_H +#define DVXBASIC_SERIALIZE_H + +#include "vm.h" + +#include + +// ============================================================ +// Module serialization +// ============================================================ + +// Serialize a compiled module to a malloc'd byte buffer. +// Returns the buffer (caller frees) and sets *outLen to the size. +// Returns NULL on failure. +uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen); + +// Deserialize a module from a byte buffer. +// Returns a malloc'd BasModuleT (caller frees with basModuleFree). +// Returns NULL on failure. +BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen); + +// Free a deserialized module and all its allocated members. +void basModuleFree(BasModuleT *mod); + +// ============================================================ +// Debug info serialization (separate resource) +// ============================================================ + +// Serialize debug info (debugVars + formVarInfo) to a malloc'd buffer. +uint8_t *basDebugSerialize(const BasModuleT *mod, int32_t *outLen); + +// Deserialize debug info and attach to an existing module. +void basDebugDeserialize(BasModuleT *mod, const uint8_t *data, int32_t dataLen); + +#endif // DVXBASIC_SERIALIZE_H diff --git a/apps/dvxbasic/runtime/vm.c b/apps/dvxbasic/runtime/vm.c index 615a0c7..9c15e56 100644 --- a/apps/dvxbasic/runtime/vm.c +++ b/apps/dvxbasic/runtime/vm.c @@ -2977,6 +2977,36 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } + case OP_CREATE_CTRL_EX: { + // Stack: [formRef, typeStr, nameStr, parentRef] -- parent on top + BasValueT parentVal; + BasValueT nameVal; + BasValueT typeVal; + BasValueT formVal; + + if (!pop(vm, &parentVal) || !pop(vm, &nameVal) || !pop(vm, &typeVal) || !pop(vm, &formVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + void *ctrlRef = NULL; + + if (vm->ui.createCtrlEx && formVal.type == BAS_TYPE_OBJECT) { + BasValueT sv1 = basValToString(typeVal); + BasValueT sv2 = basValToString(nameVal); + void *parentRef = (parentVal.type == BAS_TYPE_OBJECT) ? parentVal.objVal : NULL; + ctrlRef = vm->ui.createCtrlEx(vm->ui.ctx, formVal.objVal, sv1.strVal->data, sv2.strVal->data, parentRef); + basValRelease(&sv1); + basValRelease(&sv2); + } + + basValRelease(&parentVal); + basValRelease(&nameVal); + basValRelease(&typeVal); + basValRelease(&formVal); + push(vm, basValObject(ctrlRef)); + break; + } + case OP_FIND_CTRL: { BasValueT nameVal; BasValueT formVal; @@ -3046,6 +3076,77 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } + case OP_CREATE_FORM: { + // Stack: [nameStr, width, height] -- height on top + BasValueT heightVal; + BasValueT widthVal; + BasValueT nameVal; + + if (!pop(vm, &heightVal) || !pop(vm, &widthVal) || !pop(vm, &nameVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + void *formRef = NULL; + + if (vm->ui.createForm) { + BasValueT sv = basValToString(nameVal); + int32_t w = (int32_t)basValToNumber(widthVal); + int32_t h = (int32_t)basValToNumber(heightVal); + formRef = vm->ui.createForm(vm->ui.ctx, sv.strVal->data, w, h); + basValRelease(&sv); + } + + basValRelease(&nameVal); + basValRelease(&widthVal); + basValRelease(&heightVal); + push(vm, basValObject(formRef)); + break; + } + + case OP_SET_EVENT: { + // Stack: [ctrlRef, eventNameStr, handlerNameStr] -- handler on top + BasValueT handlerVal; + BasValueT eventVal; + BasValueT ctrlVal; + + if (!pop(vm, &handlerVal) || !pop(vm, &eventVal) || !pop(vm, &ctrlVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (vm->ui.setEvent && ctrlVal.type == BAS_TYPE_OBJECT) { + BasValueT sv1 = basValToString(eventVal); + BasValueT sv2 = basValToString(handlerVal); + vm->ui.setEvent(vm->ui.ctx, ctrlVal.objVal, sv1.strVal->data, sv2.strVal->data); + basValRelease(&sv1); + basValRelease(&sv2); + } + + basValRelease(&handlerVal); + basValRelease(&eventVal); + basValRelease(&ctrlVal); + break; + } + + case OP_REMOVE_CTRL: { + // Stack: [formRef, ctrlNameStr] -- name on top + BasValueT nameVal; + BasValueT formVal; + + if (!pop(vm, &nameVal) || !pop(vm, &formVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (vm->ui.removeCtrl && formVal.type == BAS_TYPE_OBJECT) { + BasValueT sv = basValToString(nameVal); + vm->ui.removeCtrl(vm->ui.ctx, formVal.objVal, sv.strVal->data); + basValRelease(&sv); + } + + basValRelease(&nameVal); + basValRelease(&formVal); + break; + } + // ============================================================ // Form-level variables // ============================================================ @@ -3195,204 +3296,6 @@ BasVmResultE basVmStep(BasVmT *vm) { // SQL database operations // ============================================================ - case OP_SQL_OPEN: { - BasValueT pathVal; - if (!pop(vm, &pathVal)) { return BAS_VM_STACK_UNDERFLOW; } - int32_t handle = 0; - if (vm->sql.sqlOpen) { - BasValueT sv = basValToString(pathVal); - handle = vm->sql.sqlOpen(sv.strVal->data); - basValRelease(&sv); - } - basValRelease(&pathVal); - push(vm, basValLong(handle)); - break; - } - - case OP_SQL_CLOSE: { - BasValueT dbVal; - if (!pop(vm, &dbVal)) { return BAS_VM_STACK_UNDERFLOW; } - if (vm->sql.sqlClose) { - vm->sql.sqlClose((int32_t)basValToNumber(dbVal)); - } - basValRelease(&dbVal); - break; - } - - case OP_SQL_EXEC: { - BasValueT sqlVal; - BasValueT dbVal; - if (!pop(vm, &sqlVal) || !pop(vm, &dbVal)) { return BAS_VM_STACK_UNDERFLOW; } - bool ok = false; - if (vm->sql.sqlExec) { - BasValueT sv = basValToString(sqlVal); - ok = vm->sql.sqlExec((int32_t)basValToNumber(dbVal), sv.strVal->data); - basValRelease(&sv); - } - basValRelease(&sqlVal); - basValRelease(&dbVal); - push(vm, ok ? basValInteger(-1) : basValInteger(0)); - break; - } - - case OP_SQL_ERROR: { - BasValueT dbVal; - if (!pop(vm, &dbVal)) { return BAS_VM_STACK_UNDERFLOW; } - const char *err = ""; - if (vm->sql.sqlError) { - err = vm->sql.sqlError((int32_t)basValToNumber(dbVal)); - } - basValRelease(&dbVal); - push(vm, basValStringFromC(err ? err : "")); - break; - } - - case OP_SQL_QUERY: { - BasValueT sqlVal; - BasValueT dbVal; - if (!pop(vm, &sqlVal) || !pop(vm, &dbVal)) { return BAS_VM_STACK_UNDERFLOW; } - int32_t handle = 0; - if (vm->sql.sqlQuery) { - BasValueT sv = basValToString(sqlVal); - handle = vm->sql.sqlQuery((int32_t)basValToNumber(dbVal), sv.strVal->data); - basValRelease(&sv); - } - basValRelease(&sqlVal); - basValRelease(&dbVal); - push(vm, basValLong(handle)); - break; - } - - case OP_SQL_NEXT: { - BasValueT rsVal; - if (!pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; } - bool ok = false; - if (vm->sql.sqlNext) { - ok = vm->sql.sqlNext((int32_t)basValToNumber(rsVal)); - } - basValRelease(&rsVal); - push(vm, ok ? basValInteger(-1) : basValInteger(0)); - break; - } - - case OP_SQL_EOF: { - BasValueT rsVal; - if (!pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; } - bool eof = true; - if (vm->sql.sqlEof) { - eof = vm->sql.sqlEof((int32_t)basValToNumber(rsVal)); - } - basValRelease(&rsVal); - push(vm, eof ? basValInteger(-1) : basValInteger(0)); - break; - } - - case OP_SQL_FIELD_COUNT: { - BasValueT rsVal; - if (!pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; } - int32_t count = 0; - if (vm->sql.sqlFieldCount) { - count = vm->sql.sqlFieldCount((int32_t)basValToNumber(rsVal)); - } - basValRelease(&rsVal); - push(vm, basValLong(count)); - break; - } - - case OP_SQL_FIELD_NAME: { - BasValueT colVal; - BasValueT rsVal; - if (!pop(vm, &colVal) || !pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; } - const char *name = ""; - if (vm->sql.sqlFieldName) { - name = vm->sql.sqlFieldName((int32_t)basValToNumber(rsVal), (int32_t)basValToNumber(colVal)); - } - basValRelease(&colVal); - basValRelease(&rsVal); - push(vm, basValStringFromC(name ? name : "")); - break; - } - - case OP_SQL_FIELD_TEXT: { - BasValueT colVal; - BasValueT rsVal; - if (!pop(vm, &colVal) || !pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; } - const char *text = ""; - if (vm->sql.sqlFieldText) { - text = vm->sql.sqlFieldText((int32_t)basValToNumber(rsVal), (int32_t)basValToNumber(colVal)); - } - basValRelease(&colVal); - basValRelease(&rsVal); - push(vm, basValStringFromC(text ? text : "")); - break; - } - - case OP_SQL_FIELD_BYNAME: { - BasValueT nameVal; - BasValueT rsVal; - if (!pop(vm, &nameVal) || !pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; } - const char *text = ""; - if (vm->sql.sqlFieldByName) { - BasValueT sv = basValToString(nameVal); - text = vm->sql.sqlFieldByName((int32_t)basValToNumber(rsVal), sv.strVal->data); - basValRelease(&sv); - } - basValRelease(&nameVal); - basValRelease(&rsVal); - push(vm, basValStringFromC(text ? text : "")); - break; - } - - case OP_SQL_FIELD_INT: { - BasValueT colVal; - BasValueT rsVal; - if (!pop(vm, &colVal) || !pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; } - int32_t val = 0; - if (vm->sql.sqlFieldInt) { - val = vm->sql.sqlFieldInt((int32_t)basValToNumber(rsVal), (int32_t)basValToNumber(colVal)); - } - basValRelease(&colVal); - basValRelease(&rsVal); - push(vm, basValLong(val)); - break; - } - - case OP_SQL_FIELD_DBL: { - BasValueT colVal; - BasValueT rsVal; - if (!pop(vm, &colVal) || !pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; } - double val = 0.0; - if (vm->sql.sqlFieldDbl) { - val = vm->sql.sqlFieldDbl((int32_t)basValToNumber(rsVal), (int32_t)basValToNumber(colVal)); - } - basValRelease(&colVal); - basValRelease(&rsVal); - push(vm, basValDouble(val)); - break; - } - - case OP_SQL_FREE_RESULT: { - BasValueT rsVal; - if (!pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; } - if (vm->sql.sqlFreeResult) { - vm->sql.sqlFreeResult((int32_t)basValToNumber(rsVal)); - } - basValRelease(&rsVal); - break; - } - - case OP_SQL_AFFECTED: { - BasValueT dbVal; - if (!pop(vm, &dbVal)) { return BAS_VM_STACK_UNDERFLOW; } - int32_t rows = 0; - if (vm->sql.sqlAffectedRows) { - rows = vm->sql.sqlAffectedRows((int32_t)basValToNumber(dbVal)); - } - basValRelease(&dbVal); - push(vm, basValLong(rows)); - break; - } - case OP_LINE: { uint16_t lineNum = readUint16(vm); vm->currentLine = lineNum; diff --git a/apps/dvxbasic/runtime/vm.h b/apps/dvxbasic/runtime/vm.h index 2eb0fb3..929b5b1 100644 --- a/apps/dvxbasic/runtime/vm.h +++ b/apps/dvxbasic/runtime/vm.h @@ -129,21 +129,38 @@ typedef int32_t (*BasUiMsgBoxFnT)(void *ctx, const char *message, int32_t flags) // Display an input box. Returns the entered string (empty on cancel). typedef BasStringT *(*BasUiInputBoxFnT)(void *ctx, const char *prompt, const char *title, const char *defaultText); +// Create a form programmatically (no .frm file). Returns form reference. +typedef void *(*BasUiCreateFormFnT)(void *ctx, const char *formName, int32_t width, int32_t height); + +// Create a control with an explicit parent container. parentRef may be NULL +// to use the form's root content box. +typedef void *(*BasUiCreateCtrlExFnT)(void *ctx, void *formRef, const char *typeName, const char *ctrlName, void *parentRef); + +// Override the event handler for a specific control + event pair. +typedef void (*BasUiSetEventFnT)(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName); + +// Remove a control from a form by name. +typedef void (*BasUiRemoveCtrlFnT)(void *ctx, void *formRef, const char *ctrlName); + // Collected UI callbacks typedef struct { - BasUiGetPropFnT getProp; - BasUiSetPropFnT setProp; - BasUiCallMethodFnT callMethod; - BasUiCreateCtrlFnT createCtrl; - BasUiFindCtrlFnT findCtrl; - BasUiFindCtrlIdxFnT findCtrlIdx; - BasUiLoadFormFnT loadForm; - BasUiUnloadFormFnT unloadForm; - BasUiShowFormFnT showForm; - BasUiHideFormFnT hideForm; - BasUiMsgBoxFnT msgBox; - BasUiInputBoxFnT inputBox; - void *ctx; // passed as first arg to all callbacks + BasUiGetPropFnT getProp; + BasUiSetPropFnT setProp; + BasUiCallMethodFnT callMethod; + BasUiCreateCtrlFnT createCtrl; + BasUiFindCtrlFnT findCtrl; + BasUiFindCtrlIdxFnT findCtrlIdx; + BasUiLoadFormFnT loadForm; + BasUiUnloadFormFnT unloadForm; + BasUiShowFormFnT showForm; + BasUiHideFormFnT hideForm; + BasUiMsgBoxFnT msgBox; + BasUiInputBoxFnT inputBox; + BasUiCreateFormFnT createForm; + BasUiCreateCtrlExFnT createCtrlEx; + BasUiSetEventFnT setEvent; + BasUiRemoveCtrlFnT removeCtrl; + void *ctx; // passed as first arg to all callbacks } BasUiCallbacksT; // ============================================================ diff --git a/apps/dvxbasic/samples/MULTI.DBP b/apps/dvxbasic/samples/MULTI.DBP deleted file mode 100644 index 62cee7b..0000000 --- a/apps/dvxbasic/samples/MULTI.DBP +++ /dev/null @@ -1,18 +0,0 @@ -[Project] -Name = Multi Form Test 1 -Author = Scott Duensing -Company = Kangaroo Punch Studios -Version = 1.00 -Copyright = Copyright 2026 Scott Duensing -Description = Testing properties. -Icon = icon32.bmp - -[Modules] -Count = 0 - -[Forms] -File0 = multi1.frm -Count = 1 - -[Settings] -StartupForm = diff --git a/apps/dvxbasic/samples/MULTI1.FRM b/apps/dvxbasic/samples/MULTI1.FRM deleted file mode 100644 index a385540..0000000 --- a/apps/dvxbasic/samples/MULTI1.FRM +++ /dev/null @@ -1,10 +0,0 @@ -VERSION 1.00 -Begin Form multi1 - Caption = "Welcome to DVX BASIC!" - Layout = VBox - AutoSize = False - Resizable = True - Centered = True - Width = 400 - Height = 300 -End diff --git a/apps/dvxbasic/samples/clickme.bas b/apps/dvxbasic/samples/clickme.bas deleted file mode 100644 index 9b0e19b..0000000 --- a/apps/dvxbasic/samples/clickme.bas +++ /dev/null @@ -1,29 +0,0 @@ -' clickme.bas -- DVX BASIC click test -' -' Demonstrates form loading, control properties, and event handling. -' Load this file in DVX BASIC, then click Run. - -DIM clickCount AS INTEGER -clickCount = 0 - -Load ClickMe -ClickMe.Show - -PRINT "Form loaded. Click the button!" - -' Event loop -- keeps the program alive for events. -' The IDE yields to DVX between VM slices, so widget -' events fire during DoEvents pauses. -DO - DoEvents -LOOP - -SUB Command1_Click - clickCount = clickCount + 1 - Text1.Text = "Clicked " + STR$(clickCount) + " times!" - Label1.Caption = "Keep clicking!" -END SUB - -SUB ClickMe_Load - PRINT "Form_Load fired!" -END SUB diff --git a/apps/dvxbasic/samples/clickme.frm b/apps/dvxbasic/samples/clickme.frm deleted file mode 100644 index 82710f3..0000000 --- a/apps/dvxbasic/samples/clickme.frm +++ /dev/null @@ -1,12 +0,0 @@ -Begin Form ClickMe - Caption = "DVX BASIC Demo" - Begin CommandButton Command1 - Caption = "Click Me!" - End - Begin TextBox Text1 - Text = "Ready." - End - Begin Label Label1 - Caption = "Click the button above!" - End -End diff --git a/apps/dvxbasic/samples/formtest.bas b/apps/dvxbasic/samples/formtest.bas deleted file mode 100644 index ef85484..0000000 --- a/apps/dvxbasic/samples/formtest.bas +++ /dev/null @@ -1,8 +0,0 @@ -' formtest.bas -- DVX BASIC form test -' -' Demonstrates form creation and MsgBox. - -Load MyForm -MyForm.Show -MsgBox "Hello from DVX BASIC!" -PRINT "Form test complete." diff --git a/apps/dvxbasic/samples/hello.bas b/apps/dvxbasic/samples/hello.bas deleted file mode 100644 index 949bec6..0000000 --- a/apps/dvxbasic/samples/hello.bas +++ /dev/null @@ -1,27 +0,0 @@ -' hello.bas -- DVX BASIC Hello World -' -' A simple program to test the DVX BASIC runner. - -PRINT "Hello from DVX BASIC!" -PRINT -PRINT "Testing arithmetic: 2 + 3 * 4 ="; 2 + 3 * 4 -PRINT "Testing strings: "; "Hello" & " " & "World" -PRINT - -DIM i AS INTEGER -PRINT "Fibonacci sequence:" -DIM a AS INTEGER -DIM b AS INTEGER -DIM temp AS INTEGER -a = 0 -b = 1 -FOR i = 1 TO 15 - PRINT a; - temp = a + b - a = b - b = temp -NEXT i -PRINT - -PRINT -PRINT "Done!" diff --git a/apps/dvxbasic/samples/input.bas b/apps/dvxbasic/samples/input.bas deleted file mode 100644 index 18fd316..0000000 --- a/apps/dvxbasic/samples/input.bas +++ /dev/null @@ -1,13 +0,0 @@ -' input.bas -- Test INPUT statement -' -' Demonstrates the INPUT dialog. - -DIM name AS STRING -DIM age AS INTEGER - -INPUT "What is your name"; name -PRINT "Hello, " + name + "!" - -INPUT "How old are you"; age -PRINT "You are" + STR$(age) + " years old." -PRINT "Done!" diff --git a/apps/dvxbasic/stub/basstub.c b/apps/dvxbasic/stub/basstub.c new file mode 100644 index 0000000..401b682 --- /dev/null +++ b/apps/dvxbasic/stub/basstub.c @@ -0,0 +1,217 @@ +// basstub.c -- DVX BASIC standalone application stub +// +// Minimal entry point for compiled BASIC applications. Reads the +// MODULE resource (serialized bytecode), FORMn resources (.frm text), +// and optional DEBUG resource from its own DXE file. Creates a VM +// and form runtime from basrt.lib and runs the program. +// +// This file is compiled into basstub.app, which is embedded as a +// resource in dvxbasic.app. "Make Executable" extracts it, renames +// it to the project name, and attaches the compiled resources. + +#include "dvxApp.h" +#include "dvxDlg.h" +#include "dvxRes.h" +#include "dvxWgt.h" +#include "shellApp.h" +#include "../runtime/vm.h" +#include "../runtime/serialize.h" +#include "../formrt/formrt.h" + +#include +#include +#include +#include + +// ============================================================ +// App descriptor +// ============================================================ + +#define STUB_STACK_SIZE 65536 + +AppDescriptorT appDescriptor = { + .name = "BASIC App", + .hasMainLoop = true, + .multiInstance = false, + .stackSize = STUB_STACK_SIZE, + .priority = TS_PRIORITY_NORMAL +}; + + +// ============================================================ +// Callbacks +// ============================================================ + +static AppContextT *sAc = NULL; + + +static void stubPrint(void *ctx, const char *text, bool newline) { + (void)ctx; + (void)text; + (void)newline; +} + + +static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize) { + (void)ctx; + buf[0] = '\0'; + + if (!sAc) { + return false; + } + + return dvxInputBox(sAc, "Input", prompt, "", buf, bufSize); +} + + +static bool stubDoEvents(void *ctx) { + (void)ctx; + + if (!sAc) { + return false; + } + + return dvxUpdate(sAc); +} + + +// ============================================================ +// appMain +// ============================================================ + +int32_t appMain(DxeAppContextT *ctx) { + sAc = ctx->shellCtx; + + // Open our own resources + DvxResHandleT *res = dvxResOpen(ctx->appPath); + + if (!res) { + dvxMessageBox(sAc, "Error", "No compiled module found in this application.", 0); + return 1; + } + + // Read app name and update the shell's app record + uint32_t nameSize = 0; + char *appName = (char *)dvxResRead(res, "APPNAME", &nameSize); + + if (appName) { + ShellAppT *app = shellGetApp(ctx->appId); + + if (app) { + snprintf(app->name, SHELL_APP_NAME_MAX, "%s", appName); + } + + free(appName); + } + + // Load MODULE resource + uint32_t modSize = 0; + uint8_t *modData = (uint8_t *)dvxResRead(res, "MODULE", &modSize); + + if (!modData) { + dvxMessageBox(sAc, "Error", "MODULE resource not found.", 0); + dvxResClose(res); + return 1; + } + + BasModuleT *mod = basModuleDeserialize(modData, (int32_t)modSize); + free(modData); + + if (!mod) { + dvxMessageBox(sAc, "Error", "Failed to deserialize module.", 0); + dvxResClose(res); + return 1; + } + + // Load optional DEBUG resource + uint32_t dbgSize = 0; + uint8_t *dbgData = (uint8_t *)dvxResRead(res, "DEBUG", &dbgSize); + + if (dbgData) { + basDebugDeserialize(mod, dbgData, (int32_t)dbgSize); + free(dbgData); + } + + // Create VM + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, mod); + basVmSetPrintCallback(vm, stubPrint, NULL); + basVmSetInputCallback(vm, stubInput, NULL); + basVmSetDoEventsCallback(vm, stubDoEvents, NULL); + + // Set app paths + snprintf(vm->appPath, DVX_MAX_PATH, "%s", ctx->appDir); + snprintf(vm->appConfig, DVX_MAX_PATH, "%s", ctx->appDir); + snprintf(vm->appData, DVX_MAX_PATH, "%s", ctx->appDir); + + // Set extern call callbacks (required for DECLARE LIBRARY functions) + BasExternCallbacksT extCb; + memset(&extCb, 0, sizeof(extCb)); + extCb.resolveExtern = basExternResolve; + extCb.callExtern = basExternCall; + basVmSetExternCallbacks(vm, &extCb); + + // Create form runtime + BasFormRtT *rt = basFormRtCreate(sAc, vm, mod); + + // Register .frm source text for lazy loading + for (int32_t i = 0; i < 256; i++) { + char resName[16]; + snprintf(resName, sizeof(resName), "FORM%ld", (long)i); + + uint32_t frmSize = 0; + char *frmText = (char *)dvxResRead(res, resName, &frmSize); + + if (!frmText) { + break; + } + + // Extract form name from "Begin Form " line + char frmName[BAS_MAX_CTRL_NAME] = ""; + const char *p = frmText; + + while (*p) { + while (*p == ' ' || *p == '\t') { p++; } + + if (strncasecmp(p, "Begin Form ", 11) == 0) { + p += 11; + + while (*p == ' ' || *p == '\t') { p++; } + + int32_t ni = 0; + + while (*p && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && ni < BAS_MAX_CTRL_NAME - 1) { + frmName[ni++] = *p++; + } + + frmName[ni] = '\0'; + break; + } + + while (*p && *p != '\n') { p++; } + + if (*p == '\n') { p++; } + } + + if (frmName[0]) { + basFormRtRegisterFrm(rt, frmName, frmText, (int32_t)frmSize); + } + + free(frmText); + } + + dvxResClose(res); + + // Load all cached forms and show the startup form + basFormRtLoadAllForms(rt, NULL); + + // Run bytecode + VB-style event loop + basFormRtRunSimple(rt); + + // Cleanup + basFormRtDestroy(rt); + basVmDestroy(vm); + basModuleFree(mod); + + return 0; +} diff --git a/config/dvx.ini b/config/dvx.ini index c848f24..5b9736b 100644 --- a/config/dvx.ini +++ b/config/dvx.ini @@ -13,11 +13,13 @@ bpp = 16 ; wheel: normal or reversed ; doubleclick: double-click speed in milliseconds (200-900, default 500) ; acceleration: off, low, medium, high (default medium) +; speed: cursor speed (2-32, default 8; higher = faster) [mouse] wheel = normal doubleclick = 500 acceleration = medium +speed = 8 ; Shell settings. ; desktop: path to the desktop app loaded at startup diff --git a/core/dvxApp.c b/core/dvxApp.c index aeefc28..3932fef 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -4833,7 +4833,7 @@ void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b) // dvxSetMouseConfig // ============================================================ -void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold) { +void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio) { ctx->wheelDirection = (wheelDir < 0) ? -1 : 1; ctx->dblClickTicks = (clock_t)dblClickMs * CLOCKS_PER_SEC / 1000; sDblClickTicks = ctx->dblClickTicks; @@ -4841,6 +4841,10 @@ void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, i if (accelThreshold > 0) { platformMouseSetAccel(accelThreshold); } + + if (mickeyRatio > 0) { + platformMouseSetMickeys(mickeyRatio, mickeyRatio); + } } diff --git a/core/dvxApp.h b/core/dvxApp.h index d15ac10..c2960de 100644 --- a/core/dvxApp.h +++ b/core/dvxApp.h @@ -133,7 +133,8 @@ int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t request // Configure mouse behaviour. wheelDir: 1 = normal, -1 = reversed. // dblClickMs: double-click speed in milliseconds (e.g. 500). // accelThreshold: double-speed threshold in mickeys/sec (0 = don't change). -void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold); +// mickeyRatio: mickeys per 8 pixels (0 = don't change, 8 = default speed). +void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio); // ============================================================ // Color scheme diff --git a/core/dvxRes.h b/core/dvxRes.h index b19c6c1..90b364a 100644 --- a/core/dvxRes.h +++ b/core/dvxRes.h @@ -72,4 +72,9 @@ const DvxResDirEntryT *dvxResFind(DvxResHandleT *h, const char *name); // Close the handle and free all associated memory. void dvxResClose(DvxResHandleT *h); +// Append a resource to an existing DXE file. +// Preserves existing resources and adds the new one. +// Returns 0 on success, -1 on error. +int32_t dvxResAppend(const char *path, const char *name, uint32_t type, const void *data, uint32_t dataSize); + #endif // DVX_RES_H diff --git a/core/dvxResource.c b/core/dvxResource.c index e0e474d..b68258d 100644 --- a/core/dvxResource.c +++ b/core/dvxResource.c @@ -4,6 +4,7 @@ // block is located by reading the footer at the end of the file. #include "dvxRes.h" +#include "../tools/dvxResWrite.h" #include #include @@ -138,3 +139,8 @@ void dvxResClose(DvxResHandleT *h) { free(h); } } + + +int32_t dvxResAppend(const char *path, const char *name, uint32_t type, const void *data, uint32_t dataSize) { + return dvxResAppendEntry(path, name, type, data, dataSize); +} diff --git a/core/dvxWgt.h b/core/dvxWgt.h index 39df81a..54c1afe 100644 --- a/core/dvxWgt.h +++ b/core/dvxWgt.h @@ -583,6 +583,11 @@ const void *wgtGetApi(const char *name); #define WGT_SIG_INT5 13 // void fn(WidgetT *, int32_t, int32_t, int32_t, int32_t, int32_t) #define WGT_SIG_RET_STR_INT 14 // const char *fn(const WidgetT *, int32_t) #define WGT_SIG_RET_STR_INT_INT 15 // const char *fn(const WidgetT *, int32_t, int32_t) +#define WGT_SIG_STR_BOOL_BOOL 16 // bool fn(WidgetT *, const char *, bool, bool) +#define WGT_SIG_STR_STR_BOOL 17 // int32_t fn(WidgetT *, const char *, const char *, bool) +#define WGT_SIG_RET_STR 18 // const char *fn(const WidgetT *) +#define WGT_SIG_STR_INT 19 // void fn(WidgetT *, const char *, int32_t) +#define WGT_SIG_INT_STR 20 // void fn(WidgetT *, int32_t, const char *) // Property descriptor typedef struct { diff --git a/core/platform/dvxPlat.h b/core/platform/dvxPlat.h index ef15ad9..0ec6712 100644 --- a/core/platform/dvxPlat.h +++ b/core/platform/dvxPlat.h @@ -150,6 +150,11 @@ int32_t platformMouseWheelPoll(void); // A very high value (e.g. 10000) effectively disables acceleration. void platformMouseSetAccel(int32_t threshold); +// Set the mickey-to-pixel ratio. Controls base cursor speed. +// horizMickeys/vertMickeys: mickeys per 8 pixels of cursor movement. +// Higher values = slower cursor. Default is typically 8 horiz, 16 vert. +void platformMouseSetMickeys(int32_t horizMickeys, int32_t vertMickeys); + // Move the mouse cursor to an absolute screen position. Uses INT 33h // function 04h on DOS. Used to clamp the cursor to window edges during // resize operations. diff --git a/core/platform/dvxPlatformDos.c b/core/platform/dvxPlatformDos.c index 9536b3a..119daa6 100644 --- a/core/platform/dvxPlatformDos.c +++ b/core/platform/dvxPlatformDos.c @@ -1604,6 +1604,28 @@ void platformMouseSetAccel(int32_t threshold) { } +// ============================================================ +// platformMouseSetMickeys +// ============================================================ +// +// Sets the mickey-to-pixel ratio via INT 33h function 0Fh. +// horizMickeys: number of mickeys per 8 pixels horizontally +// vertMickeys: number of mickeys per 8 pixels vertically +// +// Higher values = slower cursor (more mickeys needed per pixel). +// Default is typically 8 horizontal, 16 vertical (1:1 and 2:1). + +void platformMouseSetMickeys(int32_t horizMickeys, int32_t vertMickeys) { + __dpmi_regs r; + + memset(&r, 0, sizeof(r)); + r.x.ax = 0x000F; + r.x.cx = horizMickeys; + r.x.dx = vertMickeys; + __dpmi_int(0x33, &r); +} + + // ============================================================ // platformMousePoll // ============================================================ @@ -2407,6 +2429,7 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(platformMouseInit) DXE_EXPORT(platformMousePoll) DXE_EXPORT(platformMouseSetAccel) + DXE_EXPORT(platformMouseSetMickeys) DXE_EXPORT(platformMouseWarp) DXE_EXPORT(platformMouseWheelInit) DXE_EXPORT(platformMouseWheelPoll) diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html index d0f8dc0..20b16e8 100644 --- a/docs/dvx_basic_reference.html +++ b/docs/dvx_basic_reference.html @@ -487,6 +487,8 @@ img { max-width: 100%; }
  • Terminal
  • ANSI Terminal
  • VT100
  • +
  • CommAttach
  • +
  • CommOpen
  • Frame
  • Container
  • HBox
  • @@ -499,6 +501,8 @@ img { max-width: 100%; }
  • PictureBox
  • Canvas
  • Drawing
  • +
  • SetPenColor
  • +
  • SetPenSize
  • CheckBox
  • Value
  • ComboBox
  • @@ -529,6 +533,8 @@ img { max-width: 100%; }
  • RemoveItem
  • ListView
  • Multi-Column List
  • +
  • SetColumns
  • +
  • SetSort
  • ProgressBar
  • OptionButton
  • Radio Button
  • @@ -557,12 +563,20 @@ img { max-width: 100%; }
  • DataSource
  • DataField
  • TextArea
  • +
  • FindNext
  • +
  • ReplaceAll
  • +
  • GoToLine
  • +
  • CursorLine
  • +
  • LineNumbers
  • +
  • AutoIndent
  • Timer
  • Interval
  • Start
  • Stop
  • Toolbar
  • TreeView
  • +
  • AddItem
  • +
  • AddChildItem
  • WrapBox
  • Flow Layout
  • @@ -2210,11 +2224,19 @@ End Sub Rows Integer Number of character rows (read-only). Scrollback Integer Number of scrollback lines (write-only).

    Type-Specific Methods

    -
      Method   Parameters        Description
    -  ------   ---------------   -------------------------------------------
    -  Clear    (none)            Clear the terminal screen.
    -  Write    Text As String    Write text (with ANSI escape processing) to the terminal.
    +
      Method             Description
    +  ------             -----------
    +  Clear              Clear the terminal screen.
    +  Poll               Process pending comm data and update the display.
    +  Write text$        Write text (with ANSI escape processing) to the terminal.

    No default event.

    +

    Serial Communication

    +

    Use the comm.bas library to connect a Terminal to a serial link:

    +
    Dim link As Integer
    +link = CommOpen(1, 115200)
    +CommHandshake link
    +CommAttach link, "Terminal1"
    +

    See comm.bas for the full communications API.

    Common Properties, Events, and Methods

    @@ -2286,12 +2308,32 @@ End Sub

    PictureBox

    VB Equivalent: PictureBox -- DVX Widget: canvas | Name Prefix: Picture

    A drawing surface (canvas). Supports drawing lines, rectangles, circles, text, and individual pixels. Can save and load BMP images. The default canvas size is 64x64 pixels.

    +

    Colors are specified as 0x00RRGGBB integers (e.g. &HFF0000 for red).

    Type-Specific Methods

    -
      Method   Parameters         Description
    -  ------   ----------------   -------------------------------------------
    -  Clear    Color As Integer   Fill the entire canvas with the specified color.
    -

    Additional drawing methods (DrawLine, DrawRect, FillRect, FillCircle, SetPixel, GetPixel, DrawText, Save, Load) are available through the C API but not currently exposed through BASIC interface descriptors.

    +
      Method                                        Description
    +  ------                                        -----------
    +  Clear color%                                  Fill the entire canvas with the specified color.
    +  DrawLine x0%, y0%, x1%, y1%, color%           Draw a line between two points.
    +  DrawRect x%, y%, w%, h%, color%               Draw a rectangle outline.
    +  DrawText x%, y%, text$                        Draw text at the given position.
    +  FillCircle cx%, cy%, radius%, color%          Draw a filled circle.
    +  FillRect x%, y%, w%, h%, color%               Draw a filled rectangle.
    +  GetPixel(x%, y%)                              Returns the color at a pixel.
    +  Load path$                                    Load a BMP image onto the canvas.
    +  Resize w%, h%                                 Resize the canvas to new dimensions.
    +  Save path$                                    Save the canvas as a BMP file.
    +  SetPenColor color%                            Set the drawing color for subsequent operations.
    +  SetPenSize size%                              Set the pen/brush size in pixels.
    +  SetPixel x%, y%, color%                       Set the color of a single pixel.

    Default Event: Click

    +

    Example

    +
    Picture1.Resize 200, 150
    +Picture1.Clear &HFFFFFF
    +Picture1.SetPenSize 2
    +Picture1.DrawRect 10, 10, 180, 130, &H000000
    +Picture1.FillCircle 100, 75, 40, &HFF0000
    +Picture1.DrawText 60, 130, "Hello!"
    +Picture1.Save "output.bmp"

    Common Properties, Events, and Methods

    @@ -2472,7 +2514,8 @@ End SetMultiSelect Multi As Boolean Enable or disable multi-select mode. SetReorderable Reorderable As Boolean Enable or disable drag-to-reorder. IsItemSelected Index As Integer Returns True if the item at Index is selected. - SetItemSelected Index As Integer, Selected As Boolean Select or deselect a specific item. + SetItemSelected Index As Integer, Selected As Boolean Select or deselect a specific item. + SetItems Items As String Bulk-load items from a pipe-delimited string (e.g. "Red|Green|Blue").

    Default Event: Click

    Example

    Sub Form_Load ()
    @@ -2495,21 +2538,35 @@ End Sub

    ListView

    VB Equivalent: ListView -- DVX Widget: listview

    -

    A multi-column list with column headers. Supports sorting, multi-select, and drag-to-reorder. Columns are configured via the C API (SetColumns, SetData).

    +

    A multi-column list with column headers. Supports sorting, multi-select, and drag-to-reorder.

    Type-Specific Properties

    -
      Property    Type      Description
    -  ---------   -------   -------------------------------------------
    -  ListIndex   Integer   Index of the currently selected row (-1 = none).
    +
      Property    Type      R/W   Description
    +  ---------   -------   ---   -------------------------------------------
    +  ListIndex   Integer   R/W   Index of the currently selected row (-1 = none).

    Type-Specific Methods

    -
      Method            Parameters                                Description
    -  ---------------   ---------------------------------------   -------------------------------------------
    -  SelectAll         (none)                                    Select all rows.
    -  ClearSelection    (none)                                    Deselect all rows.
    -  SetMultiSelect    Multi As Boolean                          Enable or disable multi-select.
    -  SetReorderable    Reorderable As Boolean                    Enable or disable row reordering.
    -  IsItemSelected    Index As Integer                          Returns True if the row at Index is selected.
    -  SetItemSelected   Index As Integer, Selected As Boolean     Select or deselect a specific row.
    +
      Method                                        Description
    +  ------                                        -----------
    +  AddItem text$                                 Add a row (sets first column text).
    +  Clear                                         Remove all rows.
    +  ClearSelection                                Deselect all rows.
    +  GetCell$(row%, col%)                          Returns the text of a cell.
    +  IsItemSelected(index%)                        Returns True if the row is selected.
    +  RemoveItem index%                             Remove a row by index.
    +  RowCount()                                    Returns the number of rows.
    +  SelectAll                                     Select all rows.
    +  SetCell row%, col%, text$                     Set the text of a cell.
    +  SetColumns spec$                              Define columns. Pipe-delimited "Name,Width|Name,Width" (width in characters).
    +  SetItemSelected index%, selected              Select or deselect a row.
    +  SetMultiSelect multi                          Enable or disable multi-select.
    +  SetReorderable reorderable                    Enable or disable row reordering.
    +  SetSort col%, dir%                            Sort by column. dir: 0=none, 1=ascending, 2=descending.

    Default Event: Click

    +

    Example

    +
    ListView1.SetColumns "Name,20|Size,10|Type,15"
    +ListView1.AddItem "readme.txt"
    +ListView1.SetCell 0, 1, "1024"
    +ListView1.SetCell 0, 2, "Text"
    +ListView1.SetSort 0, 1

    Common Properties, Events, and Methods

    @@ -2630,9 +2687,11 @@ End Sub -------- ------- ------------------------------------------- TabIndex Integer Index of the active tab (0-based). Note: this property name collides with the common VB-compatibility TabIndex property, which shadows it at runtime. Use the SetActive method instead to switch tabs.

    Type-Specific Methods

    -
      Method      Parameters          Description
    -  ---------   -----------------   -------------------------------------------
    -  SetActive   Index As Integer    Switch to the tab at the given index. This is the recommended way to change tabs at runtime (the TabIndex property is shadowed by the common property handler).
    +
      Method                   Description
    +  ------                   -----------
    +  AddPage title$           Add a new tab page with the given title.
    +  GetActive()              Returns the index of the active tab.
    +  SetActive index%         Switch to the tab at the given index. This is the recommended way to change tabs at runtime (the TabIndex property is shadowed by the common property handler).

    Container: Yes

    Default Event: Click

    Warning: The TabIndex property is shadowed by the common property handler at runtime. Use the SetActive method to change tabs programmatically.
    @@ -2663,12 +2722,41 @@ End Sub

    TextArea

    VB Equivalent: TextArea (DVX extension) -- DVX Widget: textarea (multi-line text input, max 4096 chars)

    -

    A multi-line text editing area. This is a DVX extension with no direct VB3 equivalent (VB uses a TextBox with MultiLine=True). Supports syntax colorization, line numbers, auto-indent, and find/replace via the C API.

    +

    A multi-line text editing area. This is a DVX extension with no direct VB3 equivalent (VB uses a TextBox with MultiLine=True). Supports line numbers, auto-indent, find/replace, and tab configuration.

    Type-Specific Properties

    -
      Property   Type     Description
    -  --------   ------   -------------------------------------------
    -  Text       String   The full text content.
    +
      Property     Type      R/W   Description
    +  ----------   -------   ---   -------------------------------------------
    +  Text         String    R/W   The full text content.
    +  CursorLine   Integer   R     Current cursor line number (0-based).
    +

    Type-Specific Methods

    +
      Method                                          Description
    +  ------                                          -----------
    +  FindNext needle$, caseSensitive, forward        Search for text. Returns True if found.
    +  GetWordAtCursor()                               Returns the word under the cursor.
    +  GoToLine line%                                  Scroll to and position cursor at the given line.
    +  ReplaceAll needle$, replacement$, caseSensitive Replace all occurrences. Returns count of replacements.
    +  SetAutoIndent enabled                           Enable or disable automatic indentation.
    +  SetCaptureTabs enabled                          When True, Tab key inserts a tab/spaces instead of moving focus.
    +  SetShowLineNumbers show                         Show or hide the line number gutter.
    +  SetSyntaxMode mode$                             Activate built-in syntax highlighting. Modes: "dhs" (help source), "bas" (BASIC). Pass "" to disable.
    +  SetTabWidth width%                              Set the tab stop width in characters.
    +  SetUseTabChar useChar                           When True, Tab inserts a tab character; when False, inserts spaces.

    Default Event: Change

    +

    Example

    +
    TextArea1.SetShowLineNumbers True
    +TextArea1.SetAutoIndent True
    +TextArea1.SetTabWidth 4
    +TextArea1.SetCaptureTabs True
    +
    +' Search for text
    +If TextArea1.FindNext("TODO", False, True) Then
    +    Print "Found at line"; TextArea1.CursorLine
    +End If
    +
    +' Replace all occurrences
    +Dim count As Integer
    +count = TextArea1.ReplaceAll("old", "new", False)
    +Print "Replaced"; count; "occurrences"

    Common Properties, Events, and Methods

    @@ -2715,13 +2803,30 @@ End Sub

    TreeView

    VB Equivalent: TreeView -- DVX Widget: treeview

    -

    A hierarchical tree of expandable/collapsible nodes. Nodes are created via the C API (wgtTreeItem). Supports multi-select and drag-to-reorder.

    +

    A hierarchical tree of expandable/collapsible nodes. Supports multi-select and drag-to-reorder. Items are accessed by depth-first index (0-based).

    Type-Specific Methods

    -
      Method           Parameters               Description
    -  --------------   ----------------------   -------------------------------------------
    -  SetMultiSelect   Multi As Boolean         Enable or disable multi-select mode.
    -  SetReorderable   Reorderable As Boolean   Enable or disable node reordering.
    +
      Method                                  Description
    +  ------                                  -----------
    +  AddChildItem parentIndex%, text$        Add a child node under the node at the given index.
    +  AddItem text$                           Add a root-level node.
    +  Clear                                   Remove all nodes.
    +  GetItemText$(index%)                    Returns the text of the node at the given index.
    +  IsExpanded(index%)                      Returns True if the node at the given index is expanded.
    +  IsItemSelected(index%)                  Returns True if the node at the given index is selected.
    +  ItemCount()                             Returns the total number of nodes (all levels).
    +  SetExpanded index%, expanded            Expand or collapse the node at the given index.
    +  SetItemSelected index%, selected        Select or deselect the node at the given index.
    +  SetMultiSelect multi                    Enable or disable multi-select mode.
    +  SetReorderable reorderable              Enable or disable node reordering.

    Default Event: Click

    +

    Example

    +
    TreeView1.AddItem "Animals"
    +TreeView1.AddChildItem 0, "Cat"
    +TreeView1.AddChildItem 0, "Dog"
    +TreeView1.AddItem "Plants"
    +TreeView1.AddChildItem 3, "Oak"
    +TreeView1.SetExpanded 0, True
    +Print "Items:"; TreeView1.ItemCount()

    Common Properties, Events, and Methods

    diff --git a/sdk/include/basic/comm.bas b/sdk/include/basic/comm.bas new file mode 100644 index 0000000..535efc6 --- /dev/null +++ b/sdk/include/basic/comm.bas @@ -0,0 +1,128 @@ +' comm.bas -- Serial Communications Library for DVX BASIC +' +' Three tiers of serial communication: +' +' 1. Raw serial (SerXxx) -- direct UART I/O with ISR-driven ring +' buffers. No framing or error correction. Use this for talking +' to existing BBSs, modems, and other plain serial devices. +' +' 2. Packet serial (CommXxx) -- adds HDLC framing, CRC-16 checksums, +' and Go-Back-N ARQ for reliable delivery. Data is split into +' packets and retransmitted on error. Requires both sides to +' speak the same protocol. +' +' 3. Encrypted serial (CommHandshake + encrypt flag) -- adds DH key +' exchange and XTEA-CTR encryption on top of packets. Use +' CommSend with encrypt=True after CommHandshake. +' +' Usage: Add this file to your project. + +' UART types (returned by SerGetUart) +CONST SER_UART_NONE = 0 +CONST SER_UART_8250 = 1 +CONST SER_UART_16450 = 2 +CONST SER_UART_16550 = 3 +CONST SER_UART_16550A = 4 + +' Handshake modes (for SerOpen / CommOpen) +CONST SER_HANDSHAKE_NONE = 0 +CONST SER_HANDSHAKE_XONXOFF = 1 +CONST SER_HANDSHAKE_RTSCTS = 2 +CONST SER_HANDSHAKE_DTRDSR = 3 + +DECLARE LIBRARY "basrt" + + ' ========================================================= + ' UART Detection and Configuration + ' ========================================================= + + ' Detect the UART type for a COM port (1-4) WITHOUT opening it. + DECLARE FUNCTION SerGetUart(BYVAL com AS INTEGER) AS INTEGER + + ' Set the I/O base address for a COM port before opening. + ' Default bases: COM1=&H3F8, COM2=&H2F8, COM3=&H3E8, COM4=&H2E8 + DECLARE SUB SerSetBase(BYVAL com AS INTEGER, BYVAL base AS INTEGER) + + ' Set the IRQ for a COM port before opening. + ' Default IRQs: COM1=4, COM2=3, COM3=4, COM4=3 + DECLARE SUB SerSetIrq(BYVAL com AS INTEGER, BYVAL irq AS INTEGER) + + ' Get the I/O base address for a COM port. + DECLARE FUNCTION SerGetBase(BYVAL com AS INTEGER) AS INTEGER + + ' Get the IRQ for a COM port. + DECLARE FUNCTION SerGetIrq(BYVAL com AS INTEGER) AS INTEGER + + ' ========================================================= + ' Raw Serial I/O (no framing, no error correction) + ' ========================================================= + + ' Open a raw serial port. Returns True on success. + DECLARE FUNCTION SerOpen(BYVAL com AS INTEGER, BYVAL baud AS LONG, BYVAL dataBits AS INTEGER, BYVAL parity AS STRING, BYVAL stopBits AS INTEGER, BYVAL handshake AS INTEGER) AS INTEGER + + ' Close a raw serial port. + DECLARE SUB SerClose(BYVAL com AS INTEGER) + + ' Write a string to the serial port. Returns True on success. + DECLARE FUNCTION SerWrite(BYVAL com AS INTEGER, BYVAL data AS STRING) AS INTEGER + + ' Read pending data from the serial port. Returns "" if none available. + DECLARE FUNCTION SerRead$(BYVAL com AS INTEGER) AS STRING + + ' Return the number of bytes waiting in the receive buffer. + DECLARE FUNCTION SerAvailable(BYVAL com AS INTEGER) AS INTEGER + + ' Clear the receive buffer. + DECLARE SUB SerFlush(BYVAL com AS INTEGER) + + ' Attach a raw serial port to a Terminal widget. + DECLARE SUB SerAttach(BYVAL com AS INTEGER, BYVAL termCtrlName AS STRING) + + ' Detach a raw serial port from a Terminal widget. + DECLARE SUB SerDetach(BYVAL com AS INTEGER) + + ' ========================================================= + ' Packet Serial (reliable delivery + channel multiplexing) + ' ========================================================= + + ' Open a packet serial link. Returns a handle (>0), or 0 on failure. + DECLARE FUNCTION CommOpen(BYVAL com AS INTEGER, BYVAL baud AS LONG, BYVAL dataBits AS INTEGER, BYVAL parity AS STRING, BYVAL stopBits AS INTEGER, BYVAL handshake AS INTEGER) AS INTEGER + + ' Close a packet serial link. + DECLARE SUB CommClose(BYVAL handle AS INTEGER) + + ' Send data on a logical channel (0-127). + ' encrypt: True to encrypt (requires CommHandshake first). + DECLARE FUNCTION CommSend(BYVAL handle AS INTEGER, BYVAL data AS STRING, BYVAL channel AS INTEGER, BYVAL encrypt AS INTEGER) AS INTEGER + + ' Receive pending data from a specific channel. Returns "" if none. + DECLARE FUNCTION CommRecv$(BYVAL handle AS INTEGER, BYVAL channel AS INTEGER) AS STRING + + ' Poll for incoming packets. Call periodically if not using CommAttach. + DECLARE FUNCTION CommPoll(BYVAL handle AS INTEGER) AS INTEGER + + ' Return the number of unacknowledged packets in the transmit window. + DECLARE FUNCTION CommPending(BYVAL handle AS INTEGER) AS INTEGER + + ' ========================================================= + ' Encryption (DH key exchange + XTEA-CTR) + ' ========================================================= + + ' Perform Diffie-Hellman key exchange. Blocks until both sides + ' complete. After this, CommSend with encrypt=True will encrypt. + DECLARE FUNCTION CommHandshake(BYVAL handle AS INTEGER) AS INTEGER + + ' Check if the key exchange is complete. + DECLARE FUNCTION CommIsReady(BYVAL handle AS INTEGER) AS INTEGER + + ' ========================================================= + ' Terminal Widget Integration + ' ========================================================= + + ' Attach a packet link to a Terminal widget on a given channel. + ' encrypt: True to encrypt terminal traffic. + DECLARE SUB CommAttach(BYVAL handle AS INTEGER, BYVAL termCtrlName AS STRING, BYVAL channel AS INTEGER, BYVAL encrypt AS INTEGER) + + ' Detach a packet link from a Terminal widget. + DECLARE SUB CommDetach(BYVAL handle AS INTEGER) +END DECLARE diff --git a/sdk/include/basic/commdlg.bas b/sdk/include/basic/commdlg.bas index 0a53de6..bc82a4a 100644 --- a/sdk/include/basic/commdlg.bas +++ b/sdk/include/basic/commdlg.bas @@ -4,14 +4,7 @@ ' Wrappers handle the AppContextT parameter internally so BASIC ' programs just pass the visible arguments. ' -' Usage: -' '$INCLUDE: 'commdlg.bas' -' -' DIM path AS STRING -' path = basFileOpen("Open Image", "*.bmp") -' IF path <> "" THEN -' ' ... use the file ... -' END IF +' Usage: Add this file to your project. DECLARE LIBRARY "basrt" ' Show a file Open dialog. Returns selected path, or "" if cancelled. diff --git a/sdk/include/basic/help.bas b/sdk/include/basic/help.bas new file mode 100644 index 0000000..ddcdd1d --- /dev/null +++ b/sdk/include/basic/help.bas @@ -0,0 +1,15 @@ +' help.bas -- Help System Library for DVX BASIC +' +' Provides access to the DVX help compiler and viewer. +' Compile .dhs source files to .hlp and open them in the viewer. +' +' Usage: Add this file to your project. + +DECLARE LIBRARY "basrt" + ' Compile a .dhs source file to a binary .hlp file. + ' Returns True on success. + DECLARE FUNCTION HelpCompile(BYVAL inputFile AS STRING, BYVAL outputFile AS STRING) AS INTEGER + + ' Open a .hlp file in the DVX Help Viewer. + DECLARE SUB HelpView(BYVAL hlpFile AS STRING) +END DECLARE diff --git a/sdk/include/basic/sql.bas b/sdk/include/basic/sql.bas new file mode 100644 index 0000000..70dd8f6 --- /dev/null +++ b/sdk/include/basic/sql.bas @@ -0,0 +1,53 @@ +' sql.bas -- SQL Database Library for DVX BASIC +' +' Provides SQLite database access through DECLARE LIBRARY wrappers. +' Handles and cursors are integers; strings are passed by value. +' +' Usage: Add this file to your project. + +DECLARE LIBRARY "basrt" + ' Open a database file. Returns a handle (>0), or 0 on failure. + DECLARE FUNCTION SQLOpen(BYVAL path AS STRING) AS INTEGER + + ' Close a database. + DECLARE SUB SQLClose(BYVAL db AS INTEGER) + + ' Execute a non-query SQL statement. Returns True on success. + DECLARE FUNCTION SQLExec(BYVAL db AS INTEGER, BYVAL sql AS STRING) AS INTEGER + + ' Return the last error message for a database. + DECLARE FUNCTION SQLError$(BYVAL db AS INTEGER) AS STRING + + ' Execute a query and return a result set cursor. + DECLARE FUNCTION SQLQuery(BYVAL db AS INTEGER, BYVAL sql AS STRING) AS INTEGER + + ' Advance the cursor to the next row. Returns True if a row is available. + DECLARE FUNCTION SQLNext(BYVAL rs AS INTEGER) AS INTEGER + + ' Test whether the cursor is past the last row. + DECLARE FUNCTION SQLEof(BYVAL rs AS INTEGER) AS INTEGER + + ' Return the number of columns in a result set. + DECLARE FUNCTION SQLFieldCount(BYVAL rs AS INTEGER) AS INTEGER + + ' Return a column name by index (0-based). + DECLARE FUNCTION SQLFieldName$(BYVAL rs AS INTEGER, BYVAL col AS INTEGER) AS STRING + + ' Return a column value as text by index (0-based). + DECLARE FUNCTION SQLFieldText$(BYVAL rs AS INTEGER, BYVAL col AS INTEGER) AS STRING + + ' Return a column value as text by column name. + DECLARE FUNCTION SQLField$(BYVAL rs AS INTEGER, BYVAL colName AS STRING) AS STRING + + ' Return a column value as an integer by index (0-based). + DECLARE FUNCTION SQLFieldInt(BYVAL rs AS INTEGER, BYVAL col AS INTEGER) AS INTEGER + + ' Return a column value as a double by index (0-based). + DECLARE FUNCTION SQLFieldDbl(BYVAL rs AS INTEGER, BYVAL col AS INTEGER) AS DOUBLE + + ' Free a result set cursor. + DECLARE SUB SQLFreeResult(BYVAL rs AS INTEGER) + + ' Return the number of rows affected by the last INSERT/UPDATE/DELETE. + DECLARE FUNCTION SQLAffected(BYVAL db AS INTEGER) AS INTEGER +END DECLARE diff --git a/sdk/samples/basic/commdlg.frm b/sdk/samples/basic/commdlg.frm new file mode 100644 index 0000000..2b75a86 --- /dev/null +++ b/sdk/samples/basic/commdlg.frm @@ -0,0 +1,156 @@ +VERSION DVX 1.00 +' commdlg.frm -- Common Dialog Library Demo +' +' Demonstrates every function in the commdlg.bas include file: +' basFileOpen - File Open dialog +' basFileSave - File Save dialog +' basInputBox2 - Text input dialog +' basChoiceDialog - Choice list dialog +' basIntInput - Integer spinner dialog +' basPromptSave - Yes/No/Cancel save prompt +' +' Add commdlg.bas to your project, then click Run. + +Begin Form CommDlgDemo + Caption = "Common Dialogs Demo" + Layout = VBox + AutoSize = True + Resizable = False + Centered = True + Begin Label LblTitle + Caption = "Common Dialog Demonstrations" + Weight = 0 + End + Begin Line Line1 + Weight = 0 + End + Begin CommandButton BtnFileOpen + Caption = "File Open..." + Weight = 0 + End + Begin CommandButton BtnFileSave + Caption = "File Save..." + Weight = 0 + End + Begin CommandButton BtnInputBox + Caption = "Input Box..." + Weight = 0 + End + Begin CommandButton BtnChoice + Caption = "Choice Dialog..." + Weight = 0 + End + Begin CommandButton BtnIntInput + Caption = "Integer Input..." + Weight = 0 + End + Begin CommandButton BtnPromptSave + Caption = "Prompt Save..." + Weight = 0 + End + Begin Line Line2 + Weight = 0 + End + Begin Label LblResult + Caption = "Result will appear here." + Weight = 0 + End +End + +Load CommDlgDemo +CommDlgDemo.Show + +PRINT "Common Dialog Demo started." + +DO + DoEvents +LOOP + + +SUB BtnFileOpen_Click + DIM path AS STRING + path = basFileOpen("Open a File", "*.bas") + IF path <> "" THEN + LblResult.Caption = "Opened: " + path + PRINT "File Open: " + path + ELSE + LblResult.Caption = "File Open cancelled." + PRINT "File Open cancelled." + END IF +END SUB + + +SUB BtnFileSave_Click + DIM path AS STRING + path = basFileSave("Save a File", "*.txt") + IF path <> "" THEN + LblResult.Caption = "Save to: " + path + PRINT "File Save: " + path + ELSE + LblResult.Caption = "File Save cancelled." + PRINT "File Save cancelled." + END IF +END SUB + + +SUB BtnInputBox_Click + DIM text AS STRING + text = basInputBox2("Text Input", "Enter your name:", "World") + IF text <> "" THEN + LblResult.Caption = "Hello, " + text + "!" + PRINT "Input: " + text + ELSE + LblResult.Caption = "Input Box cancelled." + PRINT "Input Box cancelled." + END IF +END SUB + + +SUB BtnChoice_Click + DIM idx AS INTEGER + idx = basChoiceDialog("Pick a Color", "Choose your favorite color:", "Red|Orange|Yellow|Green|Blue|Indigo|Violet", 0) + IF idx >= 0 THEN + DIM colors(6) AS STRING + colors(0) = "Red" + colors(1) = "Orange" + colors(2) = "Yellow" + colors(3) = "Green" + colors(4) = "Blue" + colors(5) = "Indigo" + colors(6) = "Violet" + LblResult.Caption = "You chose: " + colors(idx) + PRINT "Choice: " + colors(idx) + " (index" + STR$(idx) + ")" + ELSE + LblResult.Caption = "Choice Dialog cancelled." + PRINT "Choice Dialog cancelled." + END IF +END SUB + + +SUB BtnIntInput_Click + DIM val AS INTEGER + val = basIntInput("Pick a Number", "Enter a value (1-100):", 50, 1, 100) + LblResult.Caption = "You entered:" + STR$(val) + PRINT "Integer Input:" + STR$(val) +END SUB + + +SUB BtnPromptSave_Click + DIM result AS INTEGER + result = basPromptSave("Unsaved Changes") + IF result = DVX_SAVE_YES THEN + LblResult.Caption = "You chose: Yes (save)" + PRINT "Prompt Save: Yes" + ELSEIF result = DVX_SAVE_NO THEN + LblResult.Caption = "You chose: No (discard)" + PRINT "Prompt Save: No" + ELSE + LblResult.Caption = "You chose: Cancel" + PRINT "Prompt Save: Cancel" + END IF +END SUB + + +SUB CommDlgDemo_Load + PRINT "Form loaded. Click any button to try a dialog." +END SUB diff --git a/sdk/samples/basic/dynform.bas b/sdk/samples/basic/dynform.bas new file mode 100644 index 0000000..920c79e --- /dev/null +++ b/sdk/samples/basic/dynform.bas @@ -0,0 +1,81 @@ +' dynform.bas -- Dynamic Form Demo +' +' Demonstrates building a complete form entirely in code +' with no .frm file needed: +' CreateForm - create a form programmatically +' CreateControl - add controls with optional parent +' SetEvent - bind events to arbitrary handlers +' RemoveControl - remove a control at runtime +' Optional params - language extension for optional arguments +' +' Add commdlg.bas to your project, then click Run. + +DIM frm AS LONG +DIM lbl AS LONG +DIM btnBar AS LONG + +' Create a form entirely from code +SET frm = CreateForm("DynDemo", 350, 200) +frm.Caption = "Dynamic Form Demo" + +' Add a label at the top +SET lbl = CreateControl(frm, "Label", "StatusLabel") +StatusLabel.Caption = "Built entirely in code!" + +' Add a horizontal button bar container +SET btnBar = CreateControl(frm, "HBox", "ButtonBar") + +' Add buttons inside the HBox, using the optional parent parameter +DIM btnHello AS LONG +SET btnHello = CreateControl(frm, "CommandButton", "BtnHello", btnBar) +BtnHello.Caption = "Hello" + +DIM btnFile AS LONG +SET btnFile = CreateControl(frm, "CommandButton", "BtnFile", btnBar) +BtnFile.Caption = "Open File..." + +DIM btnRemove AS LONG +SET btnRemove = CreateControl(frm, "CommandButton", "BtnRemove", btnBar) +BtnRemove.Caption = "Remove Me" + +' Wire events to custom handler names using SetEvent +SetEvent btnHello, "Click", "OnHelloClick" +SetEvent btnFile, "Click", "OnFileClick" +SetEvent btnRemove, "Click", "OnRemoveClick" + +frm.Show + +PRINT "Dynamic form created. Try the buttons!" + +DO + DoEvents +LOOP + + +SUB OnHelloClick + DIM name AS STRING + name = basInputBox2("Greeting", "What is your name?", "World") + IF name <> "" THEN + StatusLabel.Caption = "Hello, " + name + "!" + PRINT "Greeted: " + name + END IF +END SUB + + +SUB OnFileClick + DIM path AS STRING + path = basFileOpen("Open a File", "*.*") + IF path <> "" THEN + StatusLabel.Caption = "Selected: " + path + PRINT "File: " + path + ELSE + StatusLabel.Caption = "No file selected." + END IF +END SUB + + +SUB OnRemoveClick + StatusLabel.Caption = "Button removed!" + RemoveControl frm, "BtnRemove" + PRINT "BtnRemove removed from form." +END SUB diff --git a/sdk/samples/basic/helpedit.frm b/sdk/samples/basic/helpedit.frm new file mode 100644 index 0000000..7ebef80 --- /dev/null +++ b/sdk/samples/basic/helpedit.frm @@ -0,0 +1,205 @@ +VERSION DVX 1.00 +' helpedit.frm -- DVX Help Editor +' +' A .dhs help source editor with syntax highlighting and +' live preview via the DVX Help Viewer. +' +' Add commdlg.bas and help.bas to your project, then click Run. + +Begin Form HelpEdit + Caption = "DVX Help Editor" + Layout = VBox + AutoSize = False + Resizable = True + Centered = True + Width = 640 + Height = 440 + Begin HBox ToolBar + Weight = 0 + Begin CommandButton BtnNew + Caption = "New" + MinWidth = 60 + End + Begin CommandButton BtnOpen + Caption = "Open" + MinWidth = 60 + End + Begin CommandButton BtnSave + Caption = "Save" + MinWidth = 60 + End + Begin Spacer Spc1 + MinWidth = 16 + End + Begin CommandButton BtnCompile + Caption = "Compile" + MinWidth = 80 + End + Begin CommandButton BtnPreview + Caption = "Preview" + MinWidth = 80 + End + Begin Spacer Spc2 + Weight = 1 + End + Begin Label LblStatus + Caption = "Ready." + MinWidth = 150 + End + End + Begin TextArea Editor + Weight = 1 + End +End + +DIM currentFile AS STRING +DIM hlpFile AS STRING +currentFile = "" +hlpFile = "" + +Load HelpEdit +HelpEdit.Show + +' Configure the editor +Editor.SetSyntaxMode "dhs" +Editor.SetShowLineNumbers True +Editor.SetAutoIndent True +Editor.SetCaptureTabs True +Editor.SetTabWidth 2 + +' Start with a template +Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10) + "This is your help file. Edit the .dhs source here," + CHR$(10) + "then click Compile and Preview to see the result." + CHR$(10) + +LblStatus.Caption = "New file." + +DO + DoEvents +LOOP + + +SUB BtnNew_Click + currentFile = "" + hlpFile = "" + Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10) + LblStatus.Caption = "New file." + HelpEdit.Caption = "DVX Help Editor" +END SUB + + +SUB BtnOpen_Click + DIM path AS STRING + path = basFileOpen("Open Help Source", "*.dhs") + IF path = "" THEN + EXIT SUB + END IF + + ' Read the entire file + DIM text AS STRING + DIM line AS STRING + text = "" + + OPEN path FOR INPUT AS #1 + DO WHILE NOT EOF(1) + LINE INPUT #1, line + IF text <> "" THEN + text = text + CHR$(10) + END IF + text = text + line + LOOP + CLOSE #1 + + Editor.Text = text + currentFile = path + hlpFile = "" + LblStatus.Caption = path + HelpEdit.Caption = "DVX Help Editor - " + path +END SUB + + +SUB BtnSave_Click + DIM path AS STRING + + IF currentFile <> "" THEN + path = currentFile + ELSE + path = basFileSave("Save Help Source", "*.dhs") + IF path = "" THEN + EXIT SUB + END IF + END IF + + ' Write the editor contents to file + OPEN path FOR OUTPUT AS #1 + PRINT #1, Editor.Text + CLOSE #1 + + currentFile = path + LblStatus.Caption = "Saved: " + path + HelpEdit.Caption = "DVX Help Editor - " + path +END SUB + + +SUB BtnCompile_Click + ' Save first if we have a file + IF currentFile = "" THEN + DIM path AS STRING + path = basFileSave("Save Help Source", "*.dhs") + IF path = "" THEN + LblStatus.Caption = "Save cancelled." + EXIT SUB + END IF + + OPEN path FOR OUTPUT AS #1 + PRINT #1, Editor.Text + CLOSE #1 + currentFile = path + ELSE + OPEN currentFile FOR OUTPUT AS #1 + PRINT #1, Editor.Text + CLOSE #1 + END IF + + ' Derive .hlp filename from .dhs filename + DIM baseName AS STRING + baseName = currentFile + + ' Strip .dhs extension if present + IF LEN(baseName) > 4 THEN + IF UCASE$(RIGHT$(baseName, 4)) = ".DHS" THEN + baseName = LEFT$(baseName, LEN(baseName) - 4) + END IF + END IF + + hlpFile = baseName + ".hlp" + + LblStatus.Caption = "Compiling..." + DoEvents + + IF HelpCompile(currentFile, hlpFile) THEN + LblStatus.Caption = "Compiled: " + hlpFile + ELSE + LblStatus.Caption = "Compile failed!" + hlpFile = "" + END IF +END SUB + + +SUB BtnPreview_Click + IF hlpFile = "" THEN + ' Try compiling first + BtnCompile_Click + END IF + + IF hlpFile = "" THEN + LblStatus.Caption = "Nothing to preview. Compile first." + EXIT SUB + END IF + + LblStatus.Caption = "Opening viewer..." + HelpView hlpFile +END SUB + + +SUB HelpEdit_QueryUnload(Cancel AS INTEGER) + ' Could prompt to save here +END SUB diff --git a/apps/dvxbasic/samples/ICON32.BMP b/sdk/samples/basic/iconed/ICON32.BMP similarity index 100% rename from apps/dvxbasic/samples/ICON32.BMP rename to sdk/samples/basic/iconed/ICON32.BMP diff --git a/sdk/samples/basic/sqltest.bas b/sdk/samples/basic/sqltest.bas new file mode 100644 index 0000000..70707a3 --- /dev/null +++ b/sdk/samples/basic/sqltest.bas @@ -0,0 +1,72 @@ +' sqltest.bas -- SQL Database Demo +' +' Demonstrates the SQL include library: +' SQLOpen / SQLClose - open and close a database +' SQLExec - execute non-query statements +' SQLQuery / SQLNext - run a query and iterate rows +' SQLField$ / SQLFieldInt / SQLFieldDbl - read column values +' SQLFieldCount / SQLFieldName$ - inspect result schema +' SQLEof / SQLFreeResult / SQLAffected - cursor and status +' SQLError$ - error reporting +' +' Add sql.bas to your project, then click Run. + +DIM db AS LONG +DIM rs AS LONG + +PRINT "Opening database..." +db = SQLOpen("sqltest.db") + +IF db = 0 THEN + PRINT "Failed to open database!" + END +END IF + +PRINT "Database opened (handle"; db; ")" +PRINT + +' Create table and insert sample data +SQLExec db, "CREATE TABLE IF NOT EXISTS parts (id INTEGER PRIMARY KEY, name TEXT, price REAL)" +SQLExec db, "DELETE FROM parts" +SQLExec db, "INSERT INTO parts VALUES (1, 'Resistor 10K', 0.05)" +SQLExec db, "INSERT INTO parts VALUES (2, 'Capacitor 100uF', 0.25)" +SQLExec db, "INSERT INTO parts VALUES (3, 'LED Red', 0.10)" +SQLExec db, "INSERT INTO parts VALUES (4, '555 Timer', 0.75)" +SQLExec db, "INSERT INTO parts VALUES (5, 'Arduino Nano', 12.50)" + +PRINT "Inserted 5 rows. Affected:"; SQLAffected(db) +PRINT + +' Query and display results +rs = SQLQuery(db, "SELECT * FROM parts ORDER BY price") + +IF rs = 0 THEN + PRINT "Query failed: "; SQLError$(db) + SQLClose db + END +END IF + +PRINT "Columns:"; SQLFieldCount(rs) + +DIM i AS INTEGER +FOR i = 0 TO SQLFieldCount(rs) - 1 + PRINT " ["; i; "] "; SQLFieldName$(rs, i) +NEXT i + +PRINT +PRINT "Parts (sorted by price):" +PRINT "-----------------------------------" + +DO WHILE SQLNext(rs) + PRINT "#"; SQLFieldInt(rs, 0); + PRINT " "; SQLField$(rs, "name"); + PRINT " $"; SQLFieldDbl(rs, 2) +LOOP + +PRINT "-----------------------------------" + +SQLFreeResult rs +SQLClose db + +PRINT +PRINT "Done!" diff --git a/shell/shellMain.c b/shell/shellMain.c index dc48dac..280fae1 100644 --- a/shell/shellMain.c +++ b/shell/shellMain.c @@ -283,8 +283,10 @@ int shellMain(int argc, char *argv[]) { else if (strcmp(accelStr, "medium") == 0) { accelVal = 64; } else if (strcmp(accelStr, "high") == 0) { accelVal = 32; } - dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal); - dvxLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s", wheelStr, (long)dblClick, accelStr); + int32_t speed = prefsGetInt(sPrefs, "mouse", "speed", 8); + + dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal, speed); + dvxLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s speed=%ld", wheelStr, (long)dblClick, accelStr, (long)speed); // Apply saved color scheme bool colorsLoaded = false; diff --git a/tools/dvxResWrite.h b/tools/dvxResWrite.h new file mode 100644 index 0000000..9893fc3 --- /dev/null +++ b/tools/dvxResWrite.h @@ -0,0 +1,280 @@ +// dvxResWrite.h -- DVX resource writing library interface +// +// Shared implementation for appending resources to DXE files. +// Used by both the dvxres command-line tool and the runtime +// dvxResAppend() function. +// +// Include this header in exactly one .c file. The tool includes +// it directly; the runtime includes it via dvxResource.c. + +#ifndef DVX_RES_WRITE_H +#define DVX_RES_WRITE_H + +#include "../core/dvxRes.h" + +#include +#include +#include + +// ============================================================ +// dvxResDxeContentSize +// ============================================================ +// +// Returns the size of the DXE3 content (before any appended +// resources). If the file has a resource footer, returns the +// start of the resource data. Otherwise returns the full file size. + +static long dvxResDxeContentSize(const char *path) { + FILE *f = fopen(path, "rb"); + + if (!f) { + return -1; + } + + fseek(f, 0, SEEK_END); + long fileSize = ftell(f); + + if (fileSize < (long)sizeof(DvxResFooterT)) { + fclose(f); + return fileSize; + } + + fseek(f, -(long)sizeof(DvxResFooterT), SEEK_END); + DvxResFooterT footer; + + if (fread(&footer, sizeof(footer), 1, f) == 1 && footer.magic == DVX_RES_MAGIC) { + fseek(f, footer.dirOffset, SEEK_SET); + DvxResDirEntryT entry; + long earliest = footer.dirOffset; + + for (uint32_t i = 0; i < footer.entryCount; i++) { + if (fread(&entry, sizeof(entry), 1, f) == 1) { + if ((long)entry.offset < earliest) { + earliest = (long)entry.offset; + } + } + } + + fclose(f); + return earliest; + } + + fclose(f); + return fileSize; +} + + +// ============================================================ +// dvxResReadExisting +// ============================================================ +// +// Reads any existing resource block from the file. Returns arrays +// of directory entries and data blobs. Caller frees everything. + +static int dvxResReadExisting(const char *path, long dxeSize, DvxResDirEntryT **outEntries, uint32_t *outCount, uint8_t ***outData) { + *outEntries = NULL; + *outCount = 0; + *outData = NULL; + + FILE *f = fopen(path, "rb"); + + if (!f) { + return -1; + } + + fseek(f, 0, SEEK_END); + long fileSize = ftell(f); + + if (fileSize <= dxeSize) { + fclose(f); + return 0; + } + + fseek(f, -(long)sizeof(DvxResFooterT), SEEK_END); + DvxResFooterT footer; + + if (fread(&footer, sizeof(footer), 1, f) != 1 || footer.magic != DVX_RES_MAGIC) { + fclose(f); + return 0; + } + + fseek(f, footer.dirOffset, SEEK_SET); + DvxResDirEntryT *entries = (DvxResDirEntryT *)malloc(footer.entryCount * sizeof(DvxResDirEntryT)); + + if (!entries) { + fclose(f); + return -1; + } + + if (fread(entries, sizeof(DvxResDirEntryT), footer.entryCount, f) != footer.entryCount) { + free(entries); + fclose(f); + return -1; + } + + uint8_t **data = (uint8_t **)malloc(footer.entryCount * sizeof(uint8_t *)); + + if (!data) { + free(entries); + fclose(f); + return -1; + } + + for (uint32_t i = 0; i < footer.entryCount; i++) { + data[i] = (uint8_t *)malloc(entries[i].size); + + if (!data[i]) { + for (uint32_t j = 0; j < i; j++) { + free(data[j]); + } + + free(data); + free(entries); + fclose(f); + return -1; + } + + fseek(f, entries[i].offset, SEEK_SET); + + if (fread(data[i], 1, entries[i].size, f) != entries[i].size) { + for (uint32_t j = 0; j <= i; j++) { + free(data[j]); + } + + free(data); + free(entries); + fclose(f); + return -1; + } + } + + fclose(f); + *outEntries = entries; + *outCount = footer.entryCount; + *outData = data; + return 0; +} + + +// ============================================================ +// dvxResWriteBlock +// ============================================================ +// +// Truncates the file to dxeSize and writes the resource block. + +static int dvxResWriteBlock(const char *path, long dxeSize, DvxResDirEntryT *entries, uint32_t count, uint8_t **data) { + FILE *f = fopen(path, "rb"); + + if (!f) { + return -1; + } + + uint8_t *dxeBuf = (uint8_t *)malloc(dxeSize); + + if (!dxeBuf) { + fclose(f); + return -1; + } + + if (fread(dxeBuf, 1, dxeSize, f) != (size_t)dxeSize) { + free(dxeBuf); + fclose(f); + return -1; + } + + fclose(f); + + f = fopen(path, "wb"); + + if (!f) { + free(dxeBuf); + return -1; + } + + fwrite(dxeBuf, 1, dxeSize, f); + free(dxeBuf); + + for (uint32_t i = 0; i < count; i++) { + entries[i].offset = (uint32_t)ftell(f); + fwrite(data[i], 1, entries[i].size, f); + } + + uint32_t dirOffset = (uint32_t)ftell(f); + fwrite(entries, sizeof(DvxResDirEntryT), count, f); + + DvxResFooterT footer; + footer.magic = DVX_RES_MAGIC; + footer.dirOffset = dirOffset; + footer.entryCount = count; + footer.reserved = 0; + fwrite(&footer, sizeof(footer), 1, f); + + fclose(f); + return 0; +} + + +// ============================================================ +// dvxResAppendEntry +// ============================================================ +// +// High-level: append a single resource to a DXE file. +// Reads existing resources, adds the new one (replacing if +// same name exists), and rewrites the resource block. +// Returns 0 on success. + +static int dvxResAppendEntry(const char *path, const char *name, uint32_t type, const void *resData, uint32_t resSize) { + if (!path || !name || !resData || resSize == 0) { + return -1; + } + + long dxeSize = dvxResDxeContentSize(path); + + if (dxeSize < 0) { + return -1; + } + + DvxResDirEntryT *entries = NULL; + uint32_t count = 0; + uint8_t **data = NULL; + dvxResReadExisting(path, dxeSize, &entries, &count, &data); + + // Remove existing entry with same name + for (uint32_t i = 0; i < count; i++) { + if (strcmp(entries[i].name, name) == 0) { + free(data[i]); + + for (uint32_t j = i; j < count - 1; j++) { + entries[j] = entries[j + 1]; + data[j] = data[j + 1]; + } + + count--; + break; + } + } + + // Add new entry + entries = (DvxResDirEntryT *)realloc(entries, (count + 1) * sizeof(DvxResDirEntryT)); + data = (uint8_t **)realloc(data, (count + 1) * sizeof(uint8_t *)); + + memset(&entries[count], 0, sizeof(DvxResDirEntryT)); + strncpy(entries[count].name, name, DVX_RES_NAME_MAX - 1); + entries[count].type = type; + entries[count].size = resSize; + data[count] = (uint8_t *)malloc(resSize); + memcpy(data[count], resData, resSize); + count++; + + int result = dvxResWriteBlock(path, dxeSize, entries, count, data); + + for (uint32_t i = 0; i < count; i++) { + free(data[i]); + } + + free(data); + free(entries); + return result; +} + +#endif // DVX_RES_WRITE_H diff --git a/tools/dvxres.c b/tools/dvxres.c index 86586b0..0f92864 100644 --- a/tools/dvxres.c +++ b/tools/dvxres.c @@ -16,13 +16,10 @@ // [resource directory] // [footer with magic + directory offset + count] -#include "../core/dvxRes.h" +#include "dvxResWrite.h" #include #include -#include -#include -#include // ============================================================ // Prototypes @@ -33,62 +30,10 @@ static int cmdBuild(const char *dxePath, const char *manifestPath); static int cmdGet(const char *dxePath, const char *name, const char *outPath); static int cmdList(const char *dxePath); static int cmdStrip(const char *dxePath); -static long dxeContentSize(const char *path); static int parseType(const char *typeStr); -static int readExistingResources(const char *path, long dxeSize, DvxResDirEntryT **outEntries, uint32_t *outCount, uint8_t ***outData); static void usage(void); -// ============================================================ -// dxeContentSize -// ============================================================ -// -// Returns the size of the DXE3 content (before any appended resources). -// If the file has a resource footer, returns the start of the resource -// data. Otherwise returns the full file size. - -static long dxeContentSize(const char *path) { - FILE *f = fopen(path, "rb"); - - if (!f) { - return -1; - } - - fseek(f, 0, SEEK_END); - long fileSize = ftell(f); - - if (fileSize < (long)sizeof(DvxResFooterT)) { - fclose(f); - return fileSize; - } - - // Check for resource footer - fseek(f, -(long)sizeof(DvxResFooterT), SEEK_END); - DvxResFooterT footer; - - if (fread(&footer, sizeof(footer), 1, f) == 1 && footer.magic == DVX_RES_MAGIC) { - // Find the earliest resource data offset - fseek(f, footer.dirOffset, SEEK_SET); - DvxResDirEntryT entry; - long earliest = footer.dirOffset; - - for (uint32_t i = 0; i < footer.entryCount; i++) { - if (fread(&entry, sizeof(entry), 1, f) == 1) { - if ((long)entry.offset < earliest) { - earliest = (long)entry.offset; - } - } - } - - fclose(f); - return earliest; - } - - fclose(f); - return fileSize; -} - - // ============================================================ // parseType // ============================================================ @@ -110,164 +55,6 @@ static int parseType(const char *typeStr) { } -// ============================================================ -// readExistingResources -// ============================================================ -// -// Reads any existing resource block from the file. Returns arrays -// of directory entries and data blobs. Caller frees everything. - -static int readExistingResources(const char *path, long dxeSize, DvxResDirEntryT **outEntries, uint32_t *outCount, uint8_t ***outData) { - *outEntries = NULL; - *outCount = 0; - *outData = NULL; - - FILE *f = fopen(path, "rb"); - - if (!f) { - return -1; - } - - fseek(f, 0, SEEK_END); - long fileSize = ftell(f); - - if (fileSize <= dxeSize) { - fclose(f); - return 0; - } - - // Check for footer - fseek(f, -(long)sizeof(DvxResFooterT), SEEK_END); - DvxResFooterT footer; - - if (fread(&footer, sizeof(footer), 1, f) != 1 || footer.magic != DVX_RES_MAGIC) { - fclose(f); - return 0; - } - - // Read directory - fseek(f, footer.dirOffset, SEEK_SET); - DvxResDirEntryT *entries = (DvxResDirEntryT *)malloc(footer.entryCount * sizeof(DvxResDirEntryT)); - - if (!entries) { - fclose(f); - return -1; - } - - if (fread(entries, sizeof(DvxResDirEntryT), footer.entryCount, f) != footer.entryCount) { - free(entries); - fclose(f); - return -1; - } - - // Read each resource's data - uint8_t **data = (uint8_t **)malloc(footer.entryCount * sizeof(uint8_t *)); - - if (!data) { - free(entries); - fclose(f); - return -1; - } - - for (uint32_t i = 0; i < footer.entryCount; i++) { - data[i] = (uint8_t *)malloc(entries[i].size); - - if (!data[i]) { - for (uint32_t j = 0; j < i; j++) { - free(data[j]); - } - - free(data); - free(entries); - fclose(f); - return -1; - } - - fseek(f, entries[i].offset, SEEK_SET); - - if (fread(data[i], 1, entries[i].size, f) != entries[i].size) { - fprintf(stderr, "Short read on entry %d\n", (int)i); - for (uint32_t j = 0; j <= i; j++) { - free(data[j]); - } - free(data); - free(entries); - fclose(f); - return -1; - } - } - - fclose(f); - *outEntries = entries; - *outCount = footer.entryCount; - *outData = data; - return 0; -} - - -// ============================================================ -// writeResources -// ============================================================ -// -// Truncates the file to dxeSize and writes the resource block. - -static int writeResources(const char *path, long dxeSize, DvxResDirEntryT *entries, uint32_t count, uint8_t **data) { - // Read the DXE content first - FILE *f = fopen(path, "rb"); - - if (!f) { - return -1; - } - - uint8_t *dxeBuf = (uint8_t *)malloc(dxeSize); - - if (!dxeBuf) { - fclose(f); - return -1; - } - - if (fread(dxeBuf, 1, dxeSize, f) != (size_t)dxeSize) { - free(dxeBuf); - fclose(f); - return -1; - } - - fclose(f); - - // Rewrite file: DXE content + resources - f = fopen(path, "wb"); - - if (!f) { - free(dxeBuf); - return -1; - } - - fwrite(dxeBuf, 1, dxeSize, f); - free(dxeBuf); - - // Write resource data entries and update offsets - for (uint32_t i = 0; i < count; i++) { - entries[i].offset = (uint32_t)ftell(f); - fwrite(data[i], 1, entries[i].size, f); - } - - // Write directory - uint32_t dirOffset = (uint32_t)ftell(f); - fwrite(entries, sizeof(DvxResDirEntryT), count, f); - - // Write footer - DvxResFooterT footer; - footer.magic = DVX_RES_MAGIC; - footer.dirOffset = dirOffset; - footer.entryCount = count; - footer.reserved = 0; - fwrite(&footer, sizeof(footer), 1, f); - - fclose(f); - return 0; -} - - // ============================================================ // cmdAdd // ============================================================ @@ -285,40 +72,11 @@ static int cmdAdd(const char *dxePath, const char *name, const char *typeStr, co return 1; } - long dxeSize = dxeContentSize(dxePath); - - if (dxeSize < 0) { - fprintf(stderr, "Cannot open: %s\n", dxePath); - return 1; - } - - // Read existing resources - DvxResDirEntryT *entries = NULL; - uint32_t count = 0; - uint8_t **data = NULL; - readExistingResources(dxePath, dxeSize, &entries, &count, &data); - - // Remove existing entry with same name (replace) - for (uint32_t i = 0; i < count; i++) { - if (strcmp(entries[i].name, name) == 0) { - free(data[i]); - - for (uint32_t j = i; j < count - 1; j++) { - entries[j] = entries[j + 1]; - data[j] = data[j + 1]; - } - - count--; - break; - } - } - - // Prepare new resource data + // Prepare resource data uint8_t *newData = NULL; uint32_t newSize = 0; if (type == DVX_RES_TEXT) { - // Text data from command line argument newSize = (uint32_t)strlen(dataArg) + 1; newData = (uint8_t *)malloc(newSize); @@ -329,11 +87,17 @@ static int cmdAdd(const char *dxePath, const char *name, const char *typeStr, co memcpy(newData, dataArg, newSize); } else { - // Binary/icon data from file - FILE *df = fopen(dataArg, "rb"); + // Binary/icon: dataArg is a file path (strip leading @ if present) + const char *filePath = dataArg; + + if (filePath[0] == '@') { + filePath++; + } + + FILE *df = fopen(filePath, "rb"); if (!df) { - fprintf(stderr, "Cannot open data file: %s\n", dataArg); + fprintf(stderr, "Cannot open data file: %s\n", filePath); return 1; } @@ -352,34 +116,15 @@ static int cmdAdd(const char *dxePath, const char *name, const char *typeStr, co if (fread(newData, 1, newSize, df) != newSize) { fclose(df); free(newData); - fprintf(stderr, "Failed to read: %s\n", dataArg); + fprintf(stderr, "Failed to read: %s\n", filePath); return 1; } fclose(df); } - // Append to arrays - entries = (DvxResDirEntryT *)realloc(entries, (count + 1) * sizeof(DvxResDirEntryT)); - data = (uint8_t **)realloc(data, (count + 1) * sizeof(uint8_t *)); - - memset(&entries[count], 0, sizeof(DvxResDirEntryT)); - strncpy(entries[count].name, name, DVX_RES_NAME_MAX - 1); - entries[count].type = (uint32_t)type; - entries[count].size = newSize; - data[count] = newData; - count++; - - // Write - int result = writeResources(dxePath, dxeSize, entries, count, data); - - // Cleanup - for (uint32_t i = 0; i < count; i++) { - free(data[i]); - } - - free(data); - free(entries); + int result = dvxResAppendEntry(dxePath, name, (uint32_t)type, newData, newSize); + free(newData); if (result != 0) { fprintf(stderr, "Failed to write resources to: %s\n", dxePath); @@ -403,7 +148,7 @@ static int cmdBuild(const char *dxePath, const char *manifestPath) { return 1; } - long dxeSize = dxeContentSize(dxePath); + long dxeSize = dvxResDxeContentSize(dxePath); if (dxeSize < 0) { fclose(mf); @@ -565,7 +310,7 @@ static int cmdBuild(const char *dxePath, const char *manifestPath) { return 0; } - int result = writeResources(dxePath, dxeSize, entries, count, data); + int result = dvxResWriteBlock(dxePath, dxeSize, entries, count, data); for (uint32_t i = 0; i < count; i++) { free(data[i]); @@ -696,7 +441,7 @@ static int cmdGet(const char *dxePath, const char *name, const char *outPath) { // ============================================================ static int cmdStrip(const char *dxePath) { - long dxeSize = dxeContentSize(dxePath); + long dxeSize = dvxResDxeContentSize(dxePath); if (dxeSize < 0) { fprintf(stderr, "Cannot open: %s\n", dxePath); diff --git a/widgets/ansiTerm/ansiterm.bhs b/widgets/ansiTerm/ansiterm.bhs index ed95f65..179b96c 100644 --- a/widgets/ansiTerm/ansiterm.bhs +++ b/widgets/ansiTerm/ansiterm.bhs @@ -22,15 +22,32 @@ A VT100/ANSI terminal emulator widget. Supports ANSI escape sequences, scrollbac Scrollback Integer Number of scrollback lines (write-only). .endtable +.index CommAttach +.index CommOpen + .h2 Type-Specific Methods .table - Method Parameters Description - ------ --------------- ------------------------------------------- - Clear (none) Clear the terminal screen. - Write Text As String Write text (with ANSI escape processing) to the terminal. + Method Description + ------ ----------- + Clear Clear the terminal screen. + Poll Process pending comm data and update the display. + Write text$ Write text (with ANSI escape processing) to the terminal. .endtable No default event. +.h2 Serial Communication + +Use the comm.bas library to connect a Terminal to a serial link: + +.code +Dim link As Integer +link = CommOpen(1, 115200) +CommHandshake link +CommAttach link, "Terminal1" +.endcode + +See comm.bas for the full communications API. + .link ctrl.common.props Common Properties, Events, and Methods diff --git a/widgets/ansiTerm/widgetAnsiTerm.c b/widgets/ansiTerm/widgetAnsiTerm.c index d3097a6..b1be3d2 100644 --- a/widgets/ansiTerm/widgetAnsiTerm.c +++ b/widgets/ansiTerm/widgetAnsiTerm.c @@ -1077,6 +1077,7 @@ static const WgtPropDescT sProps[] = { static const WgtMethodDescT sMethods[] = { { "Clear", WGT_SIG_VOID, (void *)wgtAnsiTermClear }, + { "Poll", WGT_SIG_VOID, (void *)wgtAnsiTermPoll }, { "Write", WGT_SIG_STR, (void *)wgtAnsiTermWriteString } }; @@ -1085,7 +1086,7 @@ static const WgtIfaceT sIface = { .props = sProps, .propCount = 3, .methods = sMethods, - .methodCount = 2, + .methodCount = 3, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_INT_INT, diff --git a/widgets/canvas/canvas.bhs b/widgets/canvas/canvas.bhs index 0ce0bf4..51e9f4c 100644 --- a/widgets/canvas/canvas.bhs +++ b/widgets/canvas/canvas.bhs @@ -4,6 +4,8 @@ .index PictureBox .index Canvas .index Drawing +.index SetPenColor +.index SetPenSize .h1 PictureBox @@ -11,16 +13,40 @@ VB Equivalent: PictureBox -- DVX Widget: canvas | Name Prefix: Picture A drawing surface (canvas). Supports drawing lines, rectangles, circles, text, and individual pixels. Can save and load BMP images. The default canvas size is 64x64 pixels. +Colors are specified as 0x00RRGGBB integers (e.g. &HFF0000 for red). + .h2 Type-Specific Methods .table - Method Parameters Description - ------ ---------------- ------------------------------------------- - Clear Color As Integer Fill the entire canvas with the specified color. + Method Description + ------ ----------- + Clear color% Fill the entire canvas with the specified color. + DrawLine x0%, y0%, x1%, y1%, color% Draw a line between two points. + DrawRect x%, y%, w%, h%, color% Draw a rectangle outline. + DrawText x%, y%, text$ Draw text at the given position. + FillCircle cx%, cy%, radius%, color% Draw a filled circle. + FillRect x%, y%, w%, h%, color% Draw a filled rectangle. + GetPixel(x%, y%) Returns the color at a pixel. + Load path$ Load a BMP image onto the canvas. + Resize w%, h% Resize the canvas to new dimensions. + Save path$ Save the canvas as a BMP file. + SetPenColor color% Set the drawing color for subsequent operations. + SetPenSize size% Set the pen/brush size in pixels. + SetPixel x%, y%, color% Set the color of a single pixel. .endtable -Additional drawing methods (DrawLine, DrawRect, FillRect, FillCircle, SetPixel, GetPixel, DrawText, Save, Load) are available through the C API but not currently exposed through BASIC interface descriptors. - Default Event: Click +.h2 Example + +.code +Picture1.Resize 200, 150 +Picture1.Clear &HFFFFFF +Picture1.SetPenSize 2 +Picture1.DrawRect 10, 10, 180, 130, &H000000 +Picture1.FillCircle 100, 75, 40, &HFF0000 +Picture1.DrawText 60, 130, "Hello!" +Picture1.Save "output.bmp" +.endcode + .link ctrl.common.props Common Properties, Events, and Methods diff --git a/widgets/canvas/widgetCanvas.c b/widgets/canvas/widgetCanvas.c index 9232744..2f67d1c 100644 --- a/widgets/canvas/widgetCanvas.c +++ b/widgets/canvas/widgetCanvas.c @@ -1044,18 +1044,24 @@ static const struct { .resize = wgtCanvasResize }; +static void basSetPenColor(WidgetT *w, int32_t color) { + wgtCanvasSetPenColor(w, rgbToPacked(w, (uint32_t)color)); +} + static const WgtMethodDescT sMethods[] = { - { "Clear", WGT_SIG_INT, (void *)basClear }, - { "DrawLine", WGT_SIG_INT5, (void *)basDrawLine }, - { "DrawRect", WGT_SIG_INT5, (void *)basDrawRect }, - { "DrawText", WGT_SIG_INT_INT_STR,(void *)wgtCanvasDrawText }, - { "FillCircle", WGT_SIG_INT4, (void *)basFillCircle }, - { "FillRect", WGT_SIG_INT5, (void *)basFillRect }, - { "GetPixel", WGT_SIG_RET_INT_INT_INT, (void *)basGetPixel }, - { "Load", WGT_SIG_STR, (void *)wgtCanvasLoad }, - { "Save", WGT_SIG_STR, (void *)wgtCanvasSave }, - { "Resize", WGT_SIG_INT_INT, (void *)wgtCanvasResize }, - { "SetPixel", WGT_SIG_INT3, (void *)basSetPixel }, + { "Clear", WGT_SIG_INT, (void *)basClear }, + { "DrawLine", WGT_SIG_INT5, (void *)basDrawLine }, + { "DrawRect", WGT_SIG_INT5, (void *)basDrawRect }, + { "DrawText", WGT_SIG_INT_INT_STR, (void *)wgtCanvasDrawText }, + { "FillCircle", WGT_SIG_INT4, (void *)basFillCircle }, + { "FillRect", WGT_SIG_INT5, (void *)basFillRect }, + { "GetPixel", WGT_SIG_RET_INT_INT_INT, (void *)basGetPixel }, + { "Load", WGT_SIG_STR, (void *)wgtCanvasLoad }, + { "Resize", WGT_SIG_INT_INT, (void *)wgtCanvasResize }, + { "Save", WGT_SIG_STR, (void *)wgtCanvasSave }, + { "SetPenColor", WGT_SIG_INT, (void *)basSetPenColor }, + { "SetPenSize", WGT_SIG_INT, (void *)wgtCanvasSetPenSize }, + { "SetPixel", WGT_SIG_INT3, (void *)basSetPixel }, }; static const WgtIfaceT sIface = { @@ -1063,7 +1069,7 @@ static const WgtIfaceT sIface = { .props = NULL, .propCount = 0, .methods = sMethods, - .methodCount = 11, + .methodCount = 13, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_INT_INT, diff --git a/widgets/listBox/listbox.bhs b/widgets/listBox/listbox.bhs index e11a1a6..8ae8e17 100644 --- a/widgets/listBox/listbox.bhs +++ b/widgets/listBox/listbox.bhs @@ -37,6 +37,7 @@ A scrollable list of selectable items. Items are managed via methods (AddItem, R SetReorderable Reorderable As Boolean Enable or disable drag-to-reorder. IsItemSelected Index As Integer Returns True if the item at Index is selected. SetItemSelected Index As Integer, Selected As Boolean Select or deselect a specific item. + SetItems Items As String Bulk-load items from a pipe-delimited string (e.g. "Red|Green|Blue"). .endtable Default Event: Click diff --git a/widgets/listBox/widgetListBox.c b/widgets/listBox/widgetListBox.c index b53e490..e4dda61 100644 --- a/widgets/listBox/widgetListBox.c +++ b/widgets/listBox/widgetListBox.c @@ -940,6 +940,40 @@ static const struct { .getItemCount = wgtListBoxGetItemCount }; +// BASIC wrapper: SetItems "Item1|Item2|Item3" -- bulk load from pipe-delimited string +#define BAS_MAX_LISTBOX_ITEMS 256 + +static void basSetItems(WidgetT *w, const char *spec) { + static char buf[4096]; + static const char *ptrs[BAS_MAX_LISTBOX_ITEMS]; + + if (!spec || !*spec) { + return; + } + + snprintf(buf, sizeof(buf), "%s", spec); + + int32_t count = 0; + char *tok = buf; + + while (*tok && count < BAS_MAX_LISTBOX_ITEMS) { + ptrs[count++] = tok; + char *sep = strchr(tok, '|'); + + if (sep) { + *sep = '\0'; + tok = sep + 1; + } else { + break; + } + } + + if (count > 0) { + wgtListBoxSetItems(w, ptrs, count); + } +} + + static const WgtPropDescT sProps[] = { { "ListIndex", WGT_IFACE_INT, (void *)wgtListBoxGetSelected, (void *)wgtListBoxSetSelected, NULL } }; @@ -954,6 +988,7 @@ static const WgtMethodDescT sMethods[] = { { "RemoveItem", WGT_SIG_INT, (void *)wgtListBoxRemoveItem }, { "SelectAll", WGT_SIG_VOID, (void *)wgtListBoxSelectAll }, { "SetItemSelected", WGT_SIG_INT_BOOL, (void *)wgtListBoxSetItemSelected }, + { "SetItems", WGT_SIG_STR, (void *)basSetItems }, { "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtListBoxSetMultiSelect }, { "SetReorderable", WGT_SIG_BOOL, (void *)wgtListBoxSetReorderable }, }; @@ -963,7 +998,7 @@ static const WgtIfaceT sIface = { .props = sProps, .propCount = 1, .methods = sMethods, - .methodCount = 11, + .methodCount = 12, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT, diff --git a/widgets/listView/listview.bhs b/widgets/listView/listview.bhs index 904c99b..e4f92e3 100644 --- a/widgets/listView/listview.bhs +++ b/widgets/listView/listview.bhs @@ -4,34 +4,54 @@ .toc 1 ListView .index ListView .index Multi-Column List +.index SetColumns +.index SetSort .h1 ListView VB Equivalent: ListView -- DVX Widget: listview -A multi-column list with column headers. Supports sorting, multi-select, and drag-to-reorder. Columns are configured via the C API (SetColumns, SetData). +A multi-column list with column headers. Supports sorting, multi-select, and drag-to-reorder. .h2 Type-Specific Properties .table - Property Type Description - --------- ------- ------------------------------------------- - ListIndex Integer Index of the currently selected row (-1 = none). + Property Type R/W Description + --------- ------- --- ------------------------------------------- + ListIndex Integer R/W Index of the currently selected row (-1 = none). .endtable .h2 Type-Specific Methods .table - Method Parameters Description - --------------- --------------------------------------- ------------------------------------------- - SelectAll (none) Select all rows. - ClearSelection (none) Deselect all rows. - SetMultiSelect Multi As Boolean Enable or disable multi-select. - SetReorderable Reorderable As Boolean Enable or disable row reordering. - IsItemSelected Index As Integer Returns True if the row at Index is selected. - SetItemSelected Index As Integer, Selected As Boolean Select or deselect a specific row. + Method Description + ------ ----------- + AddItem text$ Add a row (sets first column text). + Clear Remove all rows. + ClearSelection Deselect all rows. + GetCell$(row%, col%) Returns the text of a cell. + IsItemSelected(index%) Returns True if the row is selected. + RemoveItem index% Remove a row by index. + RowCount() Returns the number of rows. + SelectAll Select all rows. + SetCell row%, col%, text$ Set the text of a cell. + SetColumns spec$ Define columns. Pipe-delimited "Name,Width|Name,Width" (width in characters). + SetItemSelected index%, selected Select or deselect a row. + SetMultiSelect multi Enable or disable multi-select. + SetReorderable reorderable Enable or disable row reordering. + SetSort col%, dir% Sort by column. dir: 0=none, 1=ascending, 2=descending. .endtable Default Event: Click +.h2 Example + +.code +ListView1.SetColumns "Name,20|Size,10|Type,15" +ListView1.AddItem "readme.txt" +ListView1.SetCell 0, 1, "1024" +ListView1.SetCell 0, 2, "Text" +ListView1.SetSort 0, 1 +.endcode + .link ctrl.common.props Common Properties, Events, and Methods diff --git a/widgets/listView/widgetListView.c b/widgets/listView/widgetListView.c index 7dc668c..3228ae2 100644 --- a/widgets/listView/widgetListView.c +++ b/widgets/listView/widgetListView.c @@ -1937,23 +1937,76 @@ static const struct { .getRowCount = wgtListViewGetRowCount }; +// BASIC wrapper: SetColumns "Name,100|Description,200" +#define BAS_MAX_LISTVIEW_COLS 32 + +static void basSetColumns(WidgetT *w, const char *spec) { + if (!spec || !*spec) { + return; + } + + static char buf[1024]; + snprintf(buf, sizeof(buf), "%s", spec); + + ListViewColT cols[BAS_MAX_LISTVIEW_COLS]; + int32_t count = 0; + char *tok = buf; + + while (*tok && count < BAS_MAX_LISTVIEW_COLS) { + // Parse "Title,Width" or just "Title" + char *sep = strchr(tok, '|'); + + if (sep) { + *sep = '\0'; + } + + char *comma = strchr(tok, ','); + + if (comma) { + *comma = '\0'; + cols[count].title = tok; + cols[count].width = wgtChars(atoi(comma + 1)); + cols[count].align = ListViewAlignLeftE; + } else { + cols[count].title = tok; + cols[count].width = 0; + cols[count].align = ListViewAlignLeftE; + } + + count++; + + if (sep) { + tok = sep + 1; + } else { + break; + } + } + + if (count > 0) { + wgtListViewSetColumns(w, cols, count); + } +} + + static const WgtPropDescT sProps[] = { { "ListIndex", WGT_IFACE_INT, (void *)wgtListViewGetSelected, (void *)wgtListViewSetSelected, NULL } }; static const WgtMethodDescT sMethods[] = { - { "AddItem", WGT_SIG_STR, (void *)wgtListViewAddItem }, - { "Clear", WGT_SIG_VOID, (void *)wgtListViewClear }, - { "ClearSelection", WGT_SIG_VOID, (void *)wgtListViewClearSelection }, + { "AddItem", WGT_SIG_STR, (void *)wgtListViewAddItem }, + { "Clear", WGT_SIG_VOID, (void *)wgtListViewClear }, + { "ClearSelection", WGT_SIG_VOID, (void *)wgtListViewClearSelection }, { "GetCell", WGT_SIG_RET_STR_INT_INT, (void *)wgtListViewGetCell }, - { "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)wgtListViewIsItemSelected }, - { "RemoveItem", WGT_SIG_INT, (void *)wgtListViewRemoveRow }, - { "RowCount", WGT_SIG_RET_INT, (void *)wgtListViewGetRowCount }, - { "SelectAll", WGT_SIG_VOID, (void *)wgtListViewSelectAll }, - { "SetCell", WGT_SIG_INT_INT_STR, (void *)wgtListViewSetCell }, - { "SetItemSelected", WGT_SIG_INT_BOOL, (void *)wgtListViewSetItemSelected }, - { "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtListViewSetMultiSelect }, - { "SetReorderable", WGT_SIG_BOOL, (void *)wgtListViewSetReorderable }, + { "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)wgtListViewIsItemSelected }, + { "RemoveItem", WGT_SIG_INT, (void *)wgtListViewRemoveRow }, + { "RowCount", WGT_SIG_RET_INT, (void *)wgtListViewGetRowCount }, + { "SelectAll", WGT_SIG_VOID, (void *)wgtListViewSelectAll }, + { "SetCell", WGT_SIG_INT_INT_STR, (void *)wgtListViewSetCell }, + { "SetColumns", WGT_SIG_STR, (void *)basSetColumns }, + { "SetItemSelected", WGT_SIG_INT_BOOL, (void *)wgtListViewSetItemSelected }, + { "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtListViewSetMultiSelect }, + { "SetReorderable", WGT_SIG_BOOL, (void *)wgtListViewSetReorderable }, + { "SetSort", WGT_SIG_INT_INT, (void *)wgtListViewSetSort }, }; static const WgtIfaceT sIface = { @@ -1961,7 +2014,7 @@ static const WgtIfaceT sIface = { .props = sProps, .propCount = 1, .methods = sMethods, - .methodCount = 12, + .methodCount = 14, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT, diff --git a/widgets/tabControl/tabctrl.bhs b/widgets/tabControl/tabctrl.bhs index 8d5b422..7074aee 100644 --- a/widgets/tabControl/tabctrl.bhs +++ b/widgets/tabControl/tabctrl.bhs @@ -23,9 +23,11 @@ A tabbed container. Each tab page is a separate container that holds child contr .h2 Type-Specific Methods .table - Method Parameters Description - --------- ----------------- ------------------------------------------- - SetActive Index As Integer Switch to the tab at the given index. This is the recommended way to change tabs at runtime (the TabIndex property is shadowed by the common property handler). + Method Description + ------ ----------- + AddPage title$ Add a new tab page with the given title. + GetActive() Returns the index of the active tab. + SetActive index% Switch to the tab at the given index. This is the recommended way to change tabs at runtime (the TabIndex property is shadowed by the common property handler). .endtable Container: Yes diff --git a/widgets/tabControl/widgetTabControl.c b/widgets/tabControl/widgetTabControl.c index 57e0cc4..842a58e 100644 --- a/widgets/tabControl/widgetTabControl.c +++ b/widgets/tabControl/widgetTabControl.c @@ -699,8 +699,16 @@ static const WgtPropDescT sProps[] = { { "TabIndex", WGT_IFACE_INT, (void *)wgtTabControlGetActive, (void *)wgtTabControlSetActive, NULL } }; +// BASIC wrapper: AddPage creates a tab page and returns nothing. +// The page is accessible by index via SetActive. +static void basAddPage(WidgetT *w, const char *title) { + wgtTabPage(w, title); +} + static const WgtMethodDescT sMethods[] = { - { "SetActive", WGT_SIG_INT, (void *)wgtTabControlSetActive } + { "AddPage", WGT_SIG_STR, (void *)basAddPage }, + { "GetActive", WGT_SIG_RET_INT, (void *)wgtTabControlGetActive }, + { "SetActive", WGT_SIG_INT, (void *)wgtTabControlSetActive }, }; static const WgtIfaceT sIface = { @@ -708,7 +716,7 @@ static const WgtIfaceT sIface = { .props = sProps, .propCount = 1, .methods = sMethods, - .methodCount = 1, + .methodCount = 3, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT, diff --git a/widgets/textInput/textinpt.bhs b/widgets/textInput/textinpt.bhs index 90c3862..74e6f52 100644 --- a/widgets/textInput/textinpt.bhs +++ b/widgets/textInput/textinpt.bhs @@ -42,21 +42,64 @@ End Sub .title TextArea .toc 1 TextArea .index TextArea +.index FindNext +.index ReplaceAll +.index GoToLine +.index CursorLine +.index LineNumbers +.index AutoIndent .h1 TextArea VB Equivalent: TextArea (DVX extension) -- DVX Widget: textarea (multi-line text input, max 4096 chars) -A multi-line text editing area. This is a DVX extension with no direct VB3 equivalent (VB uses a TextBox with MultiLine=True). Supports syntax colorization, line numbers, auto-indent, and find/replace via the C API. +A multi-line text editing area. This is a DVX extension with no direct VB3 equivalent (VB uses a TextBox with MultiLine=True). Supports line numbers, auto-indent, find/replace, and tab configuration. .h2 Type-Specific Properties .table - Property Type Description - -------- ------ ------------------------------------------- - Text String The full text content. + Property Type R/W Description + ---------- ------- --- ------------------------------------------- + Text String R/W The full text content. + CursorLine Integer R Current cursor line number (0-based). +.endtable + +.h2 Type-Specific Methods + +.table + Method Description + ------ ----------- + FindNext needle$, caseSensitive, forward Search for text. Returns True if found. + GetWordAtCursor() Returns the word under the cursor. + GoToLine line% Scroll to and position cursor at the given line. + ReplaceAll needle$, replacement$, caseSensitive Replace all occurrences. Returns count of replacements. + SetAutoIndent enabled Enable or disable automatic indentation. + SetCaptureTabs enabled When True, Tab key inserts a tab/spaces instead of moving focus. + SetShowLineNumbers show Show or hide the line number gutter. + SetSyntaxMode mode$ Activate built-in syntax highlighting. Modes: "dhs" (help source), "bas" (BASIC). Pass "" to disable. + SetTabWidth width% Set the tab stop width in characters. + SetUseTabChar useChar When True, Tab inserts a tab character; when False, inserts spaces. .endtable Default Event: Change +.h2 Example + +.code +TextArea1.SetShowLineNumbers True +TextArea1.SetAutoIndent True +TextArea1.SetTabWidth 4 +TextArea1.SetCaptureTabs True + +' Search for text +If TextArea1.FindNext("TODO", False, True) Then + Print "Found at line"; TextArea1.CursorLine +End If + +' Replace all occurrences +Dim count As Integer +count = TextArea1.ReplaceAll("old", "new", False) +Print "Replaced"; count; "occurrences" +.endcode + .link ctrl.common.props Common Properties, Events, and Methods diff --git a/widgets/textInput/widgetTextInput.c b/widgets/textInput/widgetTextInput.c index 677f346..f28398c 100644 --- a/widgets/textInput/widgetTextInput.c +++ b/widgets/textInput/widgetTextInput.c @@ -3447,6 +3447,297 @@ static const struct { static const struct { WidgetT *(*create)(WidgetT *, int32_t); } sTextInputApi = { .create = wgtTextInput }; static const struct { WidgetT *(*create)(WidgetT *, int32_t); } sTextAreaApi = { .create = wgtTextArea }; + +// ============================================================ +// Built-in syntax colorizers for SetSyntaxMode +// ============================================================ + +// Color indices (match wgtTextAreaSetSyntaxColors slots): +// 0=default, 1=keyword, 2=string, 3=comment, 4=number, 5=operator, 6=type, 7=reserved + +// .dhs help source colorizer +static void dhsColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx) { + (void)ctx; + + if (lineLen == 0) { + return; + } + + // Skip leading whitespace + int32_t i = 0; + + while (i < lineLen && (line[i] == ' ' || line[i] == '\t')) { + i++; + } + + // Lines starting with . are directives + if (i < lineLen && line[i] == '.') { + int32_t dirStart = i; + i++; + + // Directive name + while (i < lineLen && line[i] != ' ' && line[i] != '\t') { + i++; + } + + // Classify the directive + int32_t dirLen = i - dirStart; + const char *dir = line + dirStart; + + // Block directives: .code, .endcode, .table, .endtable, .list, .endlist, .note, .endnote + bool isBlock = false; + + if (dirLen >= 4 && (strncasecmp(dir, ".code", 5) == 0 || strncasecmp(dir, ".table", 6) == 0 || strncasecmp(dir, ".list", 5) == 0 || strncasecmp(dir, ".note", 5) == 0 || strncasecmp(dir, ".endcode", 8) == 0 || strncasecmp(dir, ".endtable", 9) == 0 || strncasecmp(dir, ".endlist", 8) == 0 || strncasecmp(dir, ".endnote", 8) == 0 || strncasecmp(dir, ".item", 5) == 0 || strncasecmp(dir, ".hr", 3) == 0)) { + isBlock = true; + } + + // Heading directives: .h1, .h2, .h3 + bool isHeading = (dirLen >= 3 && dir[0] == '.' && (dir[1] == 'h' || dir[1] == 'H') && dir[2] >= '1' && dir[2] <= '3'); + + // Link directive: .link + bool isLink = (dirLen >= 5 && strncasecmp(dir, ".link", 5) == 0); + + // Topic/index/toc/title directives + bool isMeta = false; + + if (dirLen >= 4 && (strncasecmp(dir, ".topic", 6) == 0 || strncasecmp(dir, ".title", 6) == 0 || strncasecmp(dir, ".toc", 4) == 0 || strncasecmp(dir, ".index", 6) == 0 || strncasecmp(dir, ".default", 8) == 0 || strncasecmp(dir, ".section", 8) == 0 || strncasecmp(dir, ".image", 6) == 0)) { + isMeta = true; + } + + // Color the directive name + uint8_t dirColor = 1; // keyword + + if (isHeading) { + dirColor = 6; // type (stands out more) + } else if (isLink) { + dirColor = 5; // operator + } else if (isMeta) { + dirColor = 7; // reserved + } else if (isBlock) { + dirColor = 1; // keyword + } + + for (int32_t j = dirStart; j < i; j++) { + colors[j] = dirColor; + } + + // Color the rest of the line as the directive's argument + if (isHeading) { + // Heading text gets heading color + for (int32_t j = i; j < lineLen; j++) { + colors[j] = 6; + } + } else if (isLink) { + // .link topicId displayText -- topicId is string-colored, rest is default + while (i < lineLen && (line[i] == ' ' || line[i] == '\t')) { + i++; + } + + int32_t idStart = i; + + while (i < lineLen && line[i] != ' ' && line[i] != '\t') { + i++; + } + + for (int32_t j = idStart; j < i; j++) { + colors[j] = 2; // string + } + } else if (isMeta) { + // Meta argument in string color + for (int32_t j = i; j < lineLen; j++) { + colors[j] = 2; + } + } + + return; + } + + // Non-directive lines are body text (default color), no special highlighting +} + + +// BASIC keyword colorizer (simplified version of IDE's basicColorize) +static void basColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx) { + (void)ctx; + + static const char *sBasKeywords[] = { + "AND", "AS", "BYVAL", "CALL", "CASE", "CLOSE", "CONST", + "CREATECONTROL", "CREATEFORM", + "DATA", "DECLARE", "DIM", "DO", "DOEVENTS", + "ELSE", "ELSEIF", "END", "ERASE", "EXIT", + "FOR", "FUNCTION", "GET", "GOSUB", "GOTO", + "HIDE", "IF", "INPUT", "IS", + "LET", "LIBRARY", "LINE", "LOAD", "LOOP", + "ME", "MOD", "MSGBOX", + "NEXT", "NOTHING", "NOT", + "ON", "OPEN", "OPTIONAL", "OPTION", "OR", + "PRINT", "PUT", + "RANDOMIZE", "READ", "REDIM", "REMOVECONTROL", "RESTORE", "RESUME", "RETURN", + "SEEK", "SELECT", "SET", "SETEVENT", "SHARED", "SHELL", "SHOW", "SLEEP", + "STATIC", "STEP", "SUB", "SWAP", + "THEN", "TO", "TYPE", + "UNLOAD", "UNTIL", + "WEND", "WHILE", "WRITE", + "XOR", + NULL + }; + + static const char *sBasTypes[] = { + "BOOLEAN", "DOUBLE", "FALSE", "INTEGER", "LONG", "SINGLE", "STRING", "TRUE", + NULL + }; + + int32_t i = 0; + + while (i < lineLen) { + char ch = line[i]; + + // Comment + if (ch == '\'') { + while (i < lineLen) { + colors[i++] = 3; + } + + return; + } + + // String literal + if (ch == '"') { + colors[i++] = 2; + + while (i < lineLen && line[i] != '"') { + colors[i++] = 2; + } + + if (i < lineLen) { + colors[i++] = 2; + } + + continue; + } + + // Number + if (isdigit((unsigned char)ch) || (ch == '&' && i + 1 < lineLen && (line[i + 1] == 'H' || line[i + 1] == 'h'))) { + while (i < lineLen && (isxdigit((unsigned char)line[i]) || line[i] == '.' || line[i] == '&' || line[i] == 'H' || line[i] == 'h')) { + colors[i++] = 4; + } + + continue; + } + + // Identifier or keyword + if (isalpha((unsigned char)ch) || ch == '_') { + int32_t start = i; + + while (i < lineLen && (isalnum((unsigned char)line[i]) || line[i] == '_' || line[i] == '$' || line[i] == '%' || line[i] == '&' || line[i] == '!' || line[i] == '#')) { + i++; + } + + int32_t wordLen = i - start; + + // Check for REM + if (wordLen == 3 && (line[start] == 'R' || line[start] == 'r') && (line[start + 1] == 'E' || line[start + 1] == 'e') && (line[start + 2] == 'M' || line[start + 2] == 'm')) { + for (int32_t j = start; j < lineLen; j++) { + colors[j] = 3; + } + + return; + } + + // Uppercase for comparison + char upper[48]; + int32_t uLen = wordLen < 47 ? wordLen : 47; + + for (int32_t j = 0; j < uLen; j++) { + upper[j] = (char)toupper((unsigned char)line[start + j]); + } + + // Strip type suffix for matching + while (uLen > 0 && (upper[uLen - 1] == '$' || upper[uLen - 1] == '%' || upper[uLen - 1] == '&' || upper[uLen - 1] == '!' || upper[uLen - 1] == '#')) { + uLen--; + } + + upper[uLen] = '\0'; + + // Check keywords + uint8_t c = 0; + + for (int32_t j = 0; sBasKeywords[j]; j++) { + if (strcmp(upper, sBasKeywords[j]) == 0) { + c = 1; + break; + } + } + + if (c == 0) { + for (int32_t j = 0; sBasTypes[j]; j++) { + if (strcmp(upper, sBasTypes[j]) == 0) { + c = 6; + break; + } + } + } + + for (int32_t j = start; j < i; j++) { + colors[j] = c; + } + + continue; + } + + // Operator + if (ch == '=' || ch == '<' || ch == '>' || ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '\\') { + colors[i++] = 5; + continue; + } + + colors[i++] = 0; + } +} + + +// SetSyntaxMode method: activate a built-in colorizer +static void basSetSyntaxMode(WidgetT *w, const char *mode) { + if (!mode || !*mode) { + wgtTextAreaSetColorize(w, NULL, NULL); + return; + } + + if (strcasecmp(mode, "dhs") == 0 || strcasecmp(mode, "help") == 0) { + wgtTextAreaSetColorize(w, dhsColorize, NULL); + } else if (strcasecmp(mode, "bas") == 0 || strcasecmp(mode, "basic") == 0) { + wgtTextAreaSetColorize(w, basColorize, NULL); + } else { + wgtTextAreaSetColorize(w, NULL, NULL); + } +} + + +// BASIC wrapper: GetWordAtCursor returns the word as a string +static const char *basGetWordAtCursor(const WidgetT *w) { + static char buf[256]; + wgtTextAreaGetWordAtCursor(w, buf, sizeof(buf)); + return buf; +} + +// TextArea BASIC interface +static const WgtPropDescT sTextAreaProps[] = { + { "CursorLine", WGT_IFACE_INT, (void *)wgtTextAreaGetCursorLine, NULL, NULL } +}; + +static const WgtMethodDescT sTextAreaMethods[] = { + { "FindNext", WGT_SIG_STR_BOOL_BOOL, (void *)wgtTextAreaFindNext }, + { "GetWordAtCursor", WGT_SIG_RET_STR, (void *)basGetWordAtCursor }, + { "GoToLine", WGT_SIG_INT, (void *)wgtTextAreaGoToLine }, + { "ReplaceAll", WGT_SIG_STR_STR_BOOL, (void *)wgtTextAreaReplaceAll }, + { "SetAutoIndent", WGT_SIG_BOOL, (void *)wgtTextAreaSetAutoIndent }, + { "SetCaptureTabs", WGT_SIG_BOOL, (void *)wgtTextAreaSetCaptureTabs }, + { "SetShowLineNumbers", WGT_SIG_BOOL, (void *)wgtTextAreaSetShowLineNumbers }, + { "SetSyntaxMode", WGT_SIG_STR, (void *)basSetSyntaxMode }, + { "SetTabWidth", WGT_SIG_INT, (void *)wgtTextAreaSetTabWidth }, + { "SetUseTabChar", WGT_SIG_BOOL, (void *)wgtTextAreaSetUseTabChar }, +}; + static const WgtIfaceT sIfaceTextInput = { .basName = "TextBox", .props = NULL, @@ -3462,10 +3753,10 @@ static const WgtIfaceT sIfaceTextInput = { static const WgtIfaceT sIfaceTextArea = { .basName = "TextArea", - .props = NULL, - .propCount = 0, - .methods = NULL, - .methodCount = 0, + .props = sTextAreaProps, + .propCount = 1, + .methods = sTextAreaMethods, + .methodCount = 10, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_INT, diff --git a/widgets/treeView/treeview.bhs b/widgets/treeView/treeview.bhs index 0578c28..b92e96c 100644 --- a/widgets/treeView/treeview.bhs +++ b/widgets/treeView/treeview.bhs @@ -2,22 +2,45 @@ .title TreeView .toc 1 TreeView .index TreeView +.index AddItem +.index AddChildItem .h1 TreeView VB Equivalent: TreeView -- DVX Widget: treeview -A hierarchical tree of expandable/collapsible nodes. Nodes are created via the C API (wgtTreeItem). Supports multi-select and drag-to-reorder. +A hierarchical tree of expandable/collapsible nodes. Supports multi-select and drag-to-reorder. Items are accessed by depth-first index (0-based). .h2 Type-Specific Methods .table - Method Parameters Description - -------------- ---------------------- ------------------------------------------- - SetMultiSelect Multi As Boolean Enable or disable multi-select mode. - SetReorderable Reorderable As Boolean Enable or disable node reordering. + Method Description + ------ ----------- + AddChildItem parentIndex%, text$ Add a child node under the node at the given index. + AddItem text$ Add a root-level node. + Clear Remove all nodes. + GetItemText$(index%) Returns the text of the node at the given index. + IsExpanded(index%) Returns True if the node at the given index is expanded. + IsItemSelected(index%) Returns True if the node at the given index is selected. + ItemCount() Returns the total number of nodes (all levels). + SetExpanded index%, expanded Expand or collapse the node at the given index. + SetItemSelected index%, selected Select or deselect the node at the given index. + SetMultiSelect multi Enable or disable multi-select mode. + SetReorderable reorderable Enable or disable node reordering. .endtable Default Event: Click +.h2 Example + +.code +TreeView1.AddItem "Animals" +TreeView1.AddChildItem 0, "Cat" +TreeView1.AddChildItem 0, "Dog" +TreeView1.AddItem "Plants" +TreeView1.AddChildItem 3, "Oak" +TreeView1.SetExpanded 0, True +Print "Items:"; TreeView1.ItemCount() +.endcode + .link ctrl.common.props Common Properties, Events, and Methods diff --git a/widgets/treeView/widgetTreeView.c b/widgets/treeView/widgetTreeView.c index f58d805..bd1ee50 100644 --- a/widgets/treeView/widgetTreeView.c +++ b/widgets/treeView/widgetTreeView.c @@ -1862,9 +1862,153 @@ static const struct { .itemSetSelected = wgtTreeItemSetSelected }; +// ============================================================ +// BASIC wrapper helpers: index-based tree item access +// ============================================================ + +static WidgetT *basTreeFindByIndex(WidgetT *parent, int32_t target, int32_t *cur) { + for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) { + if (c->type != sTreeItemTypeId) { + continue; + } + + if (*cur == target) { + return c; + } + + (*cur)++; + + WidgetT *found = basTreeFindByIndex(c, target, cur); + + if (found) { + return found; + } + } + + return NULL; +} + + +static WidgetT *basTreeItemAt(WidgetT *tree, int32_t index) { + int32_t cur = 0; + return basTreeFindByIndex(tree, index, &cur); +} + + +static int32_t basTreeCountItems(WidgetT *parent) { + int32_t count = 0; + + for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) { + if (c->type != sTreeItemTypeId) { + continue; + } + + count++; + count += basTreeCountItems(c); + } + + return count; +} + + +// AddItem text$ -- add root-level item +static void basAddItem(WidgetT *w, const char *text) { + wgtTreeItem(w, text); +} + + +// AddChildItem index%, text$ -- add child under item at depth-first index +static void basAddChildItem(WidgetT *w, int32_t parentIdx, const char *text) { + WidgetT *parent = basTreeItemAt(w, parentIdx); + + if (parent) { + wgtTreeItem(parent, text); + } +} + + +// Clear -- remove all items +static void basClear(WidgetT *w) { + // Remove all tree item children + WidgetT *c = w->firstChild; + + while (c) { + WidgetT *next = c->nextSibling; + + if (c->type == sTreeItemTypeId) { + wgtDestroy(c); + } + + c = next; + } +} + + +// ItemCount -- return total number of items (depth-first) +static int32_t basItemCount(const WidgetT *w) { + return basTreeCountItems((WidgetT *)w); +} + + +// SetExpanded index%, expanded -- expand/collapse item at index +static void basSetExpanded(WidgetT *w, int32_t idx, bool expanded) { + WidgetT *item = basTreeItemAt(w, idx); + + if (item) { + wgtTreeItemSetExpanded(item, expanded); + } +} + + +// IsExpanded(index%) -- check if item at index is expanded +static bool basIsExpanded(const WidgetT *w, int32_t idx) { + WidgetT *item = basTreeItemAt((WidgetT *)w, idx); + return item ? wgtTreeItemIsExpanded(item) : false; +} + + +// SetItemSelected index%, selected -- select/deselect item at index +static void basSetItemSelected(WidgetT *w, int32_t idx, bool selected) { + WidgetT *item = basTreeItemAt(w, idx); + + if (item) { + wgtTreeItemSetSelected(item, selected); + } +} + + +// IsItemSelected(index%) -- check if item at index is selected +static bool basIsItemSelected(const WidgetT *w, int32_t idx) { + WidgetT *item = basTreeItemAt((WidgetT *)w, idx); + return item ? wgtTreeItemIsSelected(item) : false; +} + + +// GetItemText$(index%) -- get item text at index +static const char *basGetItemText(const WidgetT *w, int32_t idx) { + WidgetT *item = basTreeItemAt((WidgetT *)w, idx); + + if (item && item->data) { + TreeItemDataT *ti = (TreeItemDataT *)item->data; + return ti->text ? ti->text : ""; + } + + return ""; +} + + static const WgtMethodDescT sMethods[] = { - { "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtTreeViewSetMultiSelect }, - { "SetReorderable", WGT_SIG_BOOL, (void *)wgtTreeViewSetReorderable } + { "AddChildItem", WGT_SIG_INT_STR, (void *)basAddChildItem }, + { "AddItem", WGT_SIG_STR, (void *)basAddItem }, + { "Clear", WGT_SIG_VOID, (void *)basClear }, + { "GetItemText", WGT_SIG_RET_STR_INT, (void *)basGetItemText }, + { "IsExpanded", WGT_SIG_RET_BOOL_INT, (void *)basIsExpanded }, + { "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)basIsItemSelected }, + { "ItemCount", WGT_SIG_RET_INT, (void *)basItemCount }, + { "SetExpanded", WGT_SIG_INT_BOOL, (void *)basSetExpanded }, + { "SetItemSelected", WGT_SIG_INT_BOOL, (void *)basSetItemSelected }, + { "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtTreeViewSetMultiSelect }, + { "SetReorderable", WGT_SIG_BOOL, (void *)wgtTreeViewSetReorderable }, }; static const WgtIfaceT sIface = { @@ -1872,7 +2016,7 @@ static const WgtIfaceT sIface = { .props = NULL, .propCount = 0, .methods = sMethods, - .methodCount = 2, + .methodCount = 11, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT,