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.c b/dvxbasic/test_compiler.c index b772ed2..d89e1d1 100644 --- a/dvxbasic/test_compiler.c +++ b/dvxbasic/test_compiler.c @@ -903,6 +903,744 @@ int main(void) { printf("\n"); } + // Test: Procedure table populated for SUBs and FUNCTIONs + { + printf("=== Procedure table ===\n"); + const char *src = + "SUB Command1_Click\n" + " PRINT \"Clicked!\"\n" + "END SUB\n" + "\n" + "SUB Form1_Load\n" + " PRINT \"Loaded\"\n" + "END SUB\n" + "\n" + "FUNCTION AddNums(a AS INTEGER, b AS INTEGER) AS INTEGER\n" + " AddNums = a + b\n" + "END FUNCTION\n" + "\n" + "PRINT \"main\"\n"; + int32_t len = (int32_t)strlen(src); + BasParserT parser; + basParserInit(&parser, src, len); + bool ok = basParse(&parser); + + if (!ok) { + printf("COMPILE ERROR: %s\n", parser.error); + } else { + BasModuleT *mod = basParserBuildModule(&parser); + + printf("procCount = %d\n", mod->procCount); + + 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); }