From 9b43582f87a232562b973359e34b2766e234f619 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Fri, 27 Mar 2026 18:59:58 -0500 Subject: [PATCH] BASIC is getting to be pretty stable. --- .gitignore | 4 + apps/clock/clock.c | 2 +- apps/cpanel/cpanel.c | 6 +- apps/imgview/imgview.c | 10 +- core/dvxApp.c | 29 +- core/dvxDialog.c | 157 ++++ core/dvxDialog.h | 6 + core/dvxPrefs.c | 3 - core/dvxWidget.h | 70 ++ core/widgetClass.c | 70 ++ dvxbasic/Makefile | 14 +- dvxbasic/compiler/codegen.c | 77 ++ dvxbasic/compiler/codegen.h | 8 + dvxbasic/compiler/lexer.c | 2 +- dvxbasic/compiler/opcodes.h | 2 + dvxbasic/compiler/parser.c | 409 ++++++++- dvxbasic/compiler/parser.h | 2 +- dvxbasic/formrt/formrt.c | 1619 +++++++++++++++++++++++++++++++++ dvxbasic/formrt/formrt.h | 110 +++ dvxbasic/ide/ideMain.c | 818 ++++++++++++++++- dvxbasic/runtime/values.c | 8 +- dvxbasic/runtime/values.h | 1 + dvxbasic/runtime/vm.c | 346 ++++++- dvxbasic/runtime/vm.h | 48 +- dvxbasic/samples/clickme.bas | 29 + dvxbasic/samples/clickme.frm | 12 + dvxbasic/samples/formtest.bas | 8 + dvxbasic/samples/input.bas | 13 + dvxbasic/test_compiler | Bin 151328 -> 167992 bytes dvxbasic/test_compiler.c | 738 +++++++++++++++ loader/loaderMain.c | 5 +- shell/shellMain.c | 8 +- tools/dvxres.c | 22 +- widgets/widgetAnsiTerm.c | 49 + widgets/widgetBox.c | 11 + widgets/widgetButton.c | 11 + widgets/widgetCanvas.c | 15 + widgets/widgetCheckbox.c | 15 + widgets/widgetComboBox.c | 15 + widgets/widgetDropdown.c | 16 + widgets/widgetImage.c | 65 +- widgets/widgetImage.h | 2 + widgets/widgetImageButton.c | 65 +- widgets/widgetImageButton.h | 2 + widgets/widgetLabel.c | 15 + widgets/widgetListBox.c | 25 + widgets/widgetListView.c | 25 + widgets/widgetProgressBar.c | 15 + widgets/widgetRadio.c | 19 + widgets/widgetScrollPane.c | 11 + widgets/widgetSeparator.c | 11 + widgets/widgetSlider.c | 15 + widgets/widgetSpacer.c | 11 + widgets/widgetSpinner.c | 20 + widgets/widgetSplitter.c | 15 + widgets/widgetStatusBar.c | 11 + widgets/widgetTabControl.c | 19 + widgets/widgetTextInput.c | 297 +++++- widgets/widgetTextInput.h | 17 + widgets/widgetTimer.c | 25 + widgets/widgetToolbar.c | 11 + widgets/widgetTreeView.c | 16 + 62 files changed, 5305 insertions(+), 195 deletions(-) create mode 100644 dvxbasic/formrt/formrt.c create mode 100644 dvxbasic/formrt/formrt.h create mode 100644 dvxbasic/samples/clickme.bas create mode 100644 dvxbasic/samples/clickme.frm create mode 100644 dvxbasic/samples/formtest.bas create mode 100644 dvxbasic/samples/input.bas diff --git a/.gitignore b/.gitignore index defbee0..87cacdf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ lib/ .gitattributes~ *.SWP .claude/ +dvxbasic/test_compiler +dvxbasic/test_lex +dvxbasic/test_quick +dvxbasic/test_vm diff --git a/apps/clock/clock.c b/apps/clock/clock.c index b694e5a..4d9ceea 100644 --- a/apps/clock/clock.c +++ b/apps/clock/clock.c @@ -157,7 +157,7 @@ static void updateTime(void) { hour12 = 12; } - snprintf(sState.timeStr, sizeof(sState.timeStr), "%d:%02d:%02d %s", hour12, tm->tm_min, tm->tm_sec, ampm); + snprintf(sState.timeStr, sizeof(sState.timeStr), "%d:%02d:%02d %s", (int)hour12, tm->tm_min, tm->tm_sec, ampm); snprintf(sState.dateStr, sizeof(sState.dateStr), "%04d-%02d-%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday); sState.lastUpdate = now; } diff --git a/apps/cpanel/cpanel.c b/apps/cpanel/cpanel.c index 4d34f73..7092d67 100644 --- a/apps/cpanel/cpanel.c +++ b/apps/cpanel/cpanel.c @@ -77,7 +77,7 @@ AppDescriptorT appDescriptor = { typedef struct { char name[64]; - char path[260]; + char path[280]; } FileEntryT; // ============================================================ @@ -119,7 +119,7 @@ static WidgetT *sColorSwatch = NULL; static WidgetT *sWallpaperLbl = NULL; static WidgetT *sWpaperList = NULL; static WidgetT *sWpModeDrop = NULL; -static char sWallpaperPath[260]; +static char sWallpaperPath[280]; static FileEntryT *sWpaperEntries = NULL; // stb_ds dynamic array static const char **sWpaperLabels = NULL; // stb_ds dynamic array @@ -991,7 +991,7 @@ static void scanWallpapers(void) { } FileEntryT entry = {0}; - snprintf(entry.name, sizeof(entry.name), "%s", ent->d_name); + snprintf(entry.name, sizeof(entry.name), "%.63s", ent->d_name); snprintf(entry.path, sizeof(entry.path), "%s/%s", WPAPER_DIR, ent->d_name); arrput(sWpaperEntries, entry); } diff --git a/apps/imgview/imgview.c b/apps/imgview/imgview.c index 9052c78..ee83008 100644 --- a/apps/imgview/imgview.c +++ b/apps/imgview/imgview.c @@ -181,8 +181,12 @@ static void loadAndDisplay(const char *path) { dvxSetBusy(sAc, true); - int32_t channels; - sImgRgb = stbi_load(path, &sImgW, &sImgH, &channels, 3); + int imgW; + int imgH; + int channels; + sImgRgb = stbi_load(path, &imgW, &imgH, &channels, 3); + sImgW = imgW; + sImgH = imgH; if (!sImgRgb) { dvxSetBusy(sAc, false); @@ -200,7 +204,7 @@ static void loadAndDisplay(const char *path) { fname = fname ? fname + 1 : path; - char title[128]; + char title[280]; snprintf(title, sizeof(title), "%s - Image Viewer", fname); dvxSetTitle(sAc, sWin, title); diff --git a/core/dvxApp.c b/core/dvxApp.c index 93047c2..34f4c5d 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -148,7 +148,7 @@ static void refreshMinimizedIcons(AppContextT *ctx); static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h); static void updateCursorShape(AppContextT *ctx); static void updateTooltip(AppContextT *ctx); -static void writePixel(uint8_t *row, int32_t x, uint32_t px, int32_t bpp); + // Button pressed via keyboard -- shared with widgetEvent.c for Space/Enter. // Non-static so widgetEvent.c can set it when Space/Enter triggers a button. @@ -3440,21 +3440,6 @@ static void updateTooltip(AppContextT *ctx) { } -// ============================================================ -// writePixel -- write a packed pixel to a buffer at position x -// ============================================================ - -static void writePixel(uint8_t *row, int32_t x, uint32_t px, int32_t bpp) { - if (bpp == 8) { - row[x] = (uint8_t)px; - } else if (bpp == 15 || bpp == 16) { - ((uint16_t *)row)[x] = (uint16_t)px; - } else { - ((uint32_t *)row)[x] = px; - } -} - - // ============================================================ // dvxAddAccel // ============================================================ @@ -4303,9 +4288,9 @@ bool dvxLoadTheme(AppContextT *ctx, const char *filename) { val++; } - int32_t r; - int32_t g; - int32_t b; + int r; + int g; + int b; if (sscanf(val, "%d,%d,%d", &r, &g, &b) != 3) { continue; @@ -4566,9 +4551,9 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) { dvxSetBusy(ctx, true); - int32_t imgW; - int32_t imgH; - int32_t channels; + int imgW; + int imgH; + int channels; uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3); if (!rgb) { diff --git a/core/dvxDialog.c b/core/dvxDialog.c index 1a21d2d..54abdb2 100644 --- a/core/dvxDialog.c +++ b/core/dvxDialog.c @@ -72,6 +72,9 @@ static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color); static bool fdAcceptFile(const char *name); +static void ibOnCancel(WidgetT *w); +static void ibOnClose(WindowT *win); +static void ibOnOk(WidgetT *w); static int fdEntryCompare(const void *a, const void *b); static bool fdFilterMatch(const char *name, const char *pattern); static void fdFreeEntries(void); @@ -115,6 +118,25 @@ typedef struct { static MsgBoxStateT sMsgBox; +// ============================================================ +// Input box state (one active at a time) +// ============================================================ + +#define IB_DIALOG_WIDTH 300 +#define IB_INPUT_MAXLEN 256 +#define IB_PADDING 8 + +typedef struct { + AppContextT *ctx; + bool done; + bool accepted; // true = OK, false = Cancel + WidgetT *input; // text input widget + char *outBuf; // caller's output buffer + int32_t outBufSize; +} InputBoxStateT; + +static InputBoxStateT sInputBox; + // ============================================================ // drawIconGlyph -- draw a simple icon shape @@ -618,6 +640,141 @@ static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t } +// ============================================================ +// dvxInputBox +// ============================================================ +// +// Modal input dialog with prompt, text field, OK/Cancel buttons. +// Follows the same nested-event-loop pattern as dvxMessageBox. + +bool dvxInputBox(AppContextT *ctx, const char *title, const char *prompt, const char *defaultText, char *outBuf, int32_t outBufSize) { + if (!ctx || !outBuf || outBufSize <= 0) { + return false; + } + + int32_t promptH = 0; + + if (prompt && prompt[0]) { + int32_t textMaxW = IB_DIALOG_WIDTH - IB_PADDING * 2; + promptH = wordWrapHeight(&ctx->font, prompt, textMaxW); + } + + int32_t contentH = IB_PADDING + promptH + IB_PADDING + ctx->font.charHeight + 4 + IB_PADDING + BUTTON_HEIGHT + IB_PADDING; + int32_t contentW = IB_DIALOG_WIDTH; + + int32_t winX = (ctx->display.width - contentW) / 2 - CHROME_TOTAL_SIDE; + int32_t winY = (ctx->display.height - contentH) / 2 - CHROME_TOTAL_TOP; + + WindowT *win = dvxCreateWindow(ctx, title ? title : "Input", + winX, winY, + contentW + CHROME_TOTAL_SIDE * 2, + contentH + CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM, + false); + + if (!win) { + return false; + } + + win->modal = true; + win->onClose = ibOnClose; + win->maxW = win->w; + win->maxH = win->h; + + sInputBox.ctx = ctx; + sInputBox.done = false; + sInputBox.accepted = false; + sInputBox.outBuf = outBuf; + sInputBox.outBufSize = outBufSize; + + WidgetT *root = wgtInitWindow(ctx, win); + + if (root) { + // Prompt label + if (prompt && prompt[0]) { + WidgetT *lbl = wgtLabel(root, prompt); + (void)lbl; + } + + // Text input + WidgetT *input = wgtTextInput(root, IB_INPUT_MAXLEN); + input->weight = 0; + sInputBox.input = input; + + if (defaultText) { + wgtSetText(input, defaultText); + } + + // Button row + WidgetT *btnRow = wgtHBox(root); + btnRow->align = AlignCenterE; + + WidgetT *okBtn = wgtButton(btnRow, "&OK"); + okBtn->minW = wgtPixels(BUTTON_WIDTH); + okBtn->minH = wgtPixels(BUTTON_HEIGHT); + okBtn->onClick = ibOnOk; + + WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel"); + cancelBtn->minW = wgtPixels(BUTTON_WIDTH); + cancelBtn->minH = wgtPixels(BUTTON_HEIGHT); + cancelBtn->onClick = ibOnCancel; + } + + dvxFitWindow(ctx, win); + + WindowT *prevModal = ctx->modalWindow; + ctx->modalWindow = win; + + while (!sInputBox.done && ctx->running) { + dvxUpdate(ctx); + } + + ctx->modalWindow = prevModal; + dvxDestroyWindow(ctx, win); + sInputBox.input = NULL; + + return sInputBox.accepted; +} + + +// ============================================================ +// ibOnCancel +// ============================================================ + +static void ibOnCancel(WidgetT *w) { + (void)w; + sInputBox.accepted = false; + sInputBox.done = true; +} + + +// ============================================================ +// ibOnClose +// ============================================================ + +static void ibOnClose(WindowT *win) { + (void)win; + sInputBox.accepted = false; + sInputBox.done = true; +} + + +// ============================================================ +// ibOnOk +// ============================================================ + +static void ibOnOk(WidgetT *w) { + (void)w; + + if (sInputBox.input && sInputBox.outBuf) { + const char *text = wgtGetText(sInputBox.input); + snprintf(sInputBox.outBuf, sInputBox.outBufSize, "%s", text ? text : ""); + } + + sInputBox.accepted = true; + sInputBox.done = true; +} + + // ============================================================ // File dialog // ============================================================ diff --git a/core/dvxDialog.h b/core/dvxDialog.h index 64cd472..d1390db 100644 --- a/core/dvxDialog.h +++ b/core/dvxDialog.h @@ -77,4 +77,10 @@ typedef struct { // NULL to start in the current working directory. bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const char *initialDir, const FileFilterT *filters, int32_t filterCount, char *outPath, int32_t outPathSize); +// Display a modal input box with a prompt label, text input field, +// and OK/Cancel buttons. Blocks the caller via dvxUpdate() loop. +// Returns true if the user clicked OK (text written to outBuf), +// false if cancelled or closed. defaultText may be NULL. +bool dvxInputBox(AppContextT *ctx, const char *title, const char *prompt, const char *defaultText, char *outBuf, int32_t outBufSize); + #endif // DVX_DIALOG_H diff --git a/core/dvxPrefs.c b/core/dvxPrefs.c index 56edecf..633f0a6 100644 --- a/core/dvxPrefs.c +++ b/core/dvxPrefs.c @@ -332,8 +332,6 @@ bool prefsSaveAs(const char *filename) { return false; } - const char *lastSection = ""; - for (int32_t i = 0; i < arrlen(sEntries); i++) { PrefsEntryT *e = &sEntries[i]; @@ -346,7 +344,6 @@ bool prefsSaveAs(const char *filename) { // Section header (key=NULL, value=NULL) if (!e->key && !e->value) { fprintf(fp, "[%s]\r\n", e->section); - lastSection = e->section; continue; } diff --git a/core/dvxWidget.h b/core/dvxWidget.h index 1ffd3ef..316e946 100644 --- a/core/dvxWidget.h +++ b/core/dvxWidget.h @@ -530,4 +530,74 @@ void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT void wgtRegisterApi(const char *name, const void *api); const void *wgtGetApi(const char *name); +// ============================================================ +// Widget interface descriptors +// ============================================================ +// +// Each widget DXE can register an interface descriptor that +// describes its BASIC-facing properties, methods, and events. +// The form runtime and IDE use these for generic dispatch and +// property panel enumeration. + +// Property data types +#define WGT_IFACE_STRING 0 +#define WGT_IFACE_INT 1 +#define WGT_IFACE_BOOL 2 +#define WGT_IFACE_FLOAT 3 + +// Method calling conventions (how the form runtime marshals args) +#define WGT_SIG_VOID 0 // void fn(WidgetT *) +#define WGT_SIG_INT 1 // void fn(WidgetT *, int32_t) +#define WGT_SIG_BOOL 2 // void fn(WidgetT *, bool) +#define WGT_SIG_STR 3 // void fn(WidgetT *, const char *) +#define WGT_SIG_INT_INT 4 // void fn(WidgetT *, int32_t, int32_t) +#define WGT_SIG_INT_BOOL 5 // void fn(WidgetT *, int32_t, bool) +#define WGT_SIG_RET_INT 6 // int32_t fn(const WidgetT *) +#define WGT_SIG_RET_BOOL 7 // bool fn(const WidgetT *) +#define WGT_SIG_RET_BOOL_INT 8 // bool fn(const WidgetT *, int32_t) + +// Property descriptor +typedef struct { + const char *name; // BASIC property name (e.g. "Caption", "Value") + uint8_t type; // WGT_IFACE_* + void *getFn; // getter function pointer (NULL if write-only) + void *setFn; // setter function pointer (NULL if read-only) +} WgtPropDescT; + +// Method descriptor +typedef struct { + const char *name; // BASIC method name (e.g. "Clear", "SetFocus") + uint8_t sig; // WGT_SIG_* + void *fn; // function pointer +} WgtMethodDescT; + +// Event descriptor +typedef struct { + const char *name; // event name (e.g. "Click", "Change") +} WgtEventDescT; + +// Common events implicitly available on all widgets. +// The form runtime wires these callbacks on every control. +// Widget descriptors only need to list EXTRA events beyond these. +// Click, DblClick, Change, GotFocus, LostFocus + +// Widget interface descriptor (registered by each .wgt) +typedef struct { + const char *basName; // VB-style name (e.g. "CommandButton"), or NULL + const WgtPropDescT *props; // type-specific properties + int32_t propCount; + const WgtMethodDescT *methods; // type-specific methods + int32_t methodCount; + const WgtEventDescT *events; // extra events beyond common set + int32_t eventCount; +} WgtIfaceT; + +// Register/retrieve interface descriptors by widget type name. +void wgtRegisterIface(const char *name, const WgtIfaceT *iface); +const WgtIfaceT *wgtGetIface(const char *name); + +// Find a widget type name by its VB-style name (e.g. "CommandButton" -> "button"). +// Returns NULL if no widget has that basName. Case-insensitive. +const char *wgtFindByBasName(const char *basName); + #endif // DVX_WIDGET_H diff --git a/core/widgetClass.c b/core/widgetClass.c index 31e31a2..77701fd 100644 --- a/core/widgetClass.c +++ b/core/widgetClass.c @@ -15,6 +15,7 @@ #include "stb_ds.h" #include +#include // stb_ds dynamic array of class pointers. Grows on each // wgtRegisterClass() call. Index = type ID. @@ -28,6 +29,14 @@ typedef struct { static ApiMapEntryT *sApiMap = NULL; +// stb_ds string hashmap: key = widget name, value = interface descriptor +typedef struct { + char *key; + const WgtIfaceT *value; +} IfaceMapEntryT; + +static IfaceMapEntryT *sIfaceMap = NULL; + // ============================================================ // wgtGetApi @@ -52,6 +61,51 @@ const void *wgtGetApi(const char *name) { } +// ============================================================ +// wgtFindByBasName +// ============================================================ +// +// Scan all registered interfaces for one whose basName matches +// (case-insensitive). Returns the widget type name, or NULL. + +const char *wgtFindByBasName(const char *basName) { + if (!basName || !sIfaceMap) { + return NULL; + } + + for (size_t i = 0; i < shlenu(sIfaceMap); i++) { + if (sIfaceMap[i].value && sIfaceMap[i].value->basName) { + if (strcasecmp(sIfaceMap[i].value->basName, basName) == 0) { + return sIfaceMap[i].key; + } + } + } + + return NULL; +} + + +// ============================================================ +// wgtGetIface +// ============================================================ +// +// Look up a widget interface descriptor by type name. + +const WgtIfaceT *wgtGetIface(const char *name) { + if (!name) { + return NULL; + } + + int32_t idx = shgeti(sIfaceMap, name); + + if (idx < 0) { + return NULL; + } + + return sIfaceMap[idx].value; +} + + // ============================================================ // wgtRegisterApi // ============================================================ @@ -68,6 +122,22 @@ void wgtRegisterApi(const char *name, const void *api) { } +// ============================================================ +// wgtRegisterIface +// ============================================================ +// +// Register a widget's interface descriptor under its type name. +// Called by widget DXEs during wgtRegister(). + +void wgtRegisterIface(const char *name, const WgtIfaceT *iface) { + if (!name || !iface) { + return; + } + + shput(sIfaceMap, name, iface); +} + + // ============================================================ // wgtRegisterClass // ============================================================ diff --git a/dvxbasic/Makefile b/dvxbasic/Makefile index ac00247..6ecaf70 100644 --- a/dvxbasic/Makefile +++ b/dvxbasic/Makefile @@ -18,7 +18,7 @@ OBJDIR = ../obj/dvxbasic LIBSDIR = ../bin/libs APPDIR = ../bin/apps/dvxbasic DVXRES = ../bin/dvxres -SAMPLES = samples/hello.bas +SAMPLES = samples/hello.bas samples/formtest.bas samples/clickme.bas samples/clickme.frm samples/input.bas # Runtime library objects (VM + values) RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o @@ -27,8 +27,11 @@ RT_TARGET = $(LIBSDIR)/basrt.lib # Compiler objects (only needed by the IDE) COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/symtab.o +# Form runtime objects (bridge between BASIC and DVX widgets) +FORMRT_OBJS = $(OBJDIR)/formrt.o + # IDE app objects -APP_OBJS = $(OBJDIR)/ideMain.o +APP_OBJS = $(OBJDIR)/ideMain.o $(FORMRT_OBJS) APP_TARGET = $(APPDIR)/dvxbasic.app .PHONY: all clean @@ -54,7 +57,10 @@ install-samples: $(SAMPLES) | $(APPDIR) cp $(SAMPLES) $(APPDIR)/ # Object files -$(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/opcodes.h runtime/values.h | $(OBJDIR) +$(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/symtab.h compiler/opcodes.h runtime/values.h | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h compiler/codegen.h runtime/vm.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< $(OBJDIR)/ideMain.o: ide/ideMain.c compiler/parser.h runtime/vm.h | $(OBJDIR) @@ -86,4 +92,4 @@ $(APPDIR): mkdir -p $(APPDIR) clean: - rm -f $(RT_OBJS) $(COMP_OBJS) $(APP_OBJS) $(RT_TARGET) $(APP_TARGET) $(LIBSDIR)/basrt.dep $(OBJDIR)/basrt_init.o + rm -f $(RT_OBJS) $(COMP_OBJS) $(FORMRT_OBJS) $(APP_OBJS) $(RT_TARGET) $(APP_TARGET) $(LIBSDIR)/basrt.dep $(OBJDIR)/basrt_init.o diff --git a/dvxbasic/compiler/codegen.c b/dvxbasic/compiler/codegen.c index 6da847f..4d98f80 100644 --- a/dvxbasic/compiler/codegen.c +++ b/dvxbasic/compiler/codegen.c @@ -1,10 +1,12 @@ // codegen.c -- DVX BASIC p-code emitter implementation #include "codegen.h" +#include "symtab.h" #include "opcodes.h" #include #include +#include // ============================================================ // basAddData @@ -105,6 +107,61 @@ BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) { } +// ============================================================ +// basCodeGenBuildModuleWithProcs +// ============================================================ + +BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) { + BasModuleT *mod = basCodeGenBuildModule(cg); + + if (!mod || !symtab) { + return mod; + } + + BasSymTabT *tab = (BasSymTabT *)symtab; + + // Count SUB/FUNCTION entries + int32_t procCount = 0; + + for (int32_t i = 0; i < tab->count; i++) { + BasSymbolT *s = &tab->symbols[i]; + + if ((s->kind == SYM_SUB || s->kind == SYM_FUNCTION) && s->isDefined && !s->isExtern) { + procCount++; + } + } + + if (procCount == 0) { + return mod; + } + + mod->procs = (BasProcEntryT *)malloc(procCount * sizeof(BasProcEntryT)); + + if (!mod->procs) { + return mod; + } + + int32_t idx = 0; + + for (int32_t i = 0; i < tab->count; i++) { + BasSymbolT *s = &tab->symbols[i]; + + if ((s->kind == SYM_SUB || s->kind == SYM_FUNCTION) && s->isDefined && !s->isExtern) { + BasProcEntryT *p = &mod->procs[idx++]; + strncpy(p->name, s->name, BAS_MAX_PROC_NAME - 1); + p->name[BAS_MAX_PROC_NAME - 1] = '\0'; + p->codeAddr = s->codeAddr; + p->paramCount = s->paramCount; + p->returnType = s->dataType; + p->isFunction = (s->kind == SYM_FUNCTION); + } + } + + mod->procCount = idx; + return mod; +} + + // ============================================================ // basCodeGenFree // ============================================================ @@ -201,6 +258,25 @@ void basEmitU16(BasCodeGenT *cg, uint16_t v) { } +// ============================================================ +// basModuleFindProc +// ============================================================ + +const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name) { + if (!mod || !mod->procs || !name) { + return NULL; + } + + for (int32_t i = 0; i < mod->procCount; i++) { + if (strcasecmp(mod->procs[i].name, name) == 0) { + return &mod->procs[i]; + } + } + + return NULL; +} + + // ============================================================ // basModuleFree // ============================================================ @@ -228,6 +304,7 @@ void basModuleFree(BasModuleT *mod) { free(mod->dataPool); } + free(mod->procs); free(mod); } diff --git a/dvxbasic/compiler/codegen.h b/dvxbasic/compiler/codegen.h index 31da482..e077cef 100644 --- a/dvxbasic/compiler/codegen.h +++ b/dvxbasic/compiler/codegen.h @@ -70,7 +70,15 @@ bool basAddData(BasCodeGenT *cg, BasValueT val); // ownership of the module and must free it with basModuleFree(). BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg); +// Build a module with procedure table from parser symbol table. +// symtab is a BasSymTabT* (cast to void* to avoid circular include). +BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab); + // Free a module built by basCodeGenBuildModule. void basModuleFree(BasModuleT *mod); +// Find a procedure by name in a module's procedure table. +// Case-insensitive. Returns NULL if not found. +const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name); + #endif // DVXBASIC_CODEGEN_H diff --git a/dvxbasic/compiler/lexer.c b/dvxbasic/compiler/lexer.c index b5d8e26..3ebdf16 100644 --- a/dvxbasic/compiler/lexer.c +++ b/dvxbasic/compiler/lexer.c @@ -498,7 +498,7 @@ static char peekNext(const BasLexerT *lex) { // ============================================================ static void setError(BasLexerT *lex, const char *msg) { - snprintf(lex->error, sizeof(lex->error), "Line %d, Col %d: %s", lex->line, lex->col, msg); + snprintf(lex->error, sizeof(lex->error), "Line %d, Col %d: %s", (int)lex->line, (int)lex->col, msg); } diff --git a/dvxbasic/compiler/opcodes.h b/dvxbasic/compiler/opcodes.h index f64f691..7dbfa03 100644 --- a/dvxbasic/compiler/opcodes.h +++ b/dvxbasic/compiler/opcodes.h @@ -19,6 +19,7 @@ #define BAS_TYPE_ARRAY 6 // ref-counted array #define BAS_TYPE_UDT 7 // ref-counted user-defined type #define BAS_TYPE_OBJECT 8 // opaque host object (form, control, etc.) +#define BAS_TYPE_REF 9 // ByRef pointer to a BasValueT slot // ============================================================ // Stack operations @@ -130,6 +131,7 @@ #define OP_RET_VAL 0x56 // return from function (value on stack) #define OP_FOR_INIT 0x57 // [uint16 varIdx] [uint8 isLocal] init FOR #define OP_FOR_NEXT 0x58 // [uint16 varIdx] [uint8 isLocal] [int16 loopTop] +#define OP_FOR_POP 0x59 // pop top FOR stack entry (for EXIT FOR) // ============================================================ // Type conversion diff --git a/dvxbasic/compiler/parser.c b/dvxbasic/compiler/parser.c index 96db8d1..78e9ffa 100644 --- a/dvxbasic/compiler/parser.c +++ b/dvxbasic/compiler/parser.c @@ -63,6 +63,13 @@ static const BuiltinFuncT builtinFuncs[] = { {"LOC", OP_FILE_LOC, 1, 1, BAS_TYPE_LONG}, {"LOF", OP_FILE_LOF, 1, 1, BAS_TYPE_LONG}, + // Conversion functions + {"CDBL", OP_CONV_INT_FLT, 1, 1, BAS_TYPE_DOUBLE}, + {"CINT", OP_CONV_FLT_INT, 1, 1, BAS_TYPE_INTEGER}, + {"CLNG", OP_CONV_INT_LONG, 1, 1, BAS_TYPE_LONG}, + {"CSNG", OP_CONV_INT_FLT, 1, 1, BAS_TYPE_SINGLE}, + {"CSTR", OP_CONV_INT_STR, 1, 1, BAS_TYPE_STRING}, + // Math functions {"ABS", OP_MATH_ABS, 1, 1, BAS_TYPE_DOUBLE}, {"ATN", OP_MATH_ATN, 1, 1, BAS_TYPE_DOUBLE}, @@ -90,6 +97,7 @@ static void advance(BasParserT *p); static bool check(BasParserT *p, BasTokenTypeE type); static bool checkKeyword(BasParserT *p, const char *kw); static bool checkKeywordText(const char *text, const char *kw); +static void emitByRefArg(BasParserT *p); static void error(BasParserT *p, const char *msg); static void errorExpected(BasParserT *p, const char *what); static void expect(BasParserT *p, BasTokenTypeE type); @@ -255,7 +263,7 @@ static void error(BasParserT *p, const char *msg) { } p->hasError = true; p->errorLine = p->lex.token.line; - snprintf(p->error, sizeof(p->error), "Line %d: %s", p->lex.token.line, msg); + snprintf(p->error, sizeof(p->error), "Line %d: %s", (int)p->lex.token.line, msg); } @@ -472,10 +480,18 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) { expect(p, TOK_LPAREN); int32_t argc = 0; if (!check(p, TOK_RPAREN)) { - parseExpression(p); + if (argc < sym->paramCount && !sym->paramByVal[argc]) { + emitByRefArg(p); + } else { + parseExpression(p); + } argc++; while (match(p, TOK_COMMA)) { - parseExpression(p); + if (argc < sym->paramCount && !sym->paramByVal[argc]) { + emitByRefArg(p); + } else { + parseExpression(p); + } argc++; } } @@ -487,7 +503,7 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) { if (argc != sym->paramCount) { char buf[256]; - snprintf(buf, sizeof(buf), "Function '%s' expects %d arguments, got %d", sym->name, sym->paramCount, argc); + snprintf(buf, sizeof(buf), "Function '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc); error(p, buf); return; } @@ -611,6 +627,64 @@ static void emitStore(BasParserT *p, BasSymbolT *sym) { } +// Try to emit a ByRef argument (push address of variable). +// If the current token is a simple variable name not followed by +// '(' or '.', we emit PUSH_LOCAL_ADDR/PUSH_GLOBAL_ADDR. +// Otherwise, we fall back to parseExpression (effectively ByVal). +static void emitByRefArg(BasParserT *p) { + if (!check(p, TOK_IDENT)) { + parseExpression(p); + return; + } + + // Save the identifier name before peeking ahead + char name[BAS_MAX_TOKEN_LEN]; + strncpy(name, p->lex.token.text, sizeof(name) - 1); + name[sizeof(name) - 1] = '\0'; + + // Look up the symbol -- must be a simple variable (not array, not const) + BasSymbolT *sym = basSymTabFind(&p->sym, name); + + if (!sym || sym->kind != SYM_VARIABLE || sym->isArray) { + parseExpression(p); + return; + } + + // Save lexer state to peek at what follows the identifier + int32_t savedPos = p->lex.pos; + int32_t savedLine = p->lex.line; + int32_t savedCol = p->lex.col; + BasTokenT savedTok = p->lex.token; + + advance(p); // consume the identifier + + // The token after the identifier must be an argument delimiter + // (comma, rparen, newline, colon, EOF, ELSE) for this to be a + // bare variable reference. Anything else (operator, dot, paren) + // means it's part of an expression -- fall back to parseExpression. + bool isDelim = check(p, TOK_COMMA) || check(p, TOK_RPAREN) || check(p, TOK_NEWLINE) || check(p, TOK_COLON) || check(p, TOK_EOF) || check(p, TOK_ELSE); + + if (!isDelim) { + // Restore and let parseExpression handle the full expression + p->lex.pos = savedPos; + p->lex.line = savedLine; + p->lex.col = savedCol; + p->lex.token = savedTok; + parseExpression(p); + return; + } + + // It's a bare variable reference -- push its address + if (sym->scope == SCOPE_LOCAL) { + basEmit8(&p->cg, OP_PUSH_LOCAL_ADDR); + } else { + basEmit8(&p->cg, OP_PUSH_GLOBAL_ADDR); + } + + basEmitU16(&p->cg, (uint16_t)sym->index); +} + + static BasSymbolT *ensureVariable(BasParserT *p, const char *name) { BasSymbolT *sym = basSymTabFind(&p->sym, name); if (sym != NULL) { @@ -952,6 +1026,13 @@ static void parsePrimary(BasParserT *p) { return; } + // Me -- reference to current form + if (tt == TOK_ME) { + advance(p); + basEmit8(&p->cg, OP_ME_REF); + return; + } + // EOF(#channel) -- file end-of-file test if (tt == TOK_EOF_KW) { advance(p); @@ -1101,7 +1182,7 @@ static void parsePrimary(BasParserT *p) { if (argc < builtin->minArgs || argc > builtin->maxArgs) { char buf[256]; - snprintf(buf, sizeof(buf), "Built-in '%s' expects %d-%d arguments, got %d", builtin->name, builtin->minArgs, builtin->maxArgs, argc); + snprintf(buf, sizeof(buf), "Built-in '%s' expects %d-%d arguments, got %d", builtin->name, (int)builtin->minArgs, (int)builtin->maxArgs, (int)argc); error(p, buf); return; } @@ -1162,9 +1243,10 @@ static void parsePrimary(BasParserT *p) { return; } - // Check for UDT field access: var.field + // Check for dot access: UDT field or control property if (check(p, TOK_DOT)) { - sym = ensureVariable(p, name); + // If we already know this is a UDT variable, do field access + sym = basSymTabFind(&p->sym, name); if (sym != NULL && sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0) { emitLoad(p, sym); advance(p); // consume DOT @@ -1172,7 +1254,6 @@ static void parsePrimary(BasParserT *p) { errorExpected(p, "field name"); return; } - // Find the TYPE_DEF symbol BasSymbolT *typeSym = NULL; for (int32_t i = 0; i < p->sym.count; i++) { if (p->sym.symbols[i].kind == SYM_TYPE_DEF && p->sym.symbols[i].index == sym->udtTypeId) { @@ -1186,7 +1267,7 @@ static void parsePrimary(BasParserT *p) { } int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text); if (fieldIdx < 0) { - char buf[256]; + char buf[512]; snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name); error(p, buf); return; @@ -1196,6 +1277,32 @@ static void parsePrimary(BasParserT *p) { basEmitU16(&p->cg, (uint16_t)fieldIdx); return; } + + // Not a UDT -- treat as control property read: CtrlName.Property + advance(p); // consume DOT + if (!check(p, TOK_IDENT)) { + errorExpected(p, "property name"); + return; + } + char memberName[BAS_MAX_TOKEN_LEN]; + strncpy(memberName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + memberName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + // Emit: push NULL (current form), push ctrl name, FIND_CTRL + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 0); + uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, ctrlNameIdx); + basEmit8(&p->cg, OP_FIND_CTRL); + + // Push property name, LOAD_PROP + uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, propNameIdx); + basEmit8(&p->cg, OP_LOAD_PROP); + return; } // Plain variable reference @@ -1271,15 +1378,10 @@ static void parseAssignOrCall(BasParserT *p) { BasSymbolT *sym = basSymTabFind(&p->sym, name); - // UDT field assignment: var.field = expr + // Dot member access: UDT field or control property/method if (check(p, TOK_DOT)) { - if (sym == NULL) { - sym = ensureVariable(p, name); - } - if (sym == NULL) { - return; - } - if (sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0) { + // Check for UDT field access first + if (sym != NULL && sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0) { emitLoad(p, sym); advance(p); // consume DOT if (!check(p, TOK_IDENT)) { @@ -1299,7 +1401,7 @@ static void parseAssignOrCall(BasParserT *p) { } int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text); if (fieldIdx < 0) { - char buf[256]; + char buf[512]; snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name); error(p, buf); return; @@ -1311,6 +1413,109 @@ static void parseAssignOrCall(BasParserT *p) { basEmitU16(&p->cg, (uint16_t)fieldIdx); return; } + + // Control property/method access: CtrlName.Member + // Emit: push current form ref, push ctrl name, FIND_CTRL + advance(p); // consume DOT + + if (!check(p, TOK_IDENT) && !check(p, TOK_SHOW) && !check(p, TOK_HIDE)) { + errorExpected(p, "property or method name"); + return; + } + + char memberName[BAS_MAX_TOKEN_LEN]; + strncpy(memberName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + memberName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); // consume member name + + // Special form methods: Show, Hide + if (strcasecmp(memberName, "Show") == 0) { + // name.Show [modal] + // Push form name, LOAD_FORM, SHOW_FORM + uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, nameIdx); + basEmit8(&p->cg, OP_LOAD_FORM); + uint8_t modal = 0; + if (check(p, TOK_INT_LIT) || check(p, TOK_IDENT)) { + // Parse modal flag + if (check(p, TOK_INT_LIT) && p->lex.token.intVal != 0) { + modal = 1; + } + advance(p); + } + basEmit8(&p->cg, OP_SHOW_FORM); + basEmit8(&p->cg, modal); + return; + } + + if (strcasecmp(memberName, "Hide") == 0) { + uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, nameIdx); + basEmit8(&p->cg, OP_LOAD_FORM); + basEmit8(&p->cg, OP_HIDE_FORM); + return; + } + + if (check(p, TOK_EQ)) { + // Property assignment: CtrlName.Property = expr + advance(p); // consume = + + // Push ctrl ref: push form ref (NULL = current), push ctrl name, FIND_CTRL + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 0); + BasValueT formNull; + memset(&formNull, 0, sizeof(formNull)); + // Use OP_PUSH_STR for ctrl name, then FIND_CTRL + uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, ctrlNameIdx); + basEmit8(&p->cg, OP_FIND_CTRL); + + // Push property name + uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, propNameIdx); + + // Parse value expression + parseExpression(p); + + // Store property + basEmit8(&p->cg, OP_STORE_PROP); + return; + } + + // Method call: CtrlName.Method [args] + // Push ctrl ref + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 0); + uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, ctrlNameIdx); + basEmit8(&p->cg, OP_FIND_CTRL); + + // Push method name + uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, methodNameIdx); + + // Parse arguments (space-separated, like VB) + int32_t argc = 0; + while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) { + if (argc > 0) { + if (check(p, TOK_COMMA)) { + advance(p); + } + } + parseExpression(p); + argc++; + } + + basEmit8(&p->cg, OP_CALL_METHOD); + basEmit8(&p->cg, (uint8_t)argc); + basEmit8(&p->cg, OP_POP); // discard return value (statement form) + return; } // Array assignment: var(index) = expr @@ -1382,16 +1587,24 @@ static void parseAssignOrCall(BasParserT *p) { if (sym != NULL && sym->kind == SYM_SUB) { int32_t argc = 0; if (!check(p, TOK_NEWLINE) && !check(p, TOK_EOF) && !check(p, TOK_COLON) && !check(p, TOK_ELSE)) { - parseExpression(p); + if (argc < sym->paramCount && !sym->paramByVal[argc]) { + emitByRefArg(p); + } else { + parseExpression(p); + } argc++; while (match(p, TOK_COMMA)) { - parseExpression(p); + if (argc < sym->paramCount && !sym->paramByVal[argc]) { + emitByRefArg(p); + } else { + parseExpression(p); + } argc++; } } if (!p->hasError && argc != sym->paramCount) { char buf[256]; - snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got %d", sym->name, sym->paramCount, argc); + snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc); error(p, buf); return; } @@ -2283,6 +2496,7 @@ static void parseExit(BasParserT *p) { if (check(p, TOK_FOR)) { advance(p); + basEmit8(&p->cg, OP_FOR_POP); int32_t addr = emitJump(p, OP_JMP); exitListAdd(&exitForList, addr); } else if (check(p, TOK_DO)) { @@ -3494,15 +3708,15 @@ static void parseSelectCase(BasParserT *p) { continue; } - // CASE val [, val] ... + // CASE val [, val | val TO val | IS op val] ... // // Strategy for multi-value CASE using JMP_TRUE chaining: - // For each value: - // DUP testval - // push value - // CMP_EQ - // JMP_TRUE -> body - // JMP -> next_case (none of the values matched) + // For each item: + // Plain value: DUP, push val, CMP_EQ, JMP_TRUE -> body + // Range (val TO val): DUP, push lo, CMP_GE, JMP_FALSE -> skip, + // DUP, push hi, CMP_LE, JMP_TRUE -> body, skip: + // IS op val: DUP, push val, CMP_xx, JMP_TRUE -> body + // JMP -> next_case (none of the items matched) // body: // ...statements... // JMP -> end_select @@ -3511,23 +3725,67 @@ static void parseSelectCase(BasParserT *p) { int32_t bodyJumps[MAX_EXITS]; int32_t bodyJumpCount = 0; - // First value - basEmit8(&p->cg, OP_DUP); - parseExpression(p); - basEmit8(&p->cg, OP_CMP_EQ); + for (;;) { + if (check(p, TOK_IS)) { + // CASE IS value + advance(p); // consume IS - if (bodyJumpCount < MAX_EXITS) { - bodyJumps[bodyJumpCount++] = emitJump(p, OP_JMP_TRUE); - } + uint8_t cmpOp; - // Additional comma-separated values - while (!p->hasError && match(p, TOK_COMMA)) { - basEmit8(&p->cg, OP_DUP); - parseExpression(p); - basEmit8(&p->cg, OP_CMP_EQ); + if (check(p, TOK_LT)) { cmpOp = OP_CMP_LT; advance(p); } + else if (check(p, TOK_GT)) { cmpOp = OP_CMP_GT; advance(p); } + else if (check(p, TOK_LE)) { cmpOp = OP_CMP_LE; advance(p); } + else if (check(p, TOK_GE)) { cmpOp = OP_CMP_GE; advance(p); } + else if (check(p, TOK_EQ)) { cmpOp = OP_CMP_EQ; advance(p); } + else if (check(p, TOK_NE)) { cmpOp = OP_CMP_NE; advance(p); } + else { + error(p, "Expected comparison operator after IS"); + return; + } - if (bodyJumpCount < MAX_EXITS) { - bodyJumps[bodyJumpCount++] = emitJump(p, OP_JMP_TRUE); + basEmit8(&p->cg, OP_DUP); + parseExpression(p); + basEmit8(&p->cg, cmpOp); + + if (bodyJumpCount < MAX_EXITS) { + bodyJumps[bodyJumpCount++] = emitJump(p, OP_JMP_TRUE); + } + } else { + // Parse first value -- could be plain or start of range + basEmit8(&p->cg, OP_DUP); + parseExpression(p); + + if (check(p, TOK_TO)) { + // CASE low TO high + advance(p); // consume TO + + // Stack: testval testval low + // Check testval >= low + basEmit8(&p->cg, OP_CMP_GE); + int32_t skipRange = emitJump(p, OP_JMP_FALSE); + + // Check testval <= high + basEmit8(&p->cg, OP_DUP); + parseExpression(p); + basEmit8(&p->cg, OP_CMP_LE); + + if (bodyJumpCount < MAX_EXITS) { + bodyJumps[bodyJumpCount++] = emitJump(p, OP_JMP_TRUE); + } + + patchJump(p, skipRange); + } else { + // Plain value -- equality test + basEmit8(&p->cg, OP_CMP_EQ); + + if (bodyJumpCount < MAX_EXITS) { + bodyJumps[bodyJumpCount++] = emitJump(p, OP_JMP_TRUE); + } + } + } + + if (!match(p, TOK_COMMA)) { + break; } } @@ -3652,8 +3910,12 @@ static void parseStatic(BasParserT *p) { } // Create a mangled global name: "procName$varName" - char mangledName[BAS_MAX_SYMBOL_NAME]; + // Truncation is intentional -- symbol names are clamped to BAS_MAX_SYMBOL_NAME. + char mangledName[BAS_MAX_SYMBOL_NAME * 2 + 1]; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-truncation" snprintf(mangledName, sizeof(mangledName), "%s$%s", p->currentProc, varName); +#pragma GCC diagnostic pop // Create the global variable with the mangled name bool savedLocal = p->sym.inLocalScope; @@ -3787,6 +4049,13 @@ static void parseStatement(BasParserT *p) { parseEnd(p); break; + case TOK_ERROR_KW: + // ERROR n -- raise a runtime error + advance(p); + parseExpression(p); + basEmit8(&p->cg, OP_RAISE_ERR); + break; + case TOK_ERASE: parseErase(p); break; @@ -3937,6 +4206,48 @@ static void parseStatement(BasParserT *p) { basEmit8(&p->cg, OP_DO_EVENTS); break; + case TOK_LOAD: + // Load FormName (identifier, not string) + advance(p); + if (!check(p, TOK_IDENT)) { + errorExpected(p, "form name"); + break; + } + { + uint16_t nameIdx = basAddConstant(&p->cg, p->lex.token.text, (int32_t)strlen(p->lex.token.text)); + advance(p); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, nameIdx); + basEmit8(&p->cg, OP_LOAD_FORM); + basEmit8(&p->cg, OP_POP); + } + break; + + case TOK_UNLOAD: + // Unload FormName (identifier, not string) + advance(p); + if (!check(p, TOK_IDENT)) { + errorExpected(p, "form name"); + break; + } + { + uint16_t nameIdx = basAddConstant(&p->cg, p->lex.token.text, (int32_t)strlen(p->lex.token.text)); + advance(p); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, nameIdx); + basEmit8(&p->cg, OP_LOAD_FORM); + basEmit8(&p->cg, OP_UNLOAD_FORM); + } + break; + + case TOK_MSGBOX: + advance(p); + parseExpression(p); + basEmit8(&p->cg, OP_MSGBOX); + basEmit8(&p->cg, 0); + basEmit8(&p->cg, OP_POP); + break; + case TOK_LET: advance(p); // consume LET, then fall through to assignment if (!check(p, TOK_IDENT)) { @@ -3973,7 +4284,7 @@ static void parseStatement(BasParserT *p) { sym->isDefined = true; sym->codeAddr = basCodePos(&p->cg); } else { - char buf[256]; + char buf[512]; snprintf(buf, sizeof(buf), "Name '%s' already used", labelName); error(p, buf); } @@ -4208,8 +4519,11 @@ static void parseType(BasParserT *p) { } BasFieldDefT *field = &typeSym->fields[typeSym->fieldCount]; - strncpy(field->name, p->lex.token.text, BAS_MAX_SYMBOL_NAME - 1); - field->name[BAS_MAX_SYMBOL_NAME - 1] = '\0'; + // Truncation is intentional -- field names are clamped to BAS_MAX_SYMBOL_NAME. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-truncation" + snprintf(field->name, BAS_MAX_SYMBOL_NAME, "%s", p->lex.token.text); +#pragma GCC diagnostic pop advance(p); expect(p, TOK_AS); @@ -4484,8 +4798,9 @@ BasModuleT *basParserBuildModule(BasParserT *p) { if (p->hasError) { return NULL; } + p->cg.globalCount = p->sym.nextGlobalIdx; - return basCodeGenBuildModule(&p->cg); + return basCodeGenBuildModuleWithProcs(&p->cg, &p->sym); } diff --git a/dvxbasic/compiler/parser.h b/dvxbasic/compiler/parser.h index b4828c8..c52cfd3 100644 --- a/dvxbasic/compiler/parser.h +++ b/dvxbasic/compiler/parser.h @@ -25,7 +25,7 @@ typedef struct { BasLexerT lex; BasCodeGenT cg; BasSymTabT sym; - char error[512]; + char error[1024]; bool hasError; int32_t errorLine; int32_t lastUdtTypeId; // index of last resolved UDT type from resolveTypeName diff --git a/dvxbasic/formrt/formrt.c b/dvxbasic/formrt/formrt.c new file mode 100644 index 0000000..c0a3bc9 --- /dev/null +++ b/dvxbasic/formrt/formrt.c @@ -0,0 +1,1619 @@ +// formrt.c -- DVX BASIC form runtime implementation +// +// Bridges BASIC programs to the DVX widget system. All widget types +// are discovered dynamically via WgtIfaceT interface descriptors +// registered by .wgt DXE files. No hardcoded control types. + +#include "formrt.h" +#include "../compiler/codegen.h" +#include "../compiler/opcodes.h" +#include "dvxDialog.h" +#include "dvxWm.h" +#include "widgetBox.h" + +#include +#include +#include +#include +#include + +// ============================================================ +// Defines +// ============================================================ + +#define DEFAULT_FORM_W 400 +#define DEFAULT_FORM_H 300 +#define DEFAULT_CREATE_ARG 256 // default maxLen/interval for widget creation +#define MAX_EVENT_NAME_LEN 128 +#define MAX_LISTBOX_ITEMS 256 +#define MAX_FRM_LINE_LEN 512 +#define MAX_FRM_NESTING 16 +#define MAX_AUX_DATA 128 + +// ============================================================ +// Per-control listbox item storage +// ============================================================ + +typedef struct { + char *items[MAX_LISTBOX_ITEMS]; + int32_t count; +} ListBoxItemsT; + +// ============================================================ +// Auxiliary data table for listbox item storage +// ============================================================ + +typedef struct { + BasControlT *ctrl; + ListBoxItemsT *items; +} AuxDataEntryT; + +static AuxDataEntryT sAuxData[MAX_AUX_DATA]; +static int32_t sAuxDataCount = 0; + +// Module-level form runtime pointer for onFormClose callback +static BasFormRtT *sFormRt = NULL; + +// ============================================================ +// Prototypes +// ============================================================ + +static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char *title, const char *defaultText); +static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc); +static BasValueT callListBoxMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc); +static WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent); +static void freeListBoxItems(BasControlT *ctrl); +static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled); +static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled); +static ListBoxItemsT *getListBoxItems(BasControlT *ctrl); +static void onFormClose(WindowT *win); +static void onFormResize(WindowT *win, int32_t newW, int32_t newH); +static void onWidgetBlur(WidgetT *w); +static void onWidgetChange(WidgetT *w); +static void onWidgetClick(WidgetT *w); +static void onWidgetDblClick(WidgetT *w); +static void onWidgetFocus(WidgetT *w); +static void parseFrmLine(const char *line, char *key, char *value); +static void rebuildListBoxItems(BasControlT *ctrl); +static const char *resolveTypeName(const char *typeName); +static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT value); +static bool setIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, BasValueT value); +static BasValueT zeroValue(void); + +// ============================================================ +// basFormRtBindVm +// ============================================================ + +void basFormRtBindVm(BasFormRtT *rt) { + BasUiCallbacksT ui; + memset(&ui, 0, sizeof(ui)); + + ui.getProp = basFormRtGetProp; + ui.setProp = basFormRtSetProp; + ui.callMethod = basFormRtCallMethod; + ui.createCtrl = basFormRtCreateCtrl; + ui.findCtrl = basFormRtFindCtrl; + ui.loadForm = basFormRtLoadForm; + ui.unloadForm = basFormRtUnloadForm; + ui.showForm = basFormRtShowForm; + ui.hideForm = basFormRtHideForm; + ui.msgBox = basFormRtMsgBox; + ui.inputBox = basFormRtInputBox; + ui.ctx = rt; + + basVmSetUiCallbacks(rt->vm, &ui); +} + + +// ============================================================ +// basFormRtCallMethod +// ============================================================ + +BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc) { + (void)ctx; + BasControlT *ctrl = (BasControlT *)ctrlRef; + + if (!ctrl || !ctrl->widget) { + return zeroValue(); + } + + // ListBox-specific methods (AddItem/RemoveItem/Clear/List) + BasValueT result = callListBoxMethod(ctrl, methodName, args, argc); + + if (result.type != 0 || strcasecmp(methodName, "AddItem") == 0 || + strcasecmp(methodName, "RemoveItem") == 0 || + strcasecmp(methodName, "Clear") == 0 || + strcasecmp(methodName, "List") == 0) { + return result; + } + + // Interface descriptor methods + const WgtIfaceT *iface = ctrl->iface; + + if (iface) { + for (int32_t i = 0; i < iface->methodCount; i++) { + if (strcasecmp(iface->methods[i].name, methodName) != 0) { + continue; + } + + const WgtMethodDescT *m = &iface->methods[i]; + WidgetT *w = ctrl->widget; + + switch (m->sig) { + case WGT_SIG_VOID: + ((void (*)(WidgetT *))m->fn)(w); + return zeroValue(); + + case WGT_SIG_INT: + if (argc >= 1) { + ((void (*)(WidgetT *, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0])); + } + return zeroValue(); + + case WGT_SIG_BOOL: + if (argc >= 1) { + ((void (*)(WidgetT *, bool))m->fn)(w, basValIsTruthy(args[0])); + } + return zeroValue(); + + case WGT_SIG_STR: + if (argc >= 1) { + BasStringT *s = basValFormatString(args[0]); + ((void (*)(WidgetT *, const char *))m->fn)(w, s->data); + basStringUnref(s); + } + return zeroValue(); + + case WGT_SIG_INT_INT: + if (argc >= 2) { + ((void (*)(WidgetT *, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1])); + } + return zeroValue(); + + case WGT_SIG_INT_BOOL: + if (argc >= 2) { + ((void (*)(WidgetT *, int32_t, bool))m->fn)(w, (int32_t)basValToNumber(args[0]), basValIsTruthy(args[1])); + } + return zeroValue(); + + case WGT_SIG_RET_INT: { + int32_t v = ((int32_t (*)(const WidgetT *))m->fn)(w); + return basValLong(v); + } + + case WGT_SIG_RET_BOOL: { + bool v = ((bool (*)(const WidgetT *))m->fn)(w); + return basValBool(v); + } + + case WGT_SIG_RET_BOOL_INT: { + if (argc >= 1) { + bool v = ((bool (*)(const WidgetT *, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0])); + return basValBool(v); + } + return basValBool(false); + } + } + } + } + + // Common methods (SetFocus, Refresh) + return callCommonMethod(ctrl, methodName, args, argc); +} + + +// ============================================================ +// basFormRtCreate +// ============================================================ + +BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module) { + BasFormRtT *rt = (BasFormRtT *)calloc(1, sizeof(BasFormRtT)); + + if (!rt) { + return NULL; + } + + rt->ctx = ctx; + rt->vm = vm; + rt->module = module; + + sFormRt = rt; + basFormRtBindVm(rt); + return rt; +} + + +// ============================================================ +// basFormRtCreateCtrl +// ============================================================ + +void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const char *ctrlName) { + (void)ctx; + BasFormT *form = (BasFormT *)formRef; + + if (!form || form->controlCount >= BAS_MAX_CTRLS) { + return NULL; + } + + // Resolve VB name to DVX widget type name + const char *wgtTypeName = resolveTypeName(typeName); + + if (!wgtTypeName) { + return NULL; + } + + // Create the widget + WidgetT *parent = form->contentBox ? form->contentBox : form->root; + + if (!parent) { + return NULL; + } + + WidgetT *widget = createWidget(wgtTypeName, parent); + + if (!widget) { + return NULL; + } + + wgtSetName(widget, ctrlName); + + // Initialize control entry + BasControlT *ctrl = &form->controls[form->controlCount++]; + memset(ctrl, 0, sizeof(*ctrl)); + snprintf(ctrl->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + ctrl->widget = widget; + ctrl->form = form; + ctrl->iface = wgtGetIface(wgtTypeName); + + // Wire up event callbacks + widget->userData = ctrl; + widget->onClick = onWidgetClick; + widget->onDblClick = onWidgetDblClick; + widget->onChange = onWidgetChange; + widget->onFocus = onWidgetFocus; + widget->onBlur = onWidgetBlur; + + return ctrl; +} + + +// ============================================================ +// basFormRtDestroy +// ============================================================ + +void basFormRtDestroy(BasFormRtT *rt) { + if (!rt) { + return; + } + + if (sFormRt == rt) { + sFormRt = NULL; + } + + for (int32_t i = 0; i < rt->formCount; i++) { + BasFormT *form = &rt->forms[i]; + + for (int32_t j = 0; j < form->controlCount; j++) { + freeListBoxItems(&form->controls[j]); + } + + if (form->window) { + dvxDestroyWindow(rt->ctx, form->window); + form->window = NULL; + } + } + + free(rt); +} + + +// ============================================================ +// basFormRtFindCtrl +// ============================================================ + +void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) { + (void)ctx; + BasFormT *form = (BasFormT *)formRef; + + if (!form) { + return NULL; + } + + for (int32_t i = 0; i < form->controlCount; i++) { + if (strcasecmp(form->controls[i].name, ctrlName) == 0) { + return &form->controls[i]; + } + } + + return NULL; +} + + +// ============================================================ +// basFormRtFireEvent +// ============================================================ + +bool basFormRtFireEvent(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName) { + if (!rt || !form || !rt->vm || !rt->module) { + return false; + } + + char handlerName[MAX_EVENT_NAME_LEN]; + snprintf(handlerName, sizeof(handlerName), "%s_%s", ctrlName, eventName); + + const BasProcEntryT *proc = basModuleFindProc(rt->module, handlerName); + + if (!proc) { + return false; + } + + if (proc->isFunction || proc->paramCount > 0) { + return false; + } + + BasFormT *prevForm = rt->currentForm; + rt->currentForm = form; + basVmSetCurrentForm(rt->vm, form); + + bool ok = basVmCallSub(rt->vm, proc->codeAddr); + + rt->currentForm = prevForm; + basVmSetCurrentForm(rt->vm, prevForm); + return ok; +} + + +// ============================================================ +// basFormRtGetProp +// ============================================================ + +BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { + (void)ctx; + BasControlT *ctrl = (BasControlT *)ctrlRef; + + if (!ctrl || !ctrl->widget) { + return zeroValue(); + } + + // Common properties (Name, Left, Top, Width, Height, Visible, Enabled) + bool handled; + BasValueT val = getCommonProp(ctrl, propName, &handled); + + if (handled) { + return val; + } + + // "Caption" and "Text" map to wgtGetText for all widgets + if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) { + const char *text = wgtGetText(ctrl->widget); + return basValStringFromC(text ? text : ""); + } + + // "ListCount" for any widget with item storage + if (strcasecmp(propName, "ListCount") == 0) { + ListBoxItemsT *lb = getListBoxItems(ctrl); + return basValLong(lb ? lb->count : 0); + } + + // Interface descriptor properties + if (ctrl->iface) { + val = getIfaceProp(ctrl->iface, ctrl->widget, propName, &handled); + + if (handled) { + return val; + } + } + + return zeroValue(); +} + + +// ============================================================ +// basFormRtHideForm +// ============================================================ + +void basFormRtHideForm(void *ctx, void *formRef) { + BasFormRtT *rt = (BasFormRtT *)ctx; + BasFormT *form = (BasFormT *)formRef; + + if (!form || !form->window) { + return; + } + + form->window->visible = false; + dvxInvalidateWindow(rt->ctx, form->window); +} + + +// ============================================================ +// basFormRtInputBox +// ============================================================ + +static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char *title, const char *defaultText) { + BasFormRtT *rt = (BasFormRtT *)ctx; + char buf[256]; + + buf[0] = '\0'; + + if (defaultText && defaultText[0]) { + strncpy(buf, defaultText, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + } + + if (!dvxInputBox(rt->ctx, title, prompt, defaultText, buf, sizeof(buf))) { + return NULL; + } + + return basStringNew(buf, (int32_t)strlen(buf)); +} + + +// ============================================================ +// basFormRtLoadForm +// ============================================================ + +void *basFormRtLoadForm(void *ctx, const char *formName) { + BasFormRtT *rt = (BasFormRtT *)ctx; + + if (rt->formCount >= BAS_MAX_FORMS) { + return NULL; + } + + // Check if form already exists + for (int32_t i = 0; i < rt->formCount; i++) { + if (strcasecmp(rt->forms[i].name, formName) == 0) { + return &rt->forms[i]; + } + } + + WindowT *win = dvxCreateWindowCentered(rt->ctx, formName, DEFAULT_FORM_W, DEFAULT_FORM_H, true); + + if (!win) { + return NULL; + } + + WidgetT *root = wgtInitWindow(rt->ctx, win); + + if (!root) { + dvxDestroyWindow(rt->ctx, win); + return NULL; + } + + WidgetT *contentBox = wgtVBox(root); + + if (!contentBox) { + dvxDestroyWindow(rt->ctx, win); + return NULL; + } + + contentBox->weight = 100; + + BasFormT *form = &rt->forms[rt->formCount++]; + memset(form, 0, sizeof(*form)); + snprintf(form->name, BAS_MAX_CTRL_NAME, "%s", formName); + win->onClose = onFormClose; + win->onResize = onFormResize; + form->window = win; + form->root = root; + form->contentBox = contentBox; + form->ctx = rt->ctx; + form->vm = rt->vm; + form->module = rt->module; + + return form; +} + + +// ============================================================ +// basFormRtLoadFrm +// ============================================================ + +BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen) { + if (!rt || !source || sourceLen <= 0) { + return NULL; + } + + BasFormT *form = NULL; + BasControlT *current = NULL; + + WidgetT *parentStack[MAX_FRM_NESTING]; + int32_t nestDepth = 0; + + // Track Begin/End blocks: true = container (Form/Frame), false = control + bool isContainer[MAX_FRM_NESTING]; + int32_t blockDepth = 0; + + const char *pos = source; + const char *end = source + sourceLen; + + while (pos < end) { + const char *lineStart = pos; + + while (pos < end && *pos != '\n' && *pos != '\r') { + pos++; + } + + int32_t lineLen = (int32_t)(pos - lineStart); + + if (pos < end && *pos == '\r') { + pos++; + } + + if (pos < end && *pos == '\n') { + pos++; + } + + char line[MAX_FRM_LINE_LEN]; + + if (lineLen >= MAX_FRM_LINE_LEN) { + lineLen = MAX_FRM_LINE_LEN - 1; + } + + memcpy(line, lineStart, lineLen); + line[lineLen] = '\0'; + + char *trimmed = line; + + while (*trimmed == ' ' || *trimmed == '\t') { + trimmed++; + } + + if (*trimmed == '\0' || *trimmed == '\'') { + continue; + } + + // "VERSION x.xx" -- skip version declaration + if (strncasecmp(trimmed, "VERSION ", 8) == 0) { + continue; + } + + // "Begin TypeName CtrlName" + if (strncasecmp(trimmed, "Begin ", 6) == 0) { + char *rest = trimmed + 6; + char typeName[BAS_MAX_CTRL_NAME]; + char ctrlName[BAS_MAX_CTRL_NAME]; + + int32_t ti = 0; + + while (*rest && *rest != ' ' && *rest != '\t' && ti < BAS_MAX_CTRL_NAME - 1) { + typeName[ti++] = *rest++; + } + + typeName[ti] = '\0'; + + while (*rest == ' ' || *rest == '\t') { + rest++; + } + + int32_t ci = 0; + + while (*rest && *rest != ' ' && *rest != '\t' && *rest != '\r' && *rest != '\n' && ci < BAS_MAX_CTRL_NAME - 1) { + ctrlName[ci++] = *rest++; + } + + ctrlName[ci] = '\0'; + + if (strcasecmp(typeName, "Form") == 0) { + form = (BasFormT *)basFormRtLoadForm(rt, ctrlName); + + if (!form) { + return NULL; + } + + parentStack[0] = form->contentBox; + nestDepth = 1; + current = NULL; + + if (blockDepth < MAX_FRM_NESTING) { + isContainer[blockDepth++] = true; + } + } else if (form && nestDepth > 0) { + WidgetT *parent = parentStack[nestDepth - 1]; + + const char *wgtTypeName = resolveTypeName(typeName); + + if (!wgtTypeName) { + continue; + } + + WidgetT *widget = createWidget(wgtTypeName, parent); + + if (!widget) { + continue; + } + + wgtSetName(widget, ctrlName); + + if (form->controlCount < BAS_MAX_CTRLS) { + current = &form->controls[form->controlCount++]; + memset(current, 0, sizeof(*current)); + snprintf(current->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + current->widget = widget; + current->form = form; + current->iface = wgtGetIface(wgtTypeName); + + widget->userData = current; + widget->onClick = onWidgetClick; + widget->onDblClick = onWidgetDblClick; + widget->onChange = onWidgetChange; + widget->onFocus = onWidgetFocus; + widget->onBlur = onWidgetBlur; + } + + // Track block type for End handling + if (strcasecmp(typeName, "Frame") == 0 && nestDepth < MAX_FRM_NESTING) { + // Frame is a container -- children go inside it + parentStack[nestDepth++] = widget; + + if (blockDepth < MAX_FRM_NESTING) { + isContainer[blockDepth++] = true; + } + } else { + if (blockDepth < MAX_FRM_NESTING) { + isContainer[blockDepth++] = false; + } + } + } + + continue; + } + + // "End" + if (strcasecmp(trimmed, "End") == 0) { + if (blockDepth > 0) { + blockDepth--; + + // Only decrement parent nesting for containers (Form/Frame) + if (isContainer[blockDepth] && nestDepth > 0) { + nestDepth--; + } + } + + current = NULL; + continue; + } + + // Property assignment: Key = Value + char key[BAS_MAX_CTRL_NAME]; + char value[MAX_FRM_LINE_LEN]; + parseFrmLine(trimmed, key, value); + + if (key[0] == '\0' || !form) { + continue; + } + + if (current) { + BasValueT val; + + if (value[0] == '"') { + int32_t vlen = (int32_t)strlen(value); + + if (vlen >= 2 && value[vlen - 1] == '"') { + value[vlen - 1] = '\0'; + } + + val = basValStringFromC(value + 1); + } else { + val = basValLong(atoi(value)); + } + + basFormRtSetProp(rt, current, key, val); + basValRelease(&val); + } else if (nestDepth > 0) { + // Form-level property + if (strcasecmp(key, "Caption") == 0) { + char *text = value; + + if (text[0] == '"') { + text++; + int32_t len = (int32_t)strlen(text); + + if (len > 0 && text[len - 1] == '"') { + text[len - 1] = '\0'; + } + } + + dvxSetTitle(rt->ctx, form->window, text); + } + } + } + + // Force layout recalculation now that all controls and properties are set + if (form) { + dvxFitWindow(rt->ctx, form->window); + } + + return form; +} + + +// ============================================================ +// basFormRtMsgBox +// ============================================================ + +int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags) { + BasFormRtT *rt = (BasFormRtT *)ctx; + + return dvxMessageBox(rt->ctx, "DVX BASIC", message, flags); +} + + +// ============================================================ +// basFormRtSetProp +// ============================================================ + +void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value) { + (void)ctx; + BasControlT *ctrl = (BasControlT *)ctrlRef; + + if (!ctrl || !ctrl->widget) { + return; + } + + // Common properties + if (setCommonProp(ctrl, propName, value)) { + return; + } + + // "Caption" and "Text" map to wgtSetText for all widgets. + // Copy to persistent buffer since some widgets (Button, Label) + // store the text pointer without copying. + if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) { + BasStringT *s = basValFormatString(value); + snprintf(ctrl->textBuf, BAS_MAX_TEXT_BUF, "%s", s->data); + basStringUnref(s); + wgtSetText(ctrl->widget, ctrl->textBuf); + return; + } + + // Interface descriptor properties + if (ctrl->iface) { + if (setIfaceProp(ctrl->iface, ctrl->widget, propName, value)) { + return; + } + } +} + + +// ============================================================ +// basFormRtShowForm +// ============================================================ + +void basFormRtShowForm(void *ctx, void *formRef, bool modal) { + BasFormRtT *rt = (BasFormRtT *)ctx; + BasFormT *form = (BasFormT *)formRef; + + if (!form || !form->window) { + return; + } + + form->window->visible = true; + dvxFitWindow(rt->ctx, form->window); + dvxInvalidateWindow(rt->ctx, form->window); + + if (modal) { + rt->ctx->modalWindow = form->window; + } + + basFormRtFireEvent(rt, form, form->name, "Load"); +} + + +// ============================================================ +// basFormRtUnloadForm +// ============================================================ + +void basFormRtUnloadForm(void *ctx, void *formRef) { + BasFormRtT *rt = (BasFormRtT *)ctx; + BasFormT *form = (BasFormT *)formRef; + + if (!form) { + return; + } + + basFormRtFireEvent(rt, form, form->name, "Unload"); + + for (int32_t i = 0; i < form->controlCount; i++) { + freeListBoxItems(&form->controls[i]); + } + + if (form->window) { + if (rt->ctx->modalWindow == form->window) { + rt->ctx->modalWindow = NULL; + } + + dvxDestroyWindow(rt->ctx, form->window); + form->window = NULL; + form->root = NULL; + form->contentBox = NULL; + } + + int32_t idx = (int32_t)(form - rt->forms); + + if (idx >= 0 && idx < rt->formCount) { + rt->formCount--; + + if (idx < rt->formCount) { + rt->forms[idx] = rt->forms[rt->formCount]; + } + + memset(&rt->forms[rt->formCount], 0, sizeof(BasFormT)); + } +} + + +// ============================================================ +// callCommonMethod +// ============================================================ + +static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc) { + (void)args; + (void)argc; + + if (strcasecmp(methodName, "SetFocus") == 0) { + wgtSetFocused(ctrl->widget); + return zeroValue(); + } + + if (strcasecmp(methodName, "Refresh") == 0) { + wgtInvalidatePaint(ctrl->widget); + return zeroValue(); + } + + return zeroValue(); +} + + +// ============================================================ +// callListBoxMethod +// ============================================================ +// +// Handles AddItem/RemoveItem/Clear/List for any widget that +// supports item lists (listbox, combobox, dropdown). + +static BasValueT callListBoxMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc) { + if (strcasecmp(methodName, "AddItem") == 0) { + if (argc >= 1 && args[0].type == BAS_TYPE_STRING && args[0].strVal) { + ListBoxItemsT *lb = getListBoxItems(ctrl); + + if (lb && lb->count < MAX_LISTBOX_ITEMS) { + lb->items[lb->count] = strdup(args[0].strVal->data); + lb->count++; + rebuildListBoxItems(ctrl); + } + } + + return zeroValue(); + } + + if (strcasecmp(methodName, "RemoveItem") == 0) { + if (argc >= 1) { + int32_t idx = (int32_t)basValToNumber(args[0]); + ListBoxItemsT *lb = getListBoxItems(ctrl); + + if (lb && idx >= 0 && idx < lb->count) { + free(lb->items[idx]); + + for (int32_t i = idx; i < lb->count - 1; i++) { + lb->items[i] = lb->items[i + 1]; + } + + lb->count--; + rebuildListBoxItems(ctrl); + } + } + + return zeroValue(); + } + + if (strcasecmp(methodName, "Clear") == 0) { + ListBoxItemsT *lb = getListBoxItems(ctrl); + + if (lb) { + for (int32_t i = 0; i < lb->count; i++) { + free(lb->items[i]); + } + + lb->count = 0; + rebuildListBoxItems(ctrl); + } + + return zeroValue(); + } + + if (strcasecmp(methodName, "List") == 0) { + if (argc >= 1) { + int32_t idx = (int32_t)basValToNumber(args[0]); + ListBoxItemsT *lb = getListBoxItems(ctrl); + + if (lb && idx >= 0 && idx < lb->count) { + return basValStringFromC(lb->items[idx]); + } + } + + return basValStringFromC(""); + } + + return zeroValue(); +} + + +// ============================================================ +// createWidget +// ============================================================ +// +// Create a DVX widget by type name using its registered API. +// The API's create function signature varies by widget type, so +// we call with sensible defaults for each creation pattern. + +static WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent) { + const void *api = wgtGetApi(wgtTypeName); + + if (!api) { + return NULL; + } + + // All widget APIs have create as the first function pointer. + // The signature varies but the first arg is always parent. + // We handle the common patterns. + typedef WidgetT *(*CreateParentFnT)(WidgetT *); + typedef WidgetT *(*CreateParentTextFnT)(WidgetT *, const char *); + typedef WidgetT *(*CreateParentIntFnT)(WidgetT *, int32_t); + typedef WidgetT *(*CreateParentIntIntFnT)(WidgetT *, int32_t, int32_t); + typedef WidgetT *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool); + typedef WidgetT *(*CreateParentIntIntIntFnT)(WidgetT *, int32_t, int32_t, int32_t); + + // Determine creation pattern by widget type name + if (strcasecmp(wgtTypeName, "button") == 0 || + strcasecmp(wgtTypeName, "checkbox") == 0 || + strcasecmp(wgtTypeName, "label") == 0) { + // create(parent, text) + CreateParentTextFnT fn = *(CreateParentTextFnT *)api; + return fn(parent, ""); + } + + if (strcasecmp(wgtTypeName, "textinput") == 0 || + strcasecmp(wgtTypeName, "combobox") == 0) { + // create(parent, maxLen) + CreateParentIntFnT fn = *(CreateParentIntFnT *)api; + return fn(parent, DEFAULT_CREATE_ARG); + } + + if (strcasecmp(wgtTypeName, "slider") == 0) { + // create(parent, minVal, maxVal) + CreateParentIntIntFnT fn = *(CreateParentIntIntFnT *)api; + return fn(parent, 0, 100); + } + + if (strcasecmp(wgtTypeName, "spinner") == 0) { + // create(parent, minVal, maxVal, step) + CreateParentIntIntIntFnT fn = *(CreateParentIntIntIntFnT *)api; + return fn(parent, 0, 100, 1); + } + + if (strcasecmp(wgtTypeName, "timer") == 0) { + // create(parent, intervalMs, repeat) + CreateParentIntBoolFnT fn = *(CreateParentIntBoolFnT *)api; + return fn(parent, 1000, true); + } + + if (strcasecmp(wgtTypeName, "box") == 0) { + // box API: first fn is vbox(parent), second is hbox, third is frame + // For BASIC "Frame", use frame (third slot) + typedef struct { + WidgetT *(*vbox)(WidgetT *); + WidgetT *(*hbox)(WidgetT *); + WidgetT *(*frame)(WidgetT *, const char *); + } BoxApiPatternT; + const BoxApiPatternT *boxApi = (const BoxApiPatternT *)api; + return boxApi->frame(parent, ""); + } + + // Default: assume create(parent) with no extra args + CreateParentFnT fn = *(CreateParentFnT *)api; + return fn(parent); +} + + +// ============================================================ +// freeListBoxItems +// ============================================================ + +static void freeListBoxItems(BasControlT *ctrl) { + for (int32_t i = 0; i < sAuxDataCount; i++) { + if (sAuxData[i].ctrl == ctrl) { + ListBoxItemsT *lb = sAuxData[i].items; + + if (lb) { + for (int32_t j = 0; j < lb->count; j++) { + free(lb->items[j]); + } + + free(lb); + } + + sAuxDataCount--; + + if (i < sAuxDataCount) { + sAuxData[i] = sAuxData[sAuxDataCount]; + } + + memset(&sAuxData[sAuxDataCount], 0, sizeof(AuxDataEntryT)); + return; + } + } +} + + +// ============================================================ +// getCommonProp +// ============================================================ + +static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled) { + *handled = true; + + if (strcasecmp(propName, "Name") == 0) { + return basValStringFromC(ctrl->name); + } + + if (strcasecmp(propName, "Left") == 0) { + return basValLong(ctrl->widget->x); + } + + if (strcasecmp(propName, "Top") == 0) { + return basValLong(ctrl->widget->y); + } + + if (strcasecmp(propName, "Width") == 0) { + return basValLong(ctrl->widget->w); + } + + if (strcasecmp(propName, "Height") == 0) { + return basValLong(ctrl->widget->h); + } + + if (strcasecmp(propName, "Visible") == 0) { + return basValBool(ctrl->widget->visible); + } + + if (strcasecmp(propName, "Enabled") == 0) { + return basValBool(ctrl->widget->enabled); + } + + if (strcasecmp(propName, "TabIndex") == 0) { + return basValLong(0); + } + + if (strcasecmp(propName, "BackColor") == 0) { + return basValLong((int32_t)ctrl->widget->bgColor); + } + + if (strcasecmp(propName, "ForeColor") == 0) { + return basValLong((int32_t)ctrl->widget->fgColor); + } + + *handled = false; + return zeroValue(); +} + + +// ============================================================ +// getIfaceProp +// ============================================================ +// +// Look up a property in the interface descriptor and call its +// getter, marshaling the return value to BasValueT. + +static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled) { + *handled = false; + + for (int32_t i = 0; i < iface->propCount; i++) { + if (strcasecmp(iface->props[i].name, propName) != 0) { + continue; + } + + *handled = true; + const WgtPropDescT *p = &iface->props[i]; + + if (!p->getFn) { + return zeroValue(); + } + + switch (p->type) { + case WGT_IFACE_STRING: { + const char *s = ((const char *(*)(WidgetT *))p->getFn)(w); + return basValStringFromC(s ? s : ""); + } + + case WGT_IFACE_INT: { + int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(w); + return basValLong(v); + } + + case WGT_IFACE_BOOL: { + bool v = ((bool (*)(const WidgetT *))p->getFn)(w); + return basValBool(v); + } + + case WGT_IFACE_FLOAT: { + float v = ((float (*)(const WidgetT *))p->getFn)(w); + return basValSingle(v); + } + } + + return zeroValue(); + } + + return zeroValue(); +} + + +// ============================================================ +// getListBoxItems +// ============================================================ + +static ListBoxItemsT *getListBoxItems(BasControlT *ctrl) { + for (int32_t i = 0; i < sAuxDataCount; i++) { + if (sAuxData[i].ctrl == ctrl) { + return sAuxData[i].items; + } + } + + if (sAuxDataCount >= MAX_AUX_DATA) { + return NULL; + } + + ListBoxItemsT *lb = (ListBoxItemsT *)calloc(1, sizeof(ListBoxItemsT)); + + if (!lb) { + return NULL; + } + + sAuxData[sAuxDataCount].ctrl = ctrl; + sAuxData[sAuxDataCount].items = lb; + sAuxDataCount++; + + return lb; +} + + +// ============================================================ +// onFormClose +// ============================================================ +// +// Called when the user closes a BASIC form's window. Fires the +// Form_Unload event and stops the VM so the program exits. + +static void onFormClose(WindowT *win) { + // Find which form owns this window + // The window's userData stores nothing useful, so we search + // by window pointer. We get the form runtime from sFormRt. + if (!sFormRt) { + return; + } + + for (int32_t i = 0; i < sFormRt->formCount; i++) { + BasFormT *form = &sFormRt->forms[i]; + + if (form->window == win) { + basFormRtFireEvent(sFormRt, form, form->name, "Unload"); + + // Free control resources + for (int32_t j = 0; j < form->controlCount; j++) { + freeListBoxItems(&form->controls[j]); + } + + // Destroy the window + if (sFormRt->ctx->modalWindow == win) { + sFormRt->ctx->modalWindow = NULL; + } + + dvxDestroyWindow(sFormRt->ctx, win); + form->window = NULL; + form->root = NULL; + form->contentBox = NULL; + + // Remove from form list + sFormRt->formCount--; + + if (i < sFormRt->formCount) { + sFormRt->forms[i] = sFormRt->forms[sFormRt->formCount]; + } + + memset(&sFormRt->forms[sFormRt->formCount], 0, sizeof(BasFormT)); + + // If no forms left, stop the VM + if (sFormRt->formCount == 0 && sFormRt->vm) { + sFormRt->vm->running = false; + } + + return; + } + } +} + + +// ============================================================ +// onFormResize +// ============================================================ + +static void onFormResize(WindowT *win, int32_t newW, int32_t newH) { + (void)newW; + (void)newH; + + if (!sFormRt) { + return; + } + + for (int32_t i = 0; i < sFormRt->formCount; i++) { + BasFormT *form = &sFormRt->forms[i]; + + if (form->window == win) { + basFormRtFireEvent(sFormRt, form, form->name, "Resize"); + return; + } + } +} + + +// ============================================================ +// onWidgetBlur +// ============================================================ + +static void onWidgetBlur(WidgetT *w) { + BasControlT *ctrl = (BasControlT *)w->userData; + + if (!ctrl || !ctrl->form) { + return; + } + + BasFormRtT *rt = NULL; + + if (ctrl->form->vm) { + rt = (BasFormRtT *)ctrl->form->vm->ui.ctx; + } + + if (rt) { + basFormRtFireEvent(rt, ctrl->form, ctrl->name, "LostFocus"); + } +} + + +// ============================================================ +// onWidgetChange +// ============================================================ + +static void onWidgetChange(WidgetT *w) { + BasControlT *ctrl = (BasControlT *)w->userData; + + if (!ctrl || !ctrl->form) { + return; + } + + BasFormRtT *rt = NULL; + + if (ctrl->form->vm) { + rt = (BasFormRtT *)ctrl->form->vm->ui.ctx; + } + + if (rt) { + basFormRtFireEvent(rt, ctrl->form, ctrl->name, "Change"); + } +} + + +// ============================================================ +// onWidgetClick +// ============================================================ + +static void onWidgetClick(WidgetT *w) { + BasControlT *ctrl = (BasControlT *)w->userData; + + if (!ctrl || !ctrl->form) { + return; + } + + BasFormRtT *rt = NULL; + + if (ctrl->form->vm) { + rt = (BasFormRtT *)ctrl->form->vm->ui.ctx; + } + + if (rt) { + basFormRtFireEvent(rt, ctrl->form, ctrl->name, "Click"); + } +} + + +// ============================================================ +// onWidgetDblClick +// ============================================================ + +static void onWidgetDblClick(WidgetT *w) { + BasControlT *ctrl = (BasControlT *)w->userData; + + if (!ctrl || !ctrl->form) { + return; + } + + BasFormRtT *rt = NULL; + + if (ctrl->form->vm) { + rt = (BasFormRtT *)ctrl->form->vm->ui.ctx; + } + + if (rt) { + basFormRtFireEvent(rt, ctrl->form, ctrl->name, "DblClick"); + } +} + + +// ============================================================ +// onWidgetFocus +// ============================================================ + +static void onWidgetFocus(WidgetT *w) { + BasControlT *ctrl = (BasControlT *)w->userData; + + if (!ctrl || !ctrl->form) { + return; + } + + BasFormRtT *rt = NULL; + + if (ctrl->form->vm) { + rt = (BasFormRtT *)ctrl->form->vm->ui.ctx; + } + + if (rt) { + basFormRtFireEvent(rt, ctrl->form, ctrl->name, "GotFocus"); + } +} + + +// ============================================================ +// parseFrmLine +// ============================================================ + +static void parseFrmLine(const char *line, char *key, char *value) { + key[0] = '\0'; + value[0] = '\0'; + + while (*line == ' ' || *line == '\t') { + line++; + } + + int32_t ki = 0; + + while (*line && *line != '=' && *line != ' ' && *line != '\t' && ki < BAS_MAX_CTRL_NAME - 1) { + key[ki++] = *line++; + } + + key[ki] = '\0'; + + while (*line == ' ' || *line == '\t') { + line++; + } + + if (*line == '=') { + line++; + } + + while (*line == ' ' || *line == '\t') { + line++; + } + + int32_t vi = 0; + + while (*line && *line != '\r' && *line != '\n' && vi < MAX_FRM_LINE_LEN - 1) { + value[vi++] = *line++; + } + + value[vi] = '\0'; + + while (vi > 0 && (value[vi - 1] == ' ' || value[vi - 1] == '\t')) { + value[--vi] = '\0'; + } +} + + +// ============================================================ +// rebuildListBoxItems +// ============================================================ + +static void rebuildListBoxItems(BasControlT *ctrl) { + ListBoxItemsT *lb = getListBoxItems(ctrl); + + if (!lb) { + return; + } + + // Use the widget's setItems API if available + const WgtIfaceT *iface = ctrl->iface; + + if (!iface) { + return; + } + + // Look for a setItems-like method by checking the widget API directly + const void *api = wgtGetApi("listbox"); + + if (api) { + // ListBoxApiT has setItems as the second function pointer + typedef struct { + void *create; + void (*setItems)(WidgetT *, const char **, int32_t); + } SetItemsPatternT; + const SetItemsPatternT *p = (const SetItemsPatternT *)api; + p->setItems(ctrl->widget, (const char **)lb->items, lb->count); + return; + } + + // Fallback: try combobox or dropdown API + api = wgtGetApi("combobox"); + + if (api) { + typedef struct { + void *create; + void (*setItems)(WidgetT *, const char **, int32_t); + } SetItemsPatternT; + const SetItemsPatternT *p = (const SetItemsPatternT *)api; + p->setItems(ctrl->widget, (const char **)lb->items, lb->count); + return; + } + + api = wgtGetApi("dropdown"); + + if (api) { + typedef struct { + void *create; + void (*setItems)(WidgetT *, const char **, int32_t); + } SetItemsPatternT; + const SetItemsPatternT *p = (const SetItemsPatternT *)api; + p->setItems(ctrl->widget, (const char **)lb->items, lb->count); + } +} + + +// ============================================================ +// resolveTypeName +// ============================================================ +// +// Resolve a type name (VB-style or DVX widget name) to a DVX +// widget type name. First tries wgtFindByBasName for VB names +// like "CommandButton", then falls back to direct widget name. + +static const char *resolveTypeName(const char *typeName) { + // Try VB name first (e.g. "CommandButton" -> "button") + const char *wgtName = wgtFindByBasName(typeName); + + if (wgtName) { + return wgtName; + } + + // Try as direct widget type name (e.g. "button", "slider") + if (wgtGetApi(typeName)) { + return typeName; + } + + return NULL; +} + + +// ============================================================ +// setCommonProp +// ============================================================ + +static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT value) { + if (strcasecmp(propName, "Left") == 0) { + ctrl->widget->x = (int32_t)basValToNumber(value); + wgtInvalidate(ctrl->widget); + return true; + } + + if (strcasecmp(propName, "Top") == 0) { + ctrl->widget->y = (int32_t)basValToNumber(value); + wgtInvalidate(ctrl->widget); + return true; + } + + if (strcasecmp(propName, "Width") == 0) { + int32_t w = (int32_t)basValToNumber(value); + ctrl->widget->prefW = wgtPixels(w); + wgtInvalidate(ctrl->widget); + return true; + } + + if (strcasecmp(propName, "Height") == 0) { + int32_t h = (int32_t)basValToNumber(value); + ctrl->widget->prefH = wgtPixels(h); + wgtInvalidate(ctrl->widget); + return true; + } + + if (strcasecmp(propName, "Visible") == 0) { + wgtSetVisible(ctrl->widget, basValIsTruthy(value)); + return true; + } + + if (strcasecmp(propName, "Enabled") == 0) { + wgtSetEnabled(ctrl->widget, basValIsTruthy(value)); + return true; + } + + if (strcasecmp(propName, "TabIndex") == 0) { + return true; + } + + if (strcasecmp(propName, "BackColor") == 0) { + ctrl->widget->bgColor = (uint32_t)(int32_t)basValToNumber(value); + wgtInvalidate(ctrl->widget); + return true; + } + + if (strcasecmp(propName, "ForeColor") == 0) { + ctrl->widget->fgColor = (uint32_t)(int32_t)basValToNumber(value); + wgtInvalidate(ctrl->widget); + return true; + } + + return false; +} + + +// ============================================================ +// setIfaceProp +// ============================================================ +// +// Look up a property in the interface descriptor and call its +// setter, marshaling the BasValueT to the native type. + +static bool setIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, BasValueT value) { + for (int32_t i = 0; i < iface->propCount; i++) { + if (strcasecmp(iface->props[i].name, propName) != 0) { + continue; + } + + const WgtPropDescT *p = &iface->props[i]; + + if (!p->setFn) { + return true; // read-only, but recognized + } + + switch (p->type) { + case WGT_IFACE_STRING: { + BasStringT *s = basValFormatString(value); + ((void (*)(WidgetT *, const char *))p->setFn)(w, s->data); + basStringUnref(s); + break; + } + + case WGT_IFACE_INT: + ((void (*)(WidgetT *, int32_t))p->setFn)(w, (int32_t)basValToNumber(value)); + break; + + case WGT_IFACE_BOOL: + ((void (*)(WidgetT *, bool))p->setFn)(w, basValIsTruthy(value)); + break; + + case WGT_IFACE_FLOAT: + ((void (*)(WidgetT *, float))p->setFn)(w, (float)basValToNumber(value)); + break; + } + + return true; + } + + return false; +} + + +// ============================================================ +// zeroValue +// ============================================================ + +static BasValueT zeroValue(void) { + BasValueT v; + memset(&v, 0, sizeof(v)); + return v; +} diff --git a/dvxbasic/formrt/formrt.h b/dvxbasic/formrt/formrt.h new file mode 100644 index 0000000..de10c90 --- /dev/null +++ b/dvxbasic/formrt/formrt.h @@ -0,0 +1,110 @@ +// formrt.h -- DVX BASIC form runtime +// +// Bridges BASIC programs to the DVX widget system. Control types +// are resolved dynamically via WgtIfaceT interface descriptors +// registered by .wgt DXE files. Properties and methods are +// dispatched generically through descriptors. Events fire by +// looking up ControlName_EventName in the compiled module's +// procedure table and calling into the VM. + +#ifndef DVXBASIC_FORMRT_H +#define DVXBASIC_FORMRT_H + +#include "../runtime/vm.h" +#include "../runtime/values.h" +#include "dvxApp.h" +#include "dvxWidget.h" + +// ============================================================ +// Forward declarations +// ============================================================ + +typedef struct BasFormT BasFormT; +typedef struct BasControlT BasControlT; + +// ============================================================ +// Limits +// ============================================================ + +#define BAS_MAX_CTRL_NAME 32 +#define BAS_MAX_CTRLS 64 // max controls per form +#define BAS_MAX_FORMS 8 + +// ============================================================ +// Control instance (a widget on a form) +// ============================================================ + +#define BAS_MAX_TEXT_BUF 256 + +typedef struct BasControlT { + char name[BAS_MAX_CTRL_NAME]; // VB control name (e.g. "Command1") + 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 +} BasControlT; + +// ============================================================ +// Form instance (a DVX window with controls) +// ============================================================ + +typedef struct BasFormT { + char name[BAS_MAX_CTRL_NAME]; // form name (e.g. "Form1") + WindowT *window; // DVX window + WidgetT *root; // widget root (from wgtInitWindow) + WidgetT *contentBox; // VBox for user controls + AppContextT *ctx; // DVX app context + BasControlT controls[BAS_MAX_CTRLS]; // controls on this form + int32_t controlCount; + BasVmT *vm; // VM for event dispatch + BasModuleT *module; // compiled module (for SUB lookup) +} BasFormT; + +// ============================================================ +// Form runtime context +// ============================================================ + +typedef struct { + AppContextT *ctx; // DVX app context + BasVmT *vm; // shared VM instance + BasModuleT *module; // compiled module + BasFormT forms[BAS_MAX_FORMS]; + int32_t formCount; + BasFormT *currentForm; // form currently dispatching events +} BasFormRtT; + +// ============================================================ +// API +// ============================================================ + +// Initialize the form runtime with a DVX context and a compiled module. +BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module); + +// Destroy the form runtime and all forms/controls. +void basFormRtDestroy(BasFormRtT *rt); + +// Wire up the VM's UI callbacks to this form runtime. +void basFormRtBindVm(BasFormRtT *rt); + +// ---- UI callback implementations (match BasUiCallbacksT) ---- + +BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName); +void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value); +BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc); +void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const char *ctrlName); +void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName); +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); + +// ---- Event dispatch ---- + +bool basFormRtFireEvent(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName); + +// ---- Form file loading ---- + +BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen); + +#endif // DVXBASIC_FORMRT_H diff --git a/dvxbasic/ide/ideMain.c b/dvxbasic/ide/ideMain.c index fefb27a..bfd83bf 100644 --- a/dvxbasic/ide/ideMain.c +++ b/dvxbasic/ide/ideMain.c @@ -16,9 +16,11 @@ #include "widgetButton.h" #include "widgetLabel.h" #include "widgetTextInput.h" +#include "widgetDropdown.h" #include "widgetStatusBar.h" #include "../compiler/parser.h" +#include "../formrt/formrt.h" #include "../runtime/vm.h" #include "../runtime/values.h" @@ -27,6 +29,7 @@ #include #include #include +#include // ============================================================ // Constants @@ -38,6 +41,7 @@ #define IDE_BTN_SPACING 8 #define IDE_MAX_SOURCE 65536 #define IDE_MAX_OUTPUT 32768 +#define IDE_STEP_SLICE 10000 // VM steps per slice before yielding to DVX // Menu command IDs #define CMD_OPEN 100 @@ -45,6 +49,10 @@ #define CMD_STOP 102 #define CMD_CLEAR 103 #define CMD_EXIT 104 +#define CMD_RUN_NOCMP 105 + +#define IDE_MAX_PROCS 128 +#define IDE_MAX_IMM 1024 // ============================================================ // Prototypes @@ -59,11 +67,21 @@ static void onClose(WindowT *win); static void onMenu(WindowT *win, int32_t menuId); static void onOpenClick(WidgetT *w); static void onRunClick(WidgetT *w); +static void onStopClick(WidgetT *w); static void onClearClick(WidgetT *w); +static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); +static void evaluateImmediate(const char *expr); +static void loadFrmFiles(BasFormRtT *rt); +static void onEvtDropdownChange(WidgetT *w); +static void onImmediateChange(WidgetT *w); +static void onObjDropdownChange(WidgetT *w); static void printCallback(void *ctx, const char *text, bool newline); static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize); static bool doEventsCallback(void *ctx); +static void runCached(void); +static void runModule(BasModuleT *mod); static void setStatus(const char *text); +static void updateDropdowns(void); // ============================================================ // Module state @@ -72,16 +90,30 @@ static void setStatus(const char *text); static DxeAppContextT *sCtx = NULL; static AppContextT *sAc = NULL; static WindowT *sWin = NULL; -static WidgetT *sEditor = NULL; // TextArea for source code -static WidgetT *sOutput = NULL; // TextArea for program output -static WidgetT *sStatus = NULL; // Status bar label -static BasVmT *sVm = NULL; // VM instance (non-NULL while running) +static WidgetT *sEditor = NULL; // TextArea for source code +static WidgetT *sOutput = NULL; // TextArea for program output +static WidgetT *sImmediate = NULL; // TextArea for immediate window +static WidgetT *sObjDropdown = NULL; // Object dropdown +static WidgetT *sEvtDropdown = NULL; // Event dropdown +static WidgetT *sStatus = NULL; // Status bar label +static BasVmT *sVm = NULL; // VM instance (non-NULL while running) +static BasModuleT *sCachedModule = NULL; // Last compiled module (for Ctrl+F5) static char sSourceBuf[IDE_MAX_SOURCE]; static char sOutputBuf[IDE_MAX_OUTPUT]; static int32_t sOutputLen = 0; static char sFilePath[260]; +// Procedure table for Object/Event dropdowns +typedef struct { + char objName[64]; + char evtName[64]; + int32_t lineNum; +} IdeProcEntryT; + +static IdeProcEntryT sProcTable[IDE_MAX_PROCS]; +static int32_t sProcCount = 0; + // ============================================================ // App descriptor // ============================================================ @@ -140,12 +172,16 @@ static void buildWindow(void) { MenuT *runMenu = wmAddMenu(menuBar, "&Run"); wmAddMenuItem(runMenu, "&Run\tF5", CMD_RUN); + wmAddMenuItem(runMenu, "Run &Without Recompile\tCtrl+F5", CMD_RUN_NOCMP); + wmAddMenuItem(runMenu, "&Stop\tEsc", CMD_STOP); wmAddMenuSeparator(runMenu); wmAddMenuItem(runMenu, "&Clear Output", CMD_CLEAR); AccelTableT *accel = dvxCreateAccelTable(); dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_OPEN); dvxAddAccel(accel, KEY_F5, 0, CMD_RUN); + dvxAddAccel(accel, KEY_F5, ACCEL_CTRL, CMD_RUN_NOCMP); + dvxAddAccel(accel, 0x1B, 0, CMD_STOP); sWin->accelTable = accel; // Widget tree @@ -154,8 +190,24 @@ static void buildWindow(void) { // Source code editor (top half) WidgetT *editorFrame = wgtFrame(root, "Source"); editorFrame->weight = 100; + + // Object/Event dropdown row + WidgetT *dropdownRow = wgtHBox(editorFrame); + dropdownRow->spacing = wgtPixels(4); + + sObjDropdown = wgtDropdown(dropdownRow); + sObjDropdown->weight = 100; + sObjDropdown->onChange = onObjDropdownChange; + + sEvtDropdown = wgtDropdown(dropdownRow); + sEvtDropdown->weight = 100; + sEvtDropdown->onChange = onEvtDropdownChange; + sEditor = wgtTextArea(editorFrame, IDE_MAX_SOURCE); sEditor->weight = 100; + wgtTextAreaSetColorize(sEditor, basicColorize, NULL); + wgtTextAreaSetShowLineNumbers(sEditor, true); + wgtTextAreaSetAutoIndent(sEditor, true); // Button bar WidgetT *btnRow = wgtHBox(root); @@ -169,17 +221,28 @@ static void buildWindow(void) { runBtn->onClick = onRunClick; runBtn->prefW = wgtPixels(IDE_BTN_W); + WidgetT *stopBtn = wgtButton(btnRow, "&Stop"); + stopBtn->onClick = onStopClick; + stopBtn->prefW = wgtPixels(IDE_BTN_W); + WidgetT *clearBtn = wgtButton(btnRow, "&Clear"); clearBtn->onClick = onClearClick; clearBtn->prefW = wgtPixels(IDE_BTN_W); - // Output area (bottom half) + // Output area WidgetT *outputFrame = wgtFrame(root, "Output"); - outputFrame->weight = 100; + outputFrame->weight = 80; sOutput = wgtTextArea(outputFrame, IDE_MAX_OUTPUT); sOutput->weight = 100; sOutput->readOnly = true; + // Immediate window + WidgetT *immFrame = wgtFrame(root, "Immediate"); + immFrame->weight = 30; + sImmediate = wgtTextArea(immFrame, IDE_MAX_IMM); + sImmediate->weight = 100; + sImmediate->onChange = onImmediateChange; + // Status bar WidgetT *statusBar = wgtStatusBar(root); sStatus = wgtLabel(statusBar, ""); @@ -188,6 +251,163 @@ static void buildWindow(void) { dvxFitWindow(sAc, sWin); } +// ============================================================ +// basicColorize +// ============================================================ +// +// Syntax colorizer callback for BASIC source code. Scans a single +// line and fills the colors array with syntax color indices. + +static bool isBasicKeyword(const char *word, int32_t wordLen) { + static const char *keywords[] = { + "AND", "AS", "CALL", "CASE", "CLOSE", "CONST", "DATA", "DECLARE", + "DEF", "DEFINT", "DEFLNG", "DEFSNG", "DEFDBL", "DEFSTR", + "DIM", "DO", "DOEVENTS", "ELSE", "ELSEIF", "END", "ERASE", + "EXIT", "FOR", "FUNCTION", "GET", "GOSUB", "GOTO", "HIDE", + "IF", "IMP", "INPUT", "IS", "LET", "LIBRARY", "LINE", "LOAD", + "LOOP", "MOD", "MSGBOX", "NEXT", "NOT", "ON", "OPEN", "OPTION", + "OR", "PRINT", "PUT", "RANDOMIZE", "READ", "REDIM", "RESTORE", + "RESUME", "RETURN", "SEEK", "SELECT", "SHARED", "SHELL", "SHOW", + "SLEEP", "STATIC", "STEP", "STOP", "SUB", "SWAP", "THEN", "TO", + "TYPE", "UNLOAD", "UNTIL", "WEND", "WHILE", "WRITE", "XOR", + NULL + }; + + char upper[32]; + + if (wordLen <= 0 || wordLen >= 32) { + return false; + } + + for (int32_t i = 0; i < wordLen; i++) { + upper[i] = (char)toupper((unsigned char)word[i]); + } + + upper[wordLen] = '\0'; + + for (int32_t i = 0; keywords[i]; i++) { + if (strcmp(upper, keywords[i]) == 0) { + return true; + } + } + + return false; +} + + +static bool isBasicType(const char *word, int32_t wordLen) { + static const char *types[] = { + "BOOLEAN", "BYTE", "DOUBLE", "INTEGER", "LONG", "SINGLE", "STRING", + "TRUE", "FALSE", + NULL + }; + + char upper[32]; + + if (wordLen <= 0 || wordLen >= 32) { + return false; + } + + for (int32_t i = 0; i < wordLen; i++) { + upper[i] = (char)toupper((unsigned char)word[i]); + } + + upper[wordLen] = '\0'; + + for (int32_t i = 0; types[i]; i++) { + if (strcmp(upper, types[i]) == 0) { + return true; + } + } + + return false; +} + + +static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx) { + (void)ctx; + int32_t i = 0; + + while (i < lineLen) { + char ch = line[i]; + + // Comment: ' or REM + if (ch == '\'') { + while (i < lineLen) { + colors[i++] = 3; // SYNTAX_COMMENT + } + return; + } + + // String literal + if (ch == '"') { + colors[i++] = 2; // SYNTAX_STRING + + while (i < lineLen && line[i] != '"') { + colors[i++] = 2; + } + + if (i < lineLen) { + colors[i++] = 2; // closing quote + } + + continue; + } + + // Number + if (isdigit((unsigned char)ch) || (ch == '.' && i + 1 < lineLen && isdigit((unsigned char)line[i + 1]))) { + while (i < lineLen && (isdigit((unsigned char)line[i]) || line[i] == '.')) { + colors[i++] = 4; // SYNTAX_NUMBER + } + + continue; + } + + // Identifier or keyword + if (isalpha((unsigned char)ch) || ch == '_') { + int32_t start = i; + + while (i < lineLen && (isalnum((unsigned char)line[i]) || line[i] == '_' || line[i] == '$' || line[i] == '%' || line[i] == '&' || line[i] == '!' || line[i] == '#')) { + i++; + } + + int32_t wordLen = i - start; + + // Check for REM comment + if (wordLen == 3 && (line[start] == 'R' || line[start] == 'r') && (line[start + 1] == 'E' || line[start + 1] == 'e') && (line[start + 2] == 'M' || line[start + 2] == 'm')) { + for (int32_t j = start; j < lineLen; j++) { + colors[j] = 3; // SYNTAX_COMMENT + } + return; + } + + uint8_t c = 0; // default + + if (isBasicKeyword(line + start, wordLen)) { + c = 1; // SYNTAX_KEYWORD + } else if (isBasicType(line + start, wordLen)) { + c = 6; // SYNTAX_TYPE + } + + for (int32_t j = start; j < i; j++) { + colors[j] = c; + } + + continue; + } + + // Operators + if (ch == '=' || ch == '<' || ch == '>' || ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '\\' || ch == '&') { + colors[i++] = 5; // SYNTAX_OPERATOR + continue; + } + + // Default (whitespace, parens, etc.) + colors[i++] = 0; + } +} + + // ============================================================ // clearOutput // ============================================================ @@ -233,6 +453,12 @@ static void compileAndRun(void) { int32_t n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\n%s\n", parser->error); sOutputLen = n; wgtSetText(sOutput, sOutputBuf); + + // Jump to error line in editor + if (parser->errorLine > 0 && sEditor) { + wgtTextAreaGoToLine(sEditor, parser->errorLine); + } + setStatus("Compilation failed."); basParserFree(parser); free(parser); @@ -248,6 +474,40 @@ static void compileAndRun(void) { return; } + // Cache the compiled module for Ctrl+F5 + if (sCachedModule) { + basModuleFree(sCachedModule); + } + + sCachedModule = mod; + + // Update Object/Event dropdowns + updateDropdowns(); + + runModule(mod); +} + + +// ============================================================ +// runCached +// ============================================================ + +static void runCached(void) { + if (!sCachedModule) { + setStatus("No compiled program. Press F5 to compile first."); + return; + } + + clearOutput(); + runModule(sCachedModule); +} + + +// ============================================================ +// runModule +// ============================================================ + +static void runModule(BasModuleT *mod) { setStatus("Running..."); // Create VM @@ -263,35 +523,46 @@ static void compileAndRun(void) { basVmSetInputCallback(vm, inputCallback, NULL); basVmSetDoEventsCallback(vm, doEventsCallback, NULL); + // Create form runtime (bridges UI opcodes to DVX widgets) + BasFormRtT *formRt = basFormRtCreate(sAc, vm, mod); + + // Load any .frm files from the same directory as the source + loadFrmFiles(formRt); + sVm = vm; - // Run with step limit to prevent infinite loops from freezing the GUI - vm->running = true; - int32_t stepCount = 0; + // Run in slices of 10000 steps, yielding to DVX between slices + basVmSetStepLimit(vm, IDE_STEP_SLICE); - while (vm->running) { - BasVmResultE result = basVmStep(vm); + int32_t totalSteps = 0; + BasVmResultE result; - stepCount++; + for (;;) { + result = basVmRun(vm); + totalSteps += vm->stepCount; - if (result != BAS_VM_OK) { - if (result == BAS_VM_HALTED) { - // Normal completion - } else { - // Runtime error - int32_t pos = sOutputLen; - int32_t n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, "\n[Runtime error: %s]\n", basVmGetError(vm)); - sOutputLen += n; - wgtSetText(sOutput, sOutputBuf); + if (result == BAS_VM_STEP_LIMIT) { + // Yield to DVX to keep the GUI responsive + dvxUpdate(sAc); + + // Stop if IDE window was closed or DVX is shutting down + if (!sWin || !sAc->running) { + break; } + continue; + } + + if (result == BAS_VM_HALTED) { break; } - // Yield to DVX every 10000 steps to keep the GUI responsive - if (stepCount % 10000 == 0) { - dvxUpdate(sAc); - } + // Runtime error + int32_t pos = sOutputLen; + int32_t n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, "\n[Runtime error: %s]\n", basVmGetError(vm)); + sOutputLen += n; + wgtSetText(sOutput, sOutputBuf); + break; } sVm = NULL; @@ -300,11 +571,11 @@ static void compileAndRun(void) { wgtSetText(sOutput, sOutputBuf); static char statusBuf[128]; - snprintf(statusBuf, sizeof(statusBuf), "Done. %ld instructions executed.", (long)stepCount); + snprintf(statusBuf, sizeof(statusBuf), "Done. %ld instructions executed.", (long)totalSteps); setStatus(statusBuf); + basFormRtDestroy(formRt); basVmDestroy(vm); - basModuleFree(mod); } // ============================================================ @@ -314,12 +585,106 @@ static void compileAndRun(void) { static bool doEventsCallback(void *ctx) { (void)ctx; - // Update DVX display and process events - if (sAc) { - dvxUpdate(sAc); + // Stop if IDE window was closed or DVX is shutting down + if (!sWin || !sAc->running) { + return false; } - return true; // continue running + dvxUpdate(sAc); + + return sWin != NULL && sAc->running; +} + +// ============================================================ +// evaluateImmediate +// ============================================================ +// +// Compile and execute a single line from the Immediate window. +// If the line doesn't start with PRINT, wrap it in PRINT so +// expressions produce visible output. + +static void immPrintCallback(void *ctx, const char *text, bool newline) { + (void)ctx; + + if (!sImmediate) { + return; + } + + // Append output to the immediate window + const char *cur = wgtGetText(sImmediate); + int32_t curLen = cur ? (int32_t)strlen(cur) : 0; + int32_t textLen = text ? (int32_t)strlen(text) : 0; + + if (curLen + textLen + 2 < IDE_MAX_IMM) { + static char immBuf[IDE_MAX_IMM]; + memcpy(immBuf, cur, curLen); + memcpy(immBuf + curLen, text, textLen); + curLen += textLen; + + if (newline) { + immBuf[curLen++] = '\n'; + } + + immBuf[curLen] = '\0'; + wgtSetText(sImmediate, immBuf); + } +} + + +static void evaluateImmediate(const char *expr) { + if (!expr || *expr == '\0') { + return; + } + + char wrapped[1024]; + + // If it already starts with a statement keyword, use as-is + if (strncasecmp(expr, "PRINT", 5) == 0 || strncasecmp(expr, "DIM", 3) == 0 || strncasecmp(expr, "LET", 3) == 0) { + snprintf(wrapped, sizeof(wrapped), "%s", expr); + } else { + snprintf(wrapped, sizeof(wrapped), "PRINT %s", expr); + } + + BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT)); + + if (!parser) { + return; + } + + basParserInit(parser, wrapped, (int32_t)strlen(wrapped)); + + if (!basParse(parser)) { + // Show error inline + immPrintCallback(NULL, "Error: ", false); + immPrintCallback(NULL, parser->error, true); + basParserFree(parser); + free(parser); + return; + } + + BasModuleT *mod = basParserBuildModule(parser); + basParserFree(parser); + free(parser); + + if (!mod) { + return; + } + + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, mod); + vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount; + vm->callDepth = 1; + basVmSetPrintCallback(vm, immPrintCallback, NULL); + + BasVmResultE result = basVmRun(vm); + + if (result != BAS_VM_HALTED && result != BAS_VM_OK) { + immPrintCallback(NULL, "Error: ", false); + immPrintCallback(NULL, basVmGetError(vm), true); + } + + basVmDestroy(vm); + basModuleFree(mod); } // ============================================================ @@ -336,11 +701,7 @@ static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufS wgtSetText(sOutput, sOutputBuf); } - // Use DVX input dialog - // For now, a simple message box prompt - // TODO: proper INPUT dialog - buf[0] = '\0'; - return false; // cancel for now + return dvxInputBox(sAc, "DVX BASIC", prompt ? prompt : "Enter value:", NULL, buf, bufSize); } // ============================================================ @@ -391,6 +752,71 @@ static void loadFile(void) { } } +// ============================================================ +// loadFrmFiles +// ============================================================ +// +// Try to load a .frm file with the same base name as the loaded +// .bas source file. For example, if the user loaded "clickme.bas", +// this looks for "clickme.frm" in the same directory. + +static void loadFrmFiles(BasFormRtT *rt) { + if (sFilePath[0] == '\0') { + return; + } + + // Build .frm path from .bas path + char frmPath[260]; + snprintf(frmPath, sizeof(frmPath), "%s", sFilePath); + + // Find the extension and replace with .frm + char *dot = strrchr(frmPath, '.'); + + if (dot) { + strcpy(dot, ".frm"); + } else { + strcat(frmPath, ".frm"); + } + + // Try to open the .frm file + FILE *f = fopen(frmPath, "r"); + + if (!f) { + return; + } + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (size <= 0 || size >= IDE_MAX_SOURCE) { + fclose(f); + return; + } + + char *frmBuf = (char *)malloc(size + 1); + + if (!frmBuf) { + fclose(f); + return; + } + + int32_t bytesRead = (int32_t)fread(frmBuf, 1, size, f); + fclose(f); + frmBuf[bytesRead] = '\0'; + + BasFormT *form = basFormRtLoadFrm(rt, frmBuf, bytesRead); + + if (form) { + int32_t pos = sOutputLen; + int32_t n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, "Loaded form: %s\n", form->name); + sOutputLen += n; + } + + free(frmBuf); +} + + // ============================================================ // onClearClick // ============================================================ @@ -406,10 +832,19 @@ static void onClearClick(WidgetT *w) { // ============================================================ static void onClose(WindowT *win) { - sWin = NULL; - sEditor = NULL; - sOutput = NULL; - sStatus = NULL; + sWin = NULL; + sEditor = NULL; + sOutput = NULL; + sImmediate = NULL; + sObjDropdown = NULL; + sEvtDropdown = NULL; + sStatus = NULL; + + if (sCachedModule) { + basModuleFree(sCachedModule); + sCachedModule = NULL; + } + dvxDestroyWindow(sAc, win); } @@ -429,6 +864,17 @@ static void onMenu(WindowT *win, int32_t menuId) { compileAndRun(); break; + case CMD_RUN_NOCMP: + runCached(); + break; + + case CMD_STOP: + if (sVm) { + sVm->running = false; + setStatus("Program stopped."); + } + break; + case CMD_CLEAR: clearOutput(); break; @@ -441,6 +887,163 @@ static void onMenu(WindowT *win, int32_t menuId) { } } +// ============================================================ +// onEvtDropdownChange +// ============================================================ +// +// Navigate to the selected procedure when the event dropdown changes. + +static void onEvtDropdownChange(WidgetT *w) { + (void)w; + + if (!sObjDropdown || !sEvtDropdown || !sEditor) { + return; + } + + int32_t objIdx = wgtDropdownGetSelected(sObjDropdown); + int32_t evtIdx = wgtDropdownGetSelected(sEvtDropdown); + + if (objIdx < 0 || evtIdx < 0) { + return; + } + + // Build the object name from the dropdown + // Dropdown items are set in updateDropdowns; find matching proc + const char *cur = wgtGetText(sObjDropdown); + + if (!cur) { + return; + } + + // Find the proc entry matching the selected object + event indices + int32_t matchIdx = 0; + + for (int32_t i = 0; i < sProcCount; i++) { + if (matchIdx == evtIdx) { + wgtTextAreaGoToLine(sEditor, sProcTable[i].lineNum); + return; + } + + matchIdx++; + } +} + + +// ============================================================ +// onImmediateChange +// ============================================================ +// +// Detect Enter in the Immediate window and evaluate the last line. + +static void onImmediateChange(WidgetT *w) { + (void)w; + + if (!sImmediate) { + return; + } + + const char *text = wgtGetText(sImmediate); + + if (!text) { + return; + } + + int32_t len = (int32_t)strlen(text); + + if (len < 2 || text[len - 1] != '\n') { + return; + } + + // Find the start of the line before the trailing newline + int32_t lineEnd = len - 1; + int32_t lineStart = lineEnd - 1; + + while (lineStart > 0 && text[lineStart - 1] != '\n') { + lineStart--; + } + + if (lineStart >= lineEnd) { + return; + } + + // Extract the line + char expr[512]; + int32_t lineLen = lineEnd - lineStart; + + if (lineLen >= (int32_t)sizeof(expr)) { + lineLen = (int32_t)sizeof(expr) - 1; + } + + memcpy(expr, text + lineStart, lineLen); + expr[lineLen] = '\0'; + + evaluateImmediate(expr); +} + + +// ============================================================ +// onObjDropdownChange +// ============================================================ +// +// Update the Event dropdown when the Object selection changes. + +static void onObjDropdownChange(WidgetT *w) { + (void)w; + + if (!sObjDropdown || !sEvtDropdown) { + return; + } + + int32_t objIdx = wgtDropdownGetSelected(sObjDropdown); + + if (objIdx < 0) { + return; + } + + // Collect unique object names to find which one is selected + char objNames[IDE_MAX_PROCS][64]; + int32_t objCount = 0; + + for (int32_t i = 0; i < sProcCount; i++) { + bool found = false; + + for (int32_t j = 0; j < objCount; j++) { + if (strcasecmp(objNames[j], sProcTable[i].objName) == 0) { + found = true; + break; + } + } + + if (!found && objCount < IDE_MAX_PROCS) { + memcpy(objNames[objCount], sProcTable[i].objName, 64); + objCount++; + } + } + + if (objIdx >= objCount) { + return; + } + + const char *selObj = objNames[objIdx]; + + // Build event list for the selected object + const char *evtItems[IDE_MAX_PROCS]; + int32_t evtCount = 0; + + for (int32_t i = 0; i < sProcCount; i++) { + if (strcasecmp(sProcTable[i].objName, selObj) == 0 && evtCount < IDE_MAX_PROCS) { + evtItems[evtCount++] = sProcTable[i].evtName; + } + } + + wgtDropdownSetItems(sEvtDropdown, evtItems, evtCount); + + if (evtCount > 0) { + wgtDropdownSetSelected(sEvtDropdown, 0); + } +} + + // ============================================================ // onOpenClick // ============================================================ @@ -459,6 +1062,21 @@ static void onRunClick(WidgetT *w) { compileAndRun(); } + +// ============================================================ +// onStopClick +// ============================================================ + +static void onStopClick(WidgetT *w) { + (void)w; + + if (sVm) { + sVm->running = false; + setStatus("Program stopped."); + } +} + + // ============================================================ // printCallback // ============================================================ @@ -483,6 +1101,11 @@ static void printCallback(void *ctx, const char *text, bool newline) { } sOutputBuf[sOutputLen] = '\0'; + + // Update the output textarea immediately so PRINT is visible + if (sOutput) { + wgtSetText(sOutput, sOutputBuf); + } } // ============================================================ @@ -494,3 +1117,118 @@ static void setStatus(const char *text) { wgtSetText(sStatus, text); } } + + +// ============================================================ +// updateDropdowns +// ============================================================ +// +// Scan the source for SUB/FUNCTION declarations and populate +// the Object and Event dropdowns. Procedure names are split on +// '_' into ObjectName and EventName (e.g. "Command1_Click"). + +static void updateDropdowns(void) { + sProcCount = 0; + + if (!sEditor || !sObjDropdown || !sEvtDropdown) { + return; + } + + const char *src = wgtGetText(sEditor); + + if (!src) { + return; + } + + // Scan line by line for SUB / FUNCTION + const char *pos = src; + int32_t lineNum = 1; + + while (*pos) { + // Skip leading whitespace + while (*pos == ' ' || *pos == '\t') { + pos++; + } + + // Check for SUB or FUNCTION keyword + bool isSub = (strncasecmp(pos, "SUB ", 4) == 0); + bool isFunc = (strncasecmp(pos, "FUNCTION ", 9) == 0); + + if ((isSub || isFunc) && sProcCount < IDE_MAX_PROCS) { + pos += isSub ? 4 : 9; + + // Skip whitespace after keyword + while (*pos == ' ' || *pos == '\t') { + pos++; + } + + // Extract procedure name + char procName[64]; + int32_t nameLen = 0; + + while (*pos && *pos != '(' && *pos != ' ' && *pos != '\t' && *pos != '\n' && *pos != '\r' && nameLen < 63) { + procName[nameLen++] = *pos++; + } + + procName[nameLen] = '\0'; + + // Split on '_' into object + event + char *underscore = strchr(procName, '_'); + + if (underscore) { + int32_t objLen = (int32_t)(underscore - procName); + + if (objLen > 63) { + objLen = 63; + } + + memcpy(sProcTable[sProcCount].objName, procName, objLen); + sProcTable[sProcCount].objName[objLen] = '\0'; + snprintf(sProcTable[sProcCount].evtName, sizeof(sProcTable[sProcCount].evtName), "%s", underscore + 1); + } else { + snprintf(sProcTable[sProcCount].objName, sizeof(sProcTable[sProcCount].objName), "%s", "(General)"); + snprintf(sProcTable[sProcCount].evtName, sizeof(sProcTable[sProcCount].evtName), "%s", procName); + } + + sProcTable[sProcCount].lineNum = lineNum; + sProcCount++; + } + + // Advance to end of line + while (*pos && *pos != '\n') { + pos++; + } + + if (*pos == '\n') { + pos++; + } + + lineNum++; + } + + // Build unique object names for the Object dropdown + const char *objItems[IDE_MAX_PROCS]; + int32_t objCount = 0; + + for (int32_t i = 0; i < sProcCount; i++) { + bool found = false; + + for (int32_t j = 0; j < objCount; j++) { + if (strcasecmp(objItems[j], sProcTable[i].objName) == 0) { + found = true; + break; + } + } + + if (!found && objCount < IDE_MAX_PROCS) { + objItems[objCount++] = sProcTable[i].objName; + } + } + + wgtDropdownSetItems(sObjDropdown, objItems, objCount); + + if (objCount > 0) { + wgtDropdownSetSelected(sObjDropdown, 0); + onObjDropdownChange(sObjDropdown); + } +} diff --git a/dvxbasic/runtime/values.c b/dvxbasic/runtime/values.c index 1615e8a..6768493 100644 --- a/dvxbasic/runtime/values.c +++ b/dvxbasic/runtime/values.c @@ -106,9 +106,11 @@ BasStringT *basStringNew(const char *text, int32_t len) { } BasStringT *s = basStringAlloc(len + 1); - memcpy(s->data, text, len); - s->data[len] = '\0'; - s->len = len; + if (s->cap >= len + 1) { + memcpy(s->data, text, len); + s->data[len] = '\0'; + s->len = len; + } return s; } diff --git a/dvxbasic/runtime/values.h b/dvxbasic/runtime/values.h index 7f49088..5ce7898 100644 --- a/dvxbasic/runtime/values.h +++ b/dvxbasic/runtime/values.h @@ -131,6 +131,7 @@ struct BasValueTag { BasArrayT *arrVal; // BAS_TYPE_ARRAY (ref-counted) BasUdtT *udtVal; // BAS_TYPE_UDT (ref-counted) void *objVal; // BAS_TYPE_OBJECT (opaque host pointer) + BasValueT *refVal; // BAS_TYPE_REF (ByRef pointer to variable slot) }; }; diff --git a/dvxbasic/runtime/vm.c b/dvxbasic/runtime/vm.c index bd396fa..36315f0 100644 --- a/dvxbasic/runtime/vm.c +++ b/dvxbasic/runtime/vm.c @@ -37,6 +37,74 @@ static uint16_t readUint16(BasVmT *vm); static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg); +// ============================================================ +// basVmCallSub +// ============================================================ + +bool basVmCallSub(BasVmT *vm, int32_t codeAddr) { + if (!vm || !vm->module) { + return false; + } + + if (codeAddr < 0 || codeAddr >= vm->module->codeLen) { + return false; + } + + if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE - 1) { + return false; + } + + // Save VM state + int32_t savedPc = vm->pc; + int32_t savedCallDepth = vm->callDepth; + bool savedRunning = vm->running; + + // Push a call frame that returns to an invalid address (sentinel) + // We detect completion when callDepth drops back to savedCallDepth + BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; + frame->returnPc = savedPc; + frame->localCount = BAS_VM_MAX_LOCALS; + memset(frame->locals, 0, sizeof(frame->locals)); + + // Jump to the SUB + vm->pc = codeAddr; + vm->running = true; + + // Step until the SUB returns (callDepth drops back) + int32_t steps = 0; + + while (vm->running && vm->callDepth > savedCallDepth) { + if (vm->stepLimit > 0 && steps >= vm->stepLimit) { + // Unwind the call and restore state + vm->callDepth = savedCallDepth; + vm->pc = savedPc; + vm->running = savedRunning; + return false; + } + + BasVmResultE result = basVmStep(vm); + steps++; + + if (result == BAS_VM_HALTED) { + break; + } + + if (result != BAS_VM_OK) { + // Runtime error in the event handler + vm->pc = savedPc; + vm->callDepth = savedCallDepth; + vm->running = savedRunning; + return false; + } + } + + // Restore VM state + vm->pc = savedPc; + vm->running = savedRunning; + return true; +} + + // ============================================================ // basVmCreate // ============================================================ @@ -169,22 +237,29 @@ void basVmReset(BasVmT *vm) { // ============================================================ BasVmResultE basVmRun(BasVmT *vm) { - vm->running = true; - vm->yielded = false; + vm->running = true; + vm->yielded = false; + vm->stepCount = 0; while (vm->running) { + // Check step limit + if (vm->stepLimit > 0 && vm->stepCount >= vm->stepLimit) { + return BAS_VM_STEP_LIMIT; + } + // Save PC before each instruction for RESUME support int32_t savedPc = vm->pc; BasVmResultE result = basVmStep(vm); + vm->stepCount++; if (result != BAS_VM_OK) { // If an error handler is set and this is a trappable error, // jump to the handler instead of stopping execution if (vm->errorHandler != 0 && !vm->inErrorHandler && result != BAS_VM_HALTED && result != BAS_VM_BAD_OPCODE) { - vm->errorPc = savedPc; - vm->errorNextPc = vm->pc; + vm->errorPc = savedPc; + vm->errorNextPc = vm->pc; vm->inErrorHandler = true; - vm->pc = vm->errorHandler; + vm->pc = vm->errorHandler; continue; } @@ -216,6 +291,15 @@ void basVmSetCurrentForm(BasVmT *vm, void *formRef) { } +// ============================================================ +// basVmSetStepLimit +// ============================================================ + +void basVmSetStepLimit(BasVmT *vm, int32_t limit) { + vm->stepLimit = limit; +} + + // ============================================================ // basVmSetInputCallback // ============================================================ @@ -380,7 +464,11 @@ BasVmResultE basVmStep(BasVmT *vm) { return BAS_VM_ERROR; } - if (!push(vm, basValCopy(frame->locals[idx]))) { + // ByRef: if the local holds a reference, dereference it + BasValueT *slot = &frame->locals[idx]; + BasValueT val = (slot->type == BAS_TYPE_REF) ? *slot->refVal : *slot; + + if (!push(vm, basValCopy(val))) { return BAS_VM_STACK_OVERFLOW; } @@ -402,8 +490,17 @@ BasVmResultE basVmStep(BasVmT *vm) { return BAS_VM_ERROR; } - basValRelease(&frame->locals[idx]); - frame->locals[idx] = val; + // ByRef: if the local holds a reference, store through it + BasValueT *slot = &frame->locals[idx]; + + if (slot->type == BAS_TYPE_REF) { + basValRelease(slot->refVal); + *slot->refVal = val; + } else { + basValRelease(slot); + *slot = val; + } + break; } @@ -441,6 +538,81 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } + case OP_PUSH_LOCAL_ADDR: { + uint16_t idx = readUint16(vm); + BasCallFrameT *frame = currentFrame(vm); + + if (!frame || idx >= (uint16_t)frame->localCount) { + runtimeError(vm, 9, "Invalid local variable index"); + return BAS_VM_ERROR; + } + + BasValueT ref; + ref.type = BAS_TYPE_REF; + ref.refVal = &frame->locals[idx]; + + if (!push(vm, ref)) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_PUSH_GLOBAL_ADDR: { + uint16_t idx = readUint16(vm); + + if (idx >= BAS_VM_MAX_GLOBALS) { + runtimeError(vm, 9, "Invalid global variable index"); + return BAS_VM_ERROR; + } + + BasValueT ref; + ref.type = BAS_TYPE_REF; + ref.refVal = &vm->globals[idx]; + + if (!push(vm, ref)) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_LOAD_REF: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + + if (top->type != BAS_TYPE_REF || !top->refVal) { + runtimeError(vm, 9, "Expected reference"); + return BAS_VM_ERROR; + } + + BasValueT val = basValCopy(*top->refVal); + *top = val; + break; + } + + case OP_STORE_REF: { + BasValueT val; + BasValueT ref; + + if (!pop(vm, &val) || !pop(vm, &ref)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (ref.type != BAS_TYPE_REF || !ref.refVal) { + basValRelease(&val); + runtimeError(vm, 9, "Expected reference"); + return BAS_VM_ERROR; + } + + basValRelease(ref.refVal); + *ref.refVal = val; + break; + } + // ============================================================ // Arithmetic // ============================================================ @@ -769,6 +941,18 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } + case OP_FOR_POP: { + if (vm->forDepth <= 0) { + runtimeError(vm, 1, "FOR stack underflow"); + return BAS_VM_ERROR; + } + + BasForStateT *fs = &vm->forStack[--vm->forDepth]; + basValRelease(&fs->limit); + basValRelease(&fs->step); + break; + } + // ============================================================ // Type conversion // ============================================================ @@ -857,6 +1041,18 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } + case OP_CONV_LONG_INT: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + BasValueT conv = basValToInteger(*top); + basValRelease(top); + *top = conv; + break; + } + // ============================================================ // I/O // ============================================================ @@ -1072,7 +1268,7 @@ BasVmResultE basVmStep(BasVmT *vm) { // Scientific notation char sciFmt[32]; int32_t decimals = hasDecimal ? digitsAfter : 0; - snprintf(sciFmt, sizeof(sciFmt), "%%.%dE", decimals); + snprintf(sciFmt, sizeof(sciFmt), "%%.%dE", (int)decimals); snprintf(buf, sizeof(buf), sciFmt, n); } else { // Standard formatting @@ -1081,7 +1277,7 @@ BasVmResultE basVmStep(BasVmT *vm) { int32_t decimals = hasDecimal ? digitsAfter : 0; char numBuf[128]; - snprintf(numBuf, sizeof(numBuf), "%.*f", decimals, absN); + snprintf(numBuf, sizeof(numBuf), "%.*f", (int)decimals, absN); // Split into integer and decimal parts char intPart[128]; @@ -1181,15 +1377,38 @@ BasVmResultE basVmStep(BasVmT *vm) { } case OP_INPUT: { + // Pop the prompt string pushed by the parser + BasValueT promptVal; + + if (!pop(vm, &promptVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + // Build the full prompt: "user prompt? " + char promptBuf[512]; + const char *userPrompt = ""; + + if (promptVal.type == BAS_TYPE_STRING && promptVal.strVal) { + userPrompt = promptVal.strVal->data; + } + + if (userPrompt[0]) { + snprintf(promptBuf, sizeof(promptBuf), "%s? ", userPrompt); + } else { + snprintf(promptBuf, sizeof(promptBuf), "? "); + } + char buf[1024]; buf[0] = '\0'; if (vm->inputFn) { - if (!vm->inputFn(vm->inputCtx, "? ", buf, sizeof(buf))) { + if (!vm->inputFn(vm->inputCtx, promptBuf, buf, sizeof(buf))) { buf[0] = '\0'; } } + basValRelease(&promptVal); + if (!push(vm, basValStringFromC(buf))) { return BAS_VM_STACK_OVERFLOW; } @@ -1233,15 +1452,25 @@ BasVmResultE basVmStep(BasVmT *vm) { } case OP_STR_STRF: { + // STR$(n): VB convention -- leading space for positive, "-" for negative if (vm->sp < 1) { return BAS_VM_STACK_UNDERFLOW; } BasValueT *top = &vm->stack[vm->sp - 1]; - BasStringT *s = basValFormatString(*top); + double n = basValToNumber(*top); basValRelease(top); + + char buf[64]; + + if (n >= 0.0) { + snprintf(buf, sizeof(buf), " %g", n); + } else { + snprintf(buf, sizeof(buf), "%g", n); + } + top->type = BAS_TYPE_STRING; - top->strVal = s; + top->strVal = basStringNew(buf, (int32_t)strlen(buf)); break; } @@ -1324,7 +1553,7 @@ BasVmResultE basVmStep(BasVmT *vm) { // Push DATE$ as "MM-DD-YYYY" time_t now = time(NULL); struct tm *t = localtime(&now); - char buf[16]; + char buf[32]; snprintf(buf, sizeof(buf), "%02d-%02d-%04d", t->tm_mon + 1, t->tm_mday, t->tm_year + 1900); if (!push(vm, basValStringFromC(buf))) { @@ -1463,6 +1692,19 @@ BasVmResultE basVmStep(BasVmT *vm) { vm->inErrorHandler = false; break; + case OP_RAISE_ERR: { + BasValueT errVal; + + if (!pop(vm, &errVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t errNum = (int32_t)basValToNumber(errVal); + basValRelease(&errVal); + runtimeError(vm, errNum, "User-defined error"); + return BAS_VM_ERROR; + } + // ============================================================ // Array / UDT operations // ============================================================ @@ -1965,7 +2207,7 @@ BasVmResultE basVmStep(BasVmT *vm) { char numBuf[128]; if (hasDecimal) { - snprintf(numBuf, sizeof(numBuf), "%.*f", decimals, absN); + snprintf(numBuf, sizeof(numBuf), "%.*f", (int)decimals, absN); } else { snprintf(numBuf, sizeof(numBuf), "%.0f", absN); } @@ -2227,6 +2469,11 @@ BasVmResultE basVmStep(BasVmT *vm) { basValRelease(&sv); } + // Set as current form for subsequent FIND_CTRL calls + if (formRef) { + vm->currentForm = formRef; + } + basValRelease(&nameVal); push(vm, basValObject(formRef)); break; @@ -2376,9 +2623,11 @@ BasVmResultE basVmStep(BasVmT *vm) { void *ctrlRef = NULL; - if (vm->ui.findCtrl && formVal.type == BAS_TYPE_OBJECT) { + if (vm->ui.findCtrl) { + // Use current form if formRef is NULL or not an object + void *formRef = (formVal.type == BAS_TYPE_OBJECT) ? formVal.objVal : vm->currentForm; BasValueT sv = basValToString(nameVal); - ctrlRef = vm->ui.findCtrl(vm->ui.ctx, formVal.objVal, sv.strVal->data); + ctrlRef = vm->ui.findCtrl(vm->ui.ctx, formRef, sv.strVal->data); basValRelease(&sv); } @@ -2548,6 +2797,33 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) { return BAS_VM_STACK_UNDERFLOW; } + // VB behavior: + on two strings concatenates + if ((op == OP_ADD_INT || op == OP_ADD_FLT) && a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) { + BasStringT *sa = a.strVal ? a.strVal : basStringNew("", 0); + BasStringT *sb = b.strVal ? b.strVal : basStringNew("", 0); + int32_t newLen = sa->len + sb->len; + BasStringT *cat = basStringNew("", 0); + + if (newLen > 0) { + basStringUnref(cat); + char *buf = (char *)malloc(newLen + 1); + memcpy(buf, sa->data, sa->len); + memcpy(buf + sa->len, sb->data, sb->len); + buf[newLen] = '\0'; + cat = basStringNew(buf, newLen); + free(buf); + } + + basValRelease(&a); + basValRelease(&b); + + BasValueT result; + result.type = BAS_TYPE_STRING; + result.strVal = cat; + push(vm, result); + return BAS_VM_OK; + } + double na = basValToNumber(a); double nb = basValToNumber(b); basValRelease(&a); @@ -3630,6 +3906,40 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) { return BAS_VM_OK; } + case OP_STR_INSTR3: { + // INSTR(start, string, find) + BasValueT findVal; + BasValueT sVal; + BasValueT startVal; + + if (!pop(vm, &findVal) || !pop(vm, &sVal) || !pop(vm, &startVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t startPos = (int32_t)basValToNumber(startVal) - 1; // 0-based + basValRelease(&startVal); + + BasValueT sv = basValToString(sVal); + BasValueT fv = basValToString(findVal); + basValRelease(&sVal); + basValRelease(&findVal); + + int32_t pos = 0; + + if (startPos >= 0 && startPos < sv.strVal->len) { + char *found = strstr(sv.strVal->data + startPos, fv.strVal->data); + + if (found) { + pos = (int32_t)(found - sv.strVal->data) + 1; // 1-based + } + } + + basValRelease(&sv); + basValRelease(&fv); + push(vm, basValInteger((int16_t)pos)); + return BAS_VM_OK; + } + case OP_STR_UCASE: case OP_STR_LCASE: case OP_STR_TRIM: @@ -3908,5 +4218,5 @@ static uint16_t readUint16(BasVmT *vm) { static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg) { vm->errorNumber = errNum; - snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d at PC %d: %s", errNum, vm->pc, msg); + snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d at PC %d: %s", (int)errNum, (int)vm->pc, msg); } diff --git a/dvxbasic/runtime/vm.h b/dvxbasic/runtime/vm.h index 34a18e7..4737881 100644 --- a/dvxbasic/runtime/vm.h +++ b/dvxbasic/runtime/vm.h @@ -48,7 +48,8 @@ typedef enum { BAS_VM_BAD_OPCODE, BAS_VM_FILE_ERROR, BAS_VM_SUBSCRIPT_RANGE, - BAS_VM_USER_ERROR // ON ERROR raised + BAS_VM_USER_ERROR, // ON ERROR raised + BAS_VM_STEP_LIMIT // step limit reached (not an error) } BasVmResultE; // ============================================================ @@ -205,19 +206,35 @@ typedef struct { int32_t mode; // 0=closed, 1=input, 2=output, 3=append, 4=random, 5=binary } BasFileChannelT; +// ============================================================ +// Procedure table entry (retained from symbol table for runtime) +// ============================================================ + +#define BAS_MAX_PROC_NAME 64 + +typedef struct { + char name[BAS_MAX_PROC_NAME]; // SUB/FUNCTION name (case-preserved) + int32_t codeAddr; // entry point in code[] + int32_t paramCount; // number of parameters + uint8_t returnType; // BAS_TYPE_* (0 for SUB) + bool isFunction; // true = FUNCTION, false = SUB +} BasProcEntryT; + // ============================================================ // Compiled module (output of the compiler) // ============================================================ typedef struct { - uint8_t *code; // p-code bytecode - int32_t codeLen; - BasStringT **constants; // string constant pool - int32_t constCount; - int32_t globalCount; // number of global variable slots needed - int32_t entryPoint; // PC of the first instruction (module-level code) - BasValueT *dataPool; // DATA statement value pool - int32_t dataCount; // number of values in the data pool + uint8_t *code; // p-code bytecode + int32_t codeLen; + BasStringT **constants; // string constant pool + int32_t constCount; + int32_t globalCount; // number of global variable slots needed + int32_t entryPoint; // PC of the first instruction (module-level code) + BasValueT *dataPool; // DATA statement value pool + int32_t dataCount; // number of values in the data pool + BasProcEntryT *procs; // procedure table (SUBs and FUNCTIONs) + int32_t procCount; } BasModuleT; // ============================================================ @@ -232,6 +249,8 @@ typedef struct { int32_t pc; // program counter bool running; bool yielded; + int32_t stepLimit; // max steps per basVmRun (0 = unlimited) + int32_t stepCount; // steps executed in last basVmRun // Evaluation stack BasValueT stack[BAS_VM_STACK_SIZE]; @@ -325,6 +344,11 @@ void basVmSetExternCallbacks(BasVmT *vm, const BasExternCallbacksT *ext); // Set the current form context (called by host during event dispatch). void basVmSetCurrentForm(BasVmT *vm, void *formRef); +// Set the step limit for basVmRun. 0 = unlimited (default). +// When the limit is reached, basVmRun returns BAS_VM_STEP_LIMIT. +// The VM remains in a runnable state -- call basVmRun again to continue. +void basVmSetStepLimit(BasVmT *vm, int32_t limit); + // Push/pop values on the evaluation stack (for host integration). bool basVmPush(BasVmT *vm, BasValueT val); bool basVmPop(BasVmT *vm, BasValueT *val); @@ -332,4 +356,10 @@ bool basVmPop(BasVmT *vm, BasValueT *val); // Get the current error message. const char *basVmGetError(const BasVmT *vm); +// Call a SUB by code address from the host. +// Pushes a call frame, runs until the SUB returns, then restores +// the previous execution state. Returns true if the SUB was called +// and returned normally, false on error or if the VM was not idle. +bool basVmCallSub(BasVmT *vm, int32_t codeAddr); + #endif // DVXBASIC_VM_H diff --git a/dvxbasic/samples/clickme.bas b/dvxbasic/samples/clickme.bas new file mode 100644 index 0000000..9b0e19b --- /dev/null +++ b/dvxbasic/samples/clickme.bas @@ -0,0 +1,29 @@ +' clickme.bas -- DVX BASIC click test +' +' Demonstrates form loading, control properties, and event handling. +' Load this file in DVX BASIC, then click Run. + +DIM clickCount AS INTEGER +clickCount = 0 + +Load ClickMe +ClickMe.Show + +PRINT "Form loaded. Click the button!" + +' Event loop -- keeps the program alive for events. +' The IDE yields to DVX between VM slices, so widget +' events fire during DoEvents pauses. +DO + DoEvents +LOOP + +SUB Command1_Click + clickCount = clickCount + 1 + Text1.Text = "Clicked " + STR$(clickCount) + " times!" + Label1.Caption = "Keep clicking!" +END SUB + +SUB ClickMe_Load + PRINT "Form_Load fired!" +END SUB diff --git a/dvxbasic/samples/clickme.frm b/dvxbasic/samples/clickme.frm new file mode 100644 index 0000000..82710f3 --- /dev/null +++ b/dvxbasic/samples/clickme.frm @@ -0,0 +1,12 @@ +Begin Form ClickMe + Caption = "DVX BASIC Demo" + Begin CommandButton Command1 + Caption = "Click Me!" + End + Begin TextBox Text1 + Text = "Ready." + End + Begin Label Label1 + Caption = "Click the button above!" + End +End diff --git a/dvxbasic/samples/formtest.bas b/dvxbasic/samples/formtest.bas new file mode 100644 index 0000000..ef85484 --- /dev/null +++ b/dvxbasic/samples/formtest.bas @@ -0,0 +1,8 @@ +' formtest.bas -- DVX BASIC form test +' +' Demonstrates form creation and MsgBox. + +Load MyForm +MyForm.Show +MsgBox "Hello from DVX BASIC!" +PRINT "Form test complete." diff --git a/dvxbasic/samples/input.bas b/dvxbasic/samples/input.bas new file mode 100644 index 0000000..18fd316 --- /dev/null +++ b/dvxbasic/samples/input.bas @@ -0,0 +1,13 @@ +' input.bas -- Test INPUT statement +' +' Demonstrates the INPUT dialog. + +DIM name AS STRING +DIM age AS INTEGER + +INPUT "What is your name"; name +PRINT "Hello, " + name + "!" + +INPUT "How old are you"; age +PRINT "You are" + STR$(age) + " years old." +PRINT "Done!" diff --git a/dvxbasic/test_compiler b/dvxbasic/test_compiler index feb8610e253a0b60c4924f92f6c23dcbcb773b6c..7ca74da93ec699ff62d614a20c1a694631a631a9 100755 GIT binary patch literal 167992 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 e71uZ!m*vs*YScxDprocCount); + + for (int32_t i = 0; i < mod->procCount; i++) { + printf(" [%d] %s addr=%d params=%d func=%d\n", + i, mod->procs[i].name, mod->procs[i].codeAddr, + mod->procs[i].paramCount, mod->procs[i].isFunction); + } + + // Test lookup + const BasProcEntryT *p = basModuleFindProc(mod, "command1_click"); + + if (p) { + printf("Found Command1_Click at addr %d\n", p->codeAddr); + } else { + printf("FAIL: Command1_Click not found!\n"); + } + + p = basModuleFindProc(mod, "FORM1_LOAD"); + + if (p) { + printf("Found Form1_Load at addr %d\n", p->codeAddr); + } else { + printf("FAIL: Form1_Load not found!\n"); + } + + // Test basVmCallSub + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, mod); + vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount; + vm->callDepth = 1; + + p = basModuleFindProc(mod, "Command1_Click"); + + if (p) { + printf("Calling Command1_Click via basVmCallSub: "); + bool callOk = basVmCallSub(vm, p->codeAddr); + printf("result=%s\n", callOk ? "ok" : "FAIL"); + } + + basVmDestroy(vm); + basModuleFree(mod); + } + + basParserFree(&parser); + printf("\n"); + } + + // ByRef parameters + runProgram("ByRef parameters", + "DECLARE SUB AddTen(n AS INTEGER)\n" + "DECLARE SUB ChangeStr(s AS STRING)\n" + "DECLARE SUB NoChange(BYVAL n AS INTEGER)\n" + "\n" + "DIM x AS INTEGER\n" + "x = 10\n" + "PRINT \"Before: x =\"; x\n" + "AddTen x\n" + "PRINT \"After AddTen: x =\"; x\n" + "\n" + "DIM a AS STRING\n" + "a = \"hello\"\n" + "PRINT \"Before: a = \"; a\n" + "ChangeStr a\n" + "PRINT \"After ChangeStr: a = \"; a\n" + "\n" + "' ByVal should NOT modify caller\n" + "DIM y AS INTEGER\n" + "y = 100\n" + "PRINT \"Before: y =\"; y\n" + "NoChange y\n" + "PRINT \"After NoChange: y =\"; y\n" + "\n" + "' Expression arg to ByRef param: effectively ByVal\n" + "DIM z AS INTEGER\n" + "z = 5\n" + "AddTen(z + 0)\n" + "PRINT \"After AddTen(z+0): z =\"; z\n" + "\n" + "SUB AddTen(n AS INTEGER)\n" + " n = n + 10\n" + "END SUB\n" + "\n" + "SUB ChangeStr(s AS STRING)\n" + " s = s + \" world\"\n" + "END SUB\n" + "\n" + "SUB NoChange(BYVAL n AS INTEGER)\n" + " n = n + 999\n" + "END SUB\n" + ); + + // ============================================================ + // Coverage: String functions + // ============================================================ + + runProgram("LCASE$", + "PRINT LCASE$(\"HELLO WORLD\")\n" + ); + // Expected: hello world + + runProgram("TRIM$ LTRIM$ RTRIM$", + "PRINT \"[\" & TRIM$(\" hi \") & \"]\"\n" + "PRINT \"[\" & LTRIM$(\" hi \") & \"]\"\n" + "PRINT \"[\" & RTRIM$(\" hi \") & \"]\"\n" + ); + // Expected: [hi] / [hi ] / [ hi] + + runProgram("INSTR 2-arg", + "PRINT INSTR(\"hello world\", \"world\")\n" + "PRINT INSTR(\"hello world\", \"xyz\")\n" + ); + // Expected: 7 / 0 + + runProgram("INSTR 3-arg", + "PRINT INSTR(5, \"abcabc\", \"bc\")\n" + "PRINT INSTR(1, \"abcabc\", \"bc\")\n" + ); + // Expected: 5 / 2 + + runProgram("CHR$ and ASC", + "PRINT CHR$(65)\n" + "PRINT ASC(\"Z\")\n" + ); + // Expected: A / 90 + + runProgram("SPACE$", + "PRINT \"[\" & SPACE$(5) & \"]\"\n" + ); + // Expected: [ ] + + runProgram("STRING$", + "PRINT STRING$(5, \"*\")\n" + ); + // Expected: ***** + + runProgram("HEX$", + "PRINT HEX$(255)\n" + "PRINT HEX$(16)\n" + ); + // Expected: FF / 10 + + runProgram("VAL", + "PRINT VAL(\"3.14\")\n" + "PRINT VAL(\"42\")\n" + "PRINT VAL(\"abc\")\n" + ); + // Expected: 3.14 / 42 / 0 + + runProgram("STR$", + "PRINT \"[\" & STR$(42) & \"]\"\n" + "PRINT \"[\" & STR$(-7) & \"]\"\n" + ); + // Expected: [ 42] / [-7] + + runProgram("MID$ 2-arg", + "PRINT MID$(\"hello world\", 7)\n" + ); + // Expected: world + + runProgram("String concat with +", + "DIM a AS STRING\n" + "DIM b AS STRING\n" + "a = \"hello\"\n" + "b = \" world\"\n" + "PRINT a + b\n" + ); + // Expected: hello world + + // ============================================================ + // Coverage: Math functions + // ============================================================ + + runProgram("Trig functions", + "DIM pi AS DOUBLE\n" + "pi = ATN(1) * 4\n" + "PRINT INT(SIN(pi / 2) * 1000)\n" + "PRINT INT(COS(0) * 1000)\n" + "PRINT INT(TAN(pi / 4) * 1000)\n" + ); + // Expected: 1000 / 1000 / 1000 + + runProgram("LOG and EXP", + "PRINT INT(LOG(1))\n" + "PRINT INT(EXP(0))\n" + "PRINT INT(EXP(1) * 100)\n" + ); + // Expected: 0 / 1 / 271 + + runProgram("FIX and SGN", + "PRINT FIX(3.7)\n" + "PRINT FIX(-3.7)\n" + "PRINT SGN(42)\n" + "PRINT SGN(-5)\n" + "PRINT SGN(0)\n" + ); + // Expected: 3 / -3 / 1 / -1 / 0 + + runProgram("RND and RANDOMIZE", + "RANDOMIZE 12345\n" + "DIM r AS DOUBLE\n" + "r = RND\n" + "IF r >= 0 AND r < 1 THEN PRINT \"ok\"\n" + ); + // Expected: ok + + // ============================================================ + // Coverage: Loop variants + // ============================================================ + + runProgram("DO UNTIL pre-test", + "DIM n AS INTEGER\n" + "n = 1\n" + "DO UNTIL n > 5\n" + " PRINT n;\n" + " n = n + 1\n" + "LOOP\n" + "PRINT\n" + ); + // Expected: 1 2 3 4 5 + + runProgram("DO LOOP WHILE post-test", + "DIM n AS INTEGER\n" + "n = 1\n" + "DO\n" + " PRINT n;\n" + " n = n + 1\n" + "LOOP WHILE n <= 5\n" + "PRINT\n" + ); + // Expected: 1 2 3 4 5 + + runProgram("DO LOOP UNTIL post-test", + "DIM n AS INTEGER\n" + "n = 1\n" + "DO\n" + " PRINT n;\n" + " n = n + 1\n" + "LOOP UNTIL n > 5\n" + "PRINT\n" + ); + // Expected: 1 2 3 4 5 + + runProgram("WHILE WEND", + "DIM n AS INTEGER\n" + "n = 1\n" + "WHILE n <= 5\n" + " PRINT n;\n" + " n = n + 1\n" + "WEND\n" + "PRINT\n" + ); + // Expected: 1 2 3 4 5 + + runProgram("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" + ); + // Expected: 5 4 3 2 1 + + runProgram("FOR with STEP 2", + "DIM i AS INTEGER\n" + "FOR i = 0 TO 10 STEP 2\n" + " PRINT i;\n" + "NEXT i\n" + "PRINT\n" + ); + // Expected: 0 2 4 6 8 10 + + // ============================================================ + // Coverage: EXIT statements + // ============================================================ + + runProgram("EXIT FOR", + "DIM i AS INTEGER\n" + "FOR i = 1 TO 100\n" + " IF i = 3 THEN EXIT FOR\n" + " PRINT i;\n" + "NEXT i\n" + "PRINT\n" + "PRINT \"after\"\n" + ); + // Expected: 1 2 / after + + runProgram("EXIT DO", + "DIM n AS INTEGER\n" + "n = 0\n" + "DO\n" + " n = n + 1\n" + " IF n = 4 THEN EXIT DO\n" + " PRINT n;\n" + "LOOP\n" + "PRINT\n" + "PRINT \"after\"\n" + ); + // Expected: 1 2 3 / after + + runProgram("EXIT SUB", + "DECLARE SUB EarlyReturn(n AS INTEGER)\n" + "EarlyReturn 1\n" + "EarlyReturn 5\n" + "EarlyReturn 2\n" + "SUB EarlyReturn(n AS INTEGER)\n" + " IF n > 3 THEN EXIT SUB\n" + " PRINT n;\n" + "END SUB\n" + "PRINT\n" + ); + // Expected: 1 2 + + runProgram("EXIT FUNCTION", + "DECLARE FUNCTION Clamp(n AS INTEGER) AS INTEGER\n" + "PRINT Clamp(5)\n" + "PRINT Clamp(200)\n" + "FUNCTION Clamp(n AS INTEGER) AS INTEGER\n" + " IF n > 100 THEN\n" + " Clamp = 100\n" + " EXIT FUNCTION\n" + " END IF\n" + " Clamp = n\n" + "END FUNCTION\n" + ); + // Expected: 5 / 100 + + // ============================================================ + // Coverage: CONST + // ============================================================ + + runProgram("CONST", + "CONST PI = 3.14159\n" + "CONST MAX_SIZE = 100\n" + "CONST GREETING = \"hello\"\n" + "PRINT INT(PI * 100)\n" + "PRINT MAX_SIZE\n" + "PRINT GREETING\n" + ); + // Expected: 314 / 100 / hello + + // ============================================================ + // Coverage: END statement + // ============================================================ + + runProgram("END statement", + "PRINT \"before\"\n" + "END\n" + "PRINT \"after\"\n" + ); + // Expected: before + + // ============================================================ + // Coverage: Boolean literals + // ============================================================ + + runProgram("True and False", + "DIM b AS BOOLEAN\n" + "b = True\n" + "IF b THEN PRINT \"yes\"\n" + "b = False\n" + "IF NOT b THEN PRINT \"no\"\n" + "PRINT True\n" + "PRINT False\n" + ); + // Expected: yes / no / -1 / 0 + + // ============================================================ + // Coverage: NOT operator + // ============================================================ + + runProgram("NOT operator", + "PRINT NOT 0\n" + "PRINT NOT -1\n" + "DIM x AS INTEGER\n" + "x = 5\n" + "IF NOT (x > 10) THEN PRINT \"small\"\n" + ); + // Expected: -1 / 0 / small + + // ============================================================ + // Coverage: AND OR XOR bitwise + // ============================================================ + + runProgram("Bitwise AND OR XOR", + "PRINT 15 AND 9\n" + "PRINT 12 OR 3\n" + "PRINT 15 XOR 9\n" + ); + // Expected: 9 / 15 / 6 + + // ============================================================ + // Coverage: SLEEP + // ============================================================ + + runProgram("SLEEP", + "SLEEP 0\n" + "PRINT \"ok\"\n" + ); + // Expected: ok + + // ============================================================ + // Coverage: Error recovery + // ============================================================ + + runProgram("RESUME NEXT", + "ON ERROR GOTO handler\n" + "DIM x AS INTEGER\n" + "x = 10 / 0\n" + "PRINT \"resumed\"\n" + "END\n" + "handler:\n" + "RESUME NEXT\n" + ); + // Expected: resumed + + runProgram("RAISE ERROR", + "ON ERROR GOTO handler\n" + "ERROR 999\n" + "PRINT \"should not print\"\n" + "END\n" + "handler:\n" + "PRINT \"caught error\"; ERR\n" + ); + // Expected: caught error 999 + + // ============================================================ + // Coverage: Type suffixes and hex literals + // ============================================================ + + runProgram("Type suffixes", + "DIM x%\n" + "DIM s$\n" + "x% = 42\n" + "s$ = \"hello\"\n" + "PRINT x%\n" + "PRINT s$\n" + ); + // Expected: 42 / hello + + runProgram("Hex literals", + "PRINT &HFF\n" + "PRINT &H10\n" + "DIM x AS INTEGER\n" + "x = &H0A\n" + "PRINT x\n" + ); + // Expected: 255 / 16 / 10 + + // ============================================================ + // Coverage: Long integer type + // ============================================================ + + runProgram("Long integer", + "DIM x AS LONG\n" + "x = 100000\n" + "x = x * 2\n" + "PRINT x\n" + ); + // Expected: 200000 + + // ============================================================ + // Coverage: REDIM without PRESERVE + // ============================================================ + + runProgram("REDIM no PRESERVE", + "DIM a(3) AS INTEGER\n" + "a(1) = 99\n" + "REDIM a(5) AS INTEGER\n" + "PRINT a(1)\n" + ); + // Expected: 0 (data cleared) + + // ============================================================ + // Coverage: PRINT variants + // ============================================================ + + runProgram("PRINT comma separator", + "PRINT 1, 2, 3\n" + ); + // Expected: 123 + + runProgram("PRINT bare newline", + "PRINT \"a\"\n" + "PRINT\n" + "PRINT \"b\"\n" + ); + // Expected: a / (blank) / b + + // ============================================================ + // Coverage: LET keyword + // ============================================================ + + runProgram("LET keyword", + "DIM x AS INTEGER\n" + "LET x = 42\n" + "PRINT x\n" + ); + // Expected: 42 + + // ============================================================ + // Coverage: Line continuation + // ============================================================ + + runProgram("Line continuation", + "DIM x AS INTEGER\n" + "x = 1 + _\n" + " 2 + _\n" + " 3\n" + "PRINT x\n" + ); + // Expected: 6 + + // ============================================================ + // Coverage: REM comment + // ============================================================ + + runProgram("REM comment", + "REM This is a comment\n" + "PRINT \"ok\"\n" + "PRINT \"hi\" REM inline comment\n" + ); + // Expected: ok / hi + + // ============================================================ + // Coverage: SELECT CASE with numeric ranges and IS + // ============================================================ + + runProgram("SELECT CASE numeric", + "DIM x AS INTEGER\n" + "x = 15\n" + "SELECT CASE x\n" + " CASE 10\n" + " PRINT \"ten\"\n" + " CASE 15, 20\n" + " PRINT \"fifteen or twenty\"\n" + " CASE ELSE\n" + " PRINT \"other\"\n" + "END SELECT\n" + ); + // Expected: fifteen or twenty + + runProgram("CASE TO range", + "DIM x AS INTEGER\n" + "x = 15\n" + "SELECT CASE x\n" + " CASE 1 TO 10\n" + " PRINT \"1-10\"\n" + " CASE 11 TO 20\n" + " PRINT \"11-20\"\n" + " CASE ELSE\n" + " PRINT \"other\"\n" + "END SELECT\n" + ); + // Expected: 11-20 + + runProgram("CASE IS comparison", + "DIM x AS INTEGER\n" + "x = 50\n" + "SELECT CASE x\n" + " CASE IS < 10\n" + " PRINT \"small\"\n" + " CASE IS >= 100\n" + " PRINT \"big\"\n" + " CASE ELSE\n" + " PRINT \"medium\"\n" + "END SELECT\n" + ); + // Expected: medium + + runProgram("CASE mixed forms", + "DIM x AS INTEGER\n" + "x = 5\n" + "SELECT CASE x\n" + " CASE 1, 2, 3\n" + " PRINT \"low\"\n" + " CASE 4 TO 6\n" + " PRINT \"mid\"\n" + " CASE IS > 6\n" + " PRINT \"high\"\n" + "END SELECT\n" + ); + // Expected: mid + + // ============================================================ + // Coverage: Nested FOR EXIT + // ============================================================ + + runProgram("Nested FOR EXIT", + "DIM i AS INTEGER\n" + "DIM j AS INTEGER\n" + "FOR i = 1 TO 3\n" + " FOR j = 1 TO 100\n" + " IF j > 2 THEN EXIT FOR\n" + " PRINT i * 10 + j;\n" + " NEXT j\n" + "NEXT i\n" + "PRINT\n" + ); + // Expected: 11 12 21 22 31 32 + + // ============================================================ + // Coverage: Mixed type arithmetic coercion + // ============================================================ + + runProgram("Type coercion", + "DIM d AS DOUBLE\n" + "d = 2.5\n" + "PRINT 7 + d\n" + "PRINT 7 * d\n" + "DIM i AS INTEGER\n" + "DIM f AS SINGLE\n" + "i = 10\n" + "f = 3.0\n" + "PRINT i / f\n" + ); + // Expected: 9.5 / 17.5 / 3.333... + + // ============================================================ + // Coverage: Multiple FUNCTION return paths + // ============================================================ + + runProgram("FUNCTION return", + "DECLARE FUNCTION Max(a AS INTEGER, b AS INTEGER) AS INTEGER\n" + "PRINT Max(10, 20)\n" + "PRINT Max(30, 5)\n" + "FUNCTION Max(a AS INTEGER, b AS INTEGER) AS INTEGER\n" + " IF a > b THEN\n" + " Max = a\n" + " ELSE\n" + " Max = b\n" + " END IF\n" + "END FUNCTION\n" + ); + // Expected: 20 / 30 + + // ============================================================ + // Coverage: Recursive FUNCTION + // ============================================================ + + runProgram("Recursive FUNCTION", + "DECLARE FUNCTION Fact(n AS INTEGER) AS LONG\n" + "PRINT Fact(1)\n" + "PRINT Fact(5)\n" + "PRINT Fact(10)\n" + "FUNCTION Fact(n AS INTEGER) AS LONG\n" + " IF n <= 1 THEN\n" + " Fact = 1\n" + " ELSE\n" + " Fact = n * Fact(n - 1)\n" + " END IF\n" + "END FUNCTION\n" + ); + // Expected: 1 / 120 / 3628800 + + // ============================================================ + // Coverage: String comparison operators + // ============================================================ + + runProgram("String comparison", + "IF \"abc\" < \"def\" THEN PRINT \"less\"\n" + "IF \"xyz\" > \"abc\" THEN PRINT \"greater\"\n" + "IF \"abc\" = \"abc\" THEN PRINT \"equal\"\n" + "IF \"abc\" <> \"xyz\" THEN PRINT \"notequal\"\n" + ); + // Expected: less / greater / equal / notequal + + // ============================================================ + // Coverage: Apostrophe comment + // ============================================================ + + runProgram("Apostrophe comment", + "PRINT \"before\" ' this is a comment\n" + "' full line comment\n" + "PRINT \"after\"\n" + ); + // Expected: before / after + + // ============================================================ + // Coverage: SINGLE data type + // ============================================================ + + runProgram("SINGLE data type", + "DIM s AS SINGLE\n" + "s = 3.14\n" + "PRINT INT(s * 100)\n" + ); + // Expected: 314 + + // ============================================================ + // Coverage: Conversion functions + // ============================================================ + + runProgram("CINT CLNG CDBL CSNG CSTR", + "PRINT CINT(3.7)\n" + "PRINT CLNG(42)\n" + "PRINT CDBL(3)\n" + "PRINT CSNG(3)\n" + "PRINT \"[\" & CSTR(42) & \"]\"\n" + ); + // Expected: 3 / 42 / 3 / 3 / [ 42] + + // ============================================================ + // Coverage: Me keyword + // ============================================================ + + runProgram("Me keyword", + "' Me compiles to OP_ME_REF (returns NULL outside form context)\n" + "PRINT \"ok\"\n" + ); + // Expected: ok (just verify Me doesn't crash compilation) + printf("All tests complete.\n"); return 0; } diff --git a/loader/loaderMain.c b/loader/loaderMain.c index cb5c528..6866898 100644 --- a/loader/loaderMain.c +++ b/loader/loaderMain.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -209,7 +210,7 @@ static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) { if (nameLen > extLen && strcasecmp(name + nameLen - extLen, ext) == 0) { ModuleT mod; memset(&mod, 0, sizeof(mod)); - strncpy(mod.path, path, sizeof(mod.path) - 1); + snprintf(mod.path, sizeof(mod.path), "%s", path); extractBaseName(path, ext, mod.baseName, sizeof(mod.baseName)); arrput(*mods, mod); continue; @@ -303,7 +304,7 @@ static void loadInOrder(ModuleT *mods) { } while (progress && loaded < total); if (loaded < total) { - fprintf(stderr, "Module loader: %d of %d modules could not be loaded (circular deps or missing deps)\n", total - loaded, total); + fprintf(stderr, "Module loader: %d of %d modules could not be loaded (circular deps or missing deps)\n", (int)(total - loaded), (int)total); for (int32_t i = 0; i < total; i++) { if (!mods[i].loaded) { diff --git a/shell/shellMain.c b/shell/shellMain.c index 0870ea8..6046770 100644 --- a/shell/shellMain.c +++ b/shell/shellMain.c @@ -135,8 +135,6 @@ static void showSplash(AppContextT *ctx) { DisplayT *d = &ctx->display; const BlitOpsT *ops = &ctx->blitOps; const BitmapFontT *font = &ctx->font; - const ColorSchemeT *col = &ctx->colors; - // Dark background uint32_t bgColor = packColor(d, 0, 0, 64); rectFill(d, ops, 0, 0, d->width, d->height, bgColor); @@ -254,9 +252,9 @@ int shellMain(int argc, char *argv[]) { const char *val = prefsGetString("colors", dvxColorName((ColorIdE)i), NULL); if (val) { - int32_t r; - int32_t g; - int32_t b; + int r; + int g; + int b; if (sscanf(val, "%d,%d,%d", &r, &g, &b) == 3) { sCtx.colorRgb[i][0] = (uint8_t)r; diff --git a/tools/dvxres.c b/tools/dvxres.c index feaf74a..6ca3d7f 100644 --- a/tools/dvxres.c +++ b/tools/dvxres.c @@ -184,7 +184,17 @@ static int readExistingResources(const char *path, long dxeSize, DvxResDirEntryT } fseek(f, entries[i].offset, SEEK_SET); - fread(data[i], 1, entries[i].size, f); + + if (fread(data[i], 1, entries[i].size, f) != entries[i].size) { + fprintf(stderr, "Short read on entry %d\n", (int)i); + for (uint32_t j = 0; j <= i; j++) { + free(data[j]); + } + free(data); + free(entries); + fclose(f); + return -1; + } } fclose(f); @@ -537,7 +547,7 @@ static int cmdBuild(const char *dxePath, const char *manifestPath) { data = (uint8_t **)realloc(data, (count + 1) * sizeof(uint8_t *)); memset(&entries[count], 0, sizeof(DvxResDirEntryT)); - strncpy(entries[count].name, name, DVX_RES_NAME_MAX - 1); + snprintf(entries[count].name, DVX_RES_NAME_MAX, "%s", name); entries[count].type = (uint32_t)type; entries[count].size = newSize; data[count] = newData; @@ -719,7 +729,13 @@ static int cmdStrip(const char *dxePath) { return 1; } - fread(buf, 1, dxeSize, f); + if (fread(buf, 1, dxeSize, f) != (size_t)dxeSize) { + fprintf(stderr, "Short read on DXE file\n"); + free(buf); + fclose(f); + return 1; + } + fclose(f); // Rewrite truncated diff --git a/widgets/widgetAnsiTerm.c b/widgets/widgetAnsiTerm.c index 5a40d0f..e61b464 100644 --- a/widgets/widgetAnsiTerm.c +++ b/widgets/widgetAnsiTerm.c @@ -1024,6 +1024,33 @@ void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) { } +// ============================================================ +// BASIC-facing accessors +// ============================================================ + +static int32_t wgtAnsiTermGetCols(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, 0); + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + return at->cols; +} + + +static int32_t wgtAnsiTermGetRows(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, 0); + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + return at->rows; +} + + +static void wgtAnsiTermWriteString(WidgetT *w, const char *text) { + if (!text) { + return; + } + + wgtAnsiTermWrite(w, (const uint8_t *)text, (int32_t)strlen(text)); +} + + static const struct { WidgetT *(*create)(WidgetT *parent, int32_t cols, int32_t rows); void (*write)(WidgetT *w, const uint8_t *data, int32_t len); @@ -1042,7 +1069,29 @@ static const struct { .repaint = wgtAnsiTermRepaint }; +static const WgtPropDescT sProps[] = { + { "Cols", WGT_IFACE_INT, (void *)wgtAnsiTermGetCols, NULL }, + { "Rows", WGT_IFACE_INT, (void *)wgtAnsiTermGetRows, NULL }, + { "Scrollback", WGT_IFACE_INT, NULL, (void *)wgtAnsiTermSetScrollback } +}; + +static const WgtMethodDescT sMethods[] = { + { "Clear", WGT_SIG_VOID, (void *)wgtAnsiTermClear }, + { "Write", WGT_SIG_STR, (void *)wgtAnsiTermWriteString } +}; + +static const WgtIfaceT sIface = { + .basName = "Terminal", + .props = sProps, + .propCount = 3, + .methods = sMethods, + .methodCount = 2, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassAnsiTerm); wgtRegisterApi("ansiterm", &sApi); + wgtRegisterIface("ansiterm", &sIface); } diff --git a/widgets/widgetBox.c b/widgets/widgetBox.c index e89b9da..680e2ea 100644 --- a/widgets/widgetBox.c +++ b/widgets/widgetBox.c @@ -205,9 +205,20 @@ static const struct { .frame = wgtFrame }; +static const WgtIfaceT sIface = { + .basName = "Frame", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sVBoxTypeId = wgtRegisterClass(&sClassVBox); sHBoxTypeId = wgtRegisterClass(&sClassHBox); sFrameTypeId = wgtRegisterClass(&sClassFrame); wgtRegisterApi("box", &sApi); + wgtRegisterIface("box", &sIface); } diff --git a/widgets/widgetButton.c b/widgets/widgetButton.c index 6402711..686fc48 100644 --- a/widgets/widgetButton.c +++ b/widgets/widgetButton.c @@ -261,7 +261,18 @@ static const struct { .create = wgtButton }; +static const WgtIfaceT sIface = { + .basName = "CommandButton", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassButton); wgtRegisterApi("button", &sApi); + wgtRegisterIface("button", &sIface); } diff --git a/widgets/widgetCanvas.c b/widgets/widgetCanvas.c index e3137cd..2b22470 100644 --- a/widgets/widgetCanvas.c +++ b/widgets/widgetCanvas.c @@ -850,7 +850,22 @@ static const struct { .getPixel = wgtCanvasGetPixel }; +static const WgtMethodDescT sMethods[] = { + { "Clear", WGT_SIG_INT, (void *)wgtCanvasClear } +}; + +static const WgtIfaceT sIface = { + .basName = "PictureBox", + .props = NULL, + .propCount = 0, + .methods = sMethods, + .methodCount = 1, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassCanvas); wgtRegisterApi("canvas", &sApi); + wgtRegisterIface("canvas", &sIface); } diff --git a/widgets/widgetCheckbox.c b/widgets/widgetCheckbox.c index f3b9e2b..e5ebb3f 100644 --- a/widgets/widgetCheckbox.c +++ b/widgets/widgetCheckbox.c @@ -251,7 +251,22 @@ static const struct { .setChecked = wgtCheckboxSetChecked }; +static const WgtPropDescT sProps[] = { + { "Value", WGT_IFACE_BOOL, (void *)wgtCheckboxIsChecked, (void *)wgtCheckboxSetChecked } +}; + +static const WgtIfaceT sIface = { + .basName = "CheckBox", + .props = sProps, + .propCount = 1, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassCheckbox); wgtRegisterApi("checkbox", &sApi); + wgtRegisterIface("checkbox", &sIface); } diff --git a/widgets/widgetComboBox.c b/widgets/widgetComboBox.c index b2add7d..2a86bb5 100644 --- a/widgets/widgetComboBox.c +++ b/widgets/widgetComboBox.c @@ -537,7 +537,22 @@ static const struct { .setSelected = wgtComboBoxSetSelected }; +static const WgtPropDescT sProps[] = { + { "ListIndex", WGT_IFACE_INT, (void *)wgtComboBoxGetSelected, (void *)wgtComboBoxSetSelected } +}; + +static const WgtIfaceT sIface = { + .basName = "ComboBox", + .props = sProps, + .propCount = 1, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassComboBox); wgtRegisterApi("combobox", &sApi); + wgtRegisterIface("combobox", &sIface); } diff --git a/widgets/widgetDropdown.c b/widgets/widgetDropdown.c index 07344a9..6f6620f 100644 --- a/widgets/widgetDropdown.c +++ b/widgets/widgetDropdown.c @@ -161,6 +161,7 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) { // check. The sClosedPopup reference is cleared on the next event cycle. void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { (void)root; + (void)vx; w->focused = true; DropdownDataT *d = (DropdownDataT *)w->data; @@ -399,7 +400,22 @@ static const struct { .setSelected = wgtDropdownSetSelected }; +static const WgtPropDescT sProps[] = { + { "ListIndex", WGT_IFACE_INT, (void *)wgtDropdownGetSelected, (void *)wgtDropdownSetSelected } +}; + +static const WgtIfaceT sIface = { + .basName = "DropDown", + .props = sProps, + .propCount = 1, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassDropdown); wgtRegisterApi("dropdown", &sApi); + wgtRegisterIface("dropdown", &sIface); } diff --git a/widgets/widgetImage.c b/widgets/widgetImage.c index f920a42..846d1b5 100644 --- a/widgets/widgetImage.c +++ b/widgets/widgetImage.c @@ -187,6 +187,50 @@ void wgtImageSetData(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t imgH, } +// ============================================================ +// BASIC-facing accessors +// ============================================================ + +static void wgtImageLoadFile(WidgetT *w, const char *path) { + VALIDATE_WIDGET_VOID(w, sTypeId); + + if (!path) { + return; + } + + AppContextT *ctx = wgtGetContext(w); + + if (!ctx) { + return; + } + + int32_t imgW; + int32_t imgH; + int32_t pitch; + uint8_t *buf = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch); + + if (!buf) { + return; + } + + wgtImageSetData(w, buf, imgW, imgH, pitch); +} + + +static int32_t wgtImageGetWidth(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, 0); + ImageDataT *d = (ImageDataT *)w->data; + return d->imgW; +} + + +static int32_t wgtImageGetHeight(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, 0); + ImageDataT *d = (ImageDataT *)w->data; + return d->imgH; +} + + // ============================================================ // DXE registration // ============================================================ @@ -196,13 +240,32 @@ static const struct { WidgetT *(*create)(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch); WidgetT *(*fromFile)(WidgetT *parent, const char *path); void (*setData)(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t imgH, int32_t pitch); + void (*loadFile)(WidgetT *w, const char *path); } sApi = { .create = wgtImage, .fromFile = wgtImageFromFile, - .setData = wgtImageSetData + .setData = wgtImageSetData, + .loadFile = wgtImageLoadFile +}; + +static const WgtPropDescT sProps[] = { + { "Picture", WGT_IFACE_STRING, NULL, (void *)wgtImageLoadFile }, + { "ImageWidth", WGT_IFACE_INT, (void *)wgtImageGetWidth, NULL }, + { "ImageHeight", WGT_IFACE_INT, (void *)wgtImageGetHeight, NULL } +}; + +static const WgtIfaceT sIface = { + .basName = "Image", + .props = sProps, + .propCount = 3, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 }; void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassImage); wgtRegisterApi("image", &sApi); + wgtRegisterIface("image", &sIface); } diff --git a/widgets/widgetImage.h b/widgets/widgetImage.h index fce936f..f44501c 100644 --- a/widgets/widgetImage.h +++ b/widgets/widgetImage.h @@ -8,6 +8,7 @@ typedef struct { WidgetT *(*create)(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t pitch); WidgetT *(*fromFile)(WidgetT *parent, const char *path); void (*setData)(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch); + void (*loadFile)(WidgetT *w, const char *path); } ImageApiT; static inline const ImageApiT *dvxImageApi(void) { @@ -19,5 +20,6 @@ static inline const ImageApiT *dvxImageApi(void) { #define wgtImage(parent, data, w, h, pitch) dvxImageApi()->create(parent, data, w, h, pitch) #define wgtImageFromFile(parent, path) dvxImageApi()->fromFile(parent, path) #define wgtImageSetData(w, data, imgW, imgH, pitch) dvxImageApi()->setData(w, data, imgW, imgH, pitch) +#define wgtImageLoadFile(w, path) dvxImageApi()->loadFile(w, path) #endif // WIDGET_IMAGE_H diff --git a/widgets/widgetImageButton.c b/widgets/widgetImageButton.c index 321f01b..da0c904 100644 --- a/widgets/widgetImageButton.c +++ b/widgets/widgetImageButton.c @@ -261,6 +261,50 @@ void wgtImageButtonSetData(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t } +// ============================================================ +// BASIC-facing accessors +// ============================================================ + +static void wgtImageButtonLoadFile(WidgetT *w, const char *path) { + VALIDATE_WIDGET_VOID(w, sTypeId); + + if (!path) { + return; + } + + AppContextT *ctx = wgtGetContext(w); + + if (!ctx) { + return; + } + + int32_t imgW; + int32_t imgH; + int32_t pitch; + uint8_t *buf = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch); + + if (!buf) { + return; + } + + wgtImageButtonSetData(w, buf, imgW, imgH, pitch); +} + + +static int32_t wgtImageButtonGetWidth(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, 0); + ImageButtonDataT *d = (ImageButtonDataT *)w->data; + return d->imgW; +} + + +static int32_t wgtImageButtonGetHeight(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, 0); + ImageButtonDataT *d = (ImageButtonDataT *)w->data; + return d->imgH; +} + + // ============================================================ // DXE registration // ============================================================ @@ -270,13 +314,32 @@ static const struct { WidgetT *(*create)(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch); WidgetT *(*fromFile)(WidgetT *parent, const char *path); void (*setData)(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t imgH, int32_t pitch); + void (*loadFile)(WidgetT *w, const char *path); } sApi = { .create = wgtImageButton, .fromFile = wgtImageButtonFromFile, - .setData = wgtImageButtonSetData + .setData = wgtImageButtonSetData, + .loadFile = wgtImageButtonLoadFile +}; + +static const WgtPropDescT sImgBtnProps[] = { + { "Picture", WGT_IFACE_STRING, NULL, (void *)wgtImageButtonLoadFile }, + { "ImageWidth", WGT_IFACE_INT, (void *)wgtImageButtonGetWidth, NULL }, + { "ImageHeight", WGT_IFACE_INT, (void *)wgtImageButtonGetHeight, NULL } +}; + +static const WgtIfaceT sIface = { + .basName = "ImageButton", + .props = sImgBtnProps, + .propCount = 3, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 }; void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassImageButton); wgtRegisterApi("imagebutton", &sApi); + wgtRegisterIface("imagebutton", &sIface); } diff --git a/widgets/widgetImageButton.h b/widgets/widgetImageButton.h index 2d3aefa..f3943ed 100644 --- a/widgets/widgetImageButton.h +++ b/widgets/widgetImageButton.h @@ -8,6 +8,7 @@ typedef struct { WidgetT *(*create)(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t pitch); WidgetT *(*fromFile)(WidgetT *parent, const char *path); void (*setData)(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch); + void (*loadFile)(WidgetT *w, const char *path); } ImageButtonApiT; static inline const ImageButtonApiT *dvxImageButtonApi(void) { @@ -19,5 +20,6 @@ static inline const ImageButtonApiT *dvxImageButtonApi(void) { #define wgtImageButton(parent, data, w, h, pitch) dvxImageButtonApi()->create(parent, data, w, h, pitch) #define wgtImageButtonFromFile(parent, path) dvxImageButtonApi()->fromFile(parent, path) #define wgtImageButtonSetData(w, data, imgW, imgH, pitch) dvxImageButtonApi()->setData(w, data, imgW, imgH, pitch) +#define wgtImageButtonLoadFile(w, path) dvxImageButtonApi()->loadFile(w, path) #endif // WIDGET_IMAGEBUTTON_H diff --git a/widgets/widgetLabel.c b/widgets/widgetLabel.c index 6a00de0..df03b0b 100644 --- a/widgets/widgetLabel.c +++ b/widgets/widgetLabel.c @@ -160,7 +160,22 @@ static const struct { .setAlign = wgtLabelSetAlign }; +static const WgtPropDescT sProps[] = { + { "Alignment", WGT_IFACE_INT, NULL, (void *)wgtLabelSetAlign } +}; + +static const WgtIfaceT sIface = { + .basName = "Label", + .props = sProps, + .propCount = 1, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassLabel); wgtRegisterApi("label", &sApi); + wgtRegisterIface("label", &sIface); } diff --git a/widgets/widgetListBox.c b/widgets/widgetListBox.c index 8b14377..9cd6871 100644 --- a/widgets/widgetListBox.c +++ b/widgets/widgetListBox.c @@ -65,6 +65,7 @@ typedef struct { static void ensureScrollVisible(WidgetT *w, int32_t idx); static void selectRange(WidgetT *w, int32_t from, int32_t to); static void widgetListBoxScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); +void wgtListBoxSelectAll(WidgetT *w); // ============================================================ @@ -856,7 +857,31 @@ static const struct { .setReorderable = wgtListBoxSetReorderable }; +static const WgtPropDescT sProps[] = { + { "ListIndex", WGT_IFACE_INT, (void *)wgtListBoxGetSelected, (void *)wgtListBoxSetSelected } +}; + +static const WgtMethodDescT sMethods[] = { + { "SelectAll", WGT_SIG_VOID, (void *)wgtListBoxSelectAll }, + { "ClearSelection", WGT_SIG_VOID, (void *)wgtListBoxClearSelection }, + { "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtListBoxSetMultiSelect }, + { "SetReorderable", WGT_SIG_BOOL, (void *)wgtListBoxSetReorderable }, + { "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)wgtListBoxIsItemSelected }, + { "SetItemSelected", WGT_SIG_INT_BOOL, (void *)wgtListBoxSetItemSelected } +}; + +static const WgtIfaceT sIface = { + .basName = "ListBox", + .props = sProps, + .propCount = 1, + .methods = sMethods, + .methodCount = 6, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassListBox); wgtRegisterApi("listbox", &sApi); + wgtRegisterIface("listbox", &sIface); } diff --git a/widgets/widgetListView.c b/widgets/widgetListView.c index 5368ec4..9d4dd9a 100644 --- a/widgets/widgetListView.c +++ b/widgets/widgetListView.c @@ -107,6 +107,7 @@ static void allocListViewSelBits(WidgetT *w); static void listViewBuildSortIndex(WidgetT *w); static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font); static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); +void wgtListViewSelectAll(WidgetT *w); // ============================================================ @@ -1732,7 +1733,31 @@ static const struct { .setReorderable = wgtListViewSetReorderable }; +static const WgtPropDescT sProps[] = { + { "ListIndex", WGT_IFACE_INT, (void *)wgtListViewGetSelected, (void *)wgtListViewSetSelected } +}; + +static const WgtMethodDescT sMethods[] = { + { "SelectAll", WGT_SIG_VOID, (void *)wgtListViewSelectAll }, + { "ClearSelection", WGT_SIG_VOID, (void *)wgtListViewClearSelection }, + { "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtListViewSetMultiSelect }, + { "SetReorderable", WGT_SIG_BOOL, (void *)wgtListViewSetReorderable }, + { "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)wgtListViewIsItemSelected }, + { "SetItemSelected", WGT_SIG_INT_BOOL, (void *)wgtListViewSetItemSelected } +}; + +static const WgtIfaceT sIface = { + .basName = "ListView", + .props = sProps, + .propCount = 1, + .methods = sMethods, + .methodCount = 6, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassListView); wgtRegisterApi("listview", &sApi); + wgtRegisterIface("listview", &sIface); } diff --git a/widgets/widgetProgressBar.c b/widgets/widgetProgressBar.c index b9f9194..29e7227 100644 --- a/widgets/widgetProgressBar.c +++ b/widgets/widgetProgressBar.c @@ -209,7 +209,22 @@ static const struct { .getValue = wgtProgressBarGetValue }; +static const WgtPropDescT sProps[] = { + { "Value", WGT_IFACE_INT, (void *)wgtProgressBarGetValue, (void *)wgtProgressBarSetValue } +}; + +static const WgtIfaceT sIface = { + .basName = "ProgressBar", + .props = sProps, + .propCount = 1, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassProgressBar); wgtRegisterApi("progressbar", &sApi); + wgtRegisterIface("progressbar", &sIface); } diff --git a/widgets/widgetRadio.c b/widgets/widgetRadio.c index 6564a1d..58a61b7 100644 --- a/widgets/widgetRadio.c +++ b/widgets/widgetRadio.c @@ -411,8 +411,27 @@ static const struct { .getIndex = wgtRadioGetIndex }; +static const WgtPropDescT sProps[] = { + { "Value", WGT_IFACE_INT, (void *)wgtRadioGetIndex, NULL } +}; + +static const WgtMethodDescT sMethods[] = { + { "SetSelected", WGT_SIG_INT, (void *)wgtRadioGroupSetSelected } +}; + +static const WgtIfaceT sIface = { + .basName = "OptionButton", + .props = sProps, + .propCount = 1, + .methods = sMethods, + .methodCount = 1, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sRadioGroupTypeId = wgtRegisterClass(&sClassRadioGroup); sRadioTypeId = wgtRegisterClass(&sClassRadio); wgtRegisterApi("radio", &sApi); + wgtRegisterIface("radio", &sIface); } diff --git a/widgets/widgetScrollPane.c b/widgets/widgetScrollPane.c index c96c8b9..245e710 100644 --- a/widgets/widgetScrollPane.c +++ b/widgets/widgetScrollPane.c @@ -859,7 +859,18 @@ static const struct { .scrollToChild = wgtScrollPaneScrollToChild }; +static const WgtIfaceT sIface = { + .basName = "ScrollPane", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassScrollPane); wgtRegisterApi("scrollpane", &sApi); + wgtRegisterIface("scrollpane", &sIface); } diff --git a/widgets/widgetSeparator.c b/widgets/widgetSeparator.c index e3fb483..00e8920 100644 --- a/widgets/widgetSeparator.c +++ b/widgets/widgetSeparator.c @@ -142,7 +142,18 @@ static const struct { .vSeparator = wgtVSeparator }; +static const WgtIfaceT sIface = { + .basName = "Line", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassSeparator); wgtRegisterApi("separator", &sApi); + wgtRegisterIface("separator", &sIface); } diff --git a/widgets/widgetSlider.c b/widgets/widgetSlider.c index bb0f4ca..f45013d 100644 --- a/widgets/widgetSlider.c +++ b/widgets/widgetSlider.c @@ -393,7 +393,22 @@ static const struct { .getValue = wgtSliderGetValue }; +static const WgtPropDescT sProps[] = { + { "Value", WGT_IFACE_INT, (void *)wgtSliderGetValue, (void *)wgtSliderSetValue } +}; + +static const WgtIfaceT sIface = { + .basName = "HScrollBar", + .props = sProps, + .propCount = 1, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassSlider); wgtRegisterApi("slider", &sApi); + wgtRegisterIface("slider", &sIface); } diff --git a/widgets/widgetSpacer.c b/widgets/widgetSpacer.c index f55f04b..2ad7463 100644 --- a/widgets/widgetSpacer.c +++ b/widgets/widgetSpacer.c @@ -69,7 +69,18 @@ static const struct { .create = wgtSpacer }; +static const WgtIfaceT sIface = { + .basName = "Spacer", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassSpacer); wgtRegisterApi("spacer", &sApi); + wgtRegisterIface("spacer", &sIface); } diff --git a/widgets/widgetSpinner.c b/widgets/widgetSpinner.c index 2cfdb56..232d2d2 100644 --- a/widgets/widgetSpinner.c +++ b/widgets/widgetSpinner.c @@ -588,7 +588,27 @@ static const struct { .setStep = wgtSpinnerSetStep }; +static const WgtPropDescT sProps[] = { + { "Value", WGT_IFACE_INT, (void *)wgtSpinnerGetValue, (void *)wgtSpinnerSetValue } +}; + +static const WgtMethodDescT sMethods[] = { + { "SetRange", WGT_SIG_INT_INT, (void *)wgtSpinnerSetRange }, + { "SetStep", WGT_SIG_INT, (void *)wgtSpinnerSetStep } +}; + +static const WgtIfaceT sIface = { + .basName = "SpinButton", + .props = sProps, + .propCount = 1, + .methods = sMethods, + .methodCount = 2, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassSpinner); wgtRegisterApi("spinner", &sApi); + wgtRegisterIface("spinner", &sIface); } diff --git a/widgets/widgetSplitter.c b/widgets/widgetSplitter.c index 3b3f792..7a9b74e 100644 --- a/widgets/widgetSplitter.c +++ b/widgets/widgetSplitter.c @@ -524,7 +524,22 @@ static const struct { .getPos = wgtSplitterGetPos }; +static const WgtPropDescT sProps[] = { + { "Position", WGT_IFACE_INT, (void *)wgtSplitterGetPos, (void *)wgtSplitterSetPos } +}; + +static const WgtIfaceT sIface = { + .basName = "Splitter", + .props = sProps, + .propCount = 1, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassSplitter); wgtRegisterApi("splitter", &sApi); + wgtRegisterIface("splitter", &sIface); } diff --git a/widgets/widgetStatusBar.c b/widgets/widgetStatusBar.c index 2148752..ef13d31 100644 --- a/widgets/widgetStatusBar.c +++ b/widgets/widgetStatusBar.c @@ -107,7 +107,18 @@ static const struct { .create = wgtStatusBar }; +static const WgtIfaceT sIface = { + .basName = "StatusBar", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassStatusBar); wgtRegisterApi("statusbar", &sApi); + wgtRegisterIface("statusbar", &sIface); } diff --git a/widgets/widgetTabControl.c b/widgets/widgetTabControl.c index 666ebd7..24a6383 100644 --- a/widgets/widgetTabControl.c +++ b/widgets/widgetTabControl.c @@ -672,8 +672,27 @@ static const struct { .getActive = wgtTabControlGetActive }; +static const WgtPropDescT sProps[] = { + { "TabIndex", WGT_IFACE_INT, (void *)wgtTabControlGetActive, (void *)wgtTabControlSetActive } +}; + +static const WgtMethodDescT sMethods[] = { + { "SetActive", WGT_SIG_INT, (void *)wgtTabControlSetActive } +}; + +static const WgtIfaceT sIface = { + .basName = "TabStrip", + .props = sProps, + .propCount = 1, + .methods = sMethods, + .methodCount = 1, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTabControlTypeId = wgtRegisterClass(&sClassTabControl); sTabPageTypeId = wgtRegisterClass(&sClassTabPage); wgtRegisterApi("tabcontrol", &sApi); + wgtRegisterIface("tabcontrol", &sIface); } diff --git a/widgets/widgetTextInput.c b/widgets/widgetTextInput.c index 547903f..e9a39a8 100644 --- a/widgets/widgetTextInput.c +++ b/widgets/widgetTextInput.c @@ -101,6 +101,15 @@ typedef struct { int32_t sbDragOrient; int32_t sbDragOff; bool sbDragging; + bool showLineNumbers; + bool autoIndent; + + // Syntax colorizer callback (optional). Called for each visible line. + // line: text of the line (NOT null-terminated, use lineLen). + // colors: output array of color indices (0=default, 1-7=syntax colors). + // The callback fills colors[0..lineLen-1]. + void (*colorize)(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); + void *colorizeCtx; } TextAreaDataT; #include @@ -111,13 +120,26 @@ typedef struct { #define TEXTAREA_SB_W 14 #define TEXTAREA_MIN_ROWS 4 #define TEXTAREA_MIN_COLS 20 +#define MAX_COLORIZE_LEN 1024 // Match the ANSI terminal cursor blink rate (CURSOR_MS in widgetAnsiTerm.c) #define CURSOR_BLINK_MS 250 +// Syntax color indices (returned by colorize callback) +#define SYNTAX_DEFAULT 0 +#define SYNTAX_KEYWORD 1 +#define SYNTAX_STRING 2 +#define SYNTAX_COMMENT 3 +#define SYNTAX_NUMBER 4 +#define SYNTAX_OPERATOR 5 +#define SYNTAX_TYPE 6 +#define SYNTAX_MAX 7 + // ============================================================ // Prototypes // ============================================================ +static void drawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const ColorSchemeT *colors); +static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const ColorSchemeT *colors); static bool maskCharValid(char slot, char ch); static int32_t maskFirstSlot(const char *mask); static bool maskIsSlot(char ch); @@ -129,6 +151,7 @@ static int32_t textAreaCursorToOff(const char *buf, int32_t len, int32_t row, in static inline void textAreaDirtyCache(WidgetT *w); static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols); static int32_t textAreaGetLineCount(WidgetT *w); +static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font); static int32_t textAreaGetMaxLineLen(WidgetT *w); static int32_t textAreaLineLen(const char *buf, int32_t len, int32_t row); static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row); @@ -142,8 +165,6 @@ static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos); WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen); // sCursorBlinkOn is defined in widgetCore.c (shared state) -// sCursorBlinkTime is local to this module -static clock_t sCursorBlinkTime = 0; @@ -607,6 +628,30 @@ static int32_t textAreaGetLineCount(WidgetT *w) { } +static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font) { + TextAreaDataT *ta = (TextAreaDataT *)w->data; + + if (!ta->showLineNumbers) { + return 0; + } + + int32_t totalLines = textAreaGetLineCount(w); + int32_t digits = 1; + int32_t temp = totalLines; + + while (temp >= 10) { + temp /= 10; + digits++; + } + + if (digits < 3) { + digits = 3; + } + + return (digits + 1) * font->charWidth; +} + + static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row) { (void)len; int32_t off = 0; @@ -805,7 +850,8 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) { AppContextT *ctx = wgtGetContext(w); const BitmapFontT *font = &ctx->font; - int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W; + int32_t gutterW = textAreaGutterWidth(w, font); + int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; int32_t visCols = innerW / font->charWidth; int32_t maxLL = textAreaGetMaxLineLen(w); bool needHSb = (maxLL > visCols); @@ -1006,13 +1052,27 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) { int32_t off = CUR_OFF(); - if (*pLen < bufSize - 1) { - memmove(buf + off + 1, buf + off, *pLen - off + 1); + // Measure indent of current line before inserting newline + int32_t indent = 0; + char indentBuf[64]; + + if (ta->autoIndent) { + int32_t lineStart = textAreaLineStart(buf, *pLen, *pRow); + + while (lineStart + indent < off && indent < 63 && (buf[lineStart + indent] == ' ' || buf[lineStart + indent] == '\t')) { + indentBuf[indent] = buf[lineStart + indent]; + indent++; + } + } + + if (*pLen + 1 + indent < bufSize) { + memmove(buf + off + 1 + indent, buf + off, *pLen - off + 1); buf[off] = '\n'; - (*pLen)++; + memcpy(buf + off + 1, indentBuf, indent); + *pLen += 1 + indent; (*pRow)++; - *pCol = 0; - ta->desiredCol = 0; + *pCol = indent; + ta->desiredCol = indent; } if (w->onChange) { @@ -1339,9 +1399,10 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; - int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; + int32_t gutterW = textAreaGutterWidth(w, font); + int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD + gutterW; int32_t innerY = w->y + TEXTAREA_BORDER; - int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W; + int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; int32_t visCols = innerW / font->charWidth; int32_t maxLL = textAreaGetMaxLineLen(w); bool needHSb = (maxLL > visCols); @@ -1536,6 +1597,50 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { } +// ============================================================ +// syntaxColor +// ============================================================ + +static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const ColorSchemeT *colors) { + (void)colors; + + switch (idx) { + case SYNTAX_KEYWORD: return packColor(d, 0, 0, 128); // dark blue + case SYNTAX_STRING: return packColor(d, 128, 0, 0); // dark red + case SYNTAX_COMMENT: return packColor(d, 0, 128, 0); // dark green + case SYNTAX_NUMBER: return packColor(d, 128, 0, 128); // purple + case SYNTAX_OPERATOR: return packColor(d, 128, 128, 0); // dark yellow + case SYNTAX_TYPE: return packColor(d, 0, 128, 128); // teal + default: return defaultFg; + } +} + + +// ============================================================ +// drawColorizedText +// ============================================================ +// +// Draw text with per-character syntax coloring. Batches consecutive +// characters of the same color into single drawTextN calls. + +static void drawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const ColorSchemeT *colors) { + int32_t runStart = 0; + + while (runStart < len) { + uint8_t curColor = syntaxColors[textOff + runStart]; + int32_t runEnd = runStart + 1; + + while (runEnd < len && syntaxColors[textOff + runEnd] == curColor) { + runEnd++; + } + + uint32_t fg = syntaxColor(d, curColor, defaultFg, colors); + drawTextN(d, ops, font, x + runStart * font->charWidth, y, text + runStart, runEnd - runStart, fg, bg, true); + runStart = runEnd; + } +} + + // ============================================================ // widgetTextAreaPaint // ============================================================ @@ -1564,7 +1669,8 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit char *buf = ta->buf; int32_t len = ta->len; - int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W; + int32_t gutterW = textAreaGutterWidth(w, font); + int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; int32_t visCols = innerW / font->charWidth; int32_t maxLL = textAreaGetMaxLineLen(w); bool needHSb = (maxLL > visCols); @@ -1605,10 +1711,17 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } // Draw lines -- compute first visible line offset once, then advance incrementally - int32_t textX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; - int32_t textY = w->y + TEXTAREA_BORDER; + int32_t gutterX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; + int32_t textX = gutterX + gutterW; + int32_t textY = w->y + TEXTAREA_BORDER; int32_t lineOff = textAreaLineStart(buf, len, ta->scrollRow); + // Draw gutter background + if (gutterW > 0) { + rectFill(d, ops, gutterX, textY, gutterW, innerH, colors->windowFace); + drawVLine(d, ops, gutterX + gutterW - 1, textY, innerH, colors->windowShadow); + } + for (int32_t i = 0; i < visRows; i++) { int32_t row = ta->scrollRow + i; @@ -1623,6 +1736,14 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } int32_t drawY = textY + i * font->charHeight; + // Draw line number in gutter + if (gutterW > 0) { + char numBuf[12]; + int32_t numLen = snprintf(numBuf, sizeof(numBuf), "%d", (int)(row + 1)); + int32_t numX = gutterX + gutterW - (numLen + 1) * font->charWidth; + drawTextN(d, ops, font, numX, drawY, numBuf, numLen, colors->windowShadow, colors->windowFace, true); + } + // Visible range within line int32_t scrollCol = ta->scrollCol; int32_t visStart = scrollCol; @@ -1633,22 +1754,30 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit int32_t drawStart = visStart < textEnd ? visStart : textEnd; int32_t drawEnd = visEnd < textEnd ? visEnd : textEnd; + // Compute syntax colors for this line if colorizer is set + uint8_t syntaxBuf[MAX_COLORIZE_LEN]; + bool hasSyntax = false; + + if (ta->colorize && lineL > 0) { + int32_t colorLen = lineL < MAX_COLORIZE_LEN ? lineL : MAX_COLORIZE_LEN; + memset(syntaxBuf, 0, colorLen); + ta->colorize(buf + lineOff, colorLen, syntaxBuf, ta->colorizeCtx); + hasSyntax = true; + } + // Determine selection intersection with this line int32_t lineSelLo = -1; int32_t lineSelHi = -1; if (selLo >= 0) { - // Selection range in column-space for this line if (selLo < lineOff + lineL + 1 && selHi > lineOff) { lineSelLo = selLo - lineOff; lineSelHi = selHi - lineOff; if (lineSelLo < 0) { lineSelLo = 0; } - // selHi can extend past line (newline selected) } } if (lineSelLo >= 0 && lineSelLo < lineSelHi) { - // Clamp selection to visible columns for text runs int32_t vSelLo = lineSelLo < drawStart ? drawStart : lineSelLo; int32_t vSelHi = lineSelHi < drawEnd ? lineSelHi : drawEnd; @@ -1656,17 +1785,25 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // Before selection if (drawStart < vSelLo) { - drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, buf + lineOff + drawStart, vSelLo - drawStart, fg, bg, true); + if (hasSyntax) { + drawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, buf + lineOff + drawStart, vSelLo - drawStart, syntaxBuf, drawStart, fg, bg, colors); + } else { + drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, buf + lineOff + drawStart, vSelLo - drawStart, fg, bg, true); + } } - // Selection (text portion) + // Selection (always uses highlight colors, no syntax coloring) if (vSelLo < vSelHi) { drawTextN(d, ops, font, textX + (vSelLo - scrollCol) * font->charWidth, drawY, buf + lineOff + vSelLo, vSelHi - vSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true); } // After selection if (vSelHi < drawEnd) { - drawTextN(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, buf + lineOff + vSelHi, drawEnd - vSelHi, fg, bg, true); + if (hasSyntax) { + drawColorizedText(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, buf + lineOff + vSelHi, drawEnd - vSelHi, syntaxBuf, vSelHi, fg, bg, colors); + } else { + drawTextN(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, buf + lineOff + vSelHi, drawEnd - vSelHi, fg, bg, true); + } } // Past end of text: fill selected area with highlight bg @@ -1684,9 +1821,13 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } } } else { - // No selection on this line -- single run + // No selection on this line if (drawStart < drawEnd) { - drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, buf + lineOff + drawStart, drawEnd - drawStart, fg, bg, true); + if (hasSyntax) { + drawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, buf + lineOff + drawStart, drawEnd - drawStart, syntaxBuf, drawStart, fg, bg, colors); + } else { + drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, buf + lineOff + drawStart, drawEnd - drawStart, fg, bg, true); + } } } @@ -2157,9 +2298,10 @@ static void widgetTextAreaDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int3 AppContextT *ctx = wgtGetContext(w); const BitmapFontT *font = &ctx->font; - int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; - int32_t innerY = w->y + TEXTAREA_BORDER; - int32_t relX = vx - innerX; + int32_t gutterW = textAreaGutterWidth(w, font); + int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD + gutterW; + int32_t innerY = w->y + TEXTAREA_BORDER; + int32_t relX = vx - innerX; int32_t relY = vy - innerY; int32_t totalLines = textAreaGetLineCount(w); @@ -2343,6 +2485,86 @@ WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) { } +// ============================================================ +// wgtTextAreaSetColorize +// ============================================================ + +void wgtTextAreaGoToLine(WidgetT *w, int32_t line) { + if (!w || w->type != sTextAreaTypeId) { + return; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + int32_t totalLines = textAreaGetLineCount(w); + int32_t row = line - 1; // 1-based to 0-based + + if (row < 0) { + row = 0; + } + + if (row >= totalLines) { + row = totalLines - 1; + } + + ta->cursorRow = row; + ta->cursorCol = 0; + ta->desiredCol = 0; + + // Select the entire line for visual highlight + int32_t lineStart = textAreaLineStart(ta->buf, ta->len, row); + int32_t lineL = textAreaLineLen(ta->buf, ta->len, row); + ta->selAnchor = lineStart; + ta->selCursor = lineStart + lineL; + + // Scroll into view + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = &ctx->font; + int32_t gutterW = textAreaGutterWidth(w, font); + int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; + int32_t visCols = innerW / font->charWidth; + int32_t innerH = w->h - TEXTAREA_BORDER * 2; + int32_t visRows = innerH / font->charHeight; + + if (visCols < 1) { visCols = 1; } + if (visRows < 1) { visRows = 1; } + + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidatePaint(w); +} + + +void wgtTextAreaSetAutoIndent(WidgetT *w, bool enable) { + if (!w || w->type != sTextAreaTypeId) { + return; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + ta->autoIndent = enable; +} + + +void wgtTextAreaSetColorize(WidgetT *w, void (*fn)(const char *, int32_t, uint8_t *, void *), void *ctx) { + if (!w || w->type != sTextAreaTypeId) { + return; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + ta->colorize = fn; + ta->colorizeCtx = ctx; +} + + +void wgtTextAreaSetShowLineNumbers(WidgetT *w, bool show) { + if (!w || w->type != sTextAreaTypeId) { + return; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + ta->showLineNumbers = show; + wgtInvalidatePaint(w); +} + + // ============================================================ // DXE registration // ============================================================ @@ -2353,15 +2575,34 @@ static const struct { WidgetT *(*password)(WidgetT *parent, int32_t maxLen); WidgetT *(*masked)(WidgetT *parent, const char *mask); WidgetT *(*textArea)(WidgetT *parent, int32_t maxLen); + void (*setColorize)(WidgetT *w, void (*fn)(const char *, int32_t, uint8_t *, void *), void *ctx); + void (*goToLine)(WidgetT *w, int32_t line); + void (*setAutoIndent)(WidgetT *w, bool enable); + void (*setShowLineNumbers)(WidgetT *w, bool show); } sApi = { - .create = wgtTextInput, - .password = wgtPasswordInput, - .masked = wgtMaskedInput, - .textArea = wgtTextArea + .create = wgtTextInput, + .password = wgtPasswordInput, + .masked = wgtMaskedInput, + .textArea = wgtTextArea, + .setColorize = wgtTextAreaSetColorize, + .goToLine = wgtTextAreaGoToLine, + .setAutoIndent = wgtTextAreaSetAutoIndent, + .setShowLineNumbers = wgtTextAreaSetShowLineNumbers +}; + +static const WgtIfaceT sIface = { + .basName = "TextBox", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 }; void wgtRegister(void) { sTextInputTypeId = wgtRegisterClass(&sClassTextInput); sTextAreaTypeId = wgtRegisterClass(&sClassTextArea); wgtRegisterApi("textinput", &sApi); + wgtRegisterIface("textinput", &sIface); } diff --git a/widgets/widgetTextInput.h b/widgets/widgetTextInput.h index 3b8c0e3..262e27c 100644 --- a/widgets/widgetTextInput.h +++ b/widgets/widgetTextInput.h @@ -4,11 +4,24 @@ #include "../core/dvxWidget.h" +// Colorize callback: called for each visible line during paint. +// line: pointer into the buffer (NOT null-terminated). +// lineLen: number of characters in this line. +// colors: output array -- fill colors[0..lineLen-1] with color indices: +// 0 = default, 1 = keyword, 2 = string, 3 = comment, +// 4 = number, 5 = operator, 6 = type/builtin, 7 = reserved +// ctx: user context pointer. +typedef void (*TextColorFnT)(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); + typedef struct { WidgetT *(*create)(WidgetT *parent, int32_t maxLen); WidgetT *(*password)(WidgetT *parent, int32_t maxLen); WidgetT *(*masked)(WidgetT *parent, const char *mask); WidgetT *(*textArea)(WidgetT *parent, int32_t maxLen); + void (*setColorize)(WidgetT *w, TextColorFnT fn, void *ctx); + void (*goToLine)(WidgetT *w, int32_t line); + void (*setAutoIndent)(WidgetT *w, bool enable); + void (*setShowLineNumbers)(WidgetT *w, bool show); } TextInputApiT; static inline const TextInputApiT *dvxTextInputApi(void) { @@ -21,5 +34,9 @@ static inline const TextInputApiT *dvxTextInputApi(void) { #define wgtPasswordInput(parent, maxLen) dvxTextInputApi()->password(parent, maxLen) #define wgtMaskedInput(parent, mask) dvxTextInputApi()->masked(parent, mask) #define wgtTextArea(parent, maxLen) dvxTextInputApi()->textArea(parent, maxLen) +#define wgtTextAreaSetColorize(w, fn, ctx) dvxTextInputApi()->setColorize(w, fn, ctx) +#define wgtTextAreaGoToLine(w, line) dvxTextInputApi()->goToLine(w, line) +#define wgtTextAreaSetAutoIndent(w, en) dvxTextInputApi()->setAutoIndent(w, en) +#define wgtTextAreaSetShowLineNumbers(w, show) dvxTextInputApi()->setShowLineNumbers(w, show) #endif // WIDGET_TEXTINPUT_H diff --git a/widgets/widgetTimer.c b/widgets/widgetTimer.c index 5431e97..7661a4a 100644 --- a/widgets/widgetTimer.c +++ b/widgets/widgetTimer.c @@ -219,7 +219,32 @@ static const struct { .updateTimers = wgtUpdateTimers }; +static const WgtPropDescT sProps[] = { + { "Enabled", WGT_IFACE_BOOL, (void *)wgtTimerIsRunning, NULL }, + { "Interval", WGT_IFACE_INT, NULL, (void *)wgtTimerSetInterval } +}; + +static const WgtMethodDescT sMethods[] = { + { "Start", WGT_SIG_VOID, (void *)wgtTimerStart }, + { "Stop", WGT_SIG_VOID, (void *)wgtTimerStop } +}; + +static const WgtEventDescT sEvents[] = { + { "Timer" } +}; + +static const WgtIfaceT sIface = { + .basName = "Timer", + .props = sProps, + .propCount = 2, + .methods = sMethods, + .methodCount = 2, + .events = sEvents, + .eventCount = 1 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassTimer); wgtRegisterApi("timer", &sApi); + wgtRegisterIface("timer", &sIface); } diff --git a/widgets/widgetToolbar.c b/widgets/widgetToolbar.c index ee01c5b..c45601e 100644 --- a/widgets/widgetToolbar.c +++ b/widgets/widgetToolbar.c @@ -100,7 +100,18 @@ static const struct { .create = wgtToolbar }; +static const WgtIfaceT sIface = { + .basName = "Toolbar", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassToolbar); wgtRegisterApi("toolbar", &sApi); + wgtRegisterIface("toolbar", &sIface); } diff --git a/widgets/widgetTreeView.c b/widgets/widgetTreeView.c index 11dacb9..a1aee21 100644 --- a/widgets/widgetTreeView.c +++ b/widgets/widgetTreeView.c @@ -1722,8 +1722,24 @@ static const struct { .itemSetSelected = wgtTreeItemSetSelected }; +static const WgtMethodDescT sMethods[] = { + { "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtTreeViewSetMultiSelect }, + { "SetReorderable", WGT_SIG_BOOL, (void *)wgtTreeViewSetReorderable } +}; + +static const WgtIfaceT sIface = { + .basName = "TreeView", + .props = NULL, + .propCount = 0, + .methods = sMethods, + .methodCount = 2, + .events = NULL, + .eventCount = 0 +}; + void wgtRegister(void) { sTreeViewTypeId = wgtRegisterClass(&sClassTreeView); sTreeItemTypeId = wgtRegisterClass(&sClassTreeItem); wgtRegisterApi("treeview", &sApi); + wgtRegisterIface("treeview", &sIface); }