We can make binaries from BASIC!

This commit is contained in:
Scott Duensing 2026-04-15 15:15:09 -05:00
parent 7157f97c28
commit de60200f23
63 changed files with 6032 additions and 1311 deletions

View file

@ -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
// ============================================================

View file

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

View file

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

View file

@ -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,

View file

@ -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 &currentFormVars[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

View file

@ -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

View file

@ -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 <stdlib.h>
#include <string.h>
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;
}
}

View file

@ -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

View file

@ -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

View file

@ -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 <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// 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;
}

View file

@ -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 <stdint.h>
// 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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -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

View file

@ -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) {

View file

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
// 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);
}
}
}

View file

@ -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 <stdint.h>
// ============================================================
// 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

View file

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

View file

@ -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;
// ============================================================

View file

@ -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 =

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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."

View file

@ -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!"

View file

@ -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!"

View file

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// 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 <name>" 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;
}

View file

@ -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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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 <stdio.h>
#include <stdlib.h>
@ -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);
}

View file

@ -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 {

View file

@ -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.

View file

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

View file

@ -487,6 +487,8 @@ img { max-width: 100%; }
<li><a href="#ctrl.terminal">Terminal</a></li>
<li><a href="#ctrl.terminal">ANSI Terminal</a></li>
<li><a href="#ctrl.terminal">VT100</a></li>
<li><a href="#ctrl.terminal">CommAttach</a></li>
<li><a href="#ctrl.terminal">CommOpen</a></li>
<li><a href="#ctrl.frame">Frame</a></li>
<li><a href="#ctrl.frame">Container</a></li>
<li><a href="#ctrl.hbox">HBox</a></li>
@ -499,6 +501,8 @@ img { max-width: 100%; }
<li><a href="#ctrl.picturebox">PictureBox</a></li>
<li><a href="#ctrl.picturebox">Canvas</a></li>
<li><a href="#ctrl.picturebox">Drawing</a></li>
<li><a href="#ctrl.picturebox">SetPenColor</a></li>
<li><a href="#ctrl.picturebox">SetPenSize</a></li>
<li><a href="#ctrl.checkbox">CheckBox</a></li>
<li><a href="#ctrl.checkbox">Value</a></li>
<li><a href="#ctrl.combobox">ComboBox</a></li>
@ -529,6 +533,8 @@ img { max-width: 100%; }
<li><a href="#ctrl.listbox">RemoveItem</a></li>
<li><a href="#ctrl.listview">ListView</a></li>
<li><a href="#ctrl.listview">Multi-Column List</a></li>
<li><a href="#ctrl.listview">SetColumns</a></li>
<li><a href="#ctrl.listview">SetSort</a></li>
<li><a href="#ctrl.progressbar">ProgressBar</a></li>
<li><a href="#ctrl.optionbutton">OptionButton</a></li>
<li><a href="#ctrl.optionbutton">Radio Button</a></li>
@ -557,12 +563,20 @@ img { max-width: 100%; }
<li><a href="#ctrl.textbox">DataSource</a></li>
<li><a href="#ctrl.textbox">DataField</a></li>
<li><a href="#ctrl.textarea">TextArea</a></li>
<li><a href="#ctrl.textarea">FindNext</a></li>
<li><a href="#ctrl.textarea">ReplaceAll</a></li>
<li><a href="#ctrl.textarea">GoToLine</a></li>
<li><a href="#ctrl.textarea">CursorLine</a></li>
<li><a href="#ctrl.textarea">LineNumbers</a></li>
<li><a href="#ctrl.textarea">AutoIndent</a></li>
<li><a href="#ctrl.timer">Timer</a></li>
<li><a href="#ctrl.timer">Interval</a></li>
<li><a href="#ctrl.timer">Start</a></li>
<li><a href="#ctrl.timer">Stop</a></li>
<li><a href="#ctrl.toolbar">Toolbar</a></li>
<li><a href="#ctrl.treeview">TreeView</a></li>
<li><a href="#ctrl.treeview">AddItem</a></li>
<li><a href="#ctrl.treeview">AddChildItem</a></li>
<li><a href="#ctrl.wrapbox">WrapBox</a></li>
<li><a href="#ctrl.wrapbox">Flow Layout</a></li>
</ul>
@ -2210,11 +2224,19 @@ End Sub</code></pre>
Rows Integer Number of character rows (read-only).
Scrollback Integer Number of scrollback lines (write-only).</pre>
<h2>Type-Specific Methods</h2>
<pre> Method Parameters Description
------ --------------- -------------------------------------------
Clear (none) Clear the terminal screen.
Write Text As String Write text (with ANSI escape processing) to the terminal.</pre>
<pre> 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.</pre>
<p>No default event.</p>
<h2>Serial Communication</h2>
<p>Use the comm.bas library to connect a Terminal to a serial link:</p>
<pre><code>Dim link As Integer
link = CommOpen(1, 115200)
CommHandshake link
CommAttach link, &quot;Terminal1&quot;</code></pre>
<p>See comm.bas for the full communications API.</p>
<p><a href="#ctrl.common.props">Common Properties, Events, and Methods</a></p>
</div>
<div class="topic" id="ctrl.frame">
@ -2286,12 +2308,32 @@ End Sub</code></pre>
<h1>PictureBox</h1>
<p>VB Equivalent: PictureBox -- DVX Widget: canvas | Name Prefix: Picture</p>
<p>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.</p>
<p>Colors are specified as 0x00RRGGBB integers (e.g. &amp;HFF0000 for red).</p>
<h2>Type-Specific Methods</h2>
<pre> Method Parameters Description
------ ---------------- -------------------------------------------
Clear Color As Integer Fill the entire canvas with the specified color.</pre>
<p>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.</p>
<pre> 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.</pre>
<p>Default Event: Click</p>
<h2>Example</h2>
<pre><code>Picture1.Resize 200, 150
Picture1.Clear &amp;HFFFFFF
Picture1.SetPenSize 2
Picture1.DrawRect 10, 10, 180, 130, &amp;H000000
Picture1.FillCircle 100, 75, 40, &amp;HFF0000
Picture1.DrawText 60, 130, &quot;Hello!&quot;
Picture1.Save &quot;output.bmp&quot;</code></pre>
<p><a href="#ctrl.common.props">Common Properties, Events, and Methods</a></p>
</div>
<div class="topic" id="ctrl.checkbox">
@ -2472,7 +2514,8 @@ End</code></pre>
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.</pre>
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. &quot;Red|Green|Blue&quot;).</pre>
<p>Default Event: Click</p>
<h2>Example</h2>
<pre><code>Sub Form_Load ()
@ -2495,21 +2538,35 @@ End Sub</code></pre>
<div class="topic" id="ctrl.listview">
<h1>ListView</h1>
<p>VB Equivalent: ListView -- DVX Widget: listview</p>
<p>A multi-column list with column headers. Supports sorting, multi-select, and drag-to-reorder. Columns are configured via the C API (SetColumns, SetData).</p>
<p>A multi-column list with column headers. Supports sorting, multi-select, and drag-to-reorder.</p>
<h2>Type-Specific Properties</h2>
<pre> Property Type Description
--------- ------- -------------------------------------------
ListIndex Integer Index of the currently selected row (-1 = none).</pre>
<pre> Property Type R/W Description
--------- ------- --- -------------------------------------------
ListIndex Integer R/W Index of the currently selected row (-1 = none).</pre>
<h2>Type-Specific Methods</h2>
<pre> 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.</pre>
<pre> 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 &quot;Name,Width|Name,Width&quot; (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.</pre>
<p>Default Event: Click</p>
<h2>Example</h2>
<pre><code>ListView1.SetColumns &quot;Name,20|Size,10|Type,15&quot;
ListView1.AddItem &quot;readme.txt&quot;
ListView1.SetCell 0, 1, &quot;1024&quot;
ListView1.SetCell 0, 2, &quot;Text&quot;
ListView1.SetSort 0, 1</code></pre>
<p><a href="#ctrl.common.props">Common Properties, Events, and Methods</a></p>
</div>
<div class="topic" id="ctrl.progressbar">
@ -2630,9 +2687,11 @@ End Sub</code></pre>
-------- ------- -------------------------------------------
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.</pre>
<h2>Type-Specific Methods</h2>
<pre> 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).</pre>
<pre> 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).</pre>
<p>Container: Yes</p>
<p>Default Event: Click</p>
<blockquote><strong>Warning:</strong> The TabIndex property is shadowed by the common property handler at runtime. Use the SetActive method to change tabs programmatically.</blockquote>
@ -2663,12 +2722,41 @@ End Sub</code></pre>
<div class="topic" id="ctrl.textarea">
<h1>TextArea</h1>
<p>VB Equivalent: TextArea (DVX extension) -- DVX Widget: textarea (multi-line text input, max 4096 chars)</p>
<p>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.</p>
<p>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.</p>
<h2>Type-Specific Properties</h2>
<pre> Property Type Description
-------- ------ -------------------------------------------
Text String The full text content.</pre>
<pre> Property Type R/W Description
---------- ------- --- -------------------------------------------
Text String R/W The full text content.
CursorLine Integer R Current cursor line number (0-based).</pre>
<h2>Type-Specific Methods</h2>
<pre> 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: &quot;dhs&quot; (help source), &quot;bas&quot; (BASIC). Pass &quot;&quot; to disable.
SetTabWidth width% Set the tab stop width in characters.
SetUseTabChar useChar When True, Tab inserts a tab character; when False, inserts spaces.</pre>
<p>Default Event: Change</p>
<h2>Example</h2>
<pre><code>TextArea1.SetShowLineNumbers True
TextArea1.SetAutoIndent True
TextArea1.SetTabWidth 4
TextArea1.SetCaptureTabs True
' Search for text
If TextArea1.FindNext(&quot;TODO&quot;, False, True) Then
Print &quot;Found at line&quot;; TextArea1.CursorLine
End If
' Replace all occurrences
Dim count As Integer
count = TextArea1.ReplaceAll(&quot;old&quot;, &quot;new&quot;, False)
Print &quot;Replaced&quot;; count; &quot;occurrences&quot;</code></pre>
<p><a href="#ctrl.common.props">Common Properties, Events, and Methods</a></p>
</div>
<div class="topic" id="ctrl.timer">
@ -2715,13 +2803,30 @@ End Sub</code></pre>
<div class="topic" id="ctrl.treeview">
<h1>TreeView</h1>
<p>VB Equivalent: TreeView -- DVX Widget: treeview</p>
<p>A hierarchical tree of expandable/collapsible nodes. Nodes are created via the C API (wgtTreeItem). Supports multi-select and drag-to-reorder.</p>
<p>A hierarchical tree of expandable/collapsible nodes. Supports multi-select and drag-to-reorder. Items are accessed by depth-first index (0-based).</p>
<h2>Type-Specific Methods</h2>
<pre> Method Parameters Description
-------------- ---------------------- -------------------------------------------
SetMultiSelect Multi As Boolean Enable or disable multi-select mode.
SetReorderable Reorderable As Boolean Enable or disable node reordering.</pre>
<pre> 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.</pre>
<p>Default Event: Click</p>
<h2>Example</h2>
<pre><code>TreeView1.AddItem &quot;Animals&quot;
TreeView1.AddChildItem 0, &quot;Cat&quot;
TreeView1.AddChildItem 0, &quot;Dog&quot;
TreeView1.AddItem &quot;Plants&quot;
TreeView1.AddChildItem 3, &quot;Oak&quot;
TreeView1.SetExpanded 0, True
Print &quot;Items:&quot;; TreeView1.ItemCount()</code></pre>
<p><a href="#ctrl.common.props">Common Properties, Events, and Methods</a></p>
</div>
<div class="topic" id="ctrl.wrapbox">

128
sdk/include/basic/comm.bas Normal file
View file

@ -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

View file

@ -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.

View file

@ -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

53
sdk/include/basic/sql.bas Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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!"

View file

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

280
tools/dvxResWrite.h Normal file
View file

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
// 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

View file

@ -16,13 +16,10 @@
// [resource directory]
// [footer with magic + directory offset + count]
#include "../core/dvxRes.h"
#include "dvxResWrite.h"
#include <ctype.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
// 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);

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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,