From dcd120d769a8ebcfae5216effd50bbf5e9e6a450 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Thu, 16 Apr 2026 18:18:04 -0500 Subject: [PATCH] Many BASIC compiler and VM fixes. Other fixes. New BASIC apps. --- apps/Makefile | 62 +- apps/clock/clock.res | 2 +- apps/cpanel/cpanel.c | 67 +- apps/cpanel/cpanel.res | 2 +- apps/dvxbasic/Makefile | 61 +- apps/dvxbasic/compiler/codegen.c | 41 - apps/dvxbasic/compiler/compact.c | 564 +++++++++++++ apps/dvxbasic/compiler/compact.h | 21 + apps/dvxbasic/compiler/lexer.c | 18 +- apps/dvxbasic/compiler/obfuscate.c | 590 ++++++++++++++ apps/dvxbasic/compiler/obfuscate.h | 46 ++ apps/dvxbasic/compiler/opcodes.h | 5 + apps/dvxbasic/compiler/parser.c | 117 ++- apps/dvxbasic/compiler/strip.c | 78 +- apps/dvxbasic/compiler/strip.h | 11 +- apps/dvxbasic/formrt/formrt.c | 405 +++++++++- apps/dvxbasic/formrt/formrt.h | 4 +- apps/dvxbasic/ide/ideMain.c | 130 ++- apps/dvxbasic/runtime/serialize.c | 5 +- apps/dvxbasic/runtime/vm.c | 121 ++- apps/dvxbasic/runtime/vm.h | 2 +- apps/dvxbasic/stub/bascomp.c | 667 +++++++++++++++ apps/dvxbasic/stub/basstub.c | 11 +- apps/dvxbasic/test_compact.c | 326 ++++++++ apps/dvxbasic/test_compiler.c | 151 ++++ apps/dvxdemo/dvxdemo.c | 10 +- apps/dvxdemo/dvxdemo.res | 2 +- apps/dvxhelp/dvxhelp.res | 2 +- apps/imgview/imgview.c | 4 +- apps/imgview/imgview.res | 2 +- apps/notepad/notepad.c | 8 +- apps/notepad/notepad.res | 2 +- apps/progman/progman.c | 14 +- config/dvx.ini | 2 + core/dvxApp.c | 48 +- core/dvxApp.h | 4 +- core/dvxDialog.c | 82 +- core/dvxDlg.h | 6 +- core/dvxRes.h | 4 + core/dvxResource.c | 102 +++ core/dvxTypes.h | 6 +- run.sh | 1 - sdk/include/basic/commdlg.bas | 15 +- sdk/include/basic/resource.bas | 50 ++ sdk/samples/basic/basicdemo/ICON32.BMP | 3 + sdk/samples/basic/basicdemo/basicdemo.dbp | 16 + sdk/samples/basic/basicdemo/basicdemo.frm | 937 ++++++++++++++++++++++ sdk/samples/basic/commdlg.frm | 8 +- sdk/samples/basic/dynform.bas | 6 +- sdk/samples/basic/helpedit.frm | 205 ----- sdk/samples/basic/helpedit/ICON32.BMP | 3 + sdk/samples/basic/helpedit/helpedit.dbp | 17 + sdk/samples/basic/helpedit/helpedit.frm | 310 +++++++ sdk/samples/basic/iconed/ICON32.BMP | 2 +- sdk/samples/basic/iconed/iconed.dbp | 4 +- sdk/samples/basic/iconed/iconed.frm | 17 + sdk/samples/basic/imgview/ICON32.BMP | 3 + sdk/samples/basic/imgview/imgview.dbp | 16 + sdk/samples/basic/imgview/imgview.frm | 49 ++ sdk/samples/basic/notepad/ICON32.BMP | 3 + sdk/samples/basic/notepad/notepad.dbp | 16 + sdk/samples/basic/notepad/notepad.frm | 237 ++++++ sdk/samples/basic/resedit/ICON32.BMP | 3 + sdk/samples/basic/resedit/resedit.dbp | 17 + sdk/samples/basic/resedit/resedit.frm | 431 ++++++++++ shell/shellMain.c | 7 +- taskmgr/shellTaskMgr.c | 4 +- tools/mkicon.c | 99 ++- widgets/ansiTerm/widgetAnsiTerm.c | 9 +- widgets/image/widgetImage.c | 168 +++- widgets/imageButton/widgetImageButton.c | 13 +- widgets/listView/widgetListView.c | 108 ++- widgets/textInput/widgetTextInput.c | 2 +- widgets/timer/widgetTimer.c | 12 +- 74 files changed, 6091 insertions(+), 505 deletions(-) create mode 100644 apps/dvxbasic/compiler/compact.c create mode 100644 apps/dvxbasic/compiler/compact.h create mode 100644 apps/dvxbasic/compiler/obfuscate.c create mode 100644 apps/dvxbasic/compiler/obfuscate.h create mode 100644 apps/dvxbasic/stub/bascomp.c create mode 100644 apps/dvxbasic/test_compact.c create mode 100644 sdk/include/basic/resource.bas create mode 100644 sdk/samples/basic/basicdemo/ICON32.BMP create mode 100644 sdk/samples/basic/basicdemo/basicdemo.dbp create mode 100644 sdk/samples/basic/basicdemo/basicdemo.frm delete mode 100644 sdk/samples/basic/helpedit.frm create mode 100644 sdk/samples/basic/helpedit/ICON32.BMP create mode 100644 sdk/samples/basic/helpedit/helpedit.dbp create mode 100644 sdk/samples/basic/helpedit/helpedit.frm create mode 100644 sdk/samples/basic/imgview/ICON32.BMP create mode 100644 sdk/samples/basic/imgview/imgview.dbp create mode 100644 sdk/samples/basic/imgview/imgview.frm create mode 100644 sdk/samples/basic/notepad/ICON32.BMP create mode 100644 sdk/samples/basic/notepad/notepad.dbp create mode 100644 sdk/samples/basic/notepad/notepad.frm create mode 100644 sdk/samples/basic/resedit/ICON32.BMP create mode 100644 sdk/samples/basic/resedit/resedit.dbp create mode 100644 sdk/samples/basic/resedit/resedit.frm diff --git a/apps/Makefile b/apps/Makefile index 0c696b4..1338d1a 100644 --- a/apps/Makefile +++ b/apps/Makefile @@ -11,11 +11,14 @@ BINDIR = ../bin/apps DVXRES = ../bin/host/dvxres # App definitions: each is a subdir with a single .c file -APPS = progman notepad clock dvxdemo cpanel imgview dvxhelp +APPS = progman clock dvxdemo cpanel dvxhelp -.PHONY: all clean $(APPS) dvxbasic +BASCOMP = ../bin/host/bascomp +BASICAPPS = iconed notepad-bas imgview-bas helpedit resedit basicdemo -all: $(APPS) dvxbasic +.PHONY: all clean $(APPS) dvxbasic $(BASICAPPS) + +all: $(APPS) dvxbasic $(BASICAPPS) dvxbasic: $(MAKE) -C dvxbasic @@ -30,28 +33,28 @@ dvxhelp: $(BINDIR)/kpunch/dvxhelp/dvxhelp.app $(BINDIR)/kpunch/cpanel/cpanel.app: $(OBJDIR)/cpanel.o cpanel/cpanel.res cpanel/icon32.bmp | $(BINDIR)/kpunch/cpanel $(DXE3GEN) -o $@ -U $< - $(DVXRES) build $@ cpanel/cpanel.res + cd cpanel && ../$(DVXRES) build ../$@ cpanel.res $(BINDIR)/kpunch/imgview/imgview.app: $(OBJDIR)/imgview.o imgview/imgview.res imgview/icon32.bmp | $(BINDIR)/kpunch/imgview $(DXE3GEN) -o $@ -U $< - $(DVXRES) build $@ imgview/imgview.res + cd imgview && ../$(DVXRES) build ../$@ imgview.res $(BINDIR)/kpunch/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/kpunch/progman $(DXE3GEN) -o $@ -U $< $(BINDIR)/kpunch/notepad/notepad.app: $(OBJDIR)/notepad.o notepad/notepad.res notepad/icon32.bmp | $(BINDIR)/kpunch/notepad $(DXE3GEN) -o $@ -U $< - $(DVXRES) build $@ notepad/notepad.res + cd notepad && ../$(DVXRES) build ../$@ notepad.res $(BINDIR)/kpunch/clock/clock.app: $(OBJDIR)/clock.o clock/clock.res clock/icon32.bmp | $(BINDIR)/kpunch/clock $(DXE3GEN) -o $@ -U $< - $(DVXRES) build $@ clock/clock.res + cd clock && ../$(DVXRES) build ../$@ clock.res DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp $(BINDIR)/kpunch/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) dvxdemo/dvxdemo.res dvxdemo/icon32.bmp | $(BINDIR)/kpunch/dvxdemo $(DXE3GEN) -o $@ -U $< - $(DVXRES) build $@ dvxdemo/dvxdemo.res + cd dvxdemo && ../$(DVXRES) build ../$@ dvxdemo.res cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/kpunch/dvxdemo/ $(OBJDIR)/cpanel.o: cpanel/cpanel.c | $(OBJDIR) @@ -74,11 +77,52 @@ $(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR) $(BINDIR)/kpunch/dvxhelp/dvxhelp.app: $(OBJDIR)/dvxhelp.o dvxhelp/dvxhelp.res dvxhelp/icon32.bmp | $(BINDIR)/kpunch/dvxhelp $(DXE3GEN) -o $@ -U $< - $(DVXRES) build $@ dvxhelp/dvxhelp.res + cd dvxhelp && ../$(DVXRES) build ../$@ dvxhelp.res $(OBJDIR)/dvxhelp.o: dvxhelp/dvxhelp.c dvxhelp/hlpformat.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< +# BASIC apps (compiled from .dbp projects via bascomp) +iconed: $(BINDIR)/kpunch/iconed/iconed.app + +$(BINDIR)/kpunch/iconed/iconed.app: ../sdk/samples/basic/iconed/iconed.dbp ../sdk/samples/basic/iconed/iconed.frm ../sdk/samples/basic/iconed/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/iconed dvxbasic + $(BASCOMP) ../sdk/samples/basic/iconed/iconed.dbp -o $@ -release + +notepad-bas: $(BINDIR)/kpunch/notepad/notepad.app + +$(BINDIR)/kpunch/notepad/notepad.app: ../sdk/samples/basic/notepad/notepad.dbp ../sdk/samples/basic/notepad/notepad.frm ../sdk/samples/basic/notepad/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/notepad dvxbasic + $(BASCOMP) ../sdk/samples/basic/notepad/notepad.dbp -o $@ -release + +imgview-bas: $(BINDIR)/kpunch/imgview/imgview.app + +$(BINDIR)/kpunch/imgview/imgview.app: ../sdk/samples/basic/imgview/imgview.dbp ../sdk/samples/basic/imgview/imgview.frm ../sdk/samples/basic/imgview/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/imgview dvxbasic + $(BASCOMP) ../sdk/samples/basic/imgview/imgview.dbp -o $@ -release + +helpedit: $(BINDIR)/kpunch/dvxhelp/helpedit.app + +$(BINDIR)/kpunch/dvxhelp/helpedit.app: ../sdk/samples/basic/helpedit/helpedit.dbp ../sdk/samples/basic/helpedit/helpedit.frm ../sdk/samples/basic/helpedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/dvxhelp dvxbasic + $(BASCOMP) ../sdk/samples/basic/helpedit/helpedit.dbp -o $@ -release + $(DVXRES) add $@ helpfile text "dvxhelp.hlp" + +resedit: $(BINDIR)/kpunch/resedit/resedit.app + +$(BINDIR)/kpunch/resedit/resedit.app: ../sdk/samples/basic/resedit/resedit.dbp ../sdk/samples/basic/resedit/resedit.frm ../sdk/samples/basic/resedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/resedit dvxbasic + $(BASCOMP) ../sdk/samples/basic/resedit/resedit.dbp -o $@ -release + +$(BINDIR)/kpunch/resedit: + mkdir -p $(BINDIR)/kpunch/resedit + +basicdemo: $(BINDIR)/kpunch/basicdemo/basicdemo.app + +$(BINDIR)/kpunch/basicdemo/basicdemo.app: ../sdk/samples/basic/basicdemo/basicdemo.dbp ../sdk/samples/basic/basicdemo/basicdemo.frm ../sdk/samples/basic/basicdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basicdemo dvxbasic + $(BASCOMP) ../sdk/samples/basic/basicdemo/basicdemo.dbp -o $@ -release + +$(BINDIR)/kpunch/basicdemo: + mkdir -p $(BINDIR)/kpunch/basicdemo + +$(BINDIR)/kpunch/iconed: + mkdir -p $(BINDIR)/kpunch/iconed + $(OBJDIR): mkdir -p $(OBJDIR) diff --git a/apps/clock/clock.res b/apps/clock/clock.res index 58bc945..3b1e36a 100644 --- a/apps/clock/clock.res +++ b/apps/clock/clock.res @@ -1,5 +1,5 @@ # clock.res -- Resource manifest for Clock -icon32 icon clock/icon32.bmp +icon32 icon icon32.bmp name text "Clock" author text "DVX Project" description text "Digital clock with date display" diff --git a/apps/cpanel/cpanel.c b/apps/cpanel/cpanel.c index 6d96e2d..4b90263 100644 --- a/apps/cpanel/cpanel.c +++ b/apps/cpanel/cpanel.c @@ -1,7 +1,7 @@ // ctrlpanel.c -- DVX Control Panel // // System configuration app with four tabs: -// Mouse -- scroll direction, double-click speed, acceleration, cursor speed +// Mouse -- scroll direction, wheel speed, double-click speed, acceleration, cursor speed // Colors -- all 18 system colors, theme load/save // Desktop -- wallpaper image (stretch mode) // Video -- resolution and color depth @@ -95,6 +95,7 @@ static int32_t sSavedWheelDir; static int32_t sSavedDblClick; static int32_t sSavedAccel; static int32_t sSavedSpeed; +static int32_t sSavedWheelStep; static int32_t sSavedVideoW; static int32_t sSavedVideoH; static int32_t sSavedVideoBpp; @@ -107,6 +108,8 @@ static WidgetT *sDblClickLbl = NULL; static WidgetT *sAccelDrop = NULL; static WidgetT *sSpeedSldr = NULL; static WidgetT *sSpeedLbl = NULL; +static WidgetT *sWheelStepSldr = NULL; +static WidgetT *sWheelStepLbl = NULL; static WidgetT *sDblTestLbl = NULL; // Colors tab widgets @@ -152,6 +155,7 @@ static const char *mapAccelValue(int32_t val); static void onAccelChange(WidgetT *w); static void onSpeedSlider(WidgetT *w); static void updateSpeedLabel(void); +static void updateWheelStepLabel(void); static void applyMouseConfig(void); static void onApplyTheme(WidgetT *w); static void onBrowseTheme(WidgetT *w); @@ -170,6 +174,7 @@ static void onOk(WidgetT *w); static void onSaveTheme(WidgetT *w); static void onVideoApply(WidgetT *w); static void onWheelChange(WidgetT *w); +static void onWheelStepSlider(WidgetT *w); static void saveSnapshot(void); static void restoreSnapshot(void); static void scanWallpapers(void); @@ -334,6 +339,26 @@ static void buildMouseTab(WidgetT *page) { wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL); + // Wheel speed (lines per notch) + wgtLabel(page, "Scroll Speed:"); + + WidgetT *wheelStepRow = wgtHBox(page); + wheelStepRow->spacing = wgtPixels(CP_SPACING); + + wgtLabel(wheelStepRow, "Slow"); + sWheelStepSldr = wgtSlider(wheelStepRow, MOUSE_WHEEL_STEP_MIN, MOUSE_WHEEL_STEP_MAX); + sWheelStepSldr->weight = 100; + sWheelStepSldr->onChange = onWheelStepSlider; + + int32_t wheelStep = prefsGetInt(sPrefs, "mouse", "wheelspeed", MOUSE_WHEEL_STEP_DEFAULT); + wgtSliderSetValue(sWheelStepSldr, wheelStep); + wgtLabel(wheelStepRow, "Fast "); + sWheelStepLbl = wgtLabel(wheelStepRow, ""); + sWheelStepLbl->prefW = wgtPixels(CP_DBLCLICK_LBL_W); + updateWheelStepLabel(); + + wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL); + // Double-click speed wgtLabel(page, "Double-Click Speed:"); @@ -500,6 +525,13 @@ static void onWheelChange(WidgetT *w) { } +static void onWheelStepSlider(WidgetT *w) { + (void)w; + updateWheelStepLabel(); + applyMouseConfig(); +} + + static void onDblClickSlider(WidgetT *w) { (void)w; updateDblClickLabel(); @@ -588,8 +620,8 @@ static void onBrowseTheme(WidgetT *w) { (void)w; FileFilterT filters[] = { - { "Theme Files (*.thm)", "*.thm" }, - { "All Files (*.*)", "*.*" } + { "\1" }, + { "\1" } }; char path[DVX_MAX_PATH]; @@ -604,8 +636,8 @@ static void onSaveTheme(WidgetT *w) { (void)w; FileFilterT filters[] = { - { "Theme Files (*.thm)", "*.thm" }, - { "All Files (*.*)", "*.*" } + { "\1" }, + { "\1" } }; char path[DVX_MAX_PATH]; @@ -650,8 +682,8 @@ static void onChooseWallpaper(WidgetT *w) { (void)w; FileFilterT filters[] = { - { "Images (*.bmp;*.jpg;*.png)", "*.bmp;*.jpg;*.png" }, - { "All Files (*.*)", "*.*" } + { "\1" }, + { "\1" } }; char path[DVX_MAX_PATH]; @@ -824,6 +856,7 @@ static void onOk(WidgetT *w) { prefsSetInt(sPrefs, "mouse", "doubleclick", wgtSliderGetValue(sDblClickSldr)); prefsSetString(sPrefs, "mouse", "acceleration", mapAccelValue(wgtDropdownGetSelected(sAccelDrop))); prefsSetInt(sPrefs, "mouse", "speed", wgtSliderGetValue(sSpeedSldr)); + prefsSetInt(sPrefs, "mouse", "wheelspeed", wgtSliderGetValue(sWheelStepSldr)); // Save colors to INI for (int32_t i = 0; i < ColorCountE; i++) { @@ -898,6 +931,7 @@ static void saveSnapshot(void) { sSavedDblClick = (int32_t)(sAc->dblClickTicks * 1000 / CLOCKS_PER_SEC); sSavedAccel = wgtDropdownGetSelected(sAccelDrop); sSavedSpeed = wgtSliderGetValue(sSpeedSldr); + sSavedWheelStep = sAc->wheelStep; sSavedVideoW = sAc->display.width; sSavedVideoH = sAc->display.height; sSavedVideoBpp = sAc->display.format.bitsPerPixel; @@ -918,7 +952,7 @@ static void restoreSnapshot(void) { const char *accelName = mapAccelValue(sSavedAccel); int32_t accelVal = mapAccelName(accelName); int32_t speedVal = 34 - sSavedSpeed; - dvxSetMouseConfig(sAc, sSavedWheelDir, sSavedDblClick, accelVal, speedVal); + dvxSetMouseConfig(sAc, sSavedWheelDir, sSavedDblClick, accelVal, speedVal, sSavedWheelStep); // Restore video mode if changed if (sAc->display.width != sSavedVideoW || @@ -1097,6 +1131,18 @@ static void updateSpeedLabel(void) { } +// ============================================================ +// updateWheelStepLabel +// ============================================================ + +static void updateWheelStepLabel(void) { + static char buf[32]; + int32_t val = wgtSliderGetValue(sWheelStepSldr); + snprintf(buf, sizeof(buf), "%ld", (long)val); + wgtSetText(sWheelStepLbl, buf); +} + + // ============================================================ // applyMouseConfig -- gather current widget values and apply // ============================================================ @@ -1113,9 +1159,10 @@ static void applyMouseConfig(void) { // slider 2 -> 32 mickeys/8px (slowest) // slider 8 -> 26 (near default) // slider 32 -> 2 mickeys/8px (fastest) - int32_t speedVal = 34 - wgtSliderGetValue(sSpeedSldr); + int32_t speedVal = 34 - wgtSliderGetValue(sSpeedSldr); + int32_t wheelStep = wgtSliderGetValue(sWheelStepSldr); - dvxSetMouseConfig(sAc, dir, dbl, accelVal, speedVal); + dvxSetMouseConfig(sAc, dir, dbl, accelVal, speedVal, wheelStep); } diff --git a/apps/cpanel/cpanel.res b/apps/cpanel/cpanel.res index 7eac7ee..a25bdb9 100644 --- a/apps/cpanel/cpanel.res +++ b/apps/cpanel/cpanel.res @@ -1,5 +1,5 @@ # cpanel.res -- Resource manifest for Control Panel -icon32 icon cpanel/icon32.bmp +icon32 icon icon32.bmp name text "Control Panel" author text "DVX Project" description text "System settings and preferences" diff --git a/apps/dvxbasic/Makefile b/apps/dvxbasic/Makefile index 9c1398b..6f4af44 100644 --- a/apps/dvxbasic/Makefile +++ b/apps/dvxbasic/Makefile @@ -25,7 +25,7 @@ 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 $(OBJDIR)/strip.o +COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/symtab.o $(OBJDIR)/strip.o $(OBJDIR)/obfuscate.o $(OBJDIR)/compact.o # IDE app objects IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideMenuEditor.o $(OBJDIR)/ideProject.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o @@ -38,25 +38,39 @@ 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 +HOSTCFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -D_GNU_SOURCE -I. -I../../core -I../../core/platform -I../../core/thirdparty BINDIR = ../../bin TEST_COMPILER = $(BINDIR)/test_compiler TEST_VM = $(BINDIR)/test_vm TEST_LEX = $(BINDIR)/test_lex TEST_QUICK = $(BINDIR)/test_quick +TEST_COMPACT = $(BINDIR)/test_compact STB_DS_IMPL = ../../core/thirdparty/stb_ds_impl.c -TEST_COMPILER_SRCS = test_compiler.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c $(STB_DS_IMPL) -TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c $(STB_DS_IMPL) +TEST_COMPILER_SRCS = test_compiler.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(STB_DS_IMPL) +TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c runtime/serialize.c $(STB_DS_IMPL) TEST_LEX_SRCS = test_lex.c compiler/lexer.c -TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c $(STB_DS_IMPL) +TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(STB_DS_IMPL) +TEST_COMPACT_SRCS = test_compact.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c $(STB_DS_IMPL) + +# Command-line compiler (host tool) +HOSTDIR = ../../bin/host +BASCOMP_SRCS = stub/bascomp.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/obfuscate.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c ../../core/dvxPrefs.c ../../core/dvxResource.c $(STB_DS_IMPL) +BASCOMP_TARGET = $(HOSTDIR)/bascomp + +# DOS command-line compiler +DOSCC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc +DOSCFLAGS = -O2 -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -march=i486 -mtune=i586 -I../../core -I../../core/platform -I../../core/thirdparty -I. +EXE2COFF = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/exe2coff +CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE +SYSTEMDIR = ../../bin/system .PHONY: all clean tests -all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) +all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) $(BASCOMP_TARGET) $(SYSTEMDIR)/BASCOMP.EXE $(SYSTEMDIR)/BASSTUB.APP -tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) +tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT) $(TEST_COMPILER): $(TEST_COMPILER_SRCS) | $(BINDIR) $(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm @@ -70,6 +84,31 @@ $(TEST_LEX): $(TEST_LEX_SRCS) | $(BINDIR) $(TEST_QUICK): $(TEST_QUICK_SRCS) | $(BINDIR) $(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_QUICK_SRCS) -lm +$(TEST_COMPACT): $(TEST_COMPACT_SRCS) | $(BINDIR) + $(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPACT_SRCS) -lm + +# Host command-line compiler (stub deployed alongside) +$(BASCOMP_TARGET): $(BASCOMP_SRCS) $(STUB_TARGET) | $(HOSTDIR) + $(HOSTCC) $(HOSTCFLAGS) -DBASCOMP_STANDALONE -I../../tools -o $@ $(BASCOMP_SRCS) -lm + cp $(STUB_TARGET) $(HOSTDIR)/BASSTUB.APP + +# DOS command-line compiler +$(SYSTEMDIR)/BASCOMP.EXE: $(BASCOMP_SRCS) | $(SYSTEMDIR) + $(DOSCC) $(DOSCFLAGS) -DBASCOMP_STANDALONE -I../../tools -o $(SYSTEMDIR)/bascomp.exe $(BASCOMP_SRCS) -lm + $(EXE2COFF) $(SYSTEMDIR)/bascomp.exe + cat $(CWSDSTUB) $(SYSTEMDIR)/bascomp > $@ + rm -f $(SYSTEMDIR)/bascomp $(SYSTEMDIR)/bascomp.exe + +# Deploy stub alongside DOS compiler +$(SYSTEMDIR)/BASSTUB.APP: $(STUB_TARGET) | $(SYSTEMDIR) + cp $(STUB_TARGET) $@ + +$(HOSTDIR): + mkdir -p $(HOSTDIR) + +$(SYSTEMDIR): + mkdir -p $(SYSTEMDIR) + # Runtime library DXE (exports symbols via dlregsym constructor) $(RT_TARGET): $(RT_OBJS) | $(RT_TARGETDIR) $(DXE3GEN) -o $(RT_TARGETDIR)/basrt.dxe -U $(RT_OBJS) @@ -105,6 +144,12 @@ $(OBJDIR)/serialize.o: runtime/serialize.c runtime/serialize.h runtime/vm.h | $( $(OBJDIR)/strip.o: compiler/strip.c compiler/strip.h compiler/opcodes.h runtime/vm.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< +$(OBJDIR)/obfuscate.o: compiler/obfuscate.c compiler/obfuscate.h runtime/vm.h runtime/values.h | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/compact.o: compiler/compact.c compiler/compact.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 $@ $< @@ -158,5 +203,5 @@ $(BINDIR): mkdir -p $(BINDIR) clean: - 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 -rf $(RT_OBJS) $(COMP_OBJS) $(IDE_OBJS) $(STUB_OBJS) $(RT_TARGET) $(APP_TARGET) $(STUB_TARGET) $(BASCOMP_TARGET) $(RT_TARGETDIR)/basrt.dep $(RT_TARGETDIR) $(OBJDIR)/basrt_init.o rm -f $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) diff --git a/apps/dvxbasic/compiler/codegen.c b/apps/dvxbasic/compiler/codegen.c index fcb4683..6e421da 100644 --- a/apps/dvxbasic/compiler/codegen.c +++ b/apps/dvxbasic/compiler/codegen.c @@ -372,47 +372,6 @@ const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name) } -// ============================================================ -// 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->formVarInfo); - free(mod->debugVars); - - if (mod->debugUdtDefs) { - for (int32_t i = 0; i < mod->debugUdtDefCount; i++) { - free(mod->debugUdtDefs[i].fields); - } - - free(mod->debugUdtDefs); - } - - free(mod); -} // ============================================================ diff --git a/apps/dvxbasic/compiler/compact.c b/apps/dvxbasic/compiler/compact.c new file mode 100644 index 0000000..7598f14 --- /dev/null +++ b/apps/dvxbasic/compiler/compact.c @@ -0,0 +1,564 @@ +// compact.c -- Release build bytecode compaction +// +// Walks the module's bytecode, removes OP_LINE instructions (3 bytes +// each), and rewrites all code-address references so control flow +// still lands on the correct instructions. +// +// Address references: +// - BasProcEntryT::codeAddr (absolute) +// - BasFormVarInfoT::initCodeAddr (absolute, 0 = no init) +// - OP_CALL operand (absolute uint16) +// - OP_JMP / OP_JMP_TRUE / OP_JMP_FALSE operand (relative int16) +// - OP_FOR_NEXT loopTop operand (relative int16) +// - OP_ON_ERROR handler operand (relative int16; 0 = disable, not remapped) +// - GOSUB return address (emitted as OP_PUSH_INT32 followed by OP_JMP, +// where the pushed value equals the PC immediately after the JMP) +// +// Safety: if any opcode cannot be sized, any jump overflows int16, or +// the walk doesn't reach codeLen exactly, the module is left untouched. + +#include "compact.h" +#include "opcodes.h" + +#include +#include +#include + + +// ============================================================ +// Opcode operand size table +// ============================================================ +// Returns operand byte count (excluding the 1-byte opcode), or -1 if unknown. + +static int32_t opOperandSize(uint8_t op) { + switch (op) { + // No operand bytes + case OP_NOP: + case OP_PUSH_TRUE: case OP_PUSH_FALSE: + case OP_POP: case OP_DUP: + case OP_LOAD_REF: case OP_STORE_REF: + case OP_ADD_INT: case OP_SUB_INT: case OP_MUL_INT: + case OP_IDIV_INT: case OP_MOD_INT: case OP_NEG_INT: + case OP_ADD_FLT: case OP_SUB_FLT: case OP_MUL_FLT: + case OP_DIV_FLT: case OP_NEG_FLT: case OP_POW: + case OP_STR_CONCAT: case OP_STR_LEFT: case OP_STR_RIGHT: + case OP_STR_MID: case OP_STR_MID2: case OP_STR_LEN: + case OP_STR_INSTR: case OP_STR_INSTR3: + case OP_STR_UCASE: case OP_STR_LCASE: + case OP_STR_TRIM: case OP_STR_LTRIM: case OP_STR_RTRIM: + case OP_STR_CHR: case OP_STR_ASC: case OP_STR_SPACE: + case OP_CMP_EQ: case OP_CMP_NE: case OP_CMP_LT: + case OP_CMP_GT: case OP_CMP_LE: case OP_CMP_GE: + case OP_AND: case OP_OR: case OP_NOT: + case OP_XOR: case OP_EQV: case OP_IMP: + case OP_GOSUB_RET: case OP_RET: case OP_RET_VAL: + case OP_FOR_POP: + case OP_CONV_INT_FLT: case OP_CONV_FLT_INT: + case OP_CONV_INT_STR: case OP_CONV_STR_INT: + case OP_CONV_FLT_STR: case OP_CONV_STR_FLT: + case OP_CONV_INT_LONG: case OP_CONV_LONG_INT: + case OP_PRINT: case OP_PRINT_NL: case OP_PRINT_TAB: + case OP_INPUT: + case OP_FILE_CLOSE: case OP_FILE_PRINT: case OP_FILE_INPUT: + case OP_FILE_EOF: case OP_FILE_LINE_INPUT: + case OP_LOAD_PROP: case OP_STORE_PROP: + case OP_LOAD_FORM: case OP_UNLOAD_FORM: + case OP_HIDE_FORM: case OP_DO_EVENTS: + case OP_MSGBOX: case OP_INPUTBOX: case OP_ME_REF: + case OP_CREATE_CTRL: case OP_FIND_CTRL: case OP_FIND_CTRL_IDX: + case OP_CREATE_CTRL_EX: + case OP_ERASE: + case OP_RESUME: case OP_RESUME_NEXT: + case OP_RAISE_ERR: case OP_ERR_NUM: case OP_ERR_CLEAR: + case OP_MATH_ABS: case OP_MATH_INT: case OP_MATH_FIX: + case OP_MATH_SGN: case OP_MATH_SQR: case OP_MATH_SIN: + case OP_MATH_COS: case OP_MATH_TAN: case OP_MATH_ATN: + case OP_MATH_LOG: case OP_MATH_EXP: case OP_MATH_RND: + case OP_MATH_RANDOMIZE: + case OP_RGB: + case OP_GET_RED: case OP_GET_GREEN: case OP_GET_BLUE: + case OP_STR_VAL: case OP_STR_STRF: case OP_STR_HEX: + case OP_STR_STRING: + case OP_MATH_TIMER: case OP_DATE_STR: case OP_TIME_STR: + case OP_SLEEP: case OP_ENVIRON: + case OP_READ_DATA: case OP_RESTORE: + case OP_FILE_WRITE: case OP_FILE_WRITE_SEP: case OP_FILE_WRITE_NL: + case OP_FILE_GET: case OP_FILE_PUT: case OP_FILE_SEEK: + case OP_FILE_LOF: case OP_FILE_LOC: case OP_FILE_FREEFILE: + case OP_FILE_INPUT_N: + case OP_STR_MID_ASGN: case OP_PRINT_USING: + case OP_PRINT_TAB_N: case OP_PRINT_SPC_N: + case OP_FORMAT: case OP_SHELL: + case OP_APP_PATH: case OP_APP_CONFIG: case OP_APP_DATA: + case OP_INI_READ: case OP_INI_WRITE: + case OP_FS_KILL: case OP_FS_NAME: case OP_FS_FILECOPY: + case OP_FS_MKDIR: case OP_FS_RMDIR: case OP_FS_CHDIR: + case OP_FS_CHDRIVE: case OP_FS_CURDIR: case OP_FS_DIR: + case OP_FS_DIR_NEXT: case OP_FS_FILELEN: + case OP_FS_GETATTR: case OP_FS_SETATTR: + case OP_CREATE_FORM: case OP_SET_EVENT: case OP_REMOVE_CTRL: + case OP_END: case OP_HALT: + return 0; + + case OP_LOAD_ARRAY: case OP_STORE_ARRAY: + case OP_PRINT_SPC: case OP_FILE_OPEN: + case OP_CALL_METHOD: case OP_SHOW_FORM: + case OP_LBOUND: case OP_UBOUND: + case OP_COMPARE_MODE: + return 1; + + case OP_PUSH_INT16: case OP_PUSH_STR: + case OP_LOAD_LOCAL: case OP_STORE_LOCAL: + case OP_LOAD_GLOBAL: case OP_STORE_GLOBAL: + case OP_LOAD_FIELD: case OP_STORE_FIELD: + case OP_PUSH_LOCAL_ADDR: case OP_PUSH_GLOBAL_ADDR: + case OP_JMP: case OP_JMP_TRUE: case OP_JMP_FALSE: + case OP_CTRL_REF: + case OP_LOAD_FORM_VAR: case OP_STORE_FORM_VAR: + case OP_PUSH_FORM_ADDR: + case OP_DIM_ARRAY: case OP_REDIM: + case OP_ON_ERROR: + case OP_STR_FIXLEN: + case OP_LINE: + return 2; + + case OP_STORE_ARRAY_FIELD: + case OP_FOR_INIT: + return 3; + + case OP_PUSH_INT32: case OP_PUSH_FLT32: + case OP_CALL: + return 4; + + case OP_FOR_NEXT: + return 5; + + case OP_CALL_EXTERN: + return 6; + + case OP_PUSH_FLT64: + return 8; + + default: + return -1; + } +} + + +// ============================================================ +// Little-endian helpers (bytecode is always LE regardless of host) +// ============================================================ + +static int16_t readI16LE(const uint8_t *p) { + return (int16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); +} + + +static uint16_t readU16LE(const uint8_t *p) { + return (uint16_t)p[0] | ((uint16_t)p[1] << 8); +} + + +static int32_t readI32LE(const uint8_t *p) { + return (int32_t)((uint32_t)p[0] | + ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | + ((uint32_t)p[3] << 24)); +} + + +static void writeI16LE(uint8_t *p, int16_t v) { + uint16_t u = (uint16_t)v; + p[0] = (uint8_t)(u & 0xFF); + p[1] = (uint8_t)((u >> 8) & 0xFF); +} + + +static void writeU16LE(uint8_t *p, uint16_t v) { + p[0] = (uint8_t)(v & 0xFF); + p[1] = (uint8_t)((v >> 8) & 0xFF); +} + + +static void writeI32LE(uint8_t *p, int32_t v) { + uint32_t u = (uint32_t)v; + p[0] = (uint8_t)(u & 0xFF); + p[1] = (uint8_t)((u >> 8) & 0xFF); + p[2] = (uint8_t)((u >> 16) & 0xFF); + p[3] = (uint8_t)((u >> 24) & 0xFF); +} + + +// ============================================================ +// Walk bytecode, build remap +// ============================================================ +// +// remap[oldPos] = newPos for every byte position in [0, oldCodeLen]. +// For OP_LINE bytes (removed): remap points at where the NEXT instruction +// starts in the new code. +// Final entry remap[oldCodeLen] = newCodeLen. +// +// Returns malloc'd array of size (oldCodeLen + 1), or NULL on failure. + +static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, int32_t *outNewLen) { + int32_t *remap = (int32_t *)malloc((codeLen + 1) * sizeof(int32_t)); + + if (!remap) { + return NULL; + } + + int32_t oldPc = 0; + int32_t newPc = 0; + + while (oldPc < codeLen) { + uint8_t op = code[oldPc]; + int32_t operand = opOperandSize(op); + + if (operand < 0) { + free(remap); + return NULL; + } + + int32_t instSize = 1 + operand; + + if (oldPc + instSize > codeLen) { + free(remap); + return NULL; + } + + if (op == OP_LINE) { + // These bytes are removed; they map to where the next instruction starts. + for (int32_t i = 0; i < instSize; i++) { + remap[oldPc + i] = newPc; + } + } else { + for (int32_t i = 0; i < instSize; i++) { + remap[oldPc + i] = newPc + i; + } + + newPc += instSize; + } + + oldPc += instSize; + } + + if (oldPc != codeLen) { + free(remap); + return NULL; + } + + remap[codeLen] = newPc; + *outNewLen = newPc; + return remap; +} + + +// ============================================================ +// GOSUB pattern detection +// ============================================================ +// +// GOSUB emits: +// oldPc: OP_PUSH_INT32 (1 byte) +// oldPc+1: int32 value V (4 bytes) +// oldPc+5: OP_JMP (1 byte) +// oldPc+6: int16 offset (2 bytes) +// oldPc+8: +// The invariant is V == oldPc + 8 (the pushed return address). +// +// Returns true if the given position is the start of such a pattern. + +static bool isGosubPush(const uint8_t *code, int32_t codeLen, int32_t pos) { + if (pos + 8 > codeLen) { + return false; + } + + if (code[pos] != OP_PUSH_INT32) { + return false; + } + + if (code[pos + 5] != OP_JMP) { + return false; + } + + int32_t value = readI32LE(code + pos + 1); + return value == pos + 8; +} + + +// ============================================================ +// Apply remap to a single instruction's operand +// ============================================================ +// +// Returns true on success, false if an offset overflows int16 or a +// target doesn't land on a valid instruction in the old code. + +static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, uint16_t oldAddr, const int32_t *remap, int32_t newCodeLen) { + int32_t newAddr = remap[oldAddr]; + + if (newAddr < 0 || newAddr > newCodeLen) { + return false; + } + + if (newAddr > 0xFFFF) { + return false; + } + + writeU16LE(newCode + newOpPos + operandOffset, (uint16_t)newAddr); + return true; +} + + +// Rewrite a relative int16 offset. +// oldOpPos, oldPcAfter: position of opcode and PC after reading offset +// newOpPos, newPcAfter: same in the new code +// operandOffset: byte offset from opcode to the int16 operand +// oldOffset: the offset as stored in the old code +// +// Handles ON_ERROR's special case (offset == 0 means "disable"). +// For ON_ERROR the caller passes allowZero=true; the zero is preserved as-is. + +static bool remapRelI16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, int16_t oldOffset, int32_t oldPcAfter, int32_t newPcAfter, const int32_t *remap, int32_t codeLen, int32_t newCodeLen, bool allowZero) { + if (allowZero && oldOffset == 0) { + writeI16LE(newCode + newOpPos + operandOffset, 0); + return true; + } + + int32_t oldTarget = oldPcAfter + oldOffset; + + if (oldTarget < 0 || oldTarget > codeLen) { + return false; + } + + int32_t newTarget = remap[oldTarget]; + + if (newTarget < 0 || newTarget > newCodeLen) { + return false; + } + + int32_t newOffset = newTarget - newPcAfter; + + if (newOffset < -32768 || newOffset > 32767) { + return false; + } + + writeI16LE(newCode + newOpPos + operandOffset, (int16_t)newOffset); + return true; +} + + +// ============================================================ +// basCompactBytecode +// ============================================================ + +int32_t basCompactBytecode(BasModuleT *mod) { + if (!mod || !mod->code || mod->codeLen <= 0) { + return 0; + } + + const uint8_t *oldCode = mod->code; + int32_t oldCodeLen = mod->codeLen; + + // Count OP_LINE occurrences. If none, nothing to do. + int32_t lineCount = 0; + { + int32_t pc = 0; + + while (pc < oldCodeLen) { + uint8_t op = oldCode[pc]; + int32_t operand = opOperandSize(op); + + if (operand < 0 || pc + 1 + operand > oldCodeLen) { + return 0; // unknown opcode -- skip compaction + } + + if (op == OP_LINE) { + lineCount++; + } + + pc += 1 + operand; + } + + if (pc != oldCodeLen) { + return 0; + } + + if (lineCount == 0) { + return 0; + } + } + + int32_t newCodeLen = 0; + int32_t *remap = buildRemap(oldCode, oldCodeLen, &newCodeLen); + + if (!remap) { + return 0; + } + + uint8_t *newCode = (uint8_t *)malloc(newCodeLen > 0 ? newCodeLen : 1); + + if (!newCode) { + free(remap); + return 0; + } + + // Copy bytes (skipping OP_LINE) and rewrite address operands. + bool ok = true; + int32_t oldPc = 0; + + while (oldPc < oldCodeLen && ok) { + uint8_t op = oldCode[oldPc]; + int32_t operand = opOperandSize(op); + int32_t instSize = 1 + operand; + + if (op == OP_LINE) { + oldPc += instSize; + continue; + } + + int32_t newPc = remap[oldPc]; + + // Copy the instruction verbatim first; we'll overwrite operands that + // need remapping below. + memcpy(newCode + newPc, oldCode + oldPc, instSize); + + switch (op) { + case OP_CALL: { + uint16_t oldAddr = readU16LE(oldCode + oldPc + 1); + + if (!remapAbsU16(newCode, newPc, 1, oldAddr, remap, newCodeLen)) { + ok = false; + } + + break; + } + + case OP_JMP: + case OP_JMP_TRUE: + case OP_JMP_FALSE: { + int16_t oldOff = readI16LE(oldCode + oldPc + 1); + + if (!remapRelI16(newCode, newPc, 1, oldOff, + oldPc + 3, newPc + 3, + remap, oldCodeLen, newCodeLen, false)) { + ok = false; + } + + break; + } + + case OP_FOR_NEXT: { + int16_t oldOff = readI16LE(oldCode + oldPc + 4); + + if (!remapRelI16(newCode, newPc, 4, oldOff, + oldPc + 6, newPc + 6, + remap, oldCodeLen, newCodeLen, false)) { + ok = false; + } + + break; + } + + case OP_ON_ERROR: { + int16_t oldOff = readI16LE(oldCode + oldPc + 1); + + if (!remapRelI16(newCode, newPc, 1, oldOff, + oldPc + 3, newPc + 3, + remap, oldCodeLen, newCodeLen, true)) { + ok = false; + } + + break; + } + + case OP_PUSH_INT32: { + // Detect GOSUB return-address push and remap the absolute address. + if (isGosubPush(oldCode, oldCodeLen, oldPc)) { + int32_t oldAddr = readI32LE(oldCode + oldPc + 1); + + if (oldAddr < 0 || oldAddr > oldCodeLen) { + ok = false; + break; + } + + int32_t newAddr = remap[oldAddr]; + + if (newAddr < 0 || newAddr > newCodeLen) { + ok = false; + break; + } + + writeI32LE(newCode + newPc + 1, newAddr); + } + + break; + } + + default: + break; + } + + oldPc += instSize; + } + + if (!ok) { + free(newCode); + free(remap); + return 0; + } + + // Rewrite proc entry points + for (int32_t i = 0; i < mod->procCount; i++) { + int32_t oldAddr = mod->procs[i].codeAddr; + + if (oldAddr < 0 || oldAddr > oldCodeLen) { + free(newCode); + free(remap); + return 0; + } + + mod->procs[i].codeAddr = remap[oldAddr]; + } + + // Rewrite form-var init code addresses. Negative means "no init code". + for (int32_t i = 0; i < mod->formVarInfoCount; i++) { + int32_t oldAddr = mod->formVarInfo[i].initCodeAddr; + + if (oldAddr < 0) { + continue; + } + + if (oldAddr > oldCodeLen) { + free(newCode); + free(remap); + return 0; + } + + int32_t oldLen = mod->formVarInfo[i].initCodeLen; + int32_t oldEnd = oldAddr + oldLen; + + if (oldEnd > oldCodeLen) { + oldEnd = oldCodeLen; + } + + int32_t newAddr = remap[oldAddr]; + int32_t newEnd = remap[oldEnd]; + + mod->formVarInfo[i].initCodeAddr = newAddr; + mod->formVarInfo[i].initCodeLen = newEnd - newAddr; + } + + // Rewrite entry point + if (mod->entryPoint >= 0 && mod->entryPoint <= oldCodeLen) { + mod->entryPoint = remap[mod->entryPoint]; + } + + // Swap in the new code + free(mod->code); + mod->code = newCode; + mod->codeLen = newCodeLen; + + int32_t removed = oldCodeLen - newCodeLen; + + free(remap); + return removed; +} diff --git a/apps/dvxbasic/compiler/compact.h b/apps/dvxbasic/compiler/compact.h new file mode 100644 index 0000000..5822d46 --- /dev/null +++ b/apps/dvxbasic/compiler/compact.h @@ -0,0 +1,21 @@ +// compact.h -- Release build bytecode compaction +// +// Removes OP_LINE instructions from the module's bytecode and +// rewrites all code-address references (proc entries, CALL operands, +// JMP/FOR_NEXT/ON_ERROR relative offsets, GOSUB return addresses, +// and formVarInfo init code addresses). +// +// Safe by construction: if any sanity check fails (unknown opcode, +// offset overflow, walk not landing on codeLen, etc.), the module +// is left unchanged and the function returns 0. + +#ifndef DVXBASIC_COMPACT_H +#define DVXBASIC_COMPACT_H + +#include "../runtime/vm.h" +#include + +// Returns the number of bytes removed, or 0 if compaction was skipped. +int32_t basCompactBytecode(BasModuleT *mod); + +#endif // DVXBASIC_COMPACT_H diff --git a/apps/dvxbasic/compiler/lexer.c b/apps/dvxbasic/compiler/lexer.c index fcd825b..a649e98 100644 --- a/apps/dvxbasic/compiler/lexer.c +++ b/apps/dvxbasic/compiler/lexer.c @@ -77,6 +77,7 @@ static const KeywordEntryT sKeywords[] = { { "IF", TOK_IF }, { "IMP", TOK_IMP }, { "INIREAD", TOK_INIREAD }, + { "INIREAD$", TOK_INIREAD }, { "INIWRITE", TOK_INIWRITE }, { "INPUT", TOK_INPUT }, { "INTEGER", TOK_INTEGER }, @@ -682,7 +683,16 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) { } } - BasTokenTypeE kwType = lookupKeyword(lex->token.text, baseLen); + // Try the full text first (including any type suffix). Suffix-bearing + // keywords like CURDIR$, DIR$, INIREAD$, INPUTBOX$ are listed in the + // keyword table with their $ and will match here. If the full text + // isn't a keyword, fall back to the base name (without suffix). + BasTokenTypeE kwType = lookupKeyword(lex->token.text, idx); + bool matchedWithSuffix = (kwType != TOK_IDENT && baseLen != idx); + + if (kwType == TOK_IDENT && baseLen != idx) { + kwType = lookupKeyword(lex->token.text, baseLen); + } // REM is a comment -- skip to end of line if (kwType == TOK_REM) { @@ -694,9 +704,9 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) { return TOK_NEWLINE; } - // 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_INPUTBOX)) { + // Accept the keyword if it's a plain keyword (no suffix on source) or + // if it explicitly matched a $-suffixed entry in the keyword table. + if (kwType != TOK_IDENT && (baseLen == idx || matchedWithSuffix)) { return kwType; } diff --git a/apps/dvxbasic/compiler/obfuscate.c b/apps/dvxbasic/compiler/obfuscate.c new file mode 100644 index 0000000..267312e --- /dev/null +++ b/apps/dvxbasic/compiler/obfuscate.c @@ -0,0 +1,590 @@ +// obfuscate.c -- Release build name obfuscation +// +// See obfuscate.h for the high-level description. + +#include "obfuscate.h" +#include "../runtime/values.h" + +#include +#include +#include +#include + +// ============================================================ +// Event suffix list (must match strip.c) +// ============================================================ + +static const char *sEventSuffixes[] = { + "Load", "Unload", "QueryUnload", "Resize", "Activate", "Deactivate", + "Click", "DblClick", "Change", "Timer", + "GotFocus", "LostFocus", + "KeyPress", "KeyDown", "KeyUp", + "MouseDown", "MouseUp", "MouseMove", + "Scroll", "Reposition", "Validate", + NULL +}; + + +// ============================================================ +// Name map +// ============================================================ + +typedef struct { + char *orig; // original name (strdup'd, case-preserved) + char *mapped; // new name (strdup'd, "C1" .. "Cn") +} NameEntryT; + +typedef struct { + NameEntryT *entries; + int32_t count; + int32_t cap; +} NameMapT; + + +static void nameMapInit(NameMapT *m) { + m->entries = NULL; + m->count = 0; + m->cap = 0; +} + + +static void nameMapFree(NameMapT *m) { + for (int32_t i = 0; i < m->count; i++) { + free(m->entries[i].orig); + free(m->entries[i].mapped); + } + + free(m->entries); + m->entries = NULL; + m->count = 0; + m->cap = 0; +} + + +// Look up an original name (case-insensitive). Returns mapped name or NULL. +static const char *nameMapLookup(const NameMapT *m, const char *name) { + for (int32_t i = 0; i < m->count; i++) { + if (strcasecmp(m->entries[i].orig, name) == 0) { + return m->entries[i].mapped; + } + } + + return NULL; +} + + +// Add a name if not already present. Returns mapped name. +static const char *nameMapAdd(NameMapT *m, const char *name) { + const char *existing = nameMapLookup(m, name); + + if (existing) { + return existing; + } + + if (m->count >= m->cap) { + int32_t newCap = m->cap == 0 ? 16 : m->cap * 2; + NameEntryT *newEntries = realloc(m->entries, newCap * sizeof(NameEntryT)); + + if (!newEntries) { + return NULL; + } + + m->entries = newEntries; + m->cap = newCap; + } + + char mapped[16]; + snprintf(mapped, sizeof(mapped), "C%ld", (long)(m->count + 1)); + + m->entries[m->count].orig = strdup(name); + m->entries[m->count].mapped = strdup(mapped); + m->count++; + + return m->entries[m->count - 1].mapped; +} + + +// ============================================================ +// .frm parsing helpers +// ============================================================ + +// Skip ASCII whitespace. Returns pointer past whitespace. +static const char *skipWhitespace(const char *p, const char *end) { + while (p < end && (*p == ' ' || *p == '\t')) { + p++; + } + + return p; +} + + +// Copy next whitespace-delimited token into buf. Returns pointer after token. +static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize) { + int32_t len = 0; + + while (p < end && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && len < bufSize - 1) { + buf[len++] = *p++; + } + + buf[len] = '\0'; + return p; +} + + +// Check if name is a valid identifier (letters, digits, underscore, starts non-digit) +static bool isValidIdent(const char *name) { + if (!name || !*name) { + return false; + } + + if (!isalpha((unsigned char)name[0]) && name[0] != '_') { + return false; + } + + for (const char *p = name; *p; p++) { + if (!isalnum((unsigned char)*p) && *p != '_') { + return false; + } + } + + return true; +} + + +// ============================================================ +// Pass 1: collect all form/control names from .frm texts +// ============================================================ + +// Scan a .frm text and add all "Begin " names to the map. +static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *map) { + const char *p = text; + const char *end = text + len; + + while (p < end) { + // Read one line + const char *lineStart = p; + + while (p < end && *p != '\n' && *p != '\r') { + p++; + } + + const char *lineEnd = p; + + if (p < end && *p == '\r') { + p++; + } + + if (p < end && *p == '\n') { + p++; + } + + // Trim leading whitespace + const char *l = skipWhitespace(lineStart, lineEnd); + + // Check "Begin " + if ((lineEnd - l) < 6 || strncasecmp(l, "Begin ", 6) != 0) { + continue; + } + + l += 6; + l = skipWhitespace(l, lineEnd); + + // Read type name + char typeName[64]; + l = readToken(l, lineEnd, typeName, sizeof(typeName)); + + if (typeName[0] == '\0') { + continue; + } + + // Read control name + l = skipWhitespace(l, lineEnd); + char ctrlName[64]; + l = readToken(l, lineEnd, ctrlName, sizeof(ctrlName)); + + if (ctrlName[0] && isValidIdent(ctrlName)) { + nameMapAdd(map, ctrlName); + } + } +} + + +// ============================================================ +// Pass 2: strip BASIC code from .frm text (everything after outer End) +// ============================================================ + +// Find the position just after the matching End of the outermost Begin Form. +// Returns len of the stripped .frm. If no Begin Form found, returns original len. +static int32_t findFormEndPos(const char *text, int32_t len) { + int32_t nesting = 0; + bool inForm = false; + + const char *p = text; + const char *end = text + len; + + while (p < end) { + const char *lineStart = p; + + while (p < end && *p != '\n' && *p != '\r') { + p++; + } + + const char *lineEnd = p; + + if (p < end && *p == '\r') { + p++; + } + + if (p < end && *p == '\n') { + p++; + } + + const char *l = skipWhitespace(lineStart, lineEnd); + + if ((lineEnd - l) >= 6 && strncasecmp(l, "Begin ", 6) == 0) { + // Check for "Begin Form ..." to set inForm on outer open + if (!inForm) { + const char *r = l + 6; + r = skipWhitespace(r, lineEnd); + + if ((lineEnd - r) >= 5 && strncasecmp(r, "Form ", 5) == 0) { + inForm = true; + } + } + + nesting++; + } else if ((lineEnd - l) >= 3 && strncasecmp(l, "End", 3) == 0 && + (lineEnd - l == 3 || l[3] == ' ' || l[3] == '\t' || l[3] == '\r')) { + nesting--; + + if (inForm && nesting == 0) { + return (int32_t)(p - text); + } + } + } + + return len; +} + + +// ============================================================ +// Pass 3: rewrite .frm text with mapped names +// ============================================================ + +// Returns true if c is a valid identifier character. +static bool isIdentChar(int c) { + return isalnum(c) || c == '_'; +} + + +// Scan text; for each identifier found outside of strings, if it's in +// the map, emit the mapped name instead. Output to out (returns bytes written). +static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *map, uint8_t *out, int32_t outCap) { + int32_t outLen = 0; + int32_t i = 0; + bool inStr = false; + + while (i < srcLen) { + char c = src[i]; + + if (c == '"') { + inStr = !inStr; + + if (outLen < outCap) { + out[outLen++] = (uint8_t)c; + } + + i++; + continue; + } + + // Read identifier + if (!inStr && (isalpha((unsigned char)c) || c == '_')) { + int32_t identStart = i; + + while (i < srcLen && isIdentChar((unsigned char)src[i])) { + i++; + } + + int32_t identLen = i - identStart; + char ident[128]; + + if (identLen >= (int32_t)sizeof(ident)) { + identLen = (int32_t)sizeof(ident) - 1; + } + + memcpy(ident, src + identStart, identLen); + ident[identLen] = '\0'; + + const char *mapped = nameMapLookup(map, ident); + + if (mapped) { + int32_t mLen = (int32_t)strlen(mapped); + + for (int32_t k = 0; k < mLen && outLen < outCap; k++) { + out[outLen++] = (uint8_t)mapped[k]; + } + } else { + for (int32_t k = 0; k < identLen && outLen < outCap; k++) { + out[outLen++] = (uint8_t)ident[k]; + } + } + + continue; + } + + if (outLen < outCap) { + out[outLen++] = (uint8_t)c; + } + + i++; + } + + return outLen; +} + + +// ============================================================ +// Module rewriting +// ============================================================ + +// Replace the contents of a constant pool entry with a new string. +static void replaceConstant(BasModuleT *mod, int32_t idx, const char *newText) { + BasStringT *newStr = basStringNew(newText, (int32_t)strlen(newText)); + + if (!newStr) { + return; + } + + basStringUnref(mod->constants[idx]); + mod->constants[idx] = newStr; +} + + +static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map) { + for (int32_t i = 0; i < mod->constCount; i++) { + const BasStringT *s = mod->constants[i]; + + if (!s) { + continue; + } + + const char *mapped = nameMapLookup(map, s->data); + + if (mapped) { + replaceConstant(mod, i, mapped); + } + } +} + + +// Check if suffix is a known event name. +static bool isEventSuffix(const char *suffix) { + for (int32_t i = 0; sEventSuffixes[i]; i++) { + if (strcasecmp(suffix, sEventSuffixes[i]) == 0) { + return true; + } + } + + return false; +} + + +static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) { + for (int32_t i = 0; i < mod->procCount; i++) { + BasProcEntryT *proc = &mod->procs[i]; + + if (proc->name[0] == '\0') { + continue; + } + + // Find last underscore + char *underscore = strrchr(proc->name, '_'); + + if (!underscore) { + continue; + } + + const char *suffix = underscore + 1; + + if (!isEventSuffix(suffix)) { + continue; + } + + // Split on underscore + int32_t prefixLen = (int32_t)(underscore - proc->name); + char prefix[BAS_MAX_PROC_NAME]; + + if (prefixLen >= (int32_t)sizeof(prefix)) { + prefixLen = (int32_t)sizeof(prefix) - 1; + } + + memcpy(prefix, proc->name, prefixLen); + prefix[prefixLen] = '\0'; + + const char *mapped = nameMapLookup(map, prefix); + + if (mapped) { + char newName[BAS_MAX_PROC_NAME]; + snprintf(newName, sizeof(newName), "%s_%s", mapped, suffix); + snprintf(proc->name, sizeof(proc->name), "%s", newName); + } + } +} + + +static void rewriteModuleFormVars(BasModuleT *mod, const NameMapT *map) { + for (int32_t i = 0; i < mod->formVarInfoCount; i++) { + BasFormVarInfoT *fv = &mod->formVarInfo[i]; + const char *mapped = nameMapLookup(map, fv->formName); + + if (mapped) { + snprintf(fv->formName, sizeof(fv->formName), "%s", mapped); + } + } +} + + +// ============================================================ +// basStripFrmComments +// ============================================================ + +int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap) { + if (!src || srcLen <= 0 || !outBuf || outCap <= 0) { + return 0; + } + + int32_t outLen = 0; + int32_t i = 0; + + while (i < srcLen) { + int32_t lineStart = i; + + while (i < srcLen && src[i] != '\n' && src[i] != '\r') { + i++; + } + + int32_t lineEnd = i; + + if (i < srcLen && src[i] == '\r') { + i++; + } + + if (i < srcLen && src[i] == '\n') { + i++; + } + + // Scan for first unquoted ' (comment start). + bool inStr = false; + int32_t commentStart = -1; + + for (int32_t j = lineStart; j < lineEnd; j++) { + char c = src[j]; + + if (c == '"') { + inStr = !inStr; + } else if (c == '\'' && !inStr) { + commentStart = j; + break; + } + } + + int32_t contentEnd = (commentStart >= 0) ? commentStart : lineEnd; + + // Check for whole-line REM. Find first non-whitespace position. + int32_t firstNonWs = lineStart; + + while (firstNonWs < contentEnd && (src[firstNonWs] == ' ' || src[firstNonWs] == '\t')) { + firstNonWs++; + } + + if (contentEnd - firstNonWs >= 3 && + strncasecmp(src + firstNonWs, "REM", 3) == 0 && + (contentEnd - firstNonWs == 3 || + src[firstNonWs + 3] == ' ' || + src[firstNonWs + 3] == '\t')) { + contentEnd = firstNonWs; + } + + // Trim trailing whitespace. + while (contentEnd > lineStart && (src[contentEnd - 1] == ' ' || src[contentEnd - 1] == '\t')) { + contentEnd--; + } + + // Drop lines that have no non-whitespace content. + if (contentEnd <= firstNonWs) { + continue; + } + + int32_t writeLen = contentEnd - lineStart; + + if (outLen + writeLen + 1 >= outCap) { + break; + } + + memcpy(outBuf + outLen, src + lineStart, writeLen); + outLen += writeLen; + outBuf[outLen++] = '\n'; + } + + return outLen; +} + + +// ============================================================ +// Top-level entry point +// ============================================================ + +void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms) { + if (!mod || frmCount < 0) { + return; + } + + NameMapT map; + nameMapInit(&map); + + // Pass 1: collect all names from all .frm texts + for (int32_t i = 0; i < frmCount; i++) { + if (frmTexts[i] && frmLens[i] > 0) { + collectNamesFromFrm(frmTexts[i], frmLens[i], &map); + } + } + + // Pass 2: rewrite each .frm + for (int32_t i = 0; i < frmCount; i++) { + outFrms[i].data = NULL; + outFrms[i].len = 0; + + if (!frmTexts[i] || frmLens[i] <= 0) { + continue; + } + + int32_t strippedLen = findFormEndPos(frmTexts[i], frmLens[i]); + + // Allocate generous output buffer (mapped names are usually shorter + // than originals, but allow for growth and a trailing newline). + int32_t outCap = strippedLen + 1024; + uint8_t *outBuf = malloc(outCap); + + if (!outBuf) { + continue; + } + + int32_t outLen = rewriteFrmText(frmTexts[i], strippedLen, &map, outBuf, outCap); + + // Ensure trailing newline + if (outLen > 0 && outBuf[outLen - 1] != '\n' && outLen < outCap) { + outBuf[outLen++] = '\n'; + } + + outFrms[i].data = outBuf; + outFrms[i].len = outLen; + } + + // Pass 3: rewrite module + rewriteModuleConstants(mod, &map); + rewriteModuleProcs(mod, &map); + rewriteModuleFormVars(mod, &map); + + nameMapFree(&map); +} diff --git a/apps/dvxbasic/compiler/obfuscate.h b/apps/dvxbasic/compiler/obfuscate.h new file mode 100644 index 0000000..c30baa6 --- /dev/null +++ b/apps/dvxbasic/compiler/obfuscate.h @@ -0,0 +1,46 @@ +// obfuscate.h -- Release build name obfuscation +// +// Replaces form and control names with generated tokens (C1, C2, ...) +// across the compiled module AND the raw .frm text resources. This +// hinders casual decompilation by removing meaningful identifiers +// from event handler names (e.g., BtnHello_Click -> C3_Click). + +#ifndef DVXBASIC_OBFUSCATE_H +#define DVXBASIC_OBFUSCATE_H + +#include "../runtime/vm.h" + +// One .frm text after obfuscation (data is newly-allocated). +typedef struct { + uint8_t *data; + int32_t len; +} BasObfFrmT; + +// Obfuscate form/control names in the module and all .frm texts. +// +// Reads original names from the Begin declarations in each .frm, +// generates C1..Cn, then rewrites: +// - The .frm text (form/control name declarations, stripping the +// trailing BASIC code section after the outer form closes) +// - Module string constants matching any original name +// - Procedure names matching _ +// - formVarInfo entries keyed by form name +// +// frmTexts / frmLens: input .frm buffers (one per form). +// outFrms: caller-allocated array of frmCount entries; function fills +// in .data (malloc'd, caller frees) and .len for each. +void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms); + + +// Strip comments from .frm text. +// +// Removes both whole-line comments (lines whose first non-whitespace +// token is `'` or `REM`) and trailing comments (everything from the +// first unquoted `'` to end of line). Pure-whitespace lines are +// dropped. Quoted string literals are preserved as-is. +// +// srcLen: input length; outBuf: caller-allocated buffer of outCap bytes. +// Returns the number of bytes written. +int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap); + +#endif // DVXBASIC_OBFUSCATE_H diff --git a/apps/dvxbasic/compiler/opcodes.h b/apps/dvxbasic/compiler/opcodes.h index df72326..a6c7fb5 100644 --- a/apps/dvxbasic/compiler/opcodes.h +++ b/apps/dvxbasic/compiler/opcodes.h @@ -235,6 +235,9 @@ #define OP_MATH_EXP 0xAA #define OP_MATH_RND 0xAB #define OP_MATH_RANDOMIZE 0xAC // seed on stack (or TIMER if -1) +#define OP_RGB 0xAD // pop b, g, r; push LONG = (r<<16)|(g<<8)|b +#define OP_GET_RED 0xAE // pop LONG color; push (color>>16) & 0xFF +#define OP_GET_GREEN 0xAF // pop LONG color; push (color>>8) & 0xFF // ============================================================ // Conversion built-ins @@ -317,6 +320,8 @@ #define OP_CALL_EXTERN 0xCD // [uint16 libNameIdx] [uint16 funcNameIdx] [uint8 argc] [uint8 retType] +#define OP_GET_BLUE 0xD0 // pop LONG color; push color & 0xFF + // App object #define OP_APP_PATH 0xDD // push App.Path string #define OP_APP_CONFIG 0xDE // push App.Config string diff --git a/apps/dvxbasic/compiler/parser.c b/apps/dvxbasic/compiler/parser.c index d87c7d9..31e7000 100644 --- a/apps/dvxbasic/compiler/parser.c +++ b/apps/dvxbasic/compiler/parser.c @@ -77,8 +77,12 @@ static const BuiltinFuncT builtinFuncs[] = { {"COS", OP_MATH_COS, 1, 1, BAS_TYPE_DOUBLE}, {"EXP", OP_MATH_EXP, 1, 1, BAS_TYPE_DOUBLE}, {"FIX", OP_MATH_FIX, 1, 1, BAS_TYPE_INTEGER}, + {"GETBLUE", OP_GET_BLUE, 1, 1, BAS_TYPE_INTEGER}, + {"GETGREEN", OP_GET_GREEN, 1, 1, BAS_TYPE_INTEGER}, + {"GETRED", OP_GET_RED, 1, 1, BAS_TYPE_INTEGER}, {"INT", OP_MATH_INT, 1, 1, BAS_TYPE_INTEGER}, {"LOG", OP_MATH_LOG, 1, 1, BAS_TYPE_DOUBLE}, + {"RGB", OP_RGB, 3, 3, BAS_TYPE_LONG}, {"RND", OP_MATH_RND, 0, 1, BAS_TYPE_DOUBLE}, {"SGN", OP_MATH_SGN, 1, 1, BAS_TYPE_INTEGER}, {"SIN", OP_MATH_SIN, 1, 1, BAS_TYPE_DOUBLE}, @@ -1250,7 +1254,9 @@ static void parsePrimary(BasParserT *p) { } else if (checkKeyword(p,"Config")) { advance(p); basEmit8(&p->cg, OP_APP_CONFIG); - } else if (checkKeyword(p,"Data")) { + } else if (checkKeyword(p,"Data") || p->lex.token.type == TOK_DATA) { + // "Data" tokenizes as TOK_DATA (the DATA/READ keyword) rather + // than TOK_IDENT, so accept it directly by token type too. advance(p); basEmit8(&p->cg, OP_APP_DATA); } else { @@ -1482,23 +1488,32 @@ static void parsePrimary(BasParserT *p) { return; } - // MsgBox(message [, flags]) -- as function expression returning button ID + // MsgBox(message [, flags [, title]]) -- function form returning button ID if (tt == TOK_MSGBOX) { advance(p); expect(p, TOK_LPAREN); parseExpression(p); // message + if (match(p, TOK_COMMA)) { parseExpression(p); // flags } else { basEmit8(&p->cg, OP_PUSH_INT16); basEmit16(&p->cg, 0); // default flags = vbOKOnly } + + if (match(p, TOK_COMMA)) { + parseExpression(p); // title + } else { + uint16_t emptyIdx = basAddConstant(&p->cg, "", 0); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, emptyIdx); + } + expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_MSGBOX); return; } - // SQL expression functions -- all require parentheses // IniRead$(file, section, key, default) if (tt == TOK_INIREAD) { advance(p); @@ -2251,6 +2266,17 @@ static void parseAssignOrCall(BasParserT *p) { error(p, buf); return; } + + // External library SUB: emit OP_CALL_EXTERN + if (sym->isExtern) { + basEmit8(&p->cg, OP_CALL_EXTERN); + basEmitU16(&p->cg, sym->externLibIdx); + basEmitU16(&p->cg, sym->externFuncIdx); + basEmit8(&p->cg, (uint8_t)argc); + basEmit8(&p->cg, sym->dataType); + return; + } + { uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0; basEmit8(&p->cg, OP_CALL); @@ -2371,7 +2397,7 @@ static void parseClose(BasParserT *p) { static void parseConst(BasParserT *p) { - // CONST name = value + // CONST name [AS type] = value advance(p); // consume CONST if (!check(p, TOK_IDENT)) { @@ -2384,6 +2410,12 @@ static void parseConst(BasParserT *p) { name[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); + // Optional type annotation (declarative; value's literal type still + // determines runtime representation). + if (match(p, TOK_AS)) { + (void)resolveTypeName(p); + } + expect(p, TOK_EQ); // Parse the constant value (must be a literal) @@ -2711,9 +2743,25 @@ static void parseDeclareLibrary(BasParserT *p) { char funcName[BAS_MAX_TOKEN_LEN]; strncpy(funcName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); funcName[BAS_MAX_TOKEN_LEN - 1] = '\0'; - uint16_t funcNameIdx = basAddConstant(&p->cg, funcName, (int32_t)strlen(funcName)); advance(p); + // Strip type suffix ($%&!#) from extern name so dlsym finds the + // C function. The suffix is still used for return type via suffixToType. + char externName[BAS_MAX_TOKEN_LEN]; + strncpy(externName, funcName, BAS_MAX_TOKEN_LEN - 1); + externName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + int32_t enLen = (int32_t)strlen(externName); + + if (enLen > 0) { + char last = externName[enLen - 1]; + + if (last == '$' || last == '%' || last == '&' || last == '!' || last == '#') { + externName[enLen - 1] = '\0'; + } + } + + uint16_t funcNameIdx = basAddConstant(&p->cg, externName, (int32_t)strlen(externName)); + // Parse parameter list int32_t paramCount = 0; uint8_t paramTypes[BAS_MAX_PARAMS]; @@ -4191,20 +4239,49 @@ static void parsePrint(BasParserT *p) { // PRINT USING "fmt"; expr [; expr] ... advance(p); // consume PRINT - // Check for file I/O: PRINT #channel, expr + // File I/O: PRINT #channel, expr [; expr | , expr ]* [;] + // + // Channel is pushed once and DUP'd per value. `;` means no separator + // between values; `,` is treated the same (tab-zone separator not + // supported for file output). Trailing `;` suppresses the final newline. if (check(p, TOK_HASH)) { advance(p); // consume # - // Channel number + // Channel number -- stays on stack as "keep" for the whole statement. parseExpression(p); - - // Comma separator expect(p, TOK_COMMA); - // Value to print - parseExpression(p); + bool trailingSep = false; + + for (;;) { + // Duplicate the channel for this OP_FILE_PRINT. + basEmit8(&p->cg, OP_DUP); + parseExpression(p); + basEmit8(&p->cg, OP_FILE_PRINT); + + if (check(p, TOK_SEMICOLON) || check(p, TOK_COMMA)) { + advance(p); + trailingSep = true; + + if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { + break; + } + + trailingSep = false; + continue; + } + + break; + } + + // No trailing ; or , -> write newline. Otherwise, just drop the + // kept channel value. + if (trailingSep) { + basEmit8(&p->cg, OP_POP); + } else { + basEmit8(&p->cg, OP_FILE_WRITE_NL); + } - basEmit8(&p->cg, OP_FILE_PRINT); return; } @@ -5187,21 +5264,31 @@ static void parseStatement(BasParserT *p) { basEmit8(&p->cg, OP_POP); // discard result break; - case TOK_MSGBOX: - // MsgBox message [, flags] (statement form, discard result) + case TOK_MSGBOX: { + // MsgBox message [, flags [, title]] (statement form, discards result) advance(p); parseExpression(p); // message + if (match(p, TOK_COMMA)) { parseExpression(p); // flags } else { basEmit8(&p->cg, OP_PUSH_INT16); basEmit16(&p->cg, 0); // default flags = MB_OK } + + if (match(p, TOK_COMMA)) { + parseExpression(p); // title + } else { + uint16_t emptyIdx = basAddConstant(&p->cg, "", 0); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, emptyIdx); + } + basEmit8(&p->cg, OP_MSGBOX); basEmit8(&p->cg, OP_POP); // discard result break; + } - // SQL statement forms (no return value) case TOK_INIWRITE: // IniWrite file, section, key, value advance(p); diff --git a/apps/dvxbasic/compiler/strip.c b/apps/dvxbasic/compiler/strip.c index 85d5a7e..7fd9919 100644 --- a/apps/dvxbasic/compiler/strip.c +++ b/apps/dvxbasic/compiler/strip.c @@ -3,19 +3,68 @@ // 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). +// - Mangles procedure names that aren't needed for runtime dispatch. +// The form runtime dispatches events by name (Control_Event pattern) +// and SetEvent looks up handlers by name at runtime, so those proc +// names must be preserved. Everything else becomes F1, F2, F3... // // OP_LINE removal is deferred to a future version (requires // bytecode compaction and offset rewriting). #include "strip.h" +#include "../runtime/values.h" +#include #include #include +// Events fired by name via basFormRtFireEvent* in formrt.c. Any proc +// ending in "_" must keep its name so the dispatcher can +// find it. +static const char *sEventSuffixes[] = { + "Load", "Unload", "QueryUnload", "Resize", "Activate", "Deactivate", + "Click", "DblClick", "Change", "Timer", + "GotFocus", "LostFocus", + "KeyPress", "KeyDown", "KeyUp", + "MouseDown", "MouseUp", "MouseMove", + "Scroll", "Reposition", "Validate", + NULL +}; + + +static bool nameEndsWithEventSuffix(const char *name) { + const char *underscore = strrchr(name, '_'); + + if (!underscore) { + return false; + } + + const char *suffix = underscore + 1; + + for (int32_t i = 0; sEventSuffixes[i]; i++) { + if (strcasecmp(suffix, sEventSuffixes[i]) == 0) { + return true; + } + } + + return false; +} + + +static bool nameInConstantPool(const BasModuleT *mod, const char *name) { + for (int32_t i = 0; i < mod->constCount; i++) { + const BasStringT *s = mod->constants[i]; + + if (s && strcasecmp(s->data, name) == 0) { + return true; + } + } + + return false; +} + + void basStripModule(BasModuleT *mod) { if (!mod) { return; @@ -36,4 +85,27 @@ void basStripModule(BasModuleT *mod) { mod->debugUdtDefs = NULL; mod->debugUdtDefCount = 0; } + + // Mangle proc names. Keep names that are needed for runtime name + // lookup: event handlers (Control_Event pattern) and any name + // referenced as a string constant (e.g. SetEvent's target name). + int32_t nextMangled = 1; + + for (int32_t i = 0; i < mod->procCount; i++) { + BasProcEntryT *proc = &mod->procs[i]; + + if (proc->name[0] == '\0') { + continue; + } + + if (nameEndsWithEventSuffix(proc->name)) { + continue; + } + + if (nameInConstantPool(mod, proc->name)) { + continue; + } + + snprintf(proc->name, sizeof(proc->name), "F%ld", (long)nextMangled++); + } } diff --git a/apps/dvxbasic/compiler/strip.h b/apps/dvxbasic/compiler/strip.h index bc14155..d218604 100644 --- a/apps/dvxbasic/compiler/strip.h +++ b/apps/dvxbasic/compiler/strip.h @@ -1,8 +1,9 @@ // 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. +// Removes debug information from a compiled module to hinder +// decompilation. Clears debug variable info and debug UDT +// definitions, and mangles proc names that aren't needed for +// runtime name-based dispatch. #ifndef DVXBASIC_STRIP_H #define DVXBASIC_STRIP_H @@ -10,9 +11,11 @@ #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 +// - Mangle proc names to F1, F2, ... except for event handlers +// (matched by Control_Event suffix) and names referenced as +// string constants (SetEvent dispatch targets). void basStripModule(BasModuleT *mod); #endif // DVXBASIC_STRIP_H diff --git a/apps/dvxbasic/formrt/formrt.c b/apps/dvxbasic/formrt/formrt.c index 5d825a2..06b13fc 100644 --- a/apps/dvxbasic/formrt/formrt.c +++ b/apps/dvxbasic/formrt/formrt.c @@ -8,6 +8,7 @@ #include "formcfm.h" #include "../compiler/opcodes.h" #include "dvxDlg.h" +#include "dvxRes.h" #include "dvxWm.h" #include "box/box.h" #include "ansiTerm/ansiTerm.h" @@ -1291,20 +1292,27 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { } } - // Check the .frm cache for reload after unload - for (int32_t i = 0; i < rt->frmCacheCount; i++) { - if (strcasecmp(rt->frmCache[i].formName, formName) == 0) { - // Save source and remove from cache BEFORE calling basFormRtLoadFrm, - // because basFormRtLoadFrm internally calls basFormRtLoadForm (for the - // "Begin Form" line) which would find this cache entry again and recurse. - char *src = rt->frmCache[i].frmSource; - int32_t srcLen = rt->frmCache[i].frmSourceLen; - arrdel(rt->frmCache, i); - rt->frmCacheCount = (int32_t)arrlen(rt->frmCache); + // Check the .frm cache for reload after unload. + // Use a static guard to prevent recursion: basFormRtLoadFrm calls + // basFormRtLoadForm for the "Begin Form" line, which would re-enter + // this function. The guard lets the recursive call fall through to + // bare form creation, which basFormRtLoadFrm then populates. + static bool sLoadingFrm = false; - BasFormT *form = basFormRtLoadFrm(rt, src, srcLen); - free(src); - return form; + if (!sLoadingFrm) { + for (int32_t i = 0; i < rt->frmCacheCount; i++) { + if (strcasecmp(rt->frmCache[i].formName, formName) == 0) { + char *src = rt->frmCache[i].frmSource; + int32_t srcLen = rt->frmCache[i].frmSourceLen; + arrdel(rt->frmCache, i); + rt->frmCacheCount = (int32_t)arrlen(rt->frmCache); + + sLoadingFrm = true; + BasFormT *form = basFormRtLoadFrm(rt, src, srcLen); + sLoadingFrm = false; + free(src); + return form; + } } } @@ -1701,6 +1709,10 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen } val = basValStringFromC(value + 1); + } else if (strcasecmp(value, "True") == 0) { + val = basValBool(true); + } else if (strcasecmp(value, "False") == 0) { + val = basValBool(false); } else { val = basValLong(atoi(value)); } @@ -1884,6 +1896,38 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen } } + // Allocate per-form variable storage and run init code. + // This must happen AFTER controls are created (so init code can + // reference controls by name) but BEFORE Form_Load fires. + if (form && rt->module && rt->module->formVarInfo) { + for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) { + if (strcasecmp(rt->module->formVarInfo[j].formName, form->name) != 0) { + continue; + } + + if (!form->formVars) { + int32_t vc = rt->module->formVarInfo[j].varCount; + + if (vc > 0) { + form->formVars = (BasValueT *)calloc(vc, sizeof(BasValueT)); + form->formVarCount = vc; + } + } + + int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr; + + if (initAddr >= 0 && rt->vm) { + basVmSetCurrentForm(rt->vm, form); + basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount); + basVmCallSub(rt->vm, initAddr); + basVmSetCurrentForm(rt->vm, NULL); + basVmSetCurrentFormVars(rt->vm, NULL, 0); + } + + break; + } + } + // Fire the Load event now that the form and controls are ready if (form) { basFormRtFireEvent(rt, form, form->name, "Load"); @@ -1928,10 +1972,10 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen // basFormRtMsgBox // ============================================================ -int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags) { +int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title) { BasFormRtT *rt = (BasFormRtT *)ctx; - return dvxMessageBox(rt->ctx, "DVX BASIC", message, flags); + return dvxMessageBox(rt->ctx, (title && title[0]) ? title : "DVX BASIC", message, flags); } @@ -2049,14 +2093,12 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT return; } - // "Caption" and "Text": save to the persistent textBuf and apply - // immediately. Controls are heap-allocated so textBuf addresses - // are stable across arrput calls. + // "Caption" and "Text": pass directly to the widget (all widgets + // strdup their text internally). if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) { BasStringT *s = basValFormatString(value); - snprintf(ctrl->textBuf, BAS_MAX_TEXT_BUF, "%s", s->data); + wgtSetText(ctrl->widget, s->data); basStringUnref(s); - wgtSetText(ctrl->widget, ctrl->textBuf); return; } @@ -2129,6 +2171,11 @@ void basFormRtUnloadForm(void *ctx, void *formRef) { return; } + // QueryUnload: give the form a chance to cancel + if (basFormRtFireEventWithCancel(rt, form, form->name, "QueryUnload")) { + return; + } + basFormRtFireEvent(rt, form, form->name, "Unload"); // Release per-form variables @@ -2253,8 +2300,12 @@ WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent) { CreateParentBoolFnT fn = *(CreateParentBoolFnT *)api; return fn(parent, (bool)iface->createArgs[0]); } - case WGT_CREATE_PARENT_DATA: - return NULL; + case WGT_CREATE_PARENT_DATA: { + // create(parent, NULL, 0, 0, 0) -- empty widget, load content later via properties + typedef WidgetT *(*CreateDataFnT)(WidgetT *, uint8_t *, int32_t, int32_t, int32_t); + CreateDataFnT fn = *(CreateDataFnT *)api; + return fn(parent, NULL, 0, 0, 0); + } default: { CreateParentFnT fn = *(CreateParentFnT *)api; return fn(parent); @@ -2769,8 +2820,7 @@ static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl) { const char *val = wgtDataCtrlGetField(dataCtrl->widget, ctrl->dataField); if (val) { - snprintf(ctrl->textBuf, BAS_MAX_TEXT_BUF, "%s", val); - wgtSetText(ctrl->widget, ctrl->textBuf); + wgtSetText(ctrl->widget, val); } } } @@ -3214,21 +3264,64 @@ static BasValueT zeroValue(void) { // need C-side storage since BASIC strings can't be used as // writable char pointers. +// Parse a filter string into FileFilterT entries. +// +// Format: "Label (pattern)|Label (pattern)|..." +// The pattern is extracted from inside the parentheses. +// Example: "Images (*.bmp;*.png;*.jpg)|Text (*.txt)|All Files (*.*)" +// +// If no parentheses, the entire entry is used as both label and pattern. +// If no pipe, treat as a single pattern for backward compatibility. + +#define BAS_MAX_FILE_FILTERS 16 + +static int32_t parseFileFilters(const char *filter, FileFilterT *out, char *buf, int32_t bufSize) { + if (!filter || !filter[0]) { + out[0].label = "All Files (*.*)"; + return 1; + } + + // No pipe and no parentheses = old-style single pattern, wrap it + if (!strchr(filter, '|') && !strchr(filter, '(')) { + snprintf(buf, bufSize, "%s (%s)", filter, filter); + out[0].label = buf; + out[1].label = "All Files (*.*)"; + return 2; + } + + snprintf(buf, bufSize, "%s", filter); + int32_t count = 0; + char *p = buf; + + while (*p && count < BAS_MAX_FILE_FILTERS) { + char *pipe = strchr(p, '|'); + + if (pipe) { + *pipe = '\0'; + } + + out[count].label = p; + count++; + p = pipe ? pipe + 1 : p + strlen(p); + } + + return count > 0 ? count : 1; +} + + const char *basFileOpen(const char *title, const char *filter) { if (!sFormRt) { return ""; } - FileFilterT filters[2]; - filters[0].label = filter; - filters[0].pattern = filter; - filters[1].label = "All Files (*.*)"; - filters[1].pattern = "*.*"; + FileFilterT filters[BAS_MAX_FILE_FILTERS]; + char filterBuf[1024]; + int32_t count = parseFileFilters(filter, filters, filterBuf, sizeof(filterBuf)); - static char path[260]; + static char path[DVX_MAX_PATH]; path[0] = '\0'; - if (dvxFileDialog(sFormRt->ctx, title, FD_OPEN, NULL, filters, 2, path, sizeof(path))) { + if (dvxFileDialog(sFormRt->ctx, title, FD_OPEN, NULL, filters, count, path, sizeof(path))) { return path; } @@ -3241,16 +3334,14 @@ const char *basFileSave(const char *title, const char *filter) { return ""; } - FileFilterT filters[2]; - filters[0].label = filter; - filters[0].pattern = filter; - filters[1].label = "All Files (*.*)"; - filters[1].pattern = "*.*"; + FileFilterT filters[BAS_MAX_FILE_FILTERS]; + char filterBuf[1024]; + int32_t count = parseFileFilters(filter, filters, filterBuf, sizeof(filterBuf)); - static char path[260]; + static char path[DVX_MAX_PATH]; path[0] = '\0'; - if (dvxFileDialog(sFormRt->ctx, title, FD_SAVE, NULL, filters, 2, path, sizeof(path))) { + if (dvxFileDialog(sFormRt->ctx, title, FD_SAVE, NULL, filters, count, path, sizeof(path))) { return path; } @@ -4277,10 +4368,244 @@ void HelpView(const char *hlpFile) { return; } - char viewerPath[260]; + // If hlpFile has no directory component, resolve it against the calling + // app's directory. This matches the convention where apps bundle their + // help file alongside the .app. + char resolved[DVX_MAX_PATH]; + bool hasDir = strchr(hlpFile, '/') != NULL || strchr(hlpFile, '\\') != NULL || (hlpFile[0] && hlpFile[1] == ':'); + + if (!hasDir && sFormRt->vm && sFormRt->vm->appPath[0]) { + snprintf(resolved, sizeof(resolved), "%s%c%s", sFormRt->vm->appPath, DVX_PATH_SEP, hlpFile); + } else { + snprintf(resolved, sizeof(resolved), "%s", hlpFile); + } + + char viewerPath[DVX_MAX_PATH]; snprintf(viewerPath, sizeof(viewerPath), "APPS%cKPUNCH%cDVXHELP%cDVXHELP.APP", DVX_PATH_SEP, DVX_PATH_SEP, DVX_PATH_SEP); - sShellLoadApp(sFormRt->ctx, viewerPath, hlpFile); + sShellLoadApp(sFormRt->ctx, viewerPath, resolved); +} + + +// ============================================================ +// Resource file extern wrappers (DECLARE LIBRARY "basrt") +// +// Expose the DVX resource API to BASIC programs. Each function +// is a thin wrapper around the C resource API. +// ============================================================ + +// Maximum number of simultaneously open resource handles +#define RES_MAX_HANDLES 8 + +static DvxResHandleT *sResHandles[RES_MAX_HANDLES]; + + +int32_t ResOpen(const char *path) { + if (!path) { + return 0; + } + + for (int32_t i = 0; i < RES_MAX_HANDLES; i++) { + if (!sResHandles[i]) { + sResHandles[i] = dvxResOpen(path); + + if (sResHandles[i]) { + return i + 1; + } + + return 0; + } + } + + return 0; +} + + +void ResClose(int32_t handle) { + int32_t idx = handle - 1; + + if (idx < 0 || idx >= RES_MAX_HANDLES || !sResHandles[idx]) { + return; + } + + dvxResClose(sResHandles[idx]); + sResHandles[idx] = NULL; +} + + +int32_t ResCount(int32_t handle) { + int32_t idx = handle - 1; + + if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx]) { + return (int32_t)sResHandles[idx]->entryCount; + } + + return 0; +} + + +const char *ResName(int32_t handle, int32_t index) { + int32_t idx = handle - 1; + + if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] && + index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) { + return sResHandles[idx]->entries[index].name; + } + + return ""; +} + + +int32_t ResType(int32_t handle, int32_t index) { + int32_t idx = handle - 1; + + if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] && + index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) { + return (int32_t)sResHandles[idx]->entries[index].type; + } + + return 0; +} + + +int32_t ResSize(int32_t handle, int32_t index) { + int32_t idx = handle - 1; + + if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] && + index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) { + return (int32_t)sResHandles[idx]->entries[index].size; + } + + return 0; +} + + +const char *ResGetText(const char *path, const char *name) { + static char sBuf[1024]; + sBuf[0] = '\0'; + + if (!path || !name) { + return sBuf; + } + + DvxResHandleT *h = dvxResOpen(path); + + if (!h) { + return sBuf; + } + + uint32_t size = 0; + void *data = dvxResRead(h, name, &size); + dvxResClose(h); + + if (data) { + uint32_t copyLen = size; + + if (copyLen >= sizeof(sBuf)) { + copyLen = sizeof(sBuf) - 1; + } + + memcpy(sBuf, data, copyLen); + sBuf[copyLen] = '\0'; + free(data); + } + + return sBuf; +} + + +int32_t ResAddText(const char *path, const char *name, const char *text) { + if (!path || !name) { + return 0; + } + + if (!text) { + text = ""; + } + + return dvxResAppend(path, name, DVX_RES_TEXT, text, (uint32_t)strlen(text) + 1) == 0 ? -1 : 0; +} + + +int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile) { + if (!path || !name || !srcFile) { + return 0; + } + + FILE *f = fopen(srcFile, "rb"); + + if (!f) { + return 0; + } + + fseek(f, 0, SEEK_END); + long fileSize = ftell(f); + fseek(f, 0, SEEK_SET); + + if (fileSize <= 0) { + fclose(f); + return 0; + } + + uint8_t *buf = (uint8_t *)malloc((size_t)fileSize); + + if (!buf) { + fclose(f); + return 0; + } + + if (fread(buf, 1, (size_t)fileSize, f) != (size_t)fileSize) { + free(buf); + fclose(f); + return 0; + } + + fclose(f); + int32_t result = dvxResAppend(path, name, (uint32_t)type, buf, (uint32_t)fileSize) == 0 ? -1 : 0; + free(buf); + return result; +} + + +int32_t ResRemove(const char *path, const char *name) { + if (!path || !name) { + return 0; + } + + return dvxResRemove(path, name) == 0 ? -1 : 0; +} + + +int32_t ResExtract(const char *path, const char *name, const char *outFile) { + if (!path || !name || !outFile) { + return 0; + } + + DvxResHandleT *h = dvxResOpen(path); + + if (!h) { + return 0; + } + + uint32_t size = 0; + void *data = dvxResRead(h, name, &size); + dvxResClose(h); + + if (!data) { + return 0; + } + + FILE *f = fopen(outFile, "wb"); + + if (!f) { + free(data); + return 0; + } + + int32_t result = (fwrite(data, 1, size, f) == size) ? -1 : 0; + fclose(f); + free(data); + return result; } diff --git a/apps/dvxbasic/formrt/formrt.h b/apps/dvxbasic/formrt/formrt.h index e9c248d..0498545 100644 --- a/apps/dvxbasic/formrt/formrt.h +++ b/apps/dvxbasic/formrt/formrt.h @@ -43,7 +43,6 @@ typedef struct { // Control instance (a widget on a form) // ============================================================ -#define BAS_MAX_TEXT_BUF 256 #define BAS_MAX_EVENT_OVERRIDES 16 // Event handler override (SetEvent) @@ -59,7 +58,6 @@ typedef struct BasControlT { WidgetT *widget; // the DVX widget BasFormT *form; // owning form const WgtIfaceT *iface; // interface descriptor (from .wgt) - char textBuf[BAS_MAX_TEXT_BUF]; // persistent text for Caption/Text char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding char dataField[BAS_MAX_CTRL_NAME]; // column name for binding char helpTopic[BAS_MAX_CTRL_NAME]; // help topic ID for F1 @@ -171,7 +169,7 @@ void *basFormRtLoadForm(void *ctx, const char *formName); void basFormRtUnloadForm(void *ctx, void *formRef); 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); +int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title); // ---- Extern call callbacks (shared by IDE and stub) ---- diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index d7d2717..287451d 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -3095,9 +3095,9 @@ static void recentOpen(int32_t index) { static void loadFile(void) { FileFilterT filters[] = { - { "BASIC Files (*.bas)", "*.bas" }, - { "Form Files (*.frm)", "*.frm" }, - { "All Files (*.*)", "*.*" } + { "BASIC Files (*.bas)" }, + { "Form Files (*.frm)" }, + { "All Files (*.*)" } }; char path[DVX_MAX_PATH]; @@ -3247,7 +3247,7 @@ static void newProject(void) { // Ask for directory via save dialog (file = name.dbp) FileFilterT filters[] = { - { "Project Files (*.dbp)", "*.dbp" } + { "Project Files (*.dbp)" } }; char dbpPath[DVX_MAX_PATH]; @@ -3310,8 +3310,8 @@ static void newProject(void) { static void openProject(void) { FileFilterT filters[] = { - { "Project Files (*.dbp)", "*.dbp" }, - { "All Files (*.*)", "*.*" } + { "Project Files (*.dbp)" }, + { "All Files (*.*)" } }; char path[DVX_MAX_PATH]; @@ -3964,8 +3964,8 @@ static void makeExecutable(void) { // Ask for output path FileFilterT filters[] = { - { "DVX Applications (*.app)", "*.app" }, - { "All Files (*.*)", "*.*" } + { "DVX Applications (*.app)" }, + { "All Files (*.*)" } }; char outPath[DVX_MAX_PATH]; outPath[0] = '\0'; @@ -3975,7 +3975,7 @@ static void makeExecutable(void) { } // Ask debug or release - const char *modeItems[] = { "Debug (include error info)", "Release (stripped)" }; + const char *modeItems[] = { "Debug (include error info)" }; int32_t modeChoice = 0; if (!dvxChoiceDialog(sAc, "Build Mode", "Select build mode:", modeItems, 2, 0, &modeChoice)) { @@ -4067,9 +4067,116 @@ static void makeExecutable(void) { fclose(outFile); free(stubData); - // Attach app name from project properties + // Attach project property resources const char *projName = sProject.name[0] ? sProject.name : "BASIC App"; - dvxResAppend(outPath, "APPNAME", DVX_RES_TEXT, projName, (uint32_t)strlen(projName) + 1); + dvxResAppend(outPath, "name", DVX_RES_TEXT, projName, (uint32_t)strlen(projName) + 1); + + if (sProject.author[0]) { + dvxResAppend(outPath, "author", DVX_RES_TEXT, sProject.author, (uint32_t)strlen(sProject.author) + 1); + } + + if (sProject.company[0]) { + dvxResAppend(outPath, "company", DVX_RES_TEXT, sProject.company, (uint32_t)strlen(sProject.company) + 1); + } + + if (sProject.version[0]) { + dvxResAppend(outPath, "version", DVX_RES_TEXT, sProject.version, (uint32_t)strlen(sProject.version) + 1); + } + + if (sProject.copyright[0]) { + dvxResAppend(outPath, "copyright", DVX_RES_TEXT, sProject.copyright, (uint32_t)strlen(sProject.copyright) + 1); + } + + if (sProject.description[0]) { + dvxResAppend(outPath, "description", DVX_RES_TEXT, sProject.description, (uint32_t)strlen(sProject.description) + 1); + } + + // Attach icon: project icon or fallback to IDE's noicon resource + if (sProject.iconPath[0]) { + char iconFullPath[DVX_MAX_PATH]; + snprintf(iconFullPath, sizeof(iconFullPath), "%s%c%s", sProject.projectDir, DVX_PATH_SEP, sProject.iconPath); + + FILE *iconFile = fopen(iconFullPath, "rb"); + + if (iconFile) { + fseek(iconFile, 0, SEEK_END); + long iconSize = ftell(iconFile); + fseek(iconFile, 0, SEEK_SET); + void *iconData = malloc(iconSize); + + if (iconData) { + if (fread(iconData, 1, iconSize, iconFile) == (size_t)iconSize) { + dvxResAppend(outPath, "icon32", DVX_RES_ICON, iconData, (uint32_t)iconSize); + } + + free(iconData); + } + + fclose(iconFile); + } + } else { + // Use stock noicon from IDE resources + DvxResHandleT *ideRes = dvxResOpen(sCtx->appPath); + + if (ideRes) { + uint32_t noiconSize = 0; + void *noiconData = dvxResRead(ideRes, "noicon", &noiconSize); + + if (noiconData) { + dvxResAppend(outPath, "icon32", DVX_RES_ICON, noiconData, noiconSize); + free(noiconData); + } + + dvxResClose(ideRes); + } + } + + // Copy help file alongside the output app (if specified in project) + if (sProject.helpFile[0]) { + // Store just the filename as a text resource so the stub can find it + const char *helpBase = sProject.helpFile; + const char *sep = strrchr(helpBase, DVX_PATH_SEP); + + if (sep) { + helpBase = sep + 1; + } + + dvxResAppend(outPath, "helpfile", DVX_RES_TEXT, helpBase, (uint32_t)strlen(helpBase) + 1); + + // Copy the .hlp file to sit next to the output .app + char helpSrc[DVX_MAX_PATH]; + snprintf(helpSrc, sizeof(helpSrc), "%s%c%s", sProject.projectDir, DVX_PATH_SEP, sProject.helpFile); + + char outDir[DVX_MAX_PATH]; + snprintf(outDir, sizeof(outDir), "%s", outPath); + char *lastSep = strrchr(outDir, DVX_PATH_SEP); + + if (lastSep) { + *lastSep = '\0'; + } + + char helpDst[DVX_MAX_PATH]; + snprintf(helpDst, sizeof(helpDst), "%s%c%s", outDir, DVX_PATH_SEP, helpBase); + + FILE *hSrc = fopen(helpSrc, "rb"); + + if (hSrc) { + FILE *hDst = fopen(helpDst, "wb"); + + if (hDst) { + char cpBuf[4096]; + size_t n; + + while ((n = fread(cpBuf, 1, sizeof(cpBuf), hSrc)) > 0) { + fwrite(cpBuf, 1, n, hDst); + } + + fclose(hDst); + } + + fclose(hSrc); + } + } // Attach MODULE resource dvxResAppend(outPath, "MODULE", DVX_RES_BINARY, modData, (uint32_t)modLen); @@ -8519,6 +8626,7 @@ static void updateProjectMenuState(void) { } wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, anyDirty); + wmMenuItemSetEnabled(sWin->menuBar, CMD_MAKE_EXE, hasProject && isIdle); // Edit menu wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, hasProject); diff --git a/apps/dvxbasic/runtime/serialize.c b/apps/dvxbasic/runtime/serialize.c index 6432930..2136370 100644 --- a/apps/dvxbasic/runtime/serialize.c +++ b/apps/dvxbasic/runtime/serialize.c @@ -441,6 +441,7 @@ void basModuleFree(BasModuleT *mod) { } free(mod->procs); + free(mod->formVarInfo); free(mod->debugVars); if (mod->debugUdtDefs) { @@ -451,10 +452,6 @@ void basModuleFree(BasModuleT *mod) { free(mod->debugUdtDefs); } - if (mod->formVarInfo) { - free(mod->formVarInfo); - } - free(mod); } diff --git a/apps/dvxbasic/runtime/vm.c b/apps/dvxbasic/runtime/vm.c index 9c15e56..8492293 100644 --- a/apps/dvxbasic/runtime/vm.c +++ b/apps/dvxbasic/runtime/vm.c @@ -1144,14 +1144,26 @@ BasVmResultE basVmStep(BasVmT *vm) { varSlot = &vm->globals[varIdx]; } - // Increment: var = var + step - double varVal = basValToNumber(*varSlot); - double stepVal = basValToNumber(fs->step); - double limVal = basValToNumber(fs->limit); + // Increment: var = var + step. Preserve the loop variable's + // original type so extern calls that pass it as an int32_t + // argument don't get an 8-byte double instead. + double varVal = basValToNumber(*varSlot); + double stepVal = basValToNumber(fs->step); + double limVal = basValToNumber(fs->limit); + uint8_t varType = varSlot->type; varVal += stepVal; basValRelease(varSlot); - *varSlot = basValDouble(varVal); + + if (varType == BAS_TYPE_INTEGER) { + *varSlot = basValInteger((int16_t)varVal); + } else if (varType == BAS_TYPE_LONG) { + *varSlot = basValLong((int32_t)varVal); + } else if (varType == BAS_TYPE_SINGLE) { + *varSlot = basValSingle((float)varVal); + } else { + *varSlot = basValDouble(varVal); + } // Test: if step > 0 then continue while var <= limit // if step < 0 then continue while var >= limit @@ -1669,6 +1681,67 @@ BasVmResultE basVmStep(BasVmT *vm) { case OP_MATH_RANDOMIZE: return execMath(vm, op); + case OP_RGB: { + // RGB(r, g, b) -> long color 0x00RRGGBB + BasValueT vb; + BasValueT vg; + BasValueT vr; + + if (!pop(vm, &vb) || !pop(vm, &vg) || !pop(vm, &vr)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t r = (int32_t)basValToNumber(vr); + int32_t g = (int32_t)basValToNumber(vg); + int32_t b = (int32_t)basValToNumber(vb); + + basValRelease(&vr); + basValRelease(&vg); + basValRelease(&vb); + + if (r < 0) { r = 0; } + if (r > 255) { r = 255; } + if (g < 0) { g = 0; } + if (g > 255) { g = 255; } + if (b < 0) { b = 0; } + if (b > 255) { b = 255; } + + if (!push(vm, basValLong((r << 16) | (g << 8) | b))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_GET_RED: + case OP_GET_GREEN: + case OP_GET_BLUE: { + BasValueT vc; + + if (!pop(vm, &vc)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t color = (int32_t)basValToNumber(vc); + basValRelease(&vc); + + int32_t component; + + if (op == OP_GET_RED) { + component = (color >> 16) & 0xFF; + } else if (op == OP_GET_GREEN) { + component = (color >> 8) & 0xFF; + } else { + component = color & 0xFF; + } + + if (!push(vm, basValInteger((int16_t)component))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + // ============================================================ // Conversion built-ins // ============================================================ @@ -2887,11 +2960,12 @@ BasVmResultE basVmStep(BasVmT *vm) { } case OP_MSGBOX: { - // Stack: [message, flags] — flags on top + // Stack: [message, flags, title] -- title on top + BasValueT titleVal; BasValueT flagsVal; BasValueT msgVal; - if (!pop(vm, &flagsVal) || !pop(vm, &msgVal)) { + if (!pop(vm, &titleVal) || !pop(vm, &flagsVal) || !pop(vm, &msgVal)) { return BAS_VM_STACK_UNDERFLOW; } @@ -2900,10 +2974,13 @@ BasVmResultE basVmStep(BasVmT *vm) { if (vm->ui.msgBox) { BasValueT sv = basValToString(msgVal); - result = vm->ui.msgBox(vm->ui.ctx, sv.strVal->data, flags); + BasValueT tv = basValToString(titleVal); + result = vm->ui.msgBox(vm->ui.ctx, sv.strVal->data, flags, tv.strVal->data); basValRelease(&sv); + basValRelease(&tv); } + basValRelease(&titleVal); basValRelease(&flagsVal); basValRelease(&msgVal); push(vm, basValInteger((int16_t)result)); @@ -3766,13 +3843,13 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { switch (mode) { case FILE_MODE_INPUT: - modeStr = "r"; + modeStr = "rb"; break; case FILE_MODE_OUTPUT: - modeStr = "w"; + modeStr = "wb"; break; case FILE_MODE_APPEND: - modeStr = "a"; + modeStr = "ab"; break; case FILE_MODE_RANDOM: case FILE_MODE_BINARY: @@ -3836,6 +3913,10 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { } case OP_FILE_PRINT: { + // Writes one value without a trailing newline. The parser emits + // OP_FILE_WRITE_NL separately when the PRINT statement finishes + // without a trailing ; so multi-value PRINT with `;` separator + // works correctly. BasValueT val; BasValueT channelVal; @@ -3857,7 +3938,6 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { if (s) { fputs(s->data, (FILE *)vm->files[channel].handle); - fputc('\n', (FILE *)vm->files[channel].handle); basStringUnref(s); } @@ -3913,17 +3993,12 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { return BAS_VM_FILE_ERROR; } - // Peek ahead to detect EOF before the next read - FILE *fp = (FILE *)vm->files[channel].handle; - int ch = fgetc(fp); - bool isEof; - - if (ch == EOF) { - isEof = true; - } else { - ungetc(ch, fp); - isEof = false; - } + FILE *fp = (FILE *)vm->files[channel].handle; + long curPos = ftell(fp); + fseek(fp, 0, SEEK_END); + long endPos = ftell(fp); + fseek(fp, curPos, SEEK_SET); + bool isEof = (curPos >= endPos); if (!push(vm, basValBool(isEof))) { return BAS_VM_STACK_OVERFLOW; diff --git a/apps/dvxbasic/runtime/vm.h b/apps/dvxbasic/runtime/vm.h index 929b5b1..a4f1da4 100644 --- a/apps/dvxbasic/runtime/vm.h +++ b/apps/dvxbasic/runtime/vm.h @@ -124,7 +124,7 @@ typedef void (*BasUiShowFormFnT)(void *ctx, void *formRef, bool modal); typedef void (*BasUiHideFormFnT)(void *ctx, void *formRef); // Display a message box. Returns the button clicked (1=OK, 6=Yes, 7=No, 2=Cancel). -typedef int32_t (*BasUiMsgBoxFnT)(void *ctx, const char *message, int32_t flags); +typedef int32_t (*BasUiMsgBoxFnT)(void *ctx, const char *message, int32_t flags, const char *title); // 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); diff --git a/apps/dvxbasic/stub/bascomp.c b/apps/dvxbasic/stub/bascomp.c new file mode 100644 index 0000000..1878c7a --- /dev/null +++ b/apps/dvxbasic/stub/bascomp.c @@ -0,0 +1,667 @@ +// bascomp.c -- DVX BASIC command-line compiler +// +// Compiles a .dbp project into a standalone .app file. +// +// Usage: BASCOMP project.dbp [-o output.app] [-release] +// +// The project file and all referenced source files (.bas, .frm) +// are loaded relative to the directory containing the .dbp file. +// The stub (basstub.app) is read from the same directory as the +// compiler executable. + +#include "../compiler/compact.h" +#include "../compiler/lexer.h" +#include "../compiler/obfuscate.h" +#include "../compiler/parser.h" +#include "../compiler/strip.h" +#include "../compiler/opcodes.h" +#include "../runtime/vm.h" +#include "../runtime/values.h" +#include "../runtime/serialize.h" +#include "../../core/dvxRes.h" +#include "../../core/dvxPrefs.h" +#include "../../core/dvxTypes.h" +#include "../../tools/dvxResWrite.h" + +#include +#include +#include +#include + +// ============================================================ +// Limits +// ============================================================ + +#define MAX_FILES 64 +#define MAX_PATH_LEN 260 +#define MAX_NAME 64 + +// ============================================================ +// Prototypes +// ============================================================ + +static void concatGrow(char **buf, int32_t *cap, int32_t need); +static char *readFile(const char *path, int32_t *outLen); +static const char *extractFormCode(const char *frmText); +static void usage(void); + + +// ============================================================ +// extractFormCode -- skip past the form layout to the code section +// ============================================================ + +static void concatGrow(char **buf, int32_t *cap, int32_t need) { + while (*cap < need) { + *cap *= 2; + } + + *buf = (char *)realloc(*buf, *cap); +} + + +static const char *extractFormCode(const char *frmText) { + if (!frmText) { + return NULL; + } + + const char *p = frmText; + int32_t depth = 0; + bool inForm = false; + + while (*p) { + // Skip leading whitespace + while (*p == ' ' || *p == '\t') { p++; } + + if (strncasecmp(p, "Begin ", 6) == 0) { + if (!inForm && strncasecmp(p + 6, "Form ", 5) == 0) { + inForm = true; + } + + depth++; + } else if (strncasecmp(p, "End", 3) == 0 && (p[3] == '\0' || p[3] == '\r' || p[3] == '\n' || p[3] == ' ')) { + depth--; + + if (depth <= 0 && inForm) { + // Skip past this line + while (*p && *p != '\n') { p++; } + + if (*p == '\n') { p++; } + + return p; + } + } + + // Skip to next line + while (*p && *p != '\n') { p++; } + + if (*p == '\n') { p++; } + } + + return NULL; +} + + +// ============================================================ +// readFile +// ============================================================ + +static char *readFile(const char *path, int32_t *outLen) { + FILE *f = fopen(path, "rb"); + + if (!f) { + return NULL; + } + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + char *buf = (char *)malloc(size + 1); + + if (!buf) { + fclose(f); + return NULL; + } + + int32_t n = (int32_t)fread(buf, 1, size, f); + fclose(f); + buf[n] = '\0'; + + if (outLen) { + *outLen = n; + } + + return buf; +} + + +// ============================================================ +// usage +// ============================================================ + +static void usage(void) { + fprintf(stderr, "DVX BASIC Compiler\n\n"); + fprintf(stderr, "Usage: BASCOMP project.dbp [-o output.app] [-release]\n\n"); + fprintf(stderr, " project.dbp DVX BASIC project file\n"); + fprintf(stderr, " -o output.app Output file (default: project name + .app)\n"); + fprintf(stderr, " -release Strip debug information\n"); +} + + +// ============================================================ +// main +// ============================================================ + +int main(int argc, char **argv) { + if (argc < 2) { + usage(); + return 1; + } + + const char *dbpPath = NULL; + const char *outputPath = NULL; + bool release = false; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) { + outputPath = argv[++i]; + } else if (strcmp(argv[i], "-release") == 0) { + release = true; + } else if (argv[i][0] != '-') { + dbpPath = argv[i]; + } else { + fprintf(stderr, "Unknown option: %s\n", argv[i]); + usage(); + return 1; + } + } + + if (!dbpPath) { + fprintf(stderr, "Error: no project file specified.\n"); + usage(); + return 1; + } + + // Load the project file + PrefsHandleT *prefs = prefsLoad(dbpPath); + + if (!prefs) { + fprintf(stderr, "Error: cannot open project file: %s\n", dbpPath); + return 1; + } + + // Derive project directory + char projectDir[MAX_PATH_LEN]; + snprintf(projectDir, sizeof(projectDir), "%s", dbpPath); + char *sep = strrchr(projectDir, '/'); + char *sep2 = strrchr(projectDir, '\\'); + + if (sep2 > sep) { + sep = sep2; + } + + if (sep) { + *sep = '\0'; + } else { + projectDir[0] = '.'; + projectDir[1] = '\0'; + } + + // Read project metadata + const char *projName = prefsGetString(prefs, "Project", "Name", "App"); + const char *author = prefsGetString(prefs, "Project", "Author", ""); + const char *company = prefsGetString(prefs, "Project", "Company", ""); + const char *version = prefsGetString(prefs, "Project", "Version", ""); + const char *copyright = prefsGetString(prefs, "Project", "Copyright", ""); + const char *description = prefsGetString(prefs, "Project", "Description", ""); + const char *iconPath = prefsGetString(prefs, "Project", "Icon", ""); + const char *helpFile = prefsGetString(prefs, "Project", "HelpFile", ""); + const char *startupForm = prefsGetString(prefs, "Settings", "StartupForm", ""); + (void)startupForm; // used implicitly by stub's basFormRtLoadAllForms + + // Derive output path + char outBuf[MAX_PATH_LEN]; + + if (!outputPath) { + snprintf(outBuf, sizeof(outBuf), "%s/%s.app", projectDir, projName); + outputPath = outBuf; + } + + printf("Project: %s\n", projName); + printf("Output: %s (%s)\n", outputPath, release ? "release" : "debug"); + + // Collect source files + typedef struct { + char path[MAX_PATH_LEN]; + bool isForm; + } SrcFileT; + + SrcFileT files[MAX_FILES]; + int32_t fileCount = 0; + + // Modules + for (int32_t i = 0; i < MAX_FILES; i++) { + char key[16]; + snprintf(key, sizeof(key), "File%d", (int)i); + const char *val = prefsGetString(prefs, "Modules", key, NULL); + + if (!val) { + break; + } + + snprintf(files[fileCount].path, MAX_PATH_LEN, "%s/%s", projectDir, val); + files[fileCount].isForm = false; + fileCount++; + } + + // Forms + for (int32_t i = 0; i < MAX_FILES; i++) { + char key[16]; + snprintf(key, sizeof(key), "File%d", (int)i); + const char *val = prefsGetString(prefs, "Forms", key, NULL); + + if (!val) { + break; + } + + snprintf(files[fileCount].path, MAX_PATH_LEN, "%s/%s", projectDir, val); + files[fileCount].isForm = true; + fileCount++; + } + + if (fileCount == 0) { + fprintf(stderr, "Error: project has no source files.\n"); + prefsClose(prefs); + return 1; + } + + // Concatenate sources (modules first, then form code) + int32_t concatCap = 8192; + char *concatBuf = (char *)malloc(concatCap); + + if (!concatBuf) { + fprintf(stderr, "Error: out of memory.\n"); + prefsClose(prefs); + return 1; + } + + int32_t pos = 0; + + // Pass 0: .bas modules, Pass 1: .frm code sections + for (int32_t pass = 0; pass < 2; pass++) { + for (int32_t i = 0; i < fileCount; i++) { + if (pass == 0 && files[i].isForm) { continue; } + if (pass == 1 && !files[i].isForm) { continue; } + + int32_t srcLen = 0; + char *srcBuf = readFile(files[i].path, &srcLen); + + if (!srcBuf) { + fprintf(stderr, "Error: cannot read %s\n", files[i].path); + free(concatBuf); + prefsClose(prefs); + return 1; + } + + const char *code = srcBuf; + + if (files[i].isForm) { + // Extract form name from "Begin Form " + char formName[MAX_NAME] = ""; + const char *bp = srcBuf; + + while (*bp) { + while (*bp == ' ' || *bp == '\t') { bp++; } + + if (strncasecmp(bp, "Begin Form ", 11) == 0) { + bp += 11; + + while (*bp == ' ' || *bp == '\t') { bp++; } + + int32_t ni = 0; + + while (*bp && *bp != ' ' && *bp != '\t' && *bp != '\r' && *bp != '\n' && ni < MAX_NAME - 1) { + formName[ni++] = *bp++; + } + + formName[ni] = '\0'; + break; + } + + while (*bp && *bp != '\n') { bp++; } + + if (*bp == '\n') { bp++; } + } + + code = extractFormCode(srcBuf); + + if (!code) { + code = ""; + } + + int32_t codeLen = (int32_t)strlen(code); + concatGrow(&concatBuf, &concatCap, pos + codeLen + 128); + + // Inject BEGINFORM directive before form code + if (formName[0]) { + pos += snprintf(concatBuf + pos, concatCap - pos, "BEGINFORM \"%s\"\n", formName); + } + + memcpy(concatBuf + pos, code, codeLen); + pos += codeLen; + + if (pos > 0 && concatBuf[pos - 1] != '\n') { + concatBuf[pos++] = '\n'; + } + + // Inject ENDFORM directive after form code + if (formName[0]) { + pos += snprintf(concatBuf + pos, concatCap - pos, "ENDFORM\n"); + } + } else { + int32_t codeLen = (int32_t)strlen(code); + concatGrow(&concatBuf, &concatCap, pos + codeLen + 2); + + memcpy(concatBuf + pos, code, codeLen); + pos += codeLen; + + if (pos > 0 && concatBuf[pos - 1] != '\n') { + concatBuf[pos++] = '\n'; + } + } + + free(srcBuf); + } + } + + concatBuf[pos] = '\0'; + + // Compile + printf("Compiling %d file(s)...\n", (int)fileCount); + + BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT)); + + if (!parser) { + fprintf(stderr, "Error: out of memory.\n"); + free(concatBuf); + prefsClose(prefs); + return 1; + } + + basParserInit(parser, concatBuf, pos); + + if (!basParse(parser)) { + fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error); + basParserFree(parser); + free(parser); + free(concatBuf); + prefsClose(prefs); + return 1; + } + + free(concatBuf); + + BasModuleT *mod = basParserBuildModule(parser); + basParserFree(parser); + free(parser); + + if (!mod) { + fprintf(stderr, "Error: failed to build module.\n"); + prefsClose(prefs); + return 1; + } + + printf(" code: %d bytes, %d procs, %d constants\n", (int)mod->codeLen, (int)mod->procCount, (int)mod->constCount); + + // Strip for release + if (release) { + basStripModule(mod); + printf(" stripped debug info\n"); + } + + // Read all .frm texts up front; they're used for obfuscation and + // then embedded as FORM0, FORM1, ... resources below. + int32_t frmCount = 0; + char *frmData[MAX_FILES]; + int32_t frmLens[MAX_FILES]; + int32_t frmFileIdx[MAX_FILES]; // index into files[] for this frm + + for (int32_t i = 0; i < fileCount; i++) { + if (!files[i].isForm) { + continue; + } + + int32_t flen = 0; + char *fdata = readFile(files[i].path, &flen); + + if (!fdata) { + continue; + } + + // Strip comments from the .frm text unconditionally. Comments + // are source-only; they shouldn't ship in the embedded resource + // for either debug or release builds. + int32_t stripCap = flen + 16; + uint8_t *stripped = (uint8_t *)malloc(stripCap); + + if (!stripped) { + free(fdata); + continue; + } + + int32_t strippedLen = basStripFrmComments(fdata, flen, stripped, stripCap); + free(fdata); + + frmData[frmCount] = (char *)stripped; + frmLens[frmCount] = strippedLen; + frmFileIdx[frmCount] = i; + frmCount++; + } + + // Obfuscate form/control names in release mode + BasObfFrmT obfFrms[MAX_FILES]; + + for (int32_t i = 0; i < MAX_FILES; i++) { + obfFrms[i].data = NULL; + obfFrms[i].len = 0; + } + + if (release && frmCount > 0) { + const char *frmTexts[MAX_FILES]; + + for (int32_t i = 0; i < frmCount; i++) { + frmTexts[i] = frmData[i]; + } + + basObfuscateNames(mod, frmTexts, frmLens, frmCount, obfFrms); + printf(" obfuscated %d form(s)\n", (int)frmCount); + } + + // Remove OP_LINE instructions and compact the bytecode. + if (release) { + int32_t removed = basCompactBytecode(mod); + + if (removed > 0) { + printf(" compacted bytecode (-%d bytes)\n", (int)removed); + } + } + + // Serialize module + int32_t modLen = 0; + uint8_t *modData = basModuleSerialize(mod, &modLen); + + if (!modData) { + fprintf(stderr, "Error: failed to serialize module.\n"); + basModuleFree(mod); + prefsClose(prefs); + return 1; + } + + // Serialize debug info + int32_t dbgLen = 0; + uint8_t *dbgData = NULL; + + if (!release) { + dbgData = basDebugSerialize(mod, &dbgLen); + } + + basModuleFree(mod); + + // Find the stub -- look in the compiler's own directory + char compilerDir[MAX_PATH_LEN]; + snprintf(compilerDir, sizeof(compilerDir), "%s", argv[0]); + sep = strrchr(compilerDir, '/'); + sep2 = strrchr(compilerDir, '\\'); + + if (sep2 > sep) { + sep = sep2; + } + + if (sep) { + *sep = '\0'; + } else { + compilerDir[0] = '.'; + compilerDir[1] = '\0'; + } + + char stubPath[MAX_PATH_LEN]; + snprintf(stubPath, sizeof(stubPath), "%s/BASSTUB.APP", compilerDir); + + int32_t stubLen = 0; + char *stubData = readFile(stubPath, &stubLen); + + if (!stubData) { + fprintf(stderr, "Error: cannot find stub at %s\n", stubPath); + free(modData); + free(dbgData); + prefsClose(prefs); + return 1; + } + + // Write stub to output file + FILE *out = fopen(outputPath, "wb"); + + if (!out) { + fprintf(stderr, "Error: cannot create %s\n", outputPath); + free(stubData); + free(modData); + free(dbgData); + prefsClose(prefs); + return 1; + } + + fwrite(stubData, 1, stubLen, out); + fclose(out); + free(stubData); + + // Attach resources + dvxResAppendEntry(outputPath, "name", DVX_RES_TEXT, projName, (uint32_t)strlen(projName) + 1); + + if (author[0]) { dvxResAppendEntry(outputPath, "author", DVX_RES_TEXT, author, (uint32_t)strlen(author) + 1); } + if (company[0]) { dvxResAppendEntry(outputPath, "company", DVX_RES_TEXT, company, (uint32_t)strlen(company) + 1); } + if (version[0]) { dvxResAppendEntry(outputPath, "version", DVX_RES_TEXT, version, (uint32_t)strlen(version) + 1); } + if (copyright[0]) { dvxResAppendEntry(outputPath, "copyright", DVX_RES_TEXT, copyright, (uint32_t)strlen(copyright) + 1); } + if (description[0]) { dvxResAppendEntry(outputPath, "description", DVX_RES_TEXT, description, (uint32_t)strlen(description) + 1); } + + // Icon + if (iconPath[0]) { + char iconFullPath[MAX_PATH_LEN]; + snprintf(iconFullPath, sizeof(iconFullPath), "%s/%s", projectDir, iconPath); + int32_t iconLen = 0; + char *iconData = readFile(iconFullPath, &iconLen); + + if (iconData) { + dvxResAppendEntry(outputPath, "icon32", DVX_RES_ICON, iconData, (uint32_t)iconLen); + free(iconData); + } + } + + // Help file name (file is expected alongside the .app) + if (helpFile[0]) { + const char *helpBase = helpFile; + const char *hs = strrchr(helpBase, '/'); + const char *hs2 = strrchr(helpBase, '\\'); + + if (hs2 > hs) { + hs = hs2; + } + + if (hs) { + helpBase = hs + 1; + } + + dvxResAppendEntry(outputPath, "helpfile", DVX_RES_TEXT, helpBase, (uint32_t)strlen(helpBase) + 1); + + // Copy help file to output directory + char helpSrc[MAX_PATH_LEN]; + snprintf(helpSrc, sizeof(helpSrc), "%s/%s", projectDir, helpFile); + + char outDir[MAX_PATH_LEN]; + snprintf(outDir, sizeof(outDir), "%s", outputPath); + char *outSep = strrchr(outDir, '/'); + char *outSep2 = strrchr(outDir, '\\'); + + if (outSep2 > outSep) { + outSep = outSep2; + } + + if (outSep) { + *outSep = '\0'; + } else { + outDir[0] = '.'; + outDir[1] = '\0'; + } + + char helpDst[MAX_PATH_LEN]; + snprintf(helpDst, sizeof(helpDst), "%s/%s", outDir, helpBase); + + int32_t hLen = 0; + char *hData = readFile(helpSrc, &hLen); + + if (hData) { + FILE *hf = fopen(helpDst, "wb"); + + if (hf) { + fwrite(hData, 1, hLen, hf); + fclose(hf); + } + + free(hData); + } + } + + // MODULE resource + dvxResAppendEntry(outputPath, "MODULE", DVX_RES_BINARY, modData, (uint32_t)modLen); + free(modData); + + // DEBUG resource + if (dbgData) { + dvxResAppendEntry(outputPath, "DEBUG", DVX_RES_BINARY, dbgData, (uint32_t)dbgLen); + free(dbgData); + } + + // Form resources -- use obfuscated bytes in release mode, raw otherwise + for (int32_t fi = 0; fi < frmCount; fi++) { + char resName[16]; + snprintf(resName, sizeof(resName), "FORM%d", (int)fi); + + if (release && obfFrms[fi].data) { + dvxResAppendEntry(outputPath, resName, DVX_RES_BINARY, obfFrms[fi].data, (uint32_t)obfFrms[fi].len); + } else { + dvxResAppendEntry(outputPath, resName, DVX_RES_BINARY, frmData[fi], (uint32_t)frmLens[fi]); + } + } + + // Free .frm buffers + for (int32_t i = 0; i < frmCount; i++) { + free(frmData[i]); + free(obfFrms[i].data); + } + + (void)frmFileIdx; // unused (kept in case of future per-file metadata) + + prefsClose(prefs); + + printf("Created %s (%d bytes)\n", outputPath, (int)stubLen + (int)modLen); + return 0; +} diff --git a/apps/dvxbasic/stub/basstub.c b/apps/dvxbasic/stub/basstub.c index 401b682..50a09ca 100644 --- a/apps/dvxbasic/stub/basstub.c +++ b/apps/dvxbasic/stub/basstub.c @@ -92,7 +92,7 @@ int32_t appMain(DxeAppContextT *ctx) { // Read app name and update the shell's app record uint32_t nameSize = 0; - char *appName = (char *)dvxResRead(res, "APPNAME", &nameSize); + char *appName = (char *)dvxResRead(res, "name", &nameSize); if (appName) { ShellAppT *app = shellGetApp(ctx->appId); @@ -104,6 +104,15 @@ int32_t appMain(DxeAppContextT *ctx) { free(appName); } + // Set help file path if present + uint32_t helpNameSize = 0; + char *helpName = (char *)dvxResRead(res, "helpfile", &helpNameSize); + + if (helpName) { + snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s%c%s", ctx->appDir, DVX_PATH_SEP, helpName); + free(helpName); + } + // Load MODULE resource uint32_t modSize = 0; uint8_t *modData = (uint8_t *)dvxResRead(res, "MODULE", &modSize); diff --git a/apps/dvxbasic/test_compact.c b/apps/dvxbasic/test_compact.c new file mode 100644 index 0000000..9cc9b8f --- /dev/null +++ b/apps/dvxbasic/test_compact.c @@ -0,0 +1,326 @@ +// test_compact.c -- verify that strip + compact produces identical output +// +// Compiles each test program, runs it once without compaction and once +// with strip + compact applied, and compares captured PRINT output. +// Exits nonzero if any mismatch is found. + +#include "compiler/parser.h" +#include "compiler/strip.h" +#include "compiler/compact.h" +#include "runtime/vm.h" +#include "runtime/values.h" + +#include +#include +#include +#include + + +#define MAX_OUT 65536 + + +typedef struct { + char *buf; + int32_t len; + int32_t cap; +} OutBufT; + + +static void captureCallback(void *ctx, const char *text, bool newline) { + OutBufT *ob = (OutBufT *)ctx; + int32_t tlen = (int32_t)strlen(text); + + if (ob->len + tlen + 2 >= ob->cap) { + return; + } + + memcpy(ob->buf + ob->len, text, tlen); + ob->len += tlen; + + if (newline) { + ob->buf[ob->len++] = '\n'; + } +} + + +static int32_t runAndCapture(const char *source, bool compact, char *outBuf, int32_t outCap) { + BasParserT parser; + basParserInit(&parser, source, (int32_t)strlen(source)); + + if (!basParse(&parser)) { + fprintf(stderr, "compile error: %s\n", parser.error); + basParserFree(&parser); + return -1; + } + + BasModuleT *mod = basParserBuildModule(&parser); + basParserFree(&parser); + + if (!mod) { + return -1; + } + + if (compact) { + basStripModule(mod); + basCompactBytecode(mod); + } + + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, mod); + + OutBufT ob; + ob.buf = outBuf; + ob.len = 0; + ob.cap = outCap; + + basVmSetPrintCallback(vm, captureCallback, &ob); + + vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount; + vm->callDepth = 1; + + BasVmResultE result = basVmRun(vm); + + if (result != BAS_VM_HALTED && result != BAS_VM_OK) { + fprintf(stderr, "vm error %d: %s\n", result, basVmGetError(vm)); + } + + basVmDestroy(vm); + basModuleFree(mod); + + outBuf[ob.len] = '\0'; + return ob.len; +} + + +static int32_t sTotal = 0; +static int32_t sFailed = 0; + + +static void testCompact(const char *name, const char *source) { + sTotal++; + + static char orig[MAX_OUT]; + static char comp[MAX_OUT]; + + int32_t origLen = runAndCapture(source, false, orig, MAX_OUT); + int32_t compLen = runAndCapture(source, true, comp, MAX_OUT); + + if (origLen < 0 || compLen < 0) { + printf("FAIL: %s (runtime error)\n", name); + sFailed++; + return; + } + + if (origLen != compLen || memcmp(orig, comp, origLen) != 0) { + printf("FAIL: %s\n", name); + printf(" uncompacted (%d bytes):\n%s\n", (int)origLen, orig); + printf(" compacted (%d bytes):\n%s\n", (int)compLen, comp); + sFailed++; + return; + } + + printf("PASS: %s\n", name); +} + + +int main(void) { + printf("DVX BASIC Bytecode Compaction Tests\n"); + printf("====================================\n\n"); + + basStringSystemInit(); + + // ---- Basic control flow ---- + + testCompact("FOR loop", + "DIM i AS INTEGER\n" + "FOR i = 1 TO 5\n" + " PRINT i;\n" + "NEXT i\n" + "PRINT\n" + ); + + testCompact("Nested FOR", + "DIM i AS INTEGER\n" + "DIM j AS INTEGER\n" + "FOR i = 1 TO 3\n" + " FOR j = 1 TO 2\n" + " PRINT i * 10 + j;\n" + " NEXT j\n" + "NEXT i\n" + "PRINT\n" + ); + + testCompact("FOR with negative STEP", + "DIM i AS INTEGER\n" + "FOR i = 5 TO 1 STEP -1\n" + " PRINT i;\n" + "NEXT i\n" + "PRINT\n" + ); + + testCompact("EXIT FOR", + "DIM i AS INTEGER\n" + "FOR i = 1 TO 100\n" + " IF i = 4 THEN EXIT FOR\n" + " PRINT i;\n" + "NEXT i\n" + "PRINT\n" + ); + + testCompact("DO WHILE", + "DIM n AS INTEGER\n" + "n = 0\n" + "DO WHILE n < 3\n" + " PRINT n;\n" + " n = n + 1\n" + "LOOP\n" + "PRINT\n" + ); + + testCompact("IF THEN ELSE", + "DIM x AS INTEGER\n" + "x = 5\n" + "IF x > 3 THEN\n" + " PRINT \"big\"\n" + "ELSE\n" + " PRINT \"small\"\n" + "END IF\n" + ); + + testCompact("SELECT CASE", + "DIM g AS STRING\n" + "g = \"B\"\n" + "SELECT CASE g\n" + " CASE \"A\"\n" + " PRINT \"A\"\n" + " CASE \"B\", \"C\"\n" + " PRINT \"BC\"\n" + " CASE ELSE\n" + " PRINT \"?\"\n" + "END SELECT\n" + ); + + // ---- GOSUB: exercises the absolute address in OP_PUSH_INT32 pattern ---- + + testCompact("GOSUB", + "DIM n AS INTEGER\n" + "n = 10\n" + "GOSUB doubler\n" + "PRINT n\n" + "GOSUB doubler\n" + "PRINT n\n" + "END\n" + "doubler:\n" + "n = n * 2\n" + "RETURN\n" + ); + + testCompact("Multiple GOSUBs", + "DIM x AS INTEGER\n" + "x = 1\n" + "GOSUB a\n" + "GOSUB b\n" + "GOSUB c\n" + "PRINT x\n" + "END\n" + "a:\n" + "x = x + 10\n" + "RETURN\n" + "b:\n" + "x = x + 100\n" + "RETURN\n" + "c:\n" + "x = x + 1000\n" + "RETURN\n" + ); + + // ---- SUB / FUNCTION: exercises absolute CALL addresses + proc table ---- + + testCompact("SUB with CALL", + "CALL Greet\n" + "CALL Greet\n" + "CALL Greet\n" + "SUB Greet\n" + " PRINT \"Hello\"\n" + "END SUB\n" + ); + + testCompact("FUNCTION return value", + "DECLARE FUNCTION Square(x AS INTEGER) AS INTEGER\n" + "DIM r AS INTEGER\n" + "r = Square(5)\n" + "PRINT r\n" + "r = Square(7)\n" + "PRINT r\n" + "FUNCTION Square(x AS INTEGER) AS INTEGER\n" + " Square = x * x\n" + "END FUNCTION\n" + ); + + testCompact("Recursive FUNCTION", + "DECLARE FUNCTION Fact(n AS INTEGER) AS INTEGER\n" + "PRINT Fact(5)\n" + "FUNCTION Fact(n AS INTEGER) AS INTEGER\n" + " IF n <= 1 THEN\n" + " Fact = 1\n" + " ELSE\n" + " Fact = n * Fact(n - 1)\n" + " END IF\n" + "END FUNCTION\n" + ); + + // ---- ON ERROR: exercises the relative int16 offset path with non-zero ---- + + testCompact("ON ERROR GOTO", + "ON ERROR GOTO handler\n" + "PRINT 10 / 0\n" + "PRINT \"unreached\"\n" + "END\n" + "handler:\n" + "PRINT \"caught\"\n" + ); + + // ---- Mixed: many OP_LINE between jumps ---- + + testCompact("Long function with many lines", + "DIM total AS INTEGER\n" + "total = 0\n" + "DIM i AS INTEGER\n" + "FOR i = 1 TO 10\n" + " IF i MOD 2 = 0 THEN\n" + " total = total + i\n" + " ELSE\n" + " total = total + i * 2\n" + " END IF\n" + "NEXT i\n" + "PRINT total\n" + ); + + testCompact("GOSUB inside FOR", + "DIM i AS INTEGER\n" + "DIM sum AS INTEGER\n" + "sum = 0\n" + "FOR i = 1 TO 5\n" + " GOSUB addit\n" + "NEXT i\n" + "PRINT sum\n" + "END\n" + "addit:\n" + "sum = sum + i\n" + "RETURN\n" + ); + + testCompact("FOR inside SUB", + "CALL Loop5\n" + "SUB Loop5\n" + " DIM k AS INTEGER\n" + " FOR k = 0 TO 4\n" + " PRINT k;\n" + " NEXT k\n" + " PRINT\n" + "END SUB\n" + ); + + printf("\n%d/%d tests passed\n", (int)(sTotal - sFailed), (int)sTotal); + return sFailed > 0 ? 1 : 0; +} diff --git a/apps/dvxbasic/test_compiler.c b/apps/dvxbasic/test_compiler.c index 6752eb3..d7d4b13 100644 --- a/apps/dvxbasic/test_compiler.c +++ b/apps/dvxbasic/test_compiler.c @@ -900,6 +900,107 @@ int main(void) { printf("\n"); } + // Regression test: App.Data must parse even though Data is a BASIC keyword + runProgram("App.Data parses", + "DIM p AS STRING\n" + "p = App.Data\n" + "PRINT p\n" + ); + + // Regression test: RGB(r,g,b) packs into 0x00RRGGBB + runProgram("RGB packing", + "PRINT RGB(255, 128, 0)\n" + "PRINT RGB(0, 255, 0)\n" + "PRINT RGB(0, 0, 255)\n" + ); + // Expected: 16744448 (0xFF8000), 65280 (0x00FF00), 255 (0x0000FF) + + // Regression test: GetRed/Green/Blue round-trip + runProgram("RGB round-trip", + "DIM c AS LONG\n" + "c = RGB(17, 99, 200)\n" + "PRINT GetRed(c); GetGreen(c); GetBlue(c)\n" + ); + // Expected: 17 99 200 + + // Regression test: MsgBox function accepts optional title (3rd arg) + // The test harness's default msgBox callback returns 1; we just verify + // the program compiles and runs. + runProgram("MsgBox 3-arg compile", + "DIM r AS INTEGER\n" + "r = MsgBox(\"hi\", 0, \"My Title\")\n" + "PRINT \"ok\"\n" + ); + + // Regression test: CONST accepts AS type + runProgram("CONST AS type", + "CONST PI AS DOUBLE = 3.14159\n" + "CONST N AS INTEGER = 42\n" + "PRINT PI\n" + "PRINT N\n" + ); + + // Regression test: PRINT #ch with ; separator + runProgram("PRINT # with semicolon", + "OPEN \"/tmp/dvxbasic_psemi.txt\" FOR OUTPUT AS #1\n" + "PRINT #1, \"Line one\"\n" + "PRINT #1, \"ans = \"; 42\n" + "PRINT #1, \"x\"; \"y\"; \"z\"\n" + "CLOSE #1\n" + "DIM s AS STRING\n" + "OPEN \"/tmp/dvxbasic_psemi.txt\" FOR INPUT AS #1\n" + "DO WHILE NOT EOF(1)\n" + " LINE INPUT #1, s\n" + " PRINT s\n" + "LOOP\n" + "CLOSE #1\n" + ); + // Expected: Line one / ans = 42 / xyz + + // Regression test: IniRead$ with $ suffix must tokenize as keyword + runProgram("IniRead$ tokenizes", + "DIM v AS STRING\n" + "v = IniRead$(\"/tmp/nofile.ini\", \"S\", \"K\", \"default\")\n" + "PRINT v\n" + ); + // Expected: default (file doesn't exist) + + // Regression test: SUB extern call without parens must emit OP_CALL_EXTERN + // (previously emitted OP_CALL which called nothing, because no internal + // proc at address 0 existed for the extern.) + { + printf("=== DECLARE LIBRARY SUB no-parens ===\n"); + const char *src = + "DECLARE LIBRARY \"basrt\"\n" + " DECLARE SUB DoIt(BYVAL s AS STRING)\n" + "END DECLARE\n" + "DoIt \"hello\"\n"; + int32_t len = (int32_t)strlen(src); + BasParserT parser; + basParserInit(&parser, src, len); + bool ok = basParse(&parser); + + if (!ok) { + printf("COMPILE ERROR: %s\n", parser.error); + } else { + BasModuleT *mod = basParserBuildModule(&parser); + bool found = false; + + for (int32_t i = 0; i < mod->codeLen; i++) { + if (mod->code[i] == OP_CALL_EXTERN) { + found = true; + break; + } + } + + printf(found ? "PASS: OP_CALL_EXTERN emitted\n" : "FAIL: OP_CALL_EXTERN not emitted\n"); + basModuleFree(mod); + } + + basParserFree(&parser); + printf("\n"); + } + // Test: Procedure table populated for SUBs and FUNCTIONs { printf("=== Procedure table ===\n"); @@ -1213,6 +1314,56 @@ int main(void) { ); // Expected: 1 2 / after + // Regression test: FOR loop inside a SUB with local loop variable + runProgram("FOR inside SUB with local", + "CALL MySub\n" + "SUB MySub\n" + " DIM i AS LONG\n" + " FOR i = 0 TO 3\n" + " PRINT i;\n" + " NEXT i\n" + " PRINT\n" + "END SUB\n" + ); + // Expected: 0 1 2 3 + + // Regression test: resedit pattern -- FOR in SUB with n = FUNC(...) + runProgram("FOR with func-returned limit", + "CALL DoIt\n" + "SUB DoIt\n" + " DIM n AS LONG\n" + " n = GetCount()\n" + " DIM i AS LONG\n" + " FOR i = 0 TO n - 1\n" + " PRINT i;\n" + " NEXT i\n" + " PRINT\n" + "END SUB\n" + "FUNCTION GetCount() AS LONG\n" + " GetCount = 4\n" + "END FUNCTION\n" + ); + // Expected: 0 1 2 3 + + // Regression test: resedit pattern -- FOR in SUB inside a FORM + runProgram("FOR in SUB inside FORM", + "BEGINFORM \"Form1\"\n" + "DIM formVar AS LONG\n" + "formVar = 42\n" + "CALL DoIt\n" + "SUB DoIt\n" + " DIM n AS LONG\n" + " n = 4\n" + " DIM ix AS LONG\n" + " FOR ix = 0 TO n - 1\n" + " PRINT ix;\n" + " NEXT ix\n" + " PRINT\n" + "END SUB\n" + "ENDFORM\n" + ); + // Expected: 0 1 2 3 + runProgram("EXIT DO", "DIM n AS INTEGER\n" "n = 0\n" diff --git a/apps/dvxdemo/dvxdemo.c b/apps/dvxdemo/dvxdemo.c index ac2d85d..4afad14 100644 --- a/apps/dvxdemo/dvxdemo.c +++ b/apps/dvxdemo/dvxdemo.c @@ -153,11 +153,11 @@ static void onCloseMainCb(WindowT *win) { static const FileFilterT sFileFilters[] = { - {"All Files (*.*)", "*.*"}, - {"Text Files (*.txt)", "*.txt"}, - {"Batch Files (*.bat)", "*.bat"}, - {"Executables (*.exe)", "*.exe"}, - {"Bitmap Files (*.bmp)", "*.bmp"} + {"All Files (*.*)"}, + {"Text Files (*.txt)"}, + {"Batch Files (*.bat)"}, + {"Executables (*.exe)"}, + {"Bitmap Files (*.bmp)"} }; static void onMenuCb(WindowT *win, int32_t menuId) { diff --git a/apps/dvxdemo/dvxdemo.res b/apps/dvxdemo/dvxdemo.res index 950dcdf..84e29b3 100644 --- a/apps/dvxdemo/dvxdemo.res +++ b/apps/dvxdemo/dvxdemo.res @@ -1,5 +1,5 @@ # dvxdemo.res -- Resource manifest for DVX Demo -icon32 icon dvxdemo/icon32.bmp +icon32 icon icon32.bmp name text "DVX Demo" author text "DVX Project" description text "Widget toolkit demonstration" diff --git a/apps/dvxhelp/dvxhelp.res b/apps/dvxhelp/dvxhelp.res index b3d402f..242541c 100644 --- a/apps/dvxhelp/dvxhelp.res +++ b/apps/dvxhelp/dvxhelp.res @@ -1,5 +1,5 @@ # dvxhelp.res -- Resource manifest for DVX Help Viewer -icon32 icon dvxhelp/icon32.bmp +icon32 icon icon32.bmp name text "DVX Help" author text "DVX Project" description text "Help file viewer" diff --git a/apps/imgview/imgview.c b/apps/imgview/imgview.c index ea3dc41..4855f10 100644 --- a/apps/imgview/imgview.c +++ b/apps/imgview/imgview.c @@ -328,8 +328,8 @@ static void onResize(WindowT *win, int32_t contentW, int32_t contentH) { static void openFile(void) { FileFilterT filters[] = { - { "Images (*.bmp;*.jpg;*.png;*.gif)", "*.bmp;*.jpg;*.png;*.gif" }, - { "All Files (*.*)", "*.*" } + { "Images (*.bmp;*.jpg;*.png;*.gif)" }, + { "All Files (*.*)" } }; char path[DVX_MAX_PATH]; diff --git a/apps/imgview/imgview.res b/apps/imgview/imgview.res index 8df2a48..aac2611 100644 --- a/apps/imgview/imgview.res +++ b/apps/imgview/imgview.res @@ -1,5 +1,5 @@ # imgview.res -- Resource manifest for Image Viewer -icon32 icon imgview/icon32.bmp +icon32 icon icon32.bmp name text "Image Viewer" author text "DVX Project" description text "BMP, PNG, JPEG, and GIF viewer" diff --git a/apps/notepad/notepad.c b/apps/notepad/notepad.c index c9aff87..a682326 100644 --- a/apps/notepad/notepad.c +++ b/apps/notepad/notepad.c @@ -150,8 +150,8 @@ static void doOpen(void) { } FileFilterT filters[] = { - { "Text Files (*.txt)", "*.txt" }, - { "All Files (*.*)", "*.*" } + { "Text Files (*.txt)" }, + { "All Files (*.*)" } }; char path[DVX_MAX_PATH]; @@ -236,8 +236,8 @@ static void doSave(void) { static void doSaveAs(void) { FileFilterT filters[] = { - { "Text Files (*.txt)", "*.txt" }, - { "All Files (*.*)", "*.*" } + { "Text Files (*.txt)" }, + { "All Files (*.*)" } }; char path[DVX_MAX_PATH]; diff --git a/apps/notepad/notepad.res b/apps/notepad/notepad.res index 83eab69..bbe2144 100644 --- a/apps/notepad/notepad.res +++ b/apps/notepad/notepad.res @@ -1,5 +1,5 @@ # notepad.res -- Resource manifest for Notepad -icon32 icon notepad/icon32.bmp +icon32 icon icon32.bmp name text "Notepad" author text "DVX Project" description text "Simple text editor" diff --git a/apps/progman/progman.c b/apps/progman/progman.c index c5f518c..cfec155 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -73,6 +73,7 @@ #define CMD_TILE_V 203 #define CMD_MIN_ON_RUN 104 #define CMD_RESTORE_ALONE 105 +#define CMD_RELOAD 106 #define CMD_ABOUT 300 #define CMD_TASK_MGR 301 #define CMD_SYSINFO 302 @@ -166,6 +167,7 @@ static void buildPmWindow(void) { MenuBarT *menuBar = wmAddMenuBar(sPmWindow); MenuT *fileMenu = wmAddMenu(menuBar, "&File"); wmAddMenuItem(fileMenu, "&Run...", CMD_RUN); + wmAddMenuItem(fileMenu, "Re&load", CMD_RELOAD); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "E&xit DVX", CMD_EXIT); @@ -313,8 +315,8 @@ static void onPmMenu(WindowT *win, int32_t menuId) { case CMD_RUN: { FileFilterT filters[] = { - { "Applications (*.app)", "*.app" }, - { "All Files (*.*)", "*.*" } + { "Applications (*.app)" }, + { "All Files (*.*)" } }; char path[MAX_PATH_LEN]; @@ -329,6 +331,14 @@ static void onPmMenu(WindowT *win, int32_t menuId) { } break; + case CMD_RELOAD: + dvxDestroyWindow(sAc, sPmWindow); + sPmWindow = NULL; + sStatusLabel = NULL; + scanAppsDir(); + buildPmWindow(); + break; + case CMD_EXIT: onPmClose(sPmWindow); break; diff --git a/config/dvx.ini b/config/dvx.ini index 5b9736b..df985dc 100644 --- a/config/dvx.ini +++ b/config/dvx.ini @@ -11,12 +11,14 @@ bpp = 16 ; Mouse settings. ; wheel: normal or reversed +; wheelspeed: lines per wheel notch (1-10, default 3) ; 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 +wheelspeed = 3 doubleclick = 500 acceleration = medium speed = 8 diff --git a/core/dvxApp.c b/core/dvxApp.c index 3932fef..720f6fe 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -1277,8 +1277,12 @@ static void dispatchEvents(AppContextT *ctx) { return; } - // Handle left button press + // Handle left button press. Consume the edge immediately so that if + // the click handler calls back into the event loop (e.g., BASIC's + // DoEvents yielding while a QueryUnload handler runs), the same + // physical click isn't re-seen as a fresh press. if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT)) { + ctx->prevMouseButtons |= MOUSE_LEFT; handleMouseButton(ctx, mx, my, buttons); } @@ -1289,6 +1293,7 @@ static void dispatchEvents(AppContextT *ctx) { // that apply to all their children without requiring each child to have // its own menu, while still allowing per-widget overrides. if ((buttons & MOUSE_RIGHT) && !(prevBtn & MOUSE_RIGHT)) { + ctx->prevMouseButtons |= MOUSE_RIGHT; int32_t hitPart; int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart); @@ -1396,7 +1401,7 @@ static void dispatchEvents(AppContextT *ctx) { if (target && wclsHas(target, WGT_METHOD_ON_KEY)) { int32_t delta = ctx->mouseWheel * ctx->wheelDirection; int32_t arrowKey = (delta > 0) ? (0x50 | 0x100) : (0x48 | 0x100); - int32_t steps = abs(delta) * MOUSE_WHEEL_STEP; + int32_t steps = abs(delta) * ctx->wheelStep; for (int32_t s = 0; s < steps; s++) { wclsOnKey(target, arrowKey, 0); @@ -1423,7 +1428,7 @@ static void dispatchEvents(AppContextT *ctx) { if (sb) { int32_t oldValue = sb->value; - sb->value += ctx->mouseWheel * ctx->wheelDirection * MOUSE_WHEEL_STEP; + sb->value += ctx->mouseWheel * ctx->wheelDirection * ctx->wheelStep; if (sb->value < sb->min) { sb->value = sb->min; @@ -1721,11 +1726,18 @@ static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd) { break; case SysMenuCloseE: + // Sys menu is already closed. Composite a clean frame first + // so the menu's pixels are erased before onClose runs -- the + // close handler may open a modal dialog and we don't want the + // sys menu popup peeking out from under it. + compositeAndFlush(ctx); + if (win->onClose) { WIN_CALLBACK(ctx, win, win->onClose(win)); } else { dvxDestroyWindow(ctx, win); } + break; } } @@ -1872,11 +1884,20 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t ctx->lastCloseClickId = -1; closeSysMenu(ctx); + // Composite a clean frame first so the sys menu is + // fully gone before onClose runs -- the close handler + // may open a modal dialog and we don't want the menu + // peeking out from under it. + compositeAndFlush(ctx); + if (win->onClose) { WIN_CALLBACK(ctx, win, win->onClose(win)); } else { dvxDestroyWindow(ctx, win); } + + // Ensure sys menu is closed even if onClose re-opened it + closeSysMenu(ctx); } else { ctx->lastCloseClickTime = now; ctx->lastCloseClickId = win->id; @@ -1965,8 +1986,8 @@ static void initColorScheme(AppContextT *ctx) { static void interactiveScreenshot(AppContextT *ctx) { FileFilterT filters[] = { - { "PNG Images (*.png)", "*.png" }, - { "BMP Images (*.bmp)", "*.bmp" } + { "PNG Images (*.png)" }, + { "BMP Images (*.bmp)" } }; char path[DVX_MAX_PATH]; @@ -2000,8 +2021,8 @@ static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win) { } FileFilterT filters[] = { - { "PNG Images (*.png)", "*.png" }, - { "BMP Images (*.bmp)", "*.bmp" } + { "PNG Images (*.png)" }, + { "BMP Images (*.bmp)" } }; char path[DVX_MAX_PATH]; @@ -4309,6 +4330,7 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_ ctx->lastTitleClickId = -1; ctx->lastTitleClickTime = 0; ctx->wheelDirection = 1; + ctx->wheelStep = MOUSE_WHEEL_STEP_DEFAULT; ctx->dblClickTicks = DBLCLICK_THRESHOLD; sDblClickTicks = DBLCLICK_THRESHOLD; @@ -4833,11 +4855,21 @@ 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, int32_t mickeyRatio) { +void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio, int32_t wheelStep) { ctx->wheelDirection = (wheelDir < 0) ? -1 : 1; ctx->dblClickTicks = (clock_t)dblClickMs * CLOCKS_PER_SEC / 1000; sDblClickTicks = ctx->dblClickTicks; + if (wheelStep < MOUSE_WHEEL_STEP_MIN) { + wheelStep = MOUSE_WHEEL_STEP_MIN; + } + + if (wheelStep > MOUSE_WHEEL_STEP_MAX) { + wheelStep = MOUSE_WHEEL_STEP_MAX; + } + + ctx->wheelStep = wheelStep; + if (accelThreshold > 0) { platformMouseSetAccel(accelThreshold); } diff --git a/core/dvxApp.h b/core/dvxApp.h index c2960de..ae4f0dd 100644 --- a/core/dvxApp.h +++ b/core/dvxApp.h @@ -104,6 +104,7 @@ typedef struct AppContextT { uint32_t charHeightRecip; // fixed-point 16.16 reciprocal of font.charHeight // Mouse configuration (loaded from preferences) int32_t wheelDirection; // 1 = normal, -1 = reversed + int32_t wheelStep; // lines per wheel notch (1-10, default 3) clock_t dblClickTicks; // double-click speed in clock() ticks // Color scheme source RGB values (unpacked, for theme save/get) uint8_t colorRgb[ColorCountE][3]; @@ -134,7 +135,8 @@ int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t request // dblClickMs: double-click speed in milliseconds (e.g. 500). // accelThreshold: double-speed threshold in mickeys/sec (0 = don't change). // 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); +// wheelStep: lines per wheel notch (1-10, default 3). +void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio, int32_t wheelStep); // ============================================================ // Color scheme diff --git a/core/dvxDialog.c b/core/dvxDialog.c index d41cbe6..9d8a4a1 100644 --- a/core/dvxDialog.c +++ b/core/dvxDialog.c @@ -1071,6 +1071,32 @@ typedef struct { static FileDialogStateT sFd; +// ============================================================ +// fdExtractPattern -- extract glob pattern from inside parentheses in label +// ============================================================ + +static const char *fdExtractPattern(const FileFilterT *f, char *buf, int32_t bufSize) { + const char *open = strchr(f->label, '('); + const char *close = open ? strchr(open, ')') : NULL; + + if (open && close && close > open + 1) { + int32_t len = (int32_t)(close - open - 1); + + if (len >= bufSize) { + len = bufSize - 1; + } + + memcpy(buf, open + 1, len); + buf[len] = '\0'; + return buf; + } + + // No parens -- use the whole label as the pattern + snprintf(buf, bufSize, "%s", f->label); + return buf; +} + + // ============================================================ // fdFilterMatch -- check if filename matches a glob pattern // ============================================================ @@ -1210,9 +1236,10 @@ static void fdLoadDir(void) { fdFreeEntries(); const char *pattern = NULL; + char patBuf[128]; if (sFd.filters && sFd.activeFilter >= 0 && sFd.activeFilter < sFd.filterCount) { - pattern = sFd.filters[sFd.activeFilter].pattern; + pattern = fdExtractPattern(&sFd.filters[sFd.activeFilter], patBuf, sizeof(patBuf)); } DIR *dir = opendir(sFd.curDir); @@ -1561,6 +1588,59 @@ static void fdOnOk(WidgetT *w) { return; } + // Save dialog: if the filename has no extension, append the first + // extension from the active filter pattern. Wildcards like "*.*" or + // "*" don't add an extension. + char nameWithExt[FD_MAX_PATH]; + + if ((sFd.flags & FD_SAVE) && !strchr(name, '.')) { + char extPatBuf[128]; + const char *pattern = fdExtractPattern(&sFd.filters[sFd.activeFilter], extPatBuf, sizeof(extPatBuf)); + const char *ext = NULL; + + // Find first non-wildcard extension in pattern (may be "*.txt;*.doc") + if (pattern) { + const char *p = pattern; + + while (*p) { + if (p[0] == '*' && p[1] == '.') { + const char *e = p + 2; + + // Skip wildcards like "*.*" + if (*e != '*' && *e != '\0') { + ext = p + 1; // points to ".ext" + break; + } + } + + // Skip to next semicolon-delimited pattern + const char *semi = strchr(p, ';'); + + if (semi) { + p = semi + 1; + } else { + break; + } + } + } + + if (ext) { + // Extract just the extension (up to ; or end) + char extBuf[16]; + int32_t ei = 0; + + while (ext[ei] && ext[ei] != ';' && ei < 15) { + extBuf[ei] = ext[ei]; + ei++; + } + + extBuf[ei] = '\0'; + snprintf(nameWithExt, sizeof(nameWithExt), "%s%s", name, extBuf); + name = nameWithExt; + wgtSetText(sFd.nameInput, name); + } + } + // Accept the file (with confirmation if needed) fdAcceptFile(name); } diff --git a/core/dvxDlg.h b/core/dvxDlg.h index 9c59496..9bd3b62 100644 --- a/core/dvxDlg.h +++ b/core/dvxDlg.h @@ -64,9 +64,11 @@ int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, // patterns (no semicolon-separated lists). This keeps the matching code // trivial for a DOS filesystem where filenames are short and simple. +// File filter for the file dialog. Pattern is extracted from inside +// parentheses in the label. Example: "Text Files (*.txt)" +// Multiple extensions: "Images (*.bmp;*.png;*.jpg;*.gif)" typedef struct { - const char *label; // e.g. "Text Files (*.txt)" - const char *pattern; // e.g. "*.txt" (case-insensitive, single pattern) + const char *label; // e.g. "Text Files (*.txt)" -- pattern extracted from parens } FileFilterT; // Display a modal file open/save dialog. The dialog shows a directory diff --git a/core/dvxRes.h b/core/dvxRes.h index 90b364a..dea6b98 100644 --- a/core/dvxRes.h +++ b/core/dvxRes.h @@ -77,4 +77,8 @@ void dvxResClose(DvxResHandleT *h); // 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); +// Remove a resource by name from a DXE file. +// Returns 0 on success, -1 on error (not found or I/O failure). +int32_t dvxResRemove(const char *path, const char *name); + #endif // DVX_RES_H diff --git a/core/dvxResource.c b/core/dvxResource.c index b68258d..f93c846 100644 --- a/core/dvxResource.c +++ b/core/dvxResource.c @@ -144,3 +144,105 @@ void dvxResClose(DvxResHandleT *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); } + + +int32_t dvxResRemove(const char *path, const char *name) { + if (!path || !name) { + 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); + + if (!entries || count == 0) { + return -1; + } + + bool found = false; + + 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--; + found = true; + break; + } + } + + if (!found) { + for (uint32_t i = 0; i < count; i++) { + free(data[i]); + } + + free(data); + free(entries); + return -1; + } + + int32_t result; + + if (count == 0) { + // No resources left -- truncate to DXE content only + FILE *f = fopen(path, "rb"); + + if (!f) { + free(data); + free(entries); + return -1; + } + + uint8_t *dxeBuf = (uint8_t *)malloc((size_t)dxeSize); + + if (!dxeBuf) { + fclose(f); + free(data); + free(entries); + return -1; + } + + if (fread(dxeBuf, 1, (size_t)dxeSize, f) != (size_t)dxeSize) { + free(dxeBuf); + fclose(f); + free(data); + free(entries); + return -1; + } + + fclose(f); + f = fopen(path, "wb"); + + if (f) { + fwrite(dxeBuf, 1, (size_t)dxeSize, f); + fclose(f); + result = 0; + } else { + result = -1; + } + + free(dxeBuf); + } else { + result = dvxResWriteBlock(path, dxeSize, entries, count, data); + } + + for (uint32_t i = 0; i < count; i++) { + free(data[i]); + } + + free(data); + free(entries); + return result; +} diff --git a/core/dvxTypes.h b/core/dvxTypes.h index 4ff19e0..54720ce 100644 --- a/core/dvxTypes.h +++ b/core/dvxTypes.h @@ -611,8 +611,10 @@ typedef struct { #define MOUSE_RIGHT 2 #define MOUSE_MIDDLE 4 -// Scrollbar lines to scroll per mouse wheel notch -#define MOUSE_WHEEL_STEP 3 +// Default scrollbar lines to scroll per mouse wheel notch +#define MOUSE_WHEEL_STEP_DEFAULT 3 +#define MOUSE_WHEEL_STEP_MIN 1 +#define MOUSE_WHEEL_STEP_MAX 10 // ============================================================ // Mouse cursor diff --git a/run.sh b/run.sh index 4fa9cde..fbd42cc 100755 --- a/run.sh +++ b/run.sh @@ -1,5 +1,4 @@ #!/bin/bash flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x-overrides.conf - #SDL_VIDEO_X11_VISUALID= ~/bin/dosbox-staging/dosbox -conf dosbox-staging-overrides.conf diff --git a/sdk/include/basic/commdlg.bas b/sdk/include/basic/commdlg.bas index bc82a4a..369c5f7 100644 --- a/sdk/include/basic/commdlg.bas +++ b/sdk/include/basic/commdlg.bas @@ -8,10 +8,17 @@ DECLARE LIBRARY "basrt" ' Show a file Open dialog. Returns selected path, or "" if cancelled. - ' filter$ is a DOS wildcard (e.g. "*.bmp", "*.txt"). + ' filter$ specifies file type filters, pipe-delimited: + ' "Label (pattern)|Label (pattern)|..." + ' The pattern is extracted from inside the parentheses. + ' Examples: + ' "*.txt" (single pattern, legacy) + ' "Text Files (*.txt)|All Files (*.*)" (two filters) + ' "Images (*.bmp;*.png;*.jpg)|All Files (*.*)" (multi-extension) DECLARE FUNCTION basFileOpen(BYVAL title AS STRING, BYVAL filter AS STRING) AS STRING ' Show a file Save dialog. Returns selected path, or "" if cancelled. + ' filter$ uses the same format as basFileOpen. DECLARE FUNCTION basFileSave(BYVAL title AS STRING, BYVAL filter AS STRING) AS STRING ' Show a modal text input box. Returns entered text, or "" if cancelled. @@ -29,6 +36,6 @@ DECLARE LIBRARY "basrt" END DECLARE ' Return value constants for basPromptSave -CONST DVX_SAVE_YES = 0 -CONST DVX_SAVE_NO = 1 -CONST DVX_SAVE_CANCEL = 2 +CONST DVX_SAVE_YES = 1 +CONST DVX_SAVE_NO = 2 +CONST DVX_SAVE_CANCEL = 3 diff --git a/sdk/include/basic/resource.bas b/sdk/include/basic/resource.bas new file mode 100644 index 0000000..9266c68 --- /dev/null +++ b/sdk/include/basic/resource.bas @@ -0,0 +1,50 @@ +' resource.bas -- DVX Resource File Library for DVX BASIC +' +' Provides access to DVX resource blocks appended to DXE3 +' files (.app, .wgt, .lib). Resources are named data entries +' of type icon, text, or binary. +' +' Usage: Add this file to your project. + +CONST RES_TYPE_ICON = 1 +CONST RES_TYPE_TEXT = 2 +CONST RES_TYPE_BINARY = 3 + +DECLARE LIBRARY "basrt" + ' Open a resource file for reading. Returns a handle (> 0) + ' or 0 on failure. Close with ResClose when done. + DECLARE FUNCTION ResOpen(BYVAL path AS STRING) AS LONG + + ' Close a resource handle. + DECLARE SUB ResClose(BYVAL handle AS LONG) + + ' Return the number of resources in an open handle. + DECLARE FUNCTION ResCount(BYVAL handle AS LONG) AS LONG + + ' Return the name of a resource by zero-based index. + DECLARE FUNCTION ResName$(BYVAL handle AS LONG, BYVAL idx AS LONG) + + ' Return the type of a resource by zero-based index. + ' Returns RES_TYPE_ICON (1), RES_TYPE_TEXT (2), or RES_TYPE_BINARY (3). + DECLARE FUNCTION ResType(BYVAL handle AS LONG, BYVAL idx AS LONG) AS LONG + + ' Return the size in bytes of a resource by zero-based index. + DECLARE FUNCTION ResSize(BYVAL handle AS LONG, BYVAL idx AS LONG) AS LONG + + ' Read a text resource by name. Opens and closes the file + ' internally. Returns "" if not found. + DECLARE FUNCTION ResGetText$(BYVAL path AS STRING, BYVAL resName AS STRING) + + ' Add or replace a text resource. Returns True on success. + DECLARE FUNCTION ResAddText(BYVAL path AS STRING, BYVAL resName AS STRING, BYVAL text AS STRING) AS LONG + + ' Add or replace a resource from a file. Type should be + ' RES_TYPE_ICON or RES_TYPE_BINARY. Returns True on success. + DECLARE FUNCTION ResAddFile(BYVAL path AS STRING, BYVAL resName AS STRING, BYVAL resType AS LONG, BYVAL srcFile AS STRING) AS LONG + + ' Remove a resource by name. Returns True on success. + DECLARE FUNCTION ResRemove(BYVAL path AS STRING, BYVAL resName AS STRING) AS LONG + + ' Extract a resource to a file. Returns True on success. + DECLARE FUNCTION ResExtract(BYVAL path AS STRING, BYVAL resName AS STRING, BYVAL outFile AS STRING) AS LONG +END DECLARE diff --git a/sdk/samples/basic/basicdemo/ICON32.BMP b/sdk/samples/basic/basicdemo/ICON32.BMP new file mode 100644 index 0000000..578c822 --- /dev/null +++ b/sdk/samples/basic/basicdemo/ICON32.BMP @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b09dfbd05163f80708b1921b7326f17d9ddfe86540e1a50f20a826f296dd2af +size 3126 diff --git a/sdk/samples/basic/basicdemo/basicdemo.dbp b/sdk/samples/basic/basicdemo/basicdemo.dbp new file mode 100644 index 0000000..4163016 --- /dev/null +++ b/sdk/samples/basic/basicdemo/basicdemo.dbp @@ -0,0 +1,16 @@ +[Project] +Name = BASIC Demo +Author = DVX Project +Description = Comprehensive tour of DVX BASIC features +Icon = ICON32.BMP + +[Modules] +File0 = ../../../include/basic/commdlg.bas +Count = 1 + +[Forms] +File0 = basicdemo.frm +Count = 1 + +[Settings] +StartupForm = BasicDemo diff --git a/sdk/samples/basic/basicdemo/basicdemo.frm b/sdk/samples/basic/basicdemo/basicdemo.frm new file mode 100644 index 0000000..aad17e0 --- /dev/null +++ b/sdk/samples/basic/basicdemo/basicdemo.frm @@ -0,0 +1,937 @@ +VERSION DVX 1.00 + +Begin Form BasicDemo + Caption = "DVX BASIC Feature Tour" + Layout = VBox + AutoSize = False + Resizable = True + Centered = True + Width = 600 + Height = 440 + + Begin Menu mnuFile + Caption = "&File" + Begin Menu mnuClear + Caption = "&Clear OutArea" + End + Begin Menu mnuSaveOut + Caption = "&Save OutArea..." + End + Begin Menu mnuSepF1 + Caption = "-" + End + Begin Menu mnuExit + Caption = "E&xit" + End + End + + Begin Menu mnuRun + Caption = "&Demos" + Begin Menu mnuRunAll + Caption = "Run &All Text Demos" + End + Begin Menu mnuSepR1 + Caption = "-" + End + Begin Menu mnuGraphics + Caption = "&Graphics..." + End + Begin Menu mnuDynamic + Caption = "&Dynamic Form..." + End + Begin Menu mnuTimer + Caption = "&Timer..." + End + End + + Begin Menu mnuHelp + Caption = "&Help" + Begin Menu mnuAbout + Caption = "&About..." + End + End + + Begin Frame fraButtons + Caption = "Language Demonstrations" + Layout = VBox + Weight = 0 + + Begin HBox rowA + Weight = 0 + Begin CommandButton btnTypes + Caption = "&Types" + Weight = 1 + End + Begin CommandButton btnMath + Caption = "&Math" + Weight = 1 + End + Begin CommandButton btnStrings + Caption = "&Strings" + Weight = 1 + End + Begin CommandButton btnArrays + Caption = "&Arrays" + Weight = 1 + End + Begin CommandButton btnData + Caption = "&DATA/READ" + Weight = 1 + End + End + + Begin HBox rowB + Weight = 0 + Begin CommandButton btnFlow + Caption = "Control &Flow" + Weight = 1 + End + Begin CommandButton btnUdt + Caption = "&UDT" + Weight = 1 + End + Begin CommandButton btnOpt + Caption = "&Optional" + Weight = 1 + End + Begin CommandButton btnError + Caption = "&Errors" + Weight = 1 + End + Begin CommandButton btnFormat + Caption = "F&ormat" + Weight = 1 + End + End + + Begin HBox rowC + Weight = 0 + Begin CommandButton btnFileIO + Caption = "File &I/O" + Weight = 1 + End + Begin CommandButton btnSystem + Caption = "S&ystem" + Weight = 1 + End + Begin CommandButton btnIni + Caption = "I&NI" + Weight = 1 + End + Begin CommandButton btnDialogs + Caption = "Di&alogs" + Weight = 1 + End + Begin CommandButton btnClear + Caption = "Clea&r" + Weight = 1 + End + End + End + + Begin TextArea OutArea + Weight = 1 + End + + Begin Label LblStatus + Caption = "Ready. Click any button to run a demo." + Weight = 0 + End +End + + +OPTION EXPLICIT + +TYPE PointT + x AS INTEGER + y AS INTEGER +END TYPE + + +' ============================================================ +' OutArea helpers +' ============================================================ + +SUB Say(s AS STRING) + OutArea.AppendText s + CHR$(10) +END SUB + + +SUB Header(title AS STRING) + Say "" + Say "--- " + title + " ---" +END SUB + + +Load BasicDemo +BasicDemo.Show + +OutArea.SetShowLineNumbers False +OutArea.SetReadOnly True +Say "Welcome to the DVX BASIC Feature Tour!" +Say "Each button below runs a self-contained example." +Say "Check the Demos menu for graphics, dynamic UI, and timer demos." +Say "" + + +' ============================================================ +' Menu handlers +' ============================================================ + +SUB mnuClear_Click + OutArea.Text = "" + LblStatus.Caption = "OutArea cleared." +END SUB + + +SUB mnuSaveOut_Click + DIM path AS STRING + path = basFileSave("Save OutArea", "Text Files (*.txt)|All Files (*.*)") + IF path = "" THEN + EXIT SUB + END IF + OPEN path FOR OUTPUT AS #1 + PRINT #1, OutArea.Text + CLOSE #1 + LblStatus.Caption = "Saved: " + path +END SUB + + +SUB mnuExit_Click + Unload BasicDemo +END SUB + + +SUB mnuRunAll_Click + btnTypes_Click + btnMath_Click + btnStrings_Click + btnArrays_Click + btnData_Click + btnFlow_Click + btnUdt_Click + btnOpt_Click + btnError_Click + btnFormat_Click + btnFileIO_Click + btnSystem_Click + LblStatus.Caption = "All text demos complete." +END SUB + + +SUB mnuAbout_Click + DIM msg AS STRING + msg = "DVX BASIC Feature Tour" + CHR$(10) + CHR$(10) + msg = msg + "A visual catalog of DVX BASIC language" + msg = msg + " and runtime features." + CHR$(10) + CHR$(10) + msg = msg + "(c) 2026 DVX Project" + MsgBox msg, vbOKOnly, "About" +END SUB + + +' ============================================================ +' Types: INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN +' ============================================================ + +SUB btnTypes_Click + Header "Types" + + DIM i AS INTEGER + DIM l AS LONG + DIM s AS SINGLE + DIM d AS DOUBLE + DIM t AS STRING + DIM b AS BOOLEAN + + i = 32767 + l = 2147483647 + s = 3.14159 + d = 2.718281828459045 + t = "Hello, DVX!" + b = True + + Say "INTEGER (16-bit): " + STR$(i) + Say "LONG (32-bit): " + STR$(l) + Say "SINGLE (float): " + STR$(s) + Say "DOUBLE (double): " + STR$(d) + Say "STRING: " + CHR$(34) + t + CHR$(34) + Say "BOOLEAN True: " + STR$(b) + + ' CONST with AS type annotation + CONST PI AS DOUBLE = 3.1415926535 + Say "CONST PI = " + STR$(PI) + + LblStatus.Caption = "Types demo complete." +END SUB + + +' ============================================================ +' Math: integer + float operators, built-in functions +' ============================================================ + +SUB btnMath_Click + Header "Math" + + DIM a AS INTEGER + DIM b AS INTEGER + a = 17 + b = 5 + + Say "a = 17, b = 5" + Say "a + b = " + STR$(a + b) + Say "a - b = " + STR$(a - b) + Say "a * b = " + STR$(a * b) + Say "a \ b = " + STR$(a \ b) + " (integer divide)" + Say "a MOD b = " + STR$(a MOD b) + Say "a / b = " + STR$(a / b) + " (float divide)" + + Say "" + Say "SQR(144) = " + STR$(SQR(144)) + Say "ABS(-42) = " + STR$(ABS(-42)) + Say "INT(3.7) = " + STR$(INT(3.7)) + Say "FIX(-3.7) = " + STR$(FIX(-3.7)) + Say "SGN(-9) = " + STR$(SGN(-9)) + Say "SIN(0) = " + STR$(SIN(0)) + Say "COS(0) = " + STR$(COS(0)) + Say "2 ^ 10 = " + STR$(2 ^ 10) + + RANDOMIZE TIMER + DIM r AS INTEGER + r = INT(RND * 100) + Say "RND (0-99) = " + STR$(r) + Say "TIMER = " + STR$(TIMER) + " (seconds since midnight)" + + LblStatus.Caption = "Math demo complete." +END SUB + + +' ============================================================ +' Strings: concatenation, LEFT$/RIGHT$/MID$/LEN/INSTR/UCASE$/LCASE$ +' ============================================================ + +SUB btnStrings_Click + Header "Strings" + + DIM s AS STRING + s = "The quick brown fox" + + Say "Source: " + CHR$(34) + s + CHR$(34) + Say "LEN = " + STR$(LEN(s)) + Say "LEFT$(s, 3) = " + LEFT$(s, 3) + Say "RIGHT$(s, 3) = " + RIGHT$(s, 3) + Say "MID$(s, 5, 5) = " + MID$(s, 5, 5) + Say "UCASE$ = " + UCASE$(s) + Say "LCASE$('HELLO') = " + LCASE$("HELLO") + Say "INSTR(s, 'brown')= " + STR$(INSTR(s, "brown")) + Say "TRIM$(' hi ') = " + CHR$(34) + TRIM$(" hi ") + CHR$(34) + Say "STRING$(5, 42) = " + STRING$(5, 42) + Say "CHR$(65) = " + CHR$(65) + Say "ASC('A') = " + STR$(ASC("A")) + Say "HEX$(255) = " + HEX$(255) + Say "VAL('42.5xyz') = " + STR$(VAL("42.5xyz")) + + LblStatus.Caption = "Strings demo complete." +END SUB + + +' ============================================================ +' Arrays: 1D, 2D, LBOUND/UBOUND, REDIM PRESERVE +' ============================================================ + +SUB btnArrays_Click + Header "Arrays" + + ' 1D array + DIM squares(9) AS INTEGER + DIM i AS INTEGER + FOR i = 0 TO 9 + squares(i) = i * i + NEXT i + + DIM lineS AS STRING + lineS = "squares(0..9) = " + FOR i = 0 TO 9 + lineS = lineS + STR$(squares(i)) + " " + NEXT i + Say lineS + + ' Bounds: DIM a(lo TO hi) + DIM prices(1 TO 3) AS SINGLE + prices(1) = 9.99 + prices(2) = 14.99 + prices(3) = 29.99 + Say "LBOUND(prices) = " + STR$(LBOUND(prices)) + ", UBOUND = " + STR$(UBOUND(prices)) + Say "prices(2) = " + STR$(prices(2)) + + ' 2D array + DIM matrix(2, 2) AS INTEGER + matrix(0, 0) = 1 : matrix(0, 1) = 2 : matrix(0, 2) = 3 + matrix(1, 0) = 4 : matrix(1, 1) = 5 : matrix(1, 2) = 6 + matrix(2, 0) = 7 : matrix(2, 1) = 8 : matrix(2, 2) = 9 + Say "3x3 matrix:" + DIM r AS INTEGER + DIM c AS INTEGER + FOR r = 0 TO 2 + lineS = " " + FOR c = 0 TO 2 + lineS = lineS + STR$(matrix(r, c)) + NEXT c + Say lineS + NEXT r + + ' REDIM PRESERVE + DIM nums(2) AS INTEGER + nums(0) = 10 + nums(1) = 20 + nums(2) = 30 + REDIM PRESERVE nums(4) AS INTEGER + nums(3) = 40 + nums(4) = 50 + Say "After REDIM PRESERVE: " + STR$(nums(0)) + " " + STR$(nums(1)) + " " + STR$(nums(2)) + " " + STR$(nums(3)) + " " + STR$(nums(4)) + + LblStatus.Caption = "Arrays demo complete." +END SUB + + +' ============================================================ +' DATA / READ / RESTORE +' ============================================================ + +SUB btnData_Click + Header "DATA / READ / RESTORE" + + DATA "Red", 255, 0, 0 + DATA "Green", 0, 255, 0 + DATA "Blue", 0, 0, 255 + + DIM colorName AS STRING + DIM r AS INTEGER + DIM g AS INTEGER + DIM b AS INTEGER + DIM i AS INTEGER + + FOR i = 1 TO 3 + READ colorName + READ r + READ g + READ b + Say colorName + ": (" + STR$(r) + "," + STR$(g) + "," + STR$(b) + ")" + NEXT i + + Say "" + Say "RESTORE resets pointer. Reading first entry again:" + RESTORE + READ colorName + Say " first = " + colorName + + LblStatus.Caption = "DATA/READ demo complete." +END SUB + + +' ============================================================ +' Control flow: IF, SELECT CASE, FOR, DO WHILE, GOSUB +' ============================================================ + +SUB btnFlow_Click + Header "Control Flow" + + ' FOR with STEP + Say "FOR i = 10 TO 0 STEP -2:" + DIM i AS INTEGER + DIM lineS AS STRING + lineS = " " + FOR i = 10 TO 0 STEP -2 + lineS = lineS + STR$(i) + NEXT i + Say lineS + + ' DO WHILE + Say "" + Say "DO WHILE n < 32 (doubling):" + DIM n AS LONG + n = 1 + lineS = " " + DO WHILE n < 32 + lineS = lineS + STR$(n) + n = n * 2 + LOOP + Say lineS + + ' IF / ELSEIF / ELSE + Say "" + DIM score AS INTEGER + score = 78 + IF score >= 90 THEN + Say "score " + STR$(score) + " -> A" + ELSEIF score >= 80 THEN + Say "score " + STR$(score) + " -> B" + ELSEIF score >= 70 THEN + Say "score " + STR$(score) + " -> C" + ELSE + Say "score " + STR$(score) + " -> F" + END IF + + ' SELECT CASE + Say "" + DIM day AS INTEGER + day = 3 + SELECT CASE day + CASE 1 + Say "day 1 = Monday" + CASE 2, 3 + Say "day " + STR$(day) + " = midweek" + CASE 4 TO 5 + Say "day " + STR$(day) + " = late week" + CASE ELSE + Say "day " + STR$(day) + " = weekend" + END SELECT + + ' GOSUB / RETURN + Say "" + Say "GOSUB to a local label:" + GOSUB labelHello + Say "back from subroutine" + + LblStatus.Caption = "Control flow demo complete." + EXIT SUB + + labelHello: + Say " inside GOSUB" + RETURN +END SUB + + +' ============================================================ +' User-Defined Type +' ============================================================ + +SUB btnUdt_Click + Header "User-Defined Type" + + DIM p AS PointT + p.x = 10 + p.y = 20 + Say "PointT p = (" + STR$(p.x) + "," + STR$(p.y) + ")" + + ' Array of UDT + DIM corners(3) AS PointT + corners(0).x = 0 : corners(0).y = 0 + corners(1).x = 10 : corners(1).y = 0 + corners(2).x = 10 : corners(2).y = 10 + corners(3).x = 0 : corners(3).y = 10 + Say "Rectangle corners:" + DIM i AS INTEGER + FOR i = 0 TO 3 + Say " (" + STR$(corners(i).x) + "," + STR$(corners(i).y) + ")" + NEXT i + + LblStatus.Caption = "UDT demo complete." +END SUB + + +' ============================================================ +' Optional parameters (DVX extension) +' ============================================================ + +FUNCTION Greet(who AS STRING, OPTIONAL greeting AS STRING) AS STRING + IF greeting = "" THEN + greeting = "Hello" + END IF + Greet = greeting + ", " + who + "!" +END FUNCTION + + +SUB btnOpt_Click + Header "Optional Parameters" + + Say Greet("World") + Say Greet("Scott", "Howdy") + Say Greet("DVX", "Greetings from") + + LblStatus.Caption = "Optional params demo complete." +END SUB + + +' ============================================================ +' ON ERROR GOTO +' ============================================================ + +SUB btnError_Click + Header "ON ERROR GOTO" + + ON ERROR GOTO handler + + DIM a AS INTEGER + DIM b AS INTEGER + a = 10 + b = 0 + Say "Attempting 10/0 ..." + Say " 10 / 0 = " + STR$(a / b) + Say "(should not reach here)" + EXIT SUB + + handler: + Say " caught! ERR = " + STR$(ERR) + LblStatus.Caption = "Error handler ran successfully." +END SUB + + +' ============================================================ +' PRINT USING / FORMAT$ +' ============================================================ + +SUB btnFormat_Click + Header "Formatting" + + Say "FORMAT$(1234.5, '#,##0.00') = " + FORMAT$(1234.5, "#,##0.00") + Say "FORMAT$(0.075, 'percent') = " + FORMAT$(0.075, "percent") + Say "FORMAT$(-42, '+#0') = " + FORMAT$(-42, "+#0") + Say "FORMAT$(3.14159, '0.00') = " + FORMAT$(3.14159, "0.00") + + LblStatus.Caption = "Format demo complete." +END SUB + + +' ============================================================ +' File I/O +' ============================================================ + +SUB btnFileIO_Click + Header "File I/O" + + DIM path AS STRING + path = App.Data + "/demo.txt" + Say "Writing: " + path + + OPEN path FOR OUTPUT AS #1 + PRINT #1, "Line one" + PRINT #1, "Line two" + PRINT #1, "The answer is "; 42 + CLOSE #1 + + Say "LOF = " + STR$(FILELEN(path)) + + Say "Reading back:" + OPEN path FOR INPUT AS #1 + DIM ln AS STRING + DO WHILE NOT EOF(1) + LINE INPUT #1, ln + Say " " + ln + LOOP + CLOSE #1 + + KILL path + Say "Deleted." + + LblStatus.Caption = "File I/O demo complete." +END SUB + + +' ============================================================ +' System: App object, environment, current directory +' ============================================================ + +SUB btnSystem_Click + Header "System / App" + + Say "App.Path = " + App.Path + Say "App.Config = " + App.Config + Say "App.Data = " + App.Data + Say "CurDir = " + CurDir() + Say "Date = " + Date$ + Say "Time = " + Time$ + Say "PATH env = " + LEFT$(Environ$("PATH"), 40) + "..." + + LblStatus.Caption = "System demo complete." +END SUB + + +' ============================================================ +' INI read/write +' ============================================================ + +SUB btnIni_Click + Header "INI Read/Write" + + DIM path AS STRING + path = App.Data + "/demo.ini" + Say "Writing: " + path + + IniWrite path, "General", "UserName", "Scott" + IniWrite path, "General", "Version", "1.00" + IniWrite path, "Options", "AutoSave", "True" + + Say "Reading back:" + Say " UserName = " + IniRead$(path, "General", "UserName", "(missing)") + Say " Version = " + IniRead$(path, "General", "Version", "(missing)") + Say " AutoSave = " + IniRead$(path, "Options", "AutoSave", "(missing)") + Say " Missing = " + IniRead$(path, "General", "NotThere", "(default)") + + KILL path + + LblStatus.Caption = "INI demo complete." +END SUB + + +' ============================================================ +' Dialog demos (spawns the real dialogs) +' ============================================================ + +SUB btnDialogs_Click + Header "Dialogs" + + DIM response AS INTEGER + response = MsgBox("MessageBox demo." + CHR$(10) + "Are you enjoying the demo?", vbYesNo + vbQuestion, "Feedback") + IF response = vbYes THEN + Say "MsgBox: user said yes" + ELSE + Say "MsgBox: user said no" + END IF + + DIM text AS STRING + text = basInputBox2("Input", "What is your name?", "Anonymous") + Say "InputBox returned: " + text + + DIM choice AS INTEGER + choice = basChoiceDialog("Favorite", "Pick a color:", "Red|Green|Blue|Yellow", 1) + IF choice >= 0 THEN + Say "Choice index: " + STR$(choice) + ELSE + Say "Choice cancelled" + END IF + + DIM n AS INTEGER + n = basIntInput("Number", "Pick a number (1-100):", 42, 1, 100) + Say "IntInput: " + STR$(n) + + LblStatus.Caption = "Dialog demo complete." +END SUB + + +SUB btnClear_Click + mnuClear_Click +END SUB + + +' ============================================================ +' Graphics demo (opens a second form with Canvas) +' ============================================================ + +DIM gfxWin AS LONG +gfxWin = 0 + + +SUB mnuGraphics_Click + IF gfxWin <> 0 THEN + EXIT SUB + END IF + + DIM frm AS LONG + SET frm = CreateForm("GraphicsForm", 360, 320) + GraphicsForm.Caption = "Graphics Demo" + gfxWin = frm + + DIM cv AS LONG + SET cv = CreateControl(frm, "Canvas", "GfxCanvas") + GfxCanvas.Width = 340 + GfxCanvas.Height = 260 + GfxCanvas.Weight = 1 + + DIM btnRow AS LONG + SET btnRow = CreateControl(frm, "HBox", "GfxRow") + + DIM bDraw AS LONG + SET bDraw = CreateControl(frm, "CommandButton", "GfxDraw", btnRow) + GfxDraw.Caption = "Draw" + SetEvent bDraw, "Click", "GfxDrawAll" + + DIM bClear AS LONG + SET bClear = CreateControl(frm, "CommandButton", "GfxClear", btnRow) + GfxClear.Caption = "Clear" + SetEvent bClear, "Click", "GfxClearCanvas" + + frm.Show + + GfxDrawAll +END SUB + + +SUB GfxDrawAll + DIM w AS LONG + DIM h AS LONG + w = 340 + h = 260 + + ' Gradient background bars + DIM y AS INTEGER + DIM shade AS LONG + FOR y = 0 TO h - 1 STEP 4 + shade = (y * 255) \ h + GfxCanvas.FillRect 0, y, w, 4, RGB(shade, shade \ 2, 128) + NEXT y + + ' Star field + RANDOMIZE TIMER + DIM s AS INTEGER + DIM sx AS INTEGER + DIM sy AS INTEGER + FOR s = 1 TO 40 + sx = INT(RND * w) + sy = INT(RND * h) + GfxCanvas.SetPixel sx, sy, RGB(255, 255, 255) + NEXT s + + ' Rectangle + outline + GfxCanvas.FillRect 20, 20, 80, 50, RGB(240, 240, 0) + GfxCanvas.DrawRect 20, 20, 80, 50, RGB(0, 0, 0) + + ' Circle approximated by line segments + DIM cx AS INTEGER + DIM cy AS INTEGER + DIM r AS INTEGER + cx = 250 + cy = 80 + r = 40 + DIM a AS DOUBLE + DIM px AS INTEGER + DIM py AS INTEGER + DIM qx AS INTEGER + DIM qy AS INTEGER + px = cx + r + py = cy + FOR a = 0 TO 6.3 STEP 0.2 + qx = cx + INT(r * COS(a)) + qy = cy + INT(r * SIN(a)) + GfxCanvas.DrawLine px, py, qx, qy, RGB(255, 128, 0) + px = qx + py = qy + NEXT a + + ' Text + GfxCanvas.DrawText 60, 200, "Canvas + math + colors", RGB(255, 255, 255) + GfxCanvas.DrawText 60, 220, "DVX BASIC graphics", RGB(255, 255, 0) + + GfxCanvas.Refresh +END SUB + + +SUB GfxClearCanvas + GfxCanvas.Clear RGB(0, 0, 0) + GfxCanvas.Refresh +END SUB + + +SUB GraphicsForm_Unload + gfxWin = 0 +END SUB + + +' ============================================================ +' Dynamic form demo +' ============================================================ + +DIM dynForm AS LONG +dynForm = 0 + + +SUB mnuDynamic_Click + IF dynForm <> 0 THEN + EXIT SUB + END IF + + DIM frm AS LONG + SET frm = CreateForm("DynForm", 320, 200) + DynForm.Caption = "Dynamic Form (built in code)" + dynForm = frm + + DIM lbl AS LONG + SET lbl = CreateControl(frm, "Label", "DynLabel") + DynLabel.Caption = "This form was created 100% in code." + + DIM lbl2 AS LONG + SET lbl2 = CreateControl(frm, "Label", "CountLabel") + CountLabel.Caption = "Counter: 0" + + DIM btns AS LONG + SET btns = CreateControl(frm, "HBox", "DynBtns") + + DIM bInc AS LONG + SET bInc = CreateControl(frm, "CommandButton", "BInc", btns) + BInc.Caption = "Count Up" + SetEvent bInc, "Click", "DynInc" + + DIM bBye AS LONG + SET bBye = CreateControl(frm, "CommandButton", "BBye", btns) + BBye.Caption = "Close" + SetEvent bBye, "Click", "DynBye" + + frm.Show +END SUB + + +DIM dynCount AS INTEGER +dynCount = 0 + + +SUB DynInc + dynCount = dynCount + 1 + CountLabel.Caption = "Counter: " + STR$(dynCount) +END SUB + + +SUB DynBye + Unload DynForm +END SUB + + +SUB DynForm_Unload + dynForm = 0 + dynCount = 0 +END SUB + + +' ============================================================ +' Timer demo +' ============================================================ + +DIM timerWin AS LONG +timerWin = 0 + + +SUB mnuTimer_Click + IF timerWin <> 0 THEN + EXIT SUB + END IF + + DIM frm AS LONG + SET frm = CreateForm("TimerForm", 260, 140) + TimerForm.Caption = "Timer Demo" + timerWin = frm + + DIM lbl AS LONG + SET lbl = CreateControl(frm, "Label", "TickLabel") + TickLabel.Caption = "Ticks: 0" + + DIM t AS LONG + SET t = CreateControl(frm, "Timer", "Ticker") + Ticker.Interval = 500 + SetEvent t, "Timer", "TickHandler" + + frm.Show +END SUB + + +DIM tickCount AS LONG +tickCount = 0 + + +SUB TickHandler + tickCount = tickCount + 1 + TickLabel.Caption = "Ticks: " + STR$(tickCount) + " Time: " + TIME$ +END SUB + + +SUB TimerForm_Unload + timerWin = 0 + tickCount = 0 +END SUB diff --git a/sdk/samples/basic/commdlg.frm b/sdk/samples/basic/commdlg.frm index 2b75a86..09589bb 100644 --- a/sdk/samples/basic/commdlg.frm +++ b/sdk/samples/basic/commdlg.frm @@ -62,14 +62,10 @@ CommDlgDemo.Show PRINT "Common Dialog Demo started." -DO - DoEvents -LOOP - SUB BtnFileOpen_Click DIM path AS STRING - path = basFileOpen("Open a File", "*.bas") + path = basFileOpen("Open a File", "BASIC Files (*.bas;*.frm)|All Files (*.*)") IF path <> "" THEN LblResult.Caption = "Opened: " + path PRINT "File Open: " + path @@ -82,7 +78,7 @@ END SUB SUB BtnFileSave_Click DIM path AS STRING - path = basFileSave("Save a File", "*.txt") + path = basFileSave("Save a File", "Text Files (*.txt)|All Files (*.*)") IF path <> "" THEN LblResult.Caption = "Save to: " + path PRINT "File Save: " + path diff --git a/sdk/samples/basic/dynform.bas b/sdk/samples/basic/dynform.bas index 920c79e..53af0a8 100644 --- a/sdk/samples/basic/dynform.bas +++ b/sdk/samples/basic/dynform.bas @@ -47,10 +47,6 @@ frm.Show PRINT "Dynamic form created. Try the buttons!" -DO - DoEvents -LOOP - SUB OnHelloClick DIM name AS STRING @@ -64,7 +60,7 @@ END SUB SUB OnFileClick DIM path AS STRING - path = basFileOpen("Open a File", "*.*") + path = basFileOpen("Open a File", "All Files (*.*)") IF path <> "" THEN StatusLabel.Caption = "Selected: " + path PRINT "File: " + path diff --git a/sdk/samples/basic/helpedit.frm b/sdk/samples/basic/helpedit.frm deleted file mode 100644 index 7ebef80..0000000 --- a/sdk/samples/basic/helpedit.frm +++ /dev/null @@ -1,205 +0,0 @@ -VERSION DVX 1.00 -' helpedit.frm -- DVX Help Editor -' -' A .dhs help source editor with syntax highlighting and -' live preview via the DVX Help Viewer. -' -' Add commdlg.bas and help.bas to your project, then click Run. - -Begin Form HelpEdit - Caption = "DVX Help Editor" - Layout = VBox - AutoSize = False - Resizable = True - Centered = True - Width = 640 - Height = 440 - Begin HBox ToolBar - Weight = 0 - Begin CommandButton BtnNew - Caption = "New" - MinWidth = 60 - End - Begin CommandButton BtnOpen - Caption = "Open" - MinWidth = 60 - End - Begin CommandButton BtnSave - Caption = "Save" - MinWidth = 60 - End - Begin Spacer Spc1 - MinWidth = 16 - End - Begin CommandButton BtnCompile - Caption = "Compile" - MinWidth = 80 - End - Begin CommandButton BtnPreview - Caption = "Preview" - MinWidth = 80 - End - Begin Spacer Spc2 - Weight = 1 - End - Begin Label LblStatus - Caption = "Ready." - MinWidth = 150 - End - End - Begin TextArea Editor - Weight = 1 - End -End - -DIM currentFile AS STRING -DIM hlpFile AS STRING -currentFile = "" -hlpFile = "" - -Load HelpEdit -HelpEdit.Show - -' Configure the editor -Editor.SetSyntaxMode "dhs" -Editor.SetShowLineNumbers True -Editor.SetAutoIndent True -Editor.SetCaptureTabs True -Editor.SetTabWidth 2 - -' Start with a template -Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10) + "This is your help file. Edit the .dhs source here," + CHR$(10) + "then click Compile and Preview to see the result." + CHR$(10) - -LblStatus.Caption = "New file." - -DO - DoEvents -LOOP - - -SUB BtnNew_Click - currentFile = "" - hlpFile = "" - Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10) - LblStatus.Caption = "New file." - HelpEdit.Caption = "DVX Help Editor" -END SUB - - -SUB BtnOpen_Click - DIM path AS STRING - path = basFileOpen("Open Help Source", "*.dhs") - IF path = "" THEN - EXIT SUB - END IF - - ' Read the entire file - DIM text AS STRING - DIM line AS STRING - text = "" - - OPEN path FOR INPUT AS #1 - DO WHILE NOT EOF(1) - LINE INPUT #1, line - IF text <> "" THEN - text = text + CHR$(10) - END IF - text = text + line - LOOP - CLOSE #1 - - Editor.Text = text - currentFile = path - hlpFile = "" - LblStatus.Caption = path - HelpEdit.Caption = "DVX Help Editor - " + path -END SUB - - -SUB BtnSave_Click - DIM path AS STRING - - IF currentFile <> "" THEN - path = currentFile - ELSE - path = basFileSave("Save Help Source", "*.dhs") - IF path = "" THEN - EXIT SUB - END IF - END IF - - ' Write the editor contents to file - OPEN path FOR OUTPUT AS #1 - PRINT #1, Editor.Text - CLOSE #1 - - currentFile = path - LblStatus.Caption = "Saved: " + path - HelpEdit.Caption = "DVX Help Editor - " + path -END SUB - - -SUB BtnCompile_Click - ' Save first if we have a file - IF currentFile = "" THEN - DIM path AS STRING - path = basFileSave("Save Help Source", "*.dhs") - IF path = "" THEN - LblStatus.Caption = "Save cancelled." - EXIT SUB - END IF - - OPEN path FOR OUTPUT AS #1 - PRINT #1, Editor.Text - CLOSE #1 - currentFile = path - ELSE - OPEN currentFile FOR OUTPUT AS #1 - PRINT #1, Editor.Text - CLOSE #1 - END IF - - ' Derive .hlp filename from .dhs filename - DIM baseName AS STRING - baseName = currentFile - - ' Strip .dhs extension if present - IF LEN(baseName) > 4 THEN - IF UCASE$(RIGHT$(baseName, 4)) = ".DHS" THEN - baseName = LEFT$(baseName, LEN(baseName) - 4) - END IF - END IF - - hlpFile = baseName + ".hlp" - - LblStatus.Caption = "Compiling..." - DoEvents - - IF HelpCompile(currentFile, hlpFile) THEN - LblStatus.Caption = "Compiled: " + hlpFile - ELSE - LblStatus.Caption = "Compile failed!" - hlpFile = "" - END IF -END SUB - - -SUB BtnPreview_Click - IF hlpFile = "" THEN - ' Try compiling first - BtnCompile_Click - END IF - - IF hlpFile = "" THEN - LblStatus.Caption = "Nothing to preview. Compile first." - EXIT SUB - END IF - - LblStatus.Caption = "Opening viewer..." - HelpView hlpFile -END SUB - - -SUB HelpEdit_QueryUnload(Cancel AS INTEGER) - ' Could prompt to save here -END SUB diff --git a/sdk/samples/basic/helpedit/ICON32.BMP b/sdk/samples/basic/helpedit/ICON32.BMP new file mode 100644 index 0000000..52ced7f --- /dev/null +++ b/sdk/samples/basic/helpedit/ICON32.BMP @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:414c6612a049642fca53b817761ff1376f95c7beec8146c9a6640b850d193b01 +size 3126 diff --git a/sdk/samples/basic/helpedit/helpedit.dbp b/sdk/samples/basic/helpedit/helpedit.dbp new file mode 100644 index 0000000..50a088b --- /dev/null +++ b/sdk/samples/basic/helpedit/helpedit.dbp @@ -0,0 +1,17 @@ +[Project] +Name = Help Editor +Author = DVX Project +Description = DVX Help source editor with syntax highlighting and preview +Icon = ICON32.BMP + +[Modules] +File0 = ../../../include/basic/commdlg.bas +File1 = ../../../include/basic/help.bas +Count = 2 + +[Forms] +File0 = helpedit.frm +Count = 1 + +[Settings] +StartupForm = HelpEdit diff --git a/sdk/samples/basic/helpedit/helpedit.frm b/sdk/samples/basic/helpedit/helpedit.frm new file mode 100644 index 0000000..111a1cd --- /dev/null +++ b/sdk/samples/basic/helpedit/helpedit.frm @@ -0,0 +1,310 @@ +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 Menu mnuFile + Caption = "&File" + Begin Menu mnuNew + Caption = "&New" + End + Begin Menu mnuOpen + Caption = "&Open..." + End + Begin Menu mnuSep1 + Caption = "-" + End + Begin Menu mnuSave + Caption = "&Save" + End + Begin Menu mnuSaveAs + Caption = "Save &As..." + End + Begin Menu mnuSep2 + Caption = "-" + End + Begin Menu mnuExit + Caption = "E&xit" + End + End + Begin Menu mnuBuild + Caption = "&Build" + Begin Menu mnuCompile + Caption = "&Compile" + End + Begin Menu mnuPreview + Caption = "&Preview" + End + End + Begin Menu mnuHelp + Caption = "&Help" + Begin Menu mnuHelpTopic + Caption = "Help &Topics" + End + End + Begin TextArea Editor + Weight = 1 + End + Begin Label LblStatus + Weight = 0 + End +End + +DIM currentFile AS STRING +DIM hlpFile AS STRING +DIM savedHash AS LONG +DIM dirty AS INTEGER +currentFile = "" +hlpFile = "" +savedHash = 0 +dirty = 0 + +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) + +MarkClean +LblStatus.Caption = "Ready." + + +FUNCTION TextHash() AS LONG + DIM h AS LONG + DIM t AS STRING + t = Editor.Text + h = 0 + DIM i AS INTEGER + FOR i = 1 TO LEN(t) + h = h * 31 + ASC(MID$(t, i, 1)) + NEXT i + TextHash = h +END FUNCTION + + +FUNCTION IsDirty() AS INTEGER + IsDirty = (TextHash() <> savedHash) +END FUNCTION + + +FUNCTION AskSave() AS INTEGER + IF NOT IsDirty() THEN + AskSave = 0 + EXIT FUNCTION + END IF + DIM result AS INTEGER + result = basPromptSave("Help Editor") + IF result = DVX_SAVE_YES THEN + FileSave + AskSave = 0 + ELSEIF result = DVX_SAVE_NO THEN + AskSave = 0 + ELSE + AskSave = 1 + END IF +END FUNCTION + + +SUB UpdateTitle + DIM title AS STRING + IF currentFile <> "" THEN + title = currentFile + " - DVX Help Editor" + ELSE + title = "DVX Help Editor" + END IF + IF dirty THEN + title = "* " + title + END IF + HelpEdit.Caption = title +END SUB + + +SUB MarkClean + dirty = 0 + savedHash = TextHash() + UpdateTitle +END SUB + + +SUB Editor_Change + IF NOT dirty THEN + dirty = -1 + UpdateTitle + END IF +END SUB + + +SUB mnuNew_Click + IF AskSave() THEN + EXIT SUB + END IF + 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) + MarkClean + LblStatus.Caption = "New file." + UpdateTitle +END SUB + + +SUB mnuOpen_Click + IF AskSave() THEN + EXIT SUB + END IF + + DIM path AS STRING + path = basFileOpen("Open Help Source", "Help Source (*.dhs)|All Files (*.*)") + IF path = "" THEN + EXIT SUB + END IF + + DIM text AS STRING + DIM ln AS STRING + text = "" + + OPEN path FOR INPUT AS #1 + DO WHILE NOT EOF(1) + LINE INPUT #1, ln + IF text <> "" THEN + text = text + CHR$(10) + END IF + text = text + ln + LOOP + CLOSE #1 + + Editor.Text = text + currentFile = path + hlpFile = "" + MarkClean + LblStatus.Caption = path + UpdateTitle +END SUB + + +SUB FileSave + IF currentFile = "" THEN + FileSaveAs + EXIT SUB + END IF + + OPEN currentFile FOR OUTPUT AS #1 + PRINT #1, Editor.Text + CLOSE #1 + + MarkClean + LblStatus.Caption = "Saved: " + currentFile + UpdateTitle +END SUB + + +SUB FileSaveAs + DIM path AS STRING + path = basFileSave("Save Help Source", "Help Source (*.dhs)|All Files (*.*)") + IF path = "" THEN + EXIT SUB + END IF + + currentFile = path + FileSave +END SUB + + +SUB mnuSave_Click + FileSave +END SUB + + +SUB mnuSaveAs_Click + FileSaveAs +END SUB + + +SUB mnuExit_Click + Unload HelpEdit +END SUB + + +SUB DoCompile + ' Save first + IF currentFile = "" THEN + FileSaveAs + IF currentFile = "" THEN + LblStatus.Caption = "Save cancelled." + EXIT SUB + END IF + ELSE + OPEN currentFile FOR OUTPUT AS #1 + PRINT #1, Editor.Text + CLOSE #1 + MarkClean + END IF + + ' Derive .hlp filename from .dhs filename + DIM baseName AS STRING + baseName = currentFile + + 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 mnuCompile_Click + DoCompile +END SUB + + +SUB mnuPreview_Click + IF hlpFile = "" THEN + DoCompile + END IF + + IF hlpFile = "" THEN + EXIT SUB + END IF + + LblStatus.Caption = "Opening viewer..." + HelpView hlpFile +END SUB + + +SUB mnuHelpTopic_Click + HelpView "dvxhelp.hlp" +END SUB + + +SUB HelpEdit_QueryUnload(Cancel AS INTEGER) + Cancel = AskSave() +END SUB diff --git a/sdk/samples/basic/iconed/ICON32.BMP b/sdk/samples/basic/iconed/ICON32.BMP index 7fe939d..057a878 100644 --- a/sdk/samples/basic/iconed/ICON32.BMP +++ b/sdk/samples/basic/iconed/ICON32.BMP @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3358a4f7b545b0eb14a8c3371a5d2777fc4ad59b0b7a0956e0376364c1b20d23 +oid sha256:14a57138766170edb4206b51c1ad3325fc9bebd4ced99ae8dba943595345c68d size 3126 diff --git a/sdk/samples/basic/iconed/iconed.dbp b/sdk/samples/basic/iconed/iconed.dbp index 45c43ab..34d2220 100644 --- a/sdk/samples/basic/iconed/iconed.dbp +++ b/sdk/samples/basic/iconed/iconed.dbp @@ -5,9 +5,11 @@ Company = Kangaroo Punch Studios Version = 1.00 Copyright = Copyright 2026 Scott Duensing Description = Icon editor for DVX. +Icon = ICON32.BMP [Modules] -Count = 0 +File0 = ../../../include/basic/commdlg.bas +Count = 1 [Forms] File0 = iconed.frm diff --git a/sdk/samples/basic/iconed/iconed.frm b/sdk/samples/basic/iconed/iconed.frm index 2dc985e..d0e297e 100644 --- a/sdk/samples/basic/iconed/iconed.frm +++ b/sdk/samples/basic/iconed/iconed.frm @@ -459,6 +459,23 @@ SUB cvEditor_MouseUp(button AS INTEGER, x AS INTEGER, y AS INTEGER) drawing = False END SUB +SUB IconEd_QueryUnload(Cancel AS INTEGER) + IF NOT dirty THEN + EXIT SUB + END IF + DIM result AS INTEGER + result = basPromptSave("Icon Editor") + IF result = DVX_SAVE_YES THEN + mnuSave_Click + IF dirty THEN + Cancel = 1 + END IF + ELSEIF result = DVX_SAVE_CANCEL THEN + Cancel = 1 + END IF +END SUB + + SUB cvPalette_MouseDown(button AS INTEGER, x AS INTEGER, y AS INTEGER) DIM col AS INTEGER DIM row AS INTEGER diff --git a/sdk/samples/basic/imgview/ICON32.BMP b/sdk/samples/basic/imgview/ICON32.BMP new file mode 100644 index 0000000..fec9d13 --- /dev/null +++ b/sdk/samples/basic/imgview/ICON32.BMP @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acab450e62fe1d807f507f777dcaaaf0b13f82cda61235e6bbb5744c51f0fbe8 +size 3126 diff --git a/sdk/samples/basic/imgview/imgview.dbp b/sdk/samples/basic/imgview/imgview.dbp new file mode 100644 index 0000000..7f0dc53 --- /dev/null +++ b/sdk/samples/basic/imgview/imgview.dbp @@ -0,0 +1,16 @@ +[Project] +Name = Image Viewer +Author = DVX Project +Description = BMP, PNG, JPEG, and GIF viewer +Icon = ICON32.BMP + +[Modules] +File0 = ../../../include/basic/commdlg.bas +Count = 1 + +[Forms] +File0 = imgview.frm +Count = 1 + +[Settings] +StartupForm = ImgViewForm diff --git a/sdk/samples/basic/imgview/imgview.frm b/sdk/samples/basic/imgview/imgview.frm new file mode 100644 index 0000000..83e92b0 --- /dev/null +++ b/sdk/samples/basic/imgview/imgview.frm @@ -0,0 +1,49 @@ +VERSION DVX 1.00 +Begin Form ImgViewForm + Caption = "Image Viewer" + Layout = VBox + AutoSize = False + Resizable = True + Centered = True + Width = 400 + Height = 320 + Begin Menu mnuFile + Caption = "&File" + Begin Menu mnuOpen + Caption = "&Open..." + End + Begin Menu mnuSep1 + Caption = "-" + End + Begin Menu mnuClose + Caption = "&Close" + End + End + Begin Image ImgDisplay + Weight = 1 + Stretch = True + End +End + +'$INCLUDE: 'commdlg.bas' + + +SUB mnuOpen_Click + DIM path AS STRING + path = basFileOpen("Open Image", "Images (*.bmp;*.png;*.jpg;*.gif)|All Files (*.*)") + IF path = "" THEN + EXIT SUB + END IF + ImgDisplay.Picture = path + ImgViewForm.Caption = path + " - Image Viewer" +END SUB + + +SUB mnuClose_Click + Unload ImgViewForm +END SUB + + +SUB ImgDisplay_DblClick + mnuOpen_Click +END SUB diff --git a/sdk/samples/basic/notepad/ICON32.BMP b/sdk/samples/basic/notepad/ICON32.BMP new file mode 100644 index 0000000..184ce88 --- /dev/null +++ b/sdk/samples/basic/notepad/ICON32.BMP @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee1090f31bd32600092c514d1c0449e3ce28ce98dff3a0222071046fcf007b62 +size 3126 diff --git a/sdk/samples/basic/notepad/notepad.dbp b/sdk/samples/basic/notepad/notepad.dbp new file mode 100644 index 0000000..994d09d --- /dev/null +++ b/sdk/samples/basic/notepad/notepad.dbp @@ -0,0 +1,16 @@ +[Project] +Name = Notepad +Author = DVX Project +Description = Simple text editor +Icon = ICON32.BMP + +[Modules] +File0 = ../../../include/basic/commdlg.bas +Count = 1 + +[Forms] +File0 = notepad.frm +Count = 1 + +[Settings] +StartupForm = NotepadForm diff --git a/sdk/samples/basic/notepad/notepad.frm b/sdk/samples/basic/notepad/notepad.frm new file mode 100644 index 0000000..64934f1 --- /dev/null +++ b/sdk/samples/basic/notepad/notepad.frm @@ -0,0 +1,237 @@ +VERSION DVX 1.00 +Begin Form NotepadForm + Caption = "Untitled - Notepad" + Layout = VBox + AutoSize = False + Resizable = True + Centered = True + Width = 480 + Height = 320 + Begin Menu mnuFile + Caption = "&File" + Begin Menu mnuNew + Caption = "&New" + End + Begin Menu mnuOpen + Caption = "&Open..." + End + Begin Menu mnuSep1 + Caption = "-" + End + Begin Menu mnuSave + Caption = "&Save" + End + Begin Menu mnuSaveAs + Caption = "Save &As..." + End + Begin Menu mnuSep2 + Caption = "-" + End + Begin Menu mnuExit + Caption = "E&xit" + End + End + Begin Menu mnuEdit + Caption = "&Edit" + Begin Menu mnuCut + Caption = "Cu&t" + End + Begin Menu mnuCopy + Caption = "&Copy" + End + Begin Menu mnuPaste + Caption = "&Paste" + End + Begin Menu mnuSep3 + Caption = "-" + End + Begin Menu mnuSelAll + Caption = "Select &All" + End + End + Begin TextArea Editor + Weight = 1 + End +End + +'$INCLUDE: 'commdlg.bas' + +DIM currentFile AS STRING +DIM savedHash AS LONG +DIM dirty AS INTEGER +currentFile = "" +savedHash = 0 +dirty = 0 + + +SUB NotepadForm_Load + Editor.SetCaptureTabs True + Editor.SetTabWidth 4 +END SUB + + +FUNCTION TextHash() AS LONG + DIM h AS LONG + DIM t AS STRING + t = Editor.Text + h = 0 + DIM i AS INTEGER + FOR i = 1 TO LEN(t) + h = h * 31 + ASC(MID$(t, i, 1)) + NEXT i + TextHash = h +END FUNCTION + + +FUNCTION IsDirty() AS INTEGER + IsDirty = (TextHash() <> savedHash) +END FUNCTION + + +SUB UpdateTitle + DIM title AS STRING + IF currentFile <> "" THEN + title = currentFile + " - Notepad" + ELSE + title = "Untitled - Notepad" + END IF + IF dirty THEN + title = "* " + title + END IF + NotepadForm.Caption = title +END SUB + + +SUB MarkClean + dirty = 0 + MarkClean +END SUB + + +SUB Editor_Change + IF NOT dirty THEN + dirty = -1 + UpdateTitle + END IF +END SUB + + +FUNCTION AskSave() AS INTEGER + IF NOT IsDirty() THEN + AskSave = 0 + EXIT FUNCTION + END IF + DIM result AS INTEGER + result = basPromptSave("Notepad") + IF result = DVX_SAVE_YES THEN + FileSave + AskSave = 0 + ELSEIF result = DVX_SAVE_NO THEN + AskSave = 0 + ELSE + AskSave = 1 + END IF +END FUNCTION + + +SUB FileSave + IF currentFile = "" THEN + FileSaveAs + EXIT SUB + END IF + OPEN currentFile FOR OUTPUT AS #1 + PRINT #1, Editor.Text + CLOSE #1 + MarkClean +END SUB + + +SUB FileSaveAs + DIM path AS STRING + path = basFileSave("Save As", "Text Files (*.txt)|All Files (*.*)") + IF path = "" THEN + EXIT SUB + END IF + currentFile = path + FileSave + UpdateTitle +END SUB + + +SUB mnuNew_Click + IF AskSave() THEN + EXIT SUB + END IF + Editor.Text = "" + currentFile = "" + MarkClean + UpdateTitle +END SUB + + +SUB mnuOpen_Click + IF AskSave() THEN + EXIT SUB + END IF + DIM path AS STRING + path = basFileOpen("Open", "Text Files (*.txt)|All Files (*.*)") + IF path = "" THEN + EXIT SUB + END IF + DIM text AS STRING + DIM ln AS STRING + text = "" + OPEN path FOR INPUT AS #1 + DO WHILE NOT EOF(1) + LINE INPUT #1, ln + IF text <> "" THEN + text = text + CHR$(10) + END IF + text = text + ln + LOOP + CLOSE #1 + Editor.Text = text + currentFile = path + MarkClean + UpdateTitle +END SUB + + +SUB mnuSave_Click + FileSave +END SUB + + +SUB mnuSaveAs_Click + FileSaveAs +END SUB + + +SUB mnuExit_Click + Unload NotepadForm +END SUB + + +SUB mnuCut_Click + Editor.Cut +END SUB + + +SUB mnuCopy_Click + Editor.Copy +END SUB + + +SUB mnuPaste_Click + Editor.Paste +END SUB + + +SUB mnuSelAll_Click + Editor.SelectAll +END SUB + + +SUB NotepadForm_QueryUnload(Cancel AS Integer) + Cancel = AskSave() +END SUB diff --git a/sdk/samples/basic/resedit/ICON32.BMP b/sdk/samples/basic/resedit/ICON32.BMP new file mode 100644 index 0000000..3cf25c4 --- /dev/null +++ b/sdk/samples/basic/resedit/ICON32.BMP @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e1be371e5a56cbf22d4a5caebf3b9f40dec4771242530ed52e2e09fd0465350 +size 3126 diff --git a/sdk/samples/basic/resedit/resedit.dbp b/sdk/samples/basic/resedit/resedit.dbp new file mode 100644 index 0000000..213ef2b --- /dev/null +++ b/sdk/samples/basic/resedit/resedit.dbp @@ -0,0 +1,17 @@ +[Project] +Name = Resource Editor +Author = DVX Project +Description = DVX resource file editor +Icon = ICON32.BMP + +[Modules] +File0 = ../../../include/basic/commdlg.bas +File1 = ../../../include/basic/resource.bas +Count = 2 + +[Forms] +File0 = resedit.frm +Count = 1 + +[Settings] +StartupForm = ResEdit diff --git a/sdk/samples/basic/resedit/resedit.frm b/sdk/samples/basic/resedit/resedit.frm new file mode 100644 index 0000000..ebbc30d --- /dev/null +++ b/sdk/samples/basic/resedit/resedit.frm @@ -0,0 +1,431 @@ +VERSION DVX 1.00 +' resedit.frm -- DVX Resource Editor +' +' Graphical editor for the resource blocks appended to DXE3 +' files (.app, .wgt, .lib). View, add, remove, and extract +' resources of type icon, text, or binary. +' +' Add commdlg.bas and resource.bas to your project, then click Run. + +Begin Form ResEdit + Caption = "DVX Resource Editor" + Layout = VBox + AutoSize = False + Resizable = True + Centered = True + Width = 500 + Height = 340 + + Begin Menu mnuFile + Caption = "&File" + Begin Menu mnuOpen + Caption = "&Open..." + End + Begin Menu mnuClose + Caption = "&Close" + Enabled = False + End + Begin Menu mnuSep1 + Caption = "-" + End + Begin Menu mnuExit + Caption = "E&xit" + End + End + + Begin Menu mnuResource + Caption = "&Resource" + Begin Menu mnuAddText + Caption = "Add &Text..." + Enabled = False + End + Begin Menu mnuAddFile + Caption = "Add &File..." + Enabled = False + End + Begin Menu mnuEditText + Caption = "&Edit Text..." + Enabled = False + End + Begin Menu mnuSep2 + Caption = "-" + End + Begin Menu mnuExtract + Caption = "E&xtract..." + Enabled = False + End + Begin Menu mnuRemove + Caption = "&Remove" + Enabled = False + End + End + + Begin ListView ResList + Weight = 1 + End + + Begin Label LblStatus + Caption = "No file loaded." + Weight = 0 + End +End + +OPTION EXPLICIT + +DIM filePath AS STRING +DIM resHandle AS LONG +filePath = "" +resHandle = 0 + +Load ResEdit +ResEdit.Show + +ResList.SetColumns "Name,20|Type,8|Size,12" +LblStatus.Caption = "Ready. Use File > Open to load a DXE file." + + +' ============================================================ +' Type name helper +' ============================================================ + +FUNCTION TypeName$(t AS LONG) + IF t = RES_TYPE_ICON THEN + TypeName$ = "Icon" + ELSEIF t = RES_TYPE_TEXT THEN + TypeName$ = "Text" + ELSEIF t = RES_TYPE_BINARY THEN + TypeName$ = "Binary" + ELSE + TypeName$ = "Unknown" + END IF +END FUNCTION + + +' ============================================================ +' Format a byte size for display +' ============================================================ + +FUNCTION FormatSize$(sz AS LONG) + IF sz < 1024 THEN + FormatSize$ = STR$(sz) + " B" + ELSE + FormatSize$ = STR$(sz \ 1024) + " KB" + END IF +END FUNCTION + + +' ============================================================ +' Refresh the resource list from the open handle +' ============================================================ + +SUB RefreshList + ResList.Clear + + IF resHandle = 0 THEN + EXIT SUB + END IF + + DIM n AS LONG + n = ResCount(resHandle) + + DIM ix AS LONG + FOR ix = 0 TO n - 1 + ResList.AddItem ResName$(resHandle, ix) + ResList.SetCell ix, 1, TypeName$(ResType(resHandle, ix)) + ResList.SetCell ix, 2, FormatSize$(ResSize(resHandle, ix)) + NEXT ix + + LblStatus.Caption = filePath + " - " + STR$(n) + " resource(s)" +END SUB + + +' ============================================================ +' Close the current file +' ============================================================ + +SUB CloseFile + IF resHandle <> 0 THEN + ResClose resHandle + resHandle = 0 + END IF + filePath = "" + ResList.Clear + ResEdit.Caption = "DVX Resource Editor" + mnuClose.Enabled = False + mnuAddText.Enabled = False + mnuAddFile.Enabled = False + mnuEditText.Enabled = False + mnuExtract.Enabled = False + mnuRemove.Enabled = False + LblStatus.Caption = "No file loaded." +END SUB + + +' ============================================================ +' Reopen the file (after modification) and refresh +' ============================================================ + +SUB ReopenAndRefresh + DIM path AS STRING + path = filePath + + IF resHandle <> 0 THEN + ResClose resHandle + resHandle = 0 + END IF + + resHandle = ResOpen(path) + + IF resHandle = 0 THEN + ' File may have had all resources stripped + filePath = path + ResList.Clear + LblStatus.Caption = path + " - 0 resource(s)" + ELSE + filePath = path + RefreshList + END IF +END SUB + + +' ============================================================ +' Enable/disable selection-dependent menus +' ============================================================ + +SUB UpdateMenuState + DIM hasSel AS INTEGER + hasSel = (ResList.ListIndex >= 0) + mnuExtract.Enabled = hasSel + mnuRemove.Enabled = hasSel + mnuEditText.Enabled = hasSel +END SUB + + +' ============================================================ +' Menu handlers +' ============================================================ + +SUB mnuOpen_Click + DIM path AS STRING + path = basFileOpen("Open DXE File", "Applications (*.app)|Widget Modules (*.wgt)|Libraries (*.lib)|All Files (*.*)") + IF path = "" THEN + EXIT SUB + END IF + + CloseFile + + filePath = path + ResEdit.Caption = path + " - DVX Resource Editor" + mnuClose.Enabled = True + mnuAddText.Enabled = True + mnuAddFile.Enabled = True + + resHandle = ResOpen(path) + IF resHandle = 0 THEN + LblStatus.Caption = "No resources in " + path + EXIT SUB + END IF + + RefreshList +END SUB + + +SUB mnuClose_Click + CloseFile +END SUB + + +SUB mnuExit_Click + Unload ResEdit +END SUB + + +SUB mnuAddText_Click + IF filePath = "" THEN + EXIT SUB + END IF + + DIM rName AS STRING + rName = basInputBox2("Add Text Resource", "Resource name:", "") + IF rName = "" THEN + EXIT SUB + END IF + + DIM text AS STRING + text = basInputBox2("Add Text Resource", "Text value:", "") + + IF ResAddText(filePath, rName, text) THEN + ReopenAndRefresh + LblStatus.Caption = "Added text resource: " + rName + ELSE + LblStatus.Caption = "Failed to add resource." + END IF +END SUB + + +SUB mnuAddFile_Click + IF filePath = "" THEN + EXIT SUB + END IF + + DIM rName AS STRING + rName = basInputBox2("Add File Resource", "Resource name:", "") + IF rName = "" THEN + EXIT SUB + END IF + + DIM typeChoice AS LONG + typeChoice = basChoiceDialog("Resource Type", "Select resource type:", "Icon|Binary", 0) + IF typeChoice < 0 THEN + EXIT SUB + END IF + + DIM typeVal AS LONG + IF typeChoice = 0 THEN + typeVal = RES_TYPE_ICON + ELSE + typeVal = RES_TYPE_BINARY + END IF + + DIM srcPath AS STRING + srcPath = basFileOpen("Select Source File", "All Files (*.*)") + IF srcPath = "" THEN + EXIT SUB + END IF + + IF ResAddFile(filePath, rName, typeVal, srcPath) THEN + ReopenAndRefresh + LblStatus.Caption = "Added resource: " + rName + ELSE + LblStatus.Caption = "Failed to add resource." + END IF +END SUB + + +SUB mnuEditText_Click + IF filePath = "" THEN + EXIT SUB + END IF + + DIM sel AS LONG + sel = ResList.ListIndex + IF sel < 0 THEN + EXIT SUB + END IF + + DIM rName AS STRING + rName = ResName$(resHandle, sel) + + DIM t AS LONG + t = ResType(resHandle, sel) + IF t <> RES_TYPE_TEXT THEN + LblStatus.Caption = "Only text resources can be edited inline." + EXIT SUB + END IF + + DIM oldText AS STRING + oldText = ResGetText$(filePath, rName) + + DIM newText AS STRING + newText = basInputBox2("Edit Text Resource", "Value for '" + rName + "':", oldText) + + IF ResAddText(filePath, rName, newText) THEN + ReopenAndRefresh + LblStatus.Caption = "Updated: " + rName + ELSE + LblStatus.Caption = "Failed to update resource." + END IF +END SUB + + +SUB mnuExtract_Click + IF filePath = "" OR resHandle = 0 THEN + EXIT SUB + END IF + + DIM sel AS LONG + sel = ResList.ListIndex + IF sel < 0 THEN + EXIT SUB + END IF + + DIM rName AS STRING + rName = ResName$(resHandle, sel) + + DIM outPath AS STRING + outPath = basFileSave("Extract Resource", "All Files (*.*)") + IF outPath = "" THEN + EXIT SUB + END IF + + IF ResExtract(filePath, rName, outPath) THEN + LblStatus.Caption = "Extracted '" + rName + "' to " + outPath + ELSE + LblStatus.Caption = "Failed to extract resource." + END IF +END SUB + + +SUB mnuRemove_Click + IF filePath = "" THEN + EXIT SUB + END IF + + DIM sel AS LONG + sel = ResList.ListIndex + IF sel < 0 THEN + EXIT SUB + END IF + + DIM rName AS STRING + rName = ResName$(resHandle, sel) + + DIM ans AS INTEGER + ans = MsgBox("Remove resource '" + rName + "'?", vbYesNo) + IF ans = vbNo THEN + EXIT SUB + END IF + + ' Close handle before modifying + ResClose resHandle + resHandle = 0 + + IF ResRemove(filePath, rName) THEN + ReopenAndRefresh + LblStatus.Caption = "Removed: " + rName + ELSE + ReopenAndRefresh + LblStatus.Caption = "Failed to remove resource." + END IF +END SUB + + +' ============================================================ +' ListView selection change +' ============================================================ + +SUB ResList_Click + UpdateMenuState +END SUB + + +SUB ResList_DblClick + IF filePath = "" OR resHandle = 0 THEN + EXIT SUB + END IF + + DIM sel AS LONG + sel = ResList.ListIndex + IF sel < 0 THEN + EXIT SUB + END IF + + DIM t AS LONG + t = ResType(resHandle, sel) + + IF t = RES_TYPE_TEXT THEN + mnuEditText_Click + ELSE + mnuExtract_Click + END IF +END SUB diff --git a/shell/shellMain.c b/shell/shellMain.c index 280fae1..f703f92 100644 --- a/shell/shellMain.c +++ b/shell/shellMain.c @@ -283,10 +283,11 @@ int shellMain(int argc, char *argv[]) { else if (strcmp(accelStr, "medium") == 0) { accelVal = 64; } else if (strcmp(accelStr, "high") == 0) { accelVal = 32; } - int32_t speed = prefsGetInt(sPrefs, "mouse", "speed", 8); + int32_t speed = prefsGetInt(sPrefs, "mouse", "speed", 8); + int32_t wheelStep = prefsGetInt(sPrefs, "mouse", "wheelspeed", MOUSE_WHEEL_STEP_DEFAULT); - dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal, speed); - dvxLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s speed=%ld", wheelStr, (long)dblClick, accelStr, (long)speed); + dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal, speed, wheelStep); + dvxLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s speed=%ld wheelspeed=%ld", wheelStr, (long)dblClick, accelStr, (long)speed, (long)wheelStep); // Apply saved color scheme bool colorsLoaded = false; diff --git a/taskmgr/shellTaskMgr.c b/taskmgr/shellTaskMgr.c index 82cede4..6eecb7f 100644 --- a/taskmgr/shellTaskMgr.c +++ b/taskmgr/shellTaskMgr.c @@ -137,8 +137,8 @@ static void onTmRun(WidgetT *w) { (void)w; FileFilterT filters[] = { - { "Applications (*.app)", "*.app" }, - { "All Files (*.*)", "*.*" } + { "Applications (*.app)" }, + { "All Files (*.*)" } }; char path[TM_MAX_PATH]; diff --git a/tools/mkicon.c b/tools/mkicon.c index 1316f03..16ca9cc 100644 --- a/tools/mkicon.c +++ b/tools/mkicon.c @@ -352,10 +352,103 @@ static void writeBmp(const char *path) { fclose(f); } + +// Icon editor: pixel grid with pencil drawing a pixel +static void iconIconEd(void) { + clear(220, 220, 220); + + // Draw a 5x5 grid of colored pixels (the "canvas") + int gx = 3; + int gy = 3; + int cs = 5; + + // Grid lines + for (int i = 0; i <= 5; i++) { + hline(gx, gy + i * cs, 5 * cs + 1, 160, 160, 160); + vline(gx + i * cs, gy, 5 * cs + 1, 160, 160, 160); + } + + // Some colored pixels in the grid + rect(gx + 1, gy + 1, cs - 1, cs - 1, 255, 0, 0); + rect(gx + cs + 1, gy + 1, cs - 1, cs - 1, 0, 0, 255); + rect(gx + 1, gy + cs + 1, cs - 1, cs - 1, 0, 180, 0); + rect(gx + cs + 1, gy + cs + 1, cs - 1, cs - 1, 255, 255, 0); + rect(gx + 2*cs+1, gy + 2*cs+1, cs - 1, cs - 1, 255, 128, 0); + rect(gx + 3*cs+1, gy + 1, cs - 1, cs - 1, 128, 0, 255); + + // Pencil (diagonal, bottom-right area) + for (int i = 0; i < 12; i++) { + pixel(18 + i, 28 - i, 230, 200, 80); + pixel(19 + i, 28 - i, 230, 200, 80); + pixel(18 + i, 29 - i, 230, 200, 80); + } + + // Pencil tip + pixel(17, 29, 40, 40, 40); + pixel(17, 30, 40, 40, 40); + pixel(18, 30, 40, 40, 40); + + // Eraser end + pixel(29, 17, 240, 150, 150); + pixel(30, 16, 240, 150, 150); + pixel(30, 17, 240, 150, 150); + pixel(29, 16, 240, 150, 150); + + // Border + rect(0, 0, 32, 1, 128, 128, 128); + rect(0, 31, 32, 1, 128, 128, 128); + rect(0, 0, 1, 32, 128, 128, 128); + rect(31, 0, 1, 32, 128, 128, 128); +} + + +// Resource editor: box with stacked resource entries +static void iconResedit(void) { + clear(220, 220, 220); + + // Outer box (document frame) + rect(3, 2, 26, 28, 255, 255, 255); + rect(3, 2, 26, 1, 80, 80, 80); + rect(3, 29, 26, 1, 80, 80, 80); + rect(3, 2, 1, 28, 80, 80, 80); + rect(28, 2, 1, 28, 80, 80, 80); + + // Title bar + rect(4, 3, 24, 4, 0, 0, 128); + + // Resource entry rows (alternating light shading) + rect(4, 8, 24, 4, 240, 240, 255); + rect(4, 12, 24, 1, 180, 180, 200); + rect(4, 13, 24, 4, 255, 255, 255); + rect(4, 17, 24, 1, 180, 180, 200); + rect(4, 18, 24, 4, 240, 240, 255); + rect(4, 22, 24, 1, 180, 180, 200); + rect(4, 23, 24, 4, 255, 255, 255); + + // Type indicator colored dots in each row + circle(8, 10, 1, 0, 160, 0); // icon (green) + circle(8, 15, 1, 0, 0, 200); // text (blue) + circle(8, 20, 1, 200, 0, 0); // binary (red) + circle(8, 25, 1, 0, 0, 200); // text (blue) + + // Short "name" bars in each row + rect(11, 9, 10, 2, 60, 60, 60); + rect(11, 14, 14, 2, 60, 60, 60); + rect(11, 19, 8, 2, 60, 60, 60); + rect(11, 24, 12, 2, 60, 60, 60); + + // Border + rect(0, 0, 32, 1, 128, 128, 128); + rect(0, 31, 32, 1, 128, 128, 128); + rect(0, 0, 1, 32, 128, 128, 128); + rect(31, 0, 1, 32, 128, 128, 128); +} + + int main(int argc, char **argv) { if (argc < 3) { fprintf(stderr, "Usage: mkicon \n"); - fprintf(stderr, "Types: noicon, clock, notepad, cpanel, dvxdemo, imgview, basic, help\n"); + fprintf(stderr, "Types: noicon, clock, notepad, cpanel, dvxdemo, imgview, basic, help, iconed, resedit\n"); return 1; } @@ -378,6 +471,10 @@ int main(int argc, char **argv) { iconImgview(); } else if (strcmp(type, "help") == 0) { iconHelp(); + } else if (strcmp(type, "iconed") == 0) { + iconIconEd(); + } else if (strcmp(type, "resedit") == 0) { + iconResedit(); } else { fprintf(stderr, "Unknown icon type: %s\n", type); return 1; diff --git a/widgets/ansiTerm/widgetAnsiTerm.c b/widgets/ansiTerm/widgetAnsiTerm.c index b1be3d2..c86de02 100644 --- a/widgets/ansiTerm/widgetAnsiTerm.c +++ b/widgets/ansiTerm/widgetAnsiTerm.c @@ -1024,6 +1024,13 @@ void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) { } +static int32_t wgtAnsiTermGetScrollback(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, 0); + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + return at->scrollbackMax; +} + + // ============================================================ // BASIC-facing accessors // ============================================================ @@ -1072,7 +1079,7 @@ static const struct { static const WgtPropDescT sProps[] = { { "Cols", WGT_IFACE_INT, (void *)wgtAnsiTermGetCols, NULL, NULL }, { "Rows", WGT_IFACE_INT, (void *)wgtAnsiTermGetRows, NULL, NULL }, - { "Scrollback", WGT_IFACE_INT, NULL, (void *)wgtAnsiTermSetScrollback, NULL } + { "Scrollback", WGT_IFACE_INT, (void *)wgtAnsiTermGetScrollback, (void *)wgtAnsiTermSetScrollback, NULL } }; static const WgtMethodDescT sMethods[] = { diff --git a/widgets/image/widgetImage.c b/widgets/image/widgetImage.c index 994b07b..453b1d6 100644 --- a/widgets/image/widgetImage.c +++ b/widgets/image/widgetImage.c @@ -30,6 +30,8 @@ typedef struct { bool pressed; bool hasTransparency; uint32_t keyColor; + char picturePath[DVX_MAX_PATH]; + bool stretch; // true = scale to fit widget bounds } ImageDataT; @@ -55,8 +57,17 @@ void widgetImageDestroy(WidgetT *w) { void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font) { (void)font; ImageDataT *d = (ImageDataT *)w->data; - w->calcMinW = d->imgW; - w->calcMinH = d->imgH; + + if (d->stretch) { + // Stretch mode: don't force the widget to the image size. + // Let the layout engine determine the size; the paint method + // will scale the image to fit. + w->calcMinW = 0; + w->calcMinH = 0; + } else { + w->calcMinW = d->imgW; + w->calcMinH = d->imgH; + } } @@ -94,61 +105,108 @@ void widgetImagePaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const Bit (void)colors; ImageDataT *d = (ImageDataT *)w->data; - if (!d->pixelData) { + if (!d->pixelData || d->imgW <= 0 || d->imgH <= 0) { return; } - // Center the image within the widget bounds int32_t imgW = d->imgW; int32_t imgH = d->imgH; - int32_t dx = w->x + (w->w - imgW) / 2; - int32_t dy = w->y + (w->h - imgH) / 2; + int32_t wgtW = w->w; + int32_t wgtH = w->h; + + // Scale to fit (if stretch enabled) or use native size + int32_t fitW; + int32_t fitH; + + if (d->stretch && (imgW > wgtW || imgH > wgtH)) { + fitW = wgtW; + fitH = (imgH * wgtW) / imgW; + + if (fitH > wgtH) { + fitH = wgtH; + fitW = (imgW * wgtH) / imgH; + } + + if (fitW <= 0) { fitW = 1; } + if (fitH <= 0) { fitH = 1; } + } else { + fitW = imgW; + fitH = imgH; + } + + // Center in widget bounds + int32_t dx = w->x + (wgtW - fitW) / 2; + int32_t dy = w->y + (wgtH - fitH) / 2; - // Offset by 1px when pressed (button-press effect) if (d->pressed) { dx++; dy++; } - if (w->enabled) { - if (d->hasTransparency) { + // If image fits at 1:1, blit directly (no scaling needed) + if (fitW == imgW && fitH == imgH) { + uint8_t *src = w->enabled ? d->pixelData : d->grayData; + + if (!src) { + src = d->pixelData; + } + + if (d->hasTransparency && w->enabled) { rectCopyTransparent(disp, ops, dx, dy, - d->pixelData, d->imgPitch, + src, d->imgPitch, 0, 0, imgW, imgH, d->keyColor); } else { rectCopy(disp, ops, dx, dy, - d->pixelData, d->imgPitch, + src, d->imgPitch, 0, 0, imgW, imgH); } - } else { - if (!d->grayData) { - int32_t bufSize = d->imgPitch * d->imgH; - d->grayData = (uint8_t *)malloc(bufSize); - if (d->grayData) { - DisplayT tmp = *disp; - tmp.backBuf = d->grayData; - tmp.width = d->imgW; - tmp.height = d->imgH; - tmp.pitch = d->imgPitch; - tmp.clipX = 0; - tmp.clipY = 0; - tmp.clipW = d->imgW; - tmp.clipH = d->imgH; - rectCopyGrayscale(&tmp, ops, 0, 0, - d->pixelData, d->imgPitch, - 0, 0, d->imgW, d->imgH); + return; + } + + // Nearest-neighbor scale: sample source pixels directly into the + // display backbuffer. This avoids allocating a scaled copy. + int32_t bpp = disp->format.bitsPerPixel; + + for (int32_t y = 0; y < fitH; y++) { + int32_t screenY = dy + y; + + if (screenY < disp->clipY || screenY >= disp->clipY + disp->clipH) { + continue; + } + + int32_t srcY = (y * imgH) / fitH; + + if (srcY >= imgH) { + srcY = imgH - 1; + } + + uint8_t *srcRow = d->pixelData + srcY * d->imgPitch; + + for (int32_t x = 0; x < fitW; x++) { + int32_t screenX = dx + x; + + if (screenX < disp->clipX || screenX >= disp->clipX + disp->clipW) { + continue; } - } - if (d->grayData) { - rectCopy(disp, ops, dx, dy, - d->grayData, d->imgPitch, - 0, 0, imgW, imgH); - } else { - rectCopy(disp, ops, dx, dy, - d->pixelData, d->imgPitch, - 0, 0, imgW, imgH); + int32_t srcX = (x * imgW) / fitW; + + if (srcX >= imgW) { + srcX = imgW - 1; + } + + // Copy one pixel from source to display + if (bpp == 16 || bpp == 15) { + uint16_t px = ((uint16_t *)srcRow)[srcX]; + ((uint16_t *)(disp->backBuf + screenY * disp->pitch))[screenX] = px; + } else if (bpp == 32) { + uint32_t px = ((uint32_t *)srcRow)[srcX]; + ((uint32_t *)(disp->backBuf + screenY * disp->pitch))[screenX] = px; + } else if (bpp == 8) { + uint8_t px = srcRow[srcX]; + (disp->backBuf + screenY * disp->pitch)[screenX] = px; + } } } } @@ -261,19 +319,48 @@ static void wgtImageLoadFile(WidgetT *w, const char *path) { return; } + dvxSetBusy(ctx, true); + int32_t imgW; int32_t imgH; int32_t pitch; uint8_t *buf = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch); + dvxSetBusy(ctx, false); + if (!buf) { return; } + ImageDataT *d = (ImageDataT *)w->data; + snprintf(d->picturePath, sizeof(d->picturePath), "%s", path); + wgtImageSetData(w, buf, imgW, imgH, pitch); } +static bool wgtImageGetStretch(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, false); + ImageDataT *d = (ImageDataT *)w->data; + return d->stretch; +} + + +static void wgtImageSetStretch(WidgetT *w, bool stretch) { + VALIDATE_WIDGET_VOID(w, sTypeId); + ImageDataT *d = (ImageDataT *)w->data; + d->stretch = stretch; + wgtInvalidatePaint(w); +} + + +static const char *wgtImageGetPicture(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, ""); + ImageDataT *d = (ImageDataT *)w->data; + return d->picturePath; +} + + static int32_t wgtImageGetWidth(const WidgetT *w) { VALIDATE_WIDGET(w, sTypeId, 0); ImageDataT *d = (ImageDataT *)w->data; @@ -308,15 +395,16 @@ static const struct { }; static const WgtPropDescT sProps[] = { - { "Picture", WGT_IFACE_STRING, NULL, (void *)wgtImageLoadFile, NULL }, + { "ImageHeight", WGT_IFACE_INT, (void *)wgtImageGetHeight, NULL, NULL }, { "ImageWidth", WGT_IFACE_INT, (void *)wgtImageGetWidth, NULL, NULL }, - { "ImageHeight", WGT_IFACE_INT, (void *)wgtImageGetHeight, NULL, NULL } + { "Picture", WGT_IFACE_STRING, (void *)wgtImageGetPicture, (void *)wgtImageLoadFile, NULL }, + { "Stretch", WGT_IFACE_BOOL, (void *)wgtImageGetStretch, (void *)wgtImageSetStretch, NULL } }; static const WgtIfaceT sIface = { .basName = "Image", .props = sProps, - .propCount = 3, + .propCount = 4, .methods = NULL, .methodCount = 0, .events = NULL, diff --git a/widgets/imageButton/widgetImageButton.c b/widgets/imageButton/widgetImageButton.c index cb828b7..004e082 100644 --- a/widgets/imageButton/widgetImageButton.c +++ b/widgets/imageButton/widgetImageButton.c @@ -28,6 +28,7 @@ typedef struct { int32_t imgH; int32_t imgPitch; bool pressed; + char picturePath[DVX_MAX_PATH]; } ImageButtonDataT; @@ -324,10 +325,20 @@ static void wgtImageButtonLoadFile(WidgetT *w, const char *path) { return; } + ImageButtonDataT *d = (ImageButtonDataT *)w->data; + snprintf(d->picturePath, sizeof(d->picturePath), "%s", path); + wgtImageButtonSetData(w, buf, imgW, imgH, pitch); } +static const char *wgtImageButtonGetPicture(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, ""); + ImageButtonDataT *d = (ImageButtonDataT *)w->data; + return d->picturePath; +} + + static int32_t wgtImageButtonGetWidth(const WidgetT *w) { VALIDATE_WIDGET(w, sTypeId, 0); ImageButtonDataT *d = (ImageButtonDataT *)w->data; @@ -360,7 +371,7 @@ static const struct { }; static const WgtPropDescT sImgBtnProps[] = { - { "Picture", WGT_IFACE_STRING, NULL, (void *)wgtImageButtonLoadFile, NULL }, + { "Picture", WGT_IFACE_STRING, (void *)wgtImageButtonGetPicture, (void *)wgtImageButtonLoadFile, NULL }, { "ImageWidth", WGT_IFACE_INT, (void *)wgtImageButtonGetWidth, NULL, NULL }, { "ImageHeight", WGT_IFACE_INT, (void *)wgtImageButtonGetHeight, NULL, NULL } }; diff --git a/widgets/listView/widgetListView.c b/widgets/listView/widgetListView.c index 3228ae2..447d36f 100644 --- a/widgets/listView/widgetListView.c +++ b/widgets/listView/widgetListView.c @@ -101,6 +101,10 @@ typedef struct { char **ownedCells; // stb_ds dynamic array of strdup'd strings bool ownsCells; // true if cellData points to ownedCells int32_t nextCell; // next cell position for AddItem (row-major index) + // Owned column definitions (deep copies for BASIC callers) + ListViewColT *ownedColDefs; // heap array of column defs (NULL if not owned) + char **ownedColTitles; // heap array of strdup'd title strings + int32_t ownedColCount; } ListViewDataT; @@ -369,6 +373,13 @@ void widgetListViewDestroy(WidgetT *w) { free(lv->ownedCells[i]); } arrfree(lv->ownedCells); + + for (int32_t i = 0; i < lv->ownedColCount; i++) { + free(lv->ownedColTitles[i]); + } + free(lv->ownedColTitles); + free(lv->ownedColDefs); + free(lv->selBits); free(lv->sortIndex); free(lv); @@ -1673,14 +1684,20 @@ void wgtListViewAddItem(WidgetT *w, const char *text) { return; } - // If at a row boundary, add colCount empty cells to start a new row - if (lv->nextCell % lv->colCount == 0) { - for (int32_t c = 0; c < lv->colCount; c++) { - arrput(lv->ownedCells, strdup("")); - } + // Always align to the next row boundary so AddItem always starts a new row, + // even if SetCell was used to fill remaining columns of the previous row. + int32_t rem = lv->nextCell % lv->colCount; + + if (rem != 0) { + lv->nextCell += lv->colCount - rem; } - // Replace the next empty cell with the provided text + // Add colCount empty cells for the new row + for (int32_t c = 0; c < lv->colCount; c++) { + arrput(lv->ownedCells, strdup("")); + } + + // Set the first cell of the new row to the provided text int32_t idx = lv->nextCell; free(lv->ownedCells[idx]); lv->ownedCells[idx] = strdup(text ? text : ""); @@ -1945,15 +1962,61 @@ static void basSetColumns(WidgetT *w, const char *spec) { return; } - static char buf[1024]; + VALIDATE_WIDGET_VOID(w, sTypeId); + ListViewDataT *lv = (ListViewDataT *)w->data; + + // Free previous owned columns + for (int32_t i = 0; i < lv->ownedColCount; i++) { + free(lv->ownedColTitles[i]); + } + + free(lv->ownedColTitles); + free(lv->ownedColDefs); + lv->ownedColTitles = NULL; + lv->ownedColDefs = NULL; + lv->ownedColCount = 0; + + // Parse spec into a temporary buffer + char buf[1024]; snprintf(buf, sizeof(buf), "%s", spec); - ListViewColT cols[BAS_MAX_LISTVIEW_COLS]; - int32_t count = 0; - char *tok = buf; + // First pass: count columns + int32_t count = 0; + char *p = buf; - while (*tok && count < BAS_MAX_LISTVIEW_COLS) { - // Parse "Title,Width" or just "Title" + while (*p) { + count++; + char *sep = strchr(p, '|'); + + if (sep) { + p = sep + 1; + } else { + break; + } + } + + if (count <= 0 || count > BAS_MAX_LISTVIEW_COLS) { + return; + } + + // Allocate owned storage + lv->ownedColDefs = (ListViewColT *)calloc(count, sizeof(ListViewColT)); + lv->ownedColTitles = (char **)calloc(count, sizeof(char *)); + + if (!lv->ownedColDefs || !lv->ownedColTitles) { + free(lv->ownedColDefs); + free(lv->ownedColTitles); + lv->ownedColDefs = NULL; + lv->ownedColTitles = NULL; + return; + } + + // Second pass: parse and deep-copy + snprintf(buf, sizeof(buf), "%s", spec); + int32_t idx = 0; + char *tok = buf; + + while (*tok && idx < count) { char *sep = strchr(tok, '|'); if (sep) { @@ -1964,16 +2027,18 @@ static void basSetColumns(WidgetT *w, const char *spec) { if (comma) { *comma = '\0'; - cols[count].title = tok; - cols[count].width = wgtChars(atoi(comma + 1)); - cols[count].align = ListViewAlignLeftE; + lv->ownedColTitles[idx] = strdup(tok); + lv->ownedColDefs[idx].title = lv->ownedColTitles[idx]; + lv->ownedColDefs[idx].width = wgtChars(atoi(comma + 1)); + lv->ownedColDefs[idx].align = ListViewAlignLeftE; } else { - cols[count].title = tok; - cols[count].width = 0; - cols[count].align = ListViewAlignLeftE; + lv->ownedColTitles[idx] = strdup(tok); + lv->ownedColDefs[idx].title = lv->ownedColTitles[idx]; + lv->ownedColDefs[idx].width = 0; + lv->ownedColDefs[idx].align = ListViewAlignLeftE; } - count++; + idx++; if (sep) { tok = sep + 1; @@ -1982,9 +2047,8 @@ static void basSetColumns(WidgetT *w, const char *spec) { } } - if (count > 0) { - wgtListViewSetColumns(w, cols, count); - } + lv->ownedColCount = idx; + wgtListViewSetColumns(w, lv->ownedColDefs, idx); } diff --git a/widgets/textInput/widgetTextInput.c b/widgets/textInput/widgetTextInput.c index f28398c..e75e3be 100644 --- a/widgets/textInput/widgetTextInput.c +++ b/widgets/textInput/widgetTextInput.c @@ -3760,7 +3760,7 @@ static const WgtIfaceT sIfaceTextArea = { .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_INT, - .createArgs = { 4096 }, + .createArgs = { 65536 }, .defaultEvent = "Change" }; diff --git a/widgets/timer/widgetTimer.c b/widgets/timer/widgetTimer.c index 2b5c54d..ed6b0a4 100644 --- a/widgets/timer/widgetTimer.c +++ b/widgets/timer/widgetTimer.c @@ -133,6 +133,16 @@ bool wgtTimerIsRunning(const WidgetT *w) { } +int32_t wgtTimerGetInterval(const WidgetT *w) { + if (!w || w->type != sTypeId) { + return 0; + } + + TimerDataT *d = (TimerDataT *)w->data; + return d->intervalMs; +} + + void wgtTimerSetInterval(WidgetT *w, int32_t intervalMs) { if (!w || w->type != sTypeId) { return; @@ -230,7 +240,7 @@ static const struct { static const WgtPropDescT sProps[] = { { "Enabled", WGT_IFACE_BOOL, (void *)wgtTimerIsRunning, (void *)wgtTimerSetEnabled, NULL }, - { "Interval", WGT_IFACE_INT, NULL, (void *)wgtTimerSetInterval, NULL } + { "Interval", WGT_IFACE_INT, (void *)wgtTimerGetInterval, (void *)wgtTimerSetInterval, NULL } }; static const WgtMethodDescT sMethods[] = {