// ideMain.c -- DVX BASIC Runner application // // A DVX app that loads, compiles, and runs BASIC programs. // PRINT output goes to a scrollable TextArea widget. Compile // errors are displayed with line numbers. // // This is Phase 3 of DVX BASIC: proving the compiler and VM // work on real hardware inside the DVX windowing system. #include "dvxApp.h" #include "dvxDialog.h" #include "dvxWidget.h" #include "dvxWm.h" #include "shellApp.h" #include "widgetBox.h" #include "widgetLabel.h" #include "widgetTextInput.h" #include "widgetDropdown.h" #include "widgetCanvas.h" #include "widgetSplitter.h" #include "widgetStatusBar.h" #include "ideDesigner.h" #include "ideToolbox.h" #include "ideProperties.h" #include "../compiler/parser.h" #include "../formrt/formrt.h" #include "../runtime/vm.h" #include "../runtime/values.h" #include "stb_ds_wrap.h" #include #include #include #include #include #include // ============================================================ // Constants // ============================================================ #define IDE_WIN_X 10 #define IDE_WIN_Y 10 #define IDE_WIN_W 620 #define IDE_WIN_H 460 #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 #define CMD_RUN 101 #define CMD_STOP 102 #define CMD_CLEAR 103 #define CMD_EXIT 104 #define CMD_RUN_NOCMP 105 #define CMD_VIEW_CODE 106 #define CMD_VIEW_DESIGN 107 #define IDE_MAX_IMM 1024 #define IDE_DESIGN_W 400 #define IDE_DESIGN_H 300 // ============================================================ // Prototypes // ============================================================ int32_t appMain(DxeAppContextT *ctx); static void buildWindow(void); static void clearOutput(void); static void compileAndRun(void); static void loadFile(void); static void onClose(WindowT *win); static void onMenu(WindowT *win, int32_t menuId); 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 onFormWinClose(WindowT *win); static void setStatus(const char *text); static void switchToCode(void); static void switchToDesign(void); static void dsgnMouseCb(WidgetT *w, int32_t cx, int32_t cy, bool drag); static void updateDropdowns(void); // ============================================================ // Module state // ============================================================ 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 *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 DsgnStateT sDesigner; static WindowT *sFormWin = NULL; // Form designer window (separate) static WindowT *sToolboxWin = NULL; static WindowT *sPropsWin = NULL; 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 = NULL; // stb_ds dynamic array static const char **sObjItems = NULL; // stb_ds dynamic array static const char **sEvtItems = NULL; // stb_ds dynamic array // ============================================================ // App descriptor // ============================================================ AppDescriptorT appDescriptor = { .name = "DVX BASIC", .hasMainLoop = false, .multiInstance = false, .stackSize = SHELL_STACK_DEFAULT, .priority = 0 }; // ============================================================ // appMain // ============================================================ int32_t appMain(DxeAppContextT *ctx) { sCtx = ctx; sAc = ctx->shellCtx; basStringSystemInit(); buildWindow(); sFilePath[0] = '\0'; sSourceBuf[0] = '\0'; sOutputBuf[0] = '\0'; sOutputLen = 0; setStatus("Ready. Open a .BAS file or type code and press Run."); return 0; } // ============================================================ // buildWindow // ============================================================ static void buildWindow(void) { sWin = dvxCreateWindow(sAc, "DVX BASIC", IDE_WIN_X, IDE_WIN_Y, IDE_WIN_W, IDE_WIN_H, true); if (!sWin) { return; } sWin->onClose = onClose; sWin->onMenu = onMenu; // Menu bar MenuBarT *menuBar = wmAddMenuBar(sWin); MenuT *fileMenu = wmAddMenu(menuBar, "&File"); wmAddMenuItem(fileMenu, "&Open...\tCtrl+O", CMD_OPEN); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT); 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); MenuT *viewMenu = wmAddMenu(menuBar, "&View"); wmAddMenuItem(viewMenu, "&Code\tF7", CMD_VIEW_CODE); wmAddMenuItem(viewMenu, "&Object\tShift+F7", CMD_VIEW_DESIGN); 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, KEY_F7, 0, CMD_VIEW_CODE); dvxAddAccel(accel, KEY_F7, ACCEL_SHIFT, CMD_VIEW_DESIGN); dvxAddAccel(accel, 0x1B, 0, CMD_STOP); sWin->accelTable = accel; // Widget tree WidgetT *root = wgtInitWindow(sAc, sWin); // Horizontal splitter: editor on top, output+immediate on bottom WidgetT *splitter = wgtSplitter(root, false); splitter->weight = 100; // Top pane: source code editor WidgetT *editorFrame = wgtFrame(splitter, "Source"); // Object/Event dropdown row WidgetT *dropdownRow = wgtHBox(editorFrame); dropdownRow->spacing = wgtPixels(4); sObjDropdown = wgtDropdown(dropdownRow); sObjDropdown->weight = 100; sObjDropdown->onChange = onObjDropdownChange; wgtDropdownSetItems(sObjDropdown, NULL, 0); sEvtDropdown = wgtDropdown(dropdownRow); sEvtDropdown->weight = 100; sEvtDropdown->onChange = onEvtDropdownChange; wgtDropdownSetItems(sEvtDropdown, NULL, 0); sEditor = wgtTextArea(editorFrame, IDE_MAX_SOURCE); sEditor->weight = 100; wgtTextAreaSetColorize(sEditor, basicColorize, NULL); wgtTextAreaSetShowLineNumbers(sEditor, true); wgtTextAreaSetAutoIndent(sEditor, true); // Initialize designer (form window created on demand) dsgnInit(&sDesigner, sAc); // Bottom pane: output + immediate WidgetT *bottomPane = wgtVBox(splitter); WidgetT *outputFrame = wgtFrame(bottomPane, "Output"); outputFrame->weight = 70; sOutput = wgtTextArea(outputFrame, IDE_MAX_OUTPUT); sOutput->weight = 100; sOutput->readOnly = true; WidgetT *immFrame = wgtFrame(bottomPane, "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, ""); sStatus->weight = 100; } // ============================================================ // 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 // ============================================================ static void clearOutput(void) { sOutputBuf[0] = '\0'; sOutputLen = 0; wgtSetText(sOutput, ""); } // ============================================================ // compileAndRun // ============================================================ static void compileAndRun(void) { // Get source from editor const char *src = wgtGetText(sEditor); if (!src || *src == '\0') { setStatus("No source code to run."); return; } clearOutput(); setStatus("Compiling..."); // Force a display update so the status is visible dvxInvalidateWindow(sAc, sWin); int32_t srcLen = (int32_t)strlen(src); // Compile (heap-allocated -- BasParserT is ~300KB, too large for stack) BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT)); if (!parser) { setStatus("Out of memory."); return; } basParserInit(parser, src, srcLen); if (!basParse(parser)) { 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); return; } BasModuleT *mod = basParserBuildModule(parser); basParserFree(parser); free(parser); if (!mod) { setStatus("Failed to build module."); 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 BasVmT *vm = basVmCreate(); basVmLoadModule(vm, mod); // Set up implicit main frame vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount; vm->callDepth = 1; // Set I/O callbacks basVmSetPrintCallback(vm, printCallback, NULL); 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 in slices of 10000 steps, yielding to DVX between slices basVmSetStepLimit(vm, IDE_STEP_SLICE); int32_t totalSteps = 0; BasVmResultE result; for (;;) { result = basVmRun(vm); totalSteps += vm->stepCount; 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; } // 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; // Update output display wgtSetText(sOutput, sOutputBuf); static char statusBuf[128]; snprintf(statusBuf, sizeof(statusBuf), "Done. %ld instructions executed.", (long)totalSteps); setStatus(statusBuf); basFormRtDestroy(formRt); basVmDestroy(vm); } // ============================================================ // doEventsCallback // ============================================================ static bool doEventsCallback(void *ctx) { (void)ctx; // Stop if IDE window was closed or DVX is shutting down if (!sWin || !sAc->running) { return false; } 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); } // ============================================================ // inputCallback // ============================================================ static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize) { (void)ctx; // Append prompt to output if (prompt && sOutputLen < IDE_MAX_OUTPUT - 1) { int32_t n = snprintf(sOutputBuf + sOutputLen, IDE_MAX_OUTPUT - sOutputLen, "%s", prompt); sOutputLen += n; wgtSetText(sOutput, sOutputBuf); } return dvxInputBox(sAc, "DVX BASIC", prompt ? prompt : "Enter value:", NULL, buf, bufSize); } // ============================================================ // loadFile // ============================================================ static void loadFile(void) { FileFilterT filters[] = { { "BASIC Files (*.bas)", "*.bas" }, { "Form Files (*.frm)", "*.frm" }, { "All Files (*.*)", "*.*" } }; char path[260]; if (dvxFileDialog(sAc, "Open BASIC File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) { FILE *f = fopen(path, "r"); if (!f) { dvxMessageBox(sAc, "Error", "Could not open file.", MB_OK | MB_ICONERROR); return; } fseek(f, 0, SEEK_END); long size = ftell(f); fseek(f, 0, SEEK_SET); if (size >= IDE_MAX_SOURCE - 1) { fclose(f); dvxMessageBox(sAc, "Error", "File too large.", MB_OK | MB_ICONERROR); return; } int32_t bytesRead = (int32_t)fread(sSourceBuf, 1, size, f); fclose(f); sSourceBuf[bytesRead] = '\0'; wgtSetText(sEditor, sSourceBuf); strncpy(sFilePath, path, sizeof(sFilePath) - 1); sFilePath[sizeof(sFilePath) - 1] = '\0'; // Update window title char title[300]; snprintf(title, sizeof(title), "DVX BASIC - %s", path); dvxSetTitle(sAc, sWin, title); updateDropdowns(); setStatus("File loaded."); } } // ============================================================ // 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); } // ============================================================ // onClose // ============================================================ static void onClose(WindowT *win) { sWin = NULL; sEditor = NULL; sOutput = NULL; sImmediate = NULL; sObjDropdown = NULL; sEvtDropdown = NULL; sStatus = NULL; if (sCachedModule) { basModuleFree(sCachedModule); sCachedModule = NULL; } if (sFormWin) { onFormWinClose(sFormWin); } dsgnFree(&sDesigner); arrfree(sProcTable); arrfree(sObjItems); arrfree(sEvtItems); sProcTable = NULL; sObjItems = NULL; sEvtItems = NULL; dvxDestroyWindow(sAc, win); } // ============================================================ // onMenu // ============================================================ static void onMenu(WindowT *win, int32_t menuId) { (void)win; switch (menuId) { case CMD_OPEN: loadFile(); break; case CMD_RUN: 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; case CMD_VIEW_CODE: switchToCode(); break; case CMD_VIEW_DESIGN: switchToDesign(); break; case CMD_EXIT: if (sWin) { onClose(sWin); } break; } } // ============================================================ // 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 || objIdx >= (int32_t)arrlen(sObjItems) || evtIdx < 0) { return; } const char *selObj = sObjItems[objIdx]; // Find the proc matching the selected object + event int32_t matchIdx = 0; int32_t procCount = (int32_t)arrlen(sProcTable); for (int32_t i = 0; i < procCount; i++) { if (strcasecmp(sProcTable[i].objName, selObj) == 0) { 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 || objIdx >= (int32_t)arrlen(sObjItems)) { return; } const char *selObj = sObjItems[objIdx]; // Build event list for the selected object arrsetlen(sEvtItems, 0); int32_t procCount = (int32_t)arrlen(sProcTable); for (int32_t i = 0; i < procCount; i++) { if (strcasecmp(sProcTable[i].objName, selObj) == 0) { arrput(sEvtItems, sProcTable[i].evtName); } } int32_t evtCount = (int32_t)arrlen(sEvtItems); wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount); if (evtCount > 0) { wgtDropdownSetSelected(sEvtDropdown, 0); } } // ============================================================ // onOpenClick // ============================================================ // ============================================================ // printCallback // ============================================================ static void printCallback(void *ctx, const char *text, bool newline) { (void)ctx; if (!text) { return; } int32_t textLen = (int32_t)strlen(text); // Append to output buffer if (sOutputLen + textLen < IDE_MAX_OUTPUT - 2) { memcpy(sOutputBuf + sOutputLen, text, textLen); sOutputLen += textLen; } if (newline && sOutputLen < IDE_MAX_OUTPUT - 2) { sOutputBuf[sOutputLen++] = '\n'; } sOutputBuf[sOutputLen] = '\0'; // Update the output textarea immediately so PRINT is visible if (sOutput) { wgtSetText(sOutput, sOutputBuf); } } // ============================================================ // dsgnMouseCb // ============================================================ static void dsgnMouseCb(WidgetT *w, int32_t cx, int32_t cy, bool drag) { (void)w; dsgnOnMouse(&sDesigner, cx, cy, drag); prpRefresh(&sDesigner); } // ============================================================ // onFormWinClose // ============================================================ static void onFormWinClose(WindowT *win) { dvxDestroyWindow(sAc, win); sFormWin = NULL; dsgnSetCanvas(&sDesigner, NULL); if (sToolboxWin) { tbxDestroy(sAc, sToolboxWin); sToolboxWin = NULL; } if (sPropsWin) { prpDestroy(sAc, sPropsWin); sPropsWin = NULL; } } // ============================================================ // switchToCode // ============================================================ static void switchToCode(void) { if (sFormWin) { onFormWinClose(sFormWin); } setStatus("Code view."); } // ============================================================ // switchToDesign // ============================================================ static void switchToDesign(void) { // If already open, just bring to front if (sFormWin) { return; } // Load .frm if we don't have a form yet if (!sDesigner.form) { if (sFilePath[0]) { char frmPath[260]; snprintf(frmPath, sizeof(frmPath), "%s", sFilePath); char *dot = strrchr(frmPath, '.'); if (dot) { strcpy(dot, ".frm"); } else { strcat(frmPath, ".frm"); } FILE *f = fopen(frmPath, "r"); if (f) { fseek(f, 0, SEEK_END); long size = ftell(f); fseek(f, 0, SEEK_SET); if (size > 0 && size < IDE_MAX_SOURCE) { char *buf = (char *)malloc(size + 1); if (buf) { int32_t bytesRead = (int32_t)fread(buf, 1, size, f); buf[bytesRead] = '\0'; dsgnLoadFrm(&sDesigner, buf, bytesRead); free(buf); } } fclose(f); } } if (!sDesigner.form) { dsgnNewForm(&sDesigner, "Form1"); } } // Create the form designer window const char *formName = sDesigner.form ? sDesigner.form->name : "Form1"; int32_t formW = sDesigner.form ? sDesigner.form->width : IDE_DESIGN_W; int32_t formH = sDesigner.form ? sDesigner.form->height : IDE_DESIGN_H; char title[128]; snprintf(title, sizeof(title), "%s [Design]", formName); // Position next to the IDE window int32_t winX = IDE_WIN_X; int32_t winY = IDE_WIN_Y; sFormWin = dvxCreateWindow(sAc, title, winX, winY, formW + 10, formH + 10, true); if (!sFormWin) { return; } sFormWin->onClose = onFormWinClose; WidgetT *root = wgtInitWindow(sAc, sFormWin); WidgetT *canvas = wgtCanvas(root, formW, formH); canvas->weight = 100; wgtCanvasSetMouseCallback(canvas, dsgnMouseCb); dsgnSetCanvas(&sDesigner, canvas); // Create toolbox and properties windows if (!sToolboxWin) { sToolboxWin = tbxCreate(sAc, &sDesigner); } if (!sPropsWin) { sPropsWin = prpCreate(sAc, &sDesigner); } dsgnPaint(&sDesigner); setStatus("Design view open."); } // ============================================================ // setStatus // ============================================================ static void setStatus(const char *text) { if (sStatus) { 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) { // Reset dynamic arrays arrsetlen(sProcTable, 0); arrsetlen(sObjItems, 0); arrsetlen(sEvtItems, 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) { 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 IdeProcEntryT entry; memset(&entry, 0, sizeof(entry)); entry.lineNum = lineNum; char *underscore = strchr(procName, '_'); if (underscore) { int32_t objLen = (int32_t)(underscore - procName); if (objLen > 63) { objLen = 63; } memcpy(entry.objName, procName, objLen); entry.objName[objLen] = '\0'; snprintf(entry.evtName, sizeof(entry.evtName), "%s", underscore + 1); } else { snprintf(entry.objName, sizeof(entry.objName), "%s", "(General)"); snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName); } arrput(sProcTable, entry); } // Advance to end of line while (*pos && *pos != '\n') { pos++; } if (*pos == '\n') { pos++; } lineNum++; } // Build unique object names for the Object dropdown int32_t procCount = (int32_t)arrlen(sProcTable); for (int32_t i = 0; i < procCount; i++) { bool found = false; int32_t objCount = (int32_t)arrlen(sObjItems); for (int32_t j = 0; j < objCount; j++) { if (strcasecmp(sObjItems[j], sProcTable[i].objName) == 0) { found = true; break; } } if (!found) { arrput(sObjItems, sProcTable[i].objName); } } int32_t objCount = (int32_t)arrlen(sObjItems); wgtDropdownSetItems(sObjDropdown, sObjItems, objCount); if (objCount > 0) { wgtDropdownSetSelected(sObjDropdown, 0); onObjDropdownChange(sObjDropdown); } else { wgtDropdownSetItems(sEvtDropdown, NULL, 0); } }