DVX_GUI/dvxbasic/ide/ideMain.c

1234 lines
33 KiB
C

// 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 "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"
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
// ============================================================
// Constants
// ============================================================
#define IDE_WIN_W 560
#define IDE_WIN_H 400
#define IDE_BTN_W 80
#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
#define CMD_RUN 101
#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
// ============================================================
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 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
// ============================================================
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 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
// ============================================================
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) {
int32_t winX = (sAc->display.width - IDE_WIN_W) / 2;
int32_t winY = (sAc->display.height - IDE_WIN_H) / 4;
sWin = dvxCreateWindow(sAc, "DVX BASIC", winX, winY, 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);
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
WidgetT *root = wgtInitWindow(sAc, sWin);
// 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);
btnRow->spacing = wgtPixels(IDE_BTN_SPACING);
WidgetT *openBtn = wgtButton(btnRow, "&Open...");
openBtn->onClick = onOpenClick;
openBtn->prefW = wgtPixels(IDE_BTN_W);
WidgetT *runBtn = wgtButton(btnRow, "&Run");
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
WidgetT *outputFrame = wgtFrame(root, "Output");
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, "");
sStatus->weight = 100;
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
// ============================================================
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);
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);
}
// ============================================================
// onClearClick
// ============================================================
static void onClearClick(WidgetT *w) {
(void)w;
clearOutput();
setStatus("Output cleared.");
}
// ============================================================
// 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;
}
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_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 || 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
// ============================================================
static void onOpenClick(WidgetT *w) {
(void)w;
loadFile();
}
// ============================================================
// onRunClick
// ============================================================
static void onRunClick(WidgetT *w) {
(void)w;
compileAndRun();
}
// ============================================================
// onStopClick
// ============================================================
static void onStopClick(WidgetT *w) {
(void)w;
if (sVm) {
sVm->running = false;
setStatus("Program stopped.");
}
}
// ============================================================
// 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);
}
}
// ============================================================
// 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) {
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);
}
}