From 85010d17dc3aedceadce0d8484898269f1ba6f23 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 6 Apr 2026 15:36:47 -0500 Subject: [PATCH] Breakpoints seem to be working. --- apps/dvxbasic/dvxbasic.res | 5 + apps/dvxbasic/ide/ideMain.c | 513 +++++++++++++++++++++++--- apps/dvxbasic/runtime/vm.c | 38 +- apps/dvxbasic/runtime/vm.h | 11 +- apps/dvxbasic/tb_debug.bmp | 3 + apps/dvxbasic/tb_runtocur.bmp | 3 + apps/dvxbasic/tb_stepinto.bmp | 3 + apps/dvxbasic/tb_stepout.bmp | 3 + apps/dvxbasic/tb_stepover.bmp | 3 + apps/progman/progman.c | 1 + tools/mktbicon.c | 94 ++++- widgets/scrollPane/widgetScrollPane.c | 90 +++-- widgets/textInput/widgetTextInput.c | 20 +- widgets/widgetScrollPane.h | 4 +- 14 files changed, 685 insertions(+), 106 deletions(-) create mode 100644 apps/dvxbasic/tb_debug.bmp create mode 100644 apps/dvxbasic/tb_runtocur.bmp create mode 100644 apps/dvxbasic/tb_stepinto.bmp create mode 100644 apps/dvxbasic/tb_stepout.bmp create mode 100644 apps/dvxbasic/tb_stepover.bmp diff --git a/apps/dvxbasic/dvxbasic.res b/apps/dvxbasic/dvxbasic.res index d84f195..3794eb8 100644 --- a/apps/dvxbasic/dvxbasic.res +++ b/apps/dvxbasic/dvxbasic.res @@ -10,5 +10,10 @@ tb_run icon tb_run.bmp tb_stop icon tb_stop.bmp tb_code icon tb_code.bmp tb_design icon tb_design.bmp +tb_debug icon tb_debug.bmp +tb_stepin icon tb_stepinto.bmp +tb_stepov icon tb_stepover.bmp +tb_stepou icon tb_stepout.bmp +tb_runtoc icon tb_runtocur.bmp # Placeholder icon (32x32) noicon icon noicon.bmp diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index e1681b7..973d504 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -27,6 +27,7 @@ #include "widgetSplitter.h" #include "widgetStatusBar.h" #include "widgetListView.h" +#include "widgetSeparator.h" #include "widgetToolbar.h" #include "ideDesigner.h" @@ -104,8 +105,9 @@ #define CMD_TOGGLE_BP 149 #define CMD_RUN_TO_CURSOR 150 #define CMD_WIN_LOCALS 151 -#define CMD_WIN_CALLSTACK 152 -#define CMD_WIN_WATCH 153 +#define CMD_DEBUG 152 +#define CMD_WIN_CALLSTACK 153 +#define CMD_WIN_WATCH 154 #define IDE_MAX_IMM 1024 #define IDE_DESIGN_W 400 #define IDE_DESIGN_H 300 @@ -178,8 +180,10 @@ static void handleWindowCmd(int32_t cmd); static void onMenu(WindowT *win, int32_t menuId); static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); static uint32_t debugLineDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx); +static void onBreakpointHit(void *ctx, int32_t line); static void onGutterClick(WidgetT *w, int32_t lineNum); static void debugNavigateToLine(int32_t concatLine); +static void buildVmBreakpoints(void); static void showCallStackWindow(void); static void showLocalsWindow(void); static void showWatchWindow(void); @@ -217,6 +221,11 @@ static void onTbStop(WidgetT *w); static void onTbOpen(WidgetT *w); static void onTbCode(WidgetT *w); static void onTbDesign(WidgetT *w); +static void onTbDebug(WidgetT *w); +static void onTbStepInto(WidgetT *w); +static void onTbStepOver(WidgetT *w); +static void onTbStepOut(WidgetT *w); +static void onTbRunToCur(WidgetT *w); static void selectDropdowns(const char *objName, const char *evtName); static void updateDropdowns(void); @@ -294,13 +303,21 @@ typedef enum { DBG_PAUSED // stopped at breakpoint/step } IdeDebugStateE; +typedef struct { + int32_t fileIdx; // project file index + int32_t codeLine; // line within file's code section (1-based) + int32_t procIdx; // procedure index at time of toggle (-1 = general) +} IdeBreakpointT; + static IdeDebugStateE sDbgState = DBG_IDLE; -static int32_t *sBreakpoints = NULL; // stb_ds array of line numbers +static IdeBreakpointT *sBreakpoints = NULL; // stb_ds array static int32_t sBreakpointCount = 0; +static int32_t *sVmBreakpoints = NULL; // stb_ds array of concat line numbers (built at compile time) static int32_t sDbgCurrentLine = -1; // line where paused (-1 = none) static BasFormRtT *sDbgFormRt = NULL; // form runtime for debug session static BasModuleT *sDbgModule = NULL; // module for debug session static bool sDbgBreakOnStart = false; // break at first statement +static bool sDbgEnabled = false; // true = debug mode (breakpoints active) static WindowT *sLocalsWin = NULL; // Locals window static WidgetT *sLocalsList = NULL; // Locals ListView widget static WindowT *sCallStackWin = NULL; // Call stack window @@ -506,6 +523,7 @@ static void activateFile(int32_t fileIdx, IdeViewModeE view) { sProject.activeFileIdx = fileIdx; } + updateProjectMenuState(); updateDirtyIndicators(); } @@ -655,6 +673,7 @@ static void buildWindow(void) { MenuT *runMenu = wmAddMenu(menuBar, "&Run"); wmAddMenuItem(runMenu, "&Run\tF5", CMD_RUN); + wmAddMenuItem(runMenu, "&Debug\tShift+F5", CMD_DEBUG); wmAddMenuItem(runMenu, "Run &Without Recompile\tCtrl+F5", CMD_RUN_NOCMP); wmAddMenuItem(runMenu, "&Stop\tEsc", CMD_STOP); wmAddMenuSeparator(runMenu); @@ -671,7 +690,7 @@ static void buildWindow(void) { MenuT *viewMenu = wmAddMenu(menuBar, "&View"); wmAddMenuItem(viewMenu, "&Code\tF7", CMD_VIEW_CODE); - wmAddMenuItem(viewMenu, "&Object\tShift+F7", CMD_VIEW_DESIGN); + wmAddMenuItem(viewMenu, "&Designer\tShift+F7", CMD_VIEW_DESIGN); wmAddMenuSeparator(viewMenu); wmAddMenuCheckItem(viewMenu, "&Toolbar", CMD_VIEW_TOOLBAR, true); wmAddMenuCheckItem(viewMenu, "&Status Bar", CMD_VIEW_STATUS, true); @@ -700,6 +719,7 @@ static void buildWindow(void) { dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_OPEN); dvxAddAccel(accel, 'S', ACCEL_CTRL, CMD_SAVE); dvxAddAccel(accel, KEY_F5, 0, CMD_RUN); + dvxAddAccel(accel, KEY_F5, ACCEL_SHIFT, CMD_DEBUG); 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); @@ -719,6 +739,7 @@ static void buildWindow(void) { sToolbar = wgtToolbar(tbRoot); WidgetT *tb = sToolbar; + // File group WidgetT *tbOpen = loadTbIcon(tb, "tb_open", "Open"); tbOpen->onClick = onTbOpen; wgtSetTooltip(tbOpen, "Open (Ctrl+O)"); @@ -727,6 +748,10 @@ static void buildWindow(void) { tbSave->onClick = onTbSave; wgtSetTooltip(tbSave, "Save (Ctrl+S)"); + WidgetT *sep1 = wgtVSeparator(tb); + sep1->minW = wgtPixels(10); + + // Run group WidgetT *tbRun = loadTbIcon(tb, "tb_run", "Run"); tbRun->onClick = onTbRun; wgtSetTooltip(tbRun, "Run (F5)"); @@ -735,6 +760,34 @@ static void buildWindow(void) { tbStop->onClick = onTbStop; wgtSetTooltip(tbStop, "Stop (Esc)"); + WidgetT *sep2 = wgtVSeparator(tb); + sep2->minW = wgtPixels(10); + + // Debug group + WidgetT *tbDebug = loadTbIcon(tb, "tb_debug", "Debug"); + tbDebug->onClick = onTbDebug; + wgtSetTooltip(tbDebug, "Debug (Shift+F5)"); + + WidgetT *tbStepInto = loadTbIcon(tb, "tb_stepin", "Into"); + tbStepInto->onClick = onTbStepInto; + wgtSetTooltip(tbStepInto, "Step Into (F8)"); + + WidgetT *tbStepOver = loadTbIcon(tb, "tb_stepov", "Over"); + tbStepOver->onClick = onTbStepOver; + wgtSetTooltip(tbStepOver, "Step Over (Shift+F8)"); + + WidgetT *tbStepOut = loadTbIcon(tb, "tb_stepou", "Out"); + tbStepOut->onClick = onTbStepOut; + wgtSetTooltip(tbStepOut, "Step Out (Ctrl+Shift+F8)"); + + WidgetT *tbRunToCur = loadTbIcon(tb, "tb_runtoc", "Cursor"); + tbRunToCur->onClick = onTbRunToCur; + wgtSetTooltip(tbRunToCur, "Run to Cursor (Ctrl+F8)"); + + WidgetT *sep3 = wgtVSeparator(tb); + sep3->minW = wgtPixels(10); + + // View group WidgetT *tbCode = loadTbIcon(tb, "tb_code", "Code"); tbCode->onClick = onTbCode; wgtSetTooltip(tbCode, "Code View (F7)"); @@ -948,6 +1001,88 @@ static void clearOutput(void) { } +// ============================================================ +// localToConcatLine -- convert editor-local line to concatenated source line +// ============================================================ + +// Convert an editor-local line number to a full-source line number. +// The editor shows one procedure at a time (sCurProcIdx), so we need +// to add the procedure's starting line offset within the file's code. +// For multi-file projects, we also add the file's offset in the +// concatenated source. For .frm files, an injected BEGINFORM directive +// adds one extra line before the code. +static int32_t localToConcatLine(int32_t editorLine) { + int32_t fileLine = editorLine; + + // Add the current procedure's start offset within the file's code. + // sCurProcIdx == -1 means (General) section which starts at line 1 + // of the file's code (not the concatenated source). + if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { + fileLine = sProcTable[sCurProcIdx].lineNum + editorLine - 1; + } + + // For projects, add the file's offset in the concatenated source. + // For .frm files, startLine points to the injected BEGINFORM line, + // so the actual code starts at startLine + 1. + if (sProject.sourceMapCount > 0 && sProject.activeFileIdx >= 0) { + for (int32_t i = 0; i < sProject.sourceMapCount; i++) { + if (sProject.sourceMap[i].fileIdx == sProject.activeFileIdx) { + int32_t base = sProject.sourceMap[i].startLine; + + // .frm files have an injected BEGINFORM line before the code + if (sProject.activeFileIdx < sProject.fileCount && + sProject.files[sProject.activeFileIdx].isForm) { + base++; + } + + return base + fileLine - 1; + } + } + } + + return fileLine; +} + + +// ============================================================ +// concatToLocalLine -- convert concatenated source line to editor-local line +// ============================================================ + +// Convert a full-source line number back to an editor-local line number. +// Reverses the file offset, BEGINFORM offset, and procedure offset so +// the line matches what the user sees in the editor. +static int32_t concatToLocalLine(int32_t concatLine) { + int32_t fileLine = concatLine; + + // Strip multi-file offset and BEGINFORM offset + if (sProject.sourceMapCount > 0) { + int32_t fileIdx = -1; + int32_t localLine = concatLine; + + if (prjMapLine(&sProject, concatLine, &fileIdx, &localLine)) { + fileLine = localLine; + + // For .frm files, subtract the BEGINFORM line + if (fileIdx >= 0 && fileIdx < sProject.fileCount && + sProject.files[fileIdx].isForm) { + fileLine--; + } + } + } + + // Strip procedure offset to get editor-local line + if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { + int32_t procStart = sProcTable[sCurProcIdx].lineNum; + + if (fileLine >= procStart) { + return fileLine - procStart + 1; + } + } + + return fileLine; +} + + // ============================================================ // debugLineDecorator -- highlight breakpoints and current debug line // ============================================================ @@ -956,16 +1091,22 @@ static void clearOutput(void) { // toggleBreakpointLine -- toggle breakpoint on a specific line // ============================================================ -static void toggleBreakpointLine(int32_t line) { +static void toggleBreakpointLine(int32_t editorLine) { + int32_t fileIdx = sProject.activeFileIdx; + + // Convert editor line to file code line by adding proc offset + int32_t codeLine = editorLine; + + if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { + codeLine = sProcTable[sCurProcIdx].lineNum + editorLine - 1; + } + + // Check if this breakpoint already exists — remove it for (int32_t i = 0; i < sBreakpointCount; i++) { - if (sBreakpoints[i] == line) { + if (sBreakpoints[i].fileIdx == fileIdx && sBreakpoints[i].codeLine == codeLine) { arrdel(sBreakpoints, i); sBreakpointCount = (int32_t)arrlen(sBreakpoints); - if (sVm) { - basVmSetBreakpoints(sVm, sBreakpoints, sBreakpointCount); - } - if (sEditor) { wgtInvalidatePaint(sEditor); } @@ -974,12 +1115,16 @@ static void toggleBreakpointLine(int32_t line) { } } - arrput(sBreakpoints, line); + // Add new breakpoint + IdeBreakpointT bp; + bp.fileIdx = fileIdx; + bp.codeLine = codeLine; + bp.procIdx = sCurProcIdx; + arrput(sBreakpoints, bp); sBreakpointCount = (int32_t)arrlen(sBreakpoints); - if (sVm) { - basVmSetBreakpoints(sVm, sBreakpoints, sBreakpointCount); - } + dvxLog("[BP] Added: file=%d editorLine=%d codeLine=%d procIdx=%d", + fileIdx, editorLine, codeLine, sCurProcIdx); if (sEditor) { wgtInvalidatePaint(sEditor); @@ -998,19 +1143,28 @@ static void onGutterClick(WidgetT *w, int32_t lineNum) { static uint32_t debugLineDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx) { - (void)ctx; + AppContextT *ac = (AppContextT *)ctx; + + // Convert editor line to file code line + int32_t codeLine = lineNum; + + if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { + codeLine = sProcTable[sCurProcIdx].lineNum + lineNum - 1; + } + + int32_t fileIdx = sProject.activeFileIdx; // Breakpoint: red gutter dot for (int32_t i = 0; i < sBreakpointCount; i++) { - if (sBreakpoints[i] == lineNum) { - *gutterColor = 0x00CC0000; // red + if (sBreakpoints[i].fileIdx == fileIdx && sBreakpoints[i].codeLine == codeLine) { + *gutterColor = packColor(&ac->display, 200, 0, 0); break; } } - // Current debug line: yellow background + // Current debug line: yellow background (sDbgCurrentLine is editor-local) if (sDbgState == DBG_PAUSED && lineNum == sDbgCurrentLine) { - return 0x00FFFF80; // yellow + return packColor(&ac->display, 255, 255, 128); } return 0; @@ -1029,9 +1183,15 @@ static void debugNavigateToLine(int32_t concatLine) { int32_t fileIdx = -1; int32_t localLine = concatLine; - // Try to map via source map (multi-file projects) + // Map concatenated line to file and file-local line (code section) if (sProject.sourceMapCount > 0) { prjMapLine(&sProject, concatLine, &fileIdx, &localLine); + + // For .frm files, subtract the injected BEGINFORM line + if (fileIdx >= 0 && fileIdx < sProject.fileCount && + sProject.files[fileIdx].isForm) { + localLine--; + } } // Switch to the correct file if needed @@ -1039,22 +1199,149 @@ static void debugNavigateToLine(int32_t concatLine) { activateFile(fileIdx, ViewCodeE); } - // Show code window + // Ensure the code editor is loaded for this file + if (!sEditor && sCodeWin) { + // File was activated but editor might not be set up + } + + // Show code window (recreate if it was closed). + // Don't rebuild proc tables — they were built at session start. + // Just recreate the window; showProc below will load the text. + if (!sCodeWin) { + showCodeWindow(); + sCurProcIdx = -2; // force showProc to reload even if same proc + } + if (sCodeWin && !sCodeWin->visible) { dvxShowWindow(sAc, sCodeWin); } - // Store the local line for the decorator - sDbgCurrentLine = localLine; + if (sCodeWin) { + dvxRaiseWindow(sAc, sCodeWin); + } + + // Find which procedure we're in using the VM's PC and the compiled + // module's proc table. Then match by name to the editor's proc table. + int32_t targetProcIdx = -1; // -1 = General section + int32_t procCount = (int32_t)arrlen(sProcTable); + + if (sVm && sDbgModule) { + // Find the proc whose codeAddr is closest to (but not exceeding) the PC + const char *procName = NULL; + int32_t bestAddr = -1; + + for (int32_t i = 0; i < sDbgModule->procCount; i++) { + int32_t addr = sDbgModule->procs[i].codeAddr; + + if (addr <= sVm->pc && addr > bestAddr) { + bestAddr = addr; + procName = sDbgModule->procs[i].name; + } + } + + dvxLog("[Navigate] PC=%d bestAddr=%d procName=%s localLine=%d", + sVm->pc, bestAddr, procName ? procName : "(null)", localLine); + + // Match the compiled proc name to the editor's proc table + if (procName) { + for (int32_t i = 0; i < procCount; i++) { + // Build the full proc name from obj + "_" + evt + char fullName[128]; + + if (sProcTable[i].objName[0] && + strcmp(sProcTable[i].objName, "(General)") != 0) { + snprintf(fullName, sizeof(fullName), "%s_%s", + sProcTable[i].objName, sProcTable[i].evtName); + } else { + snprintf(fullName, sizeof(fullName), "%s", sProcTable[i].evtName); + } + + dvxLog("[Navigate] match? '%s' vs '%s'", fullName, procName); + + if (strcasecmp(fullName, procName) == 0) { + targetProcIdx = i; + break; + } + } + } + } + + // Switch to the target procedure + if (targetProcIdx != sCurProcIdx) { + showProc(targetProcIdx); + } + + // Update dropdowns to match + if (targetProcIdx >= 0 && targetProcIdx < procCount) { + selectDropdowns(sProcTable[targetProcIdx].objName, + sProcTable[targetProcIdx].evtName); + } else if (sObjDropdown) { + wgtDropdownSetSelected(sObjDropdown, 0); + } + + // Compute editor-local line within the current proc + int32_t editorLine = localLine; + + if (sCurProcIdx >= 0 && sCurProcIdx < procCount) { + editorLine = localLine - sProcTable[sCurProcIdx].lineNum + 1; + } + + // Store the editor-local line for the decorator + sDbgCurrentLine = editorLine; + dvxLog("[Navigate] targetProc=%d sCurProc=%d editorLine=%d", targetProcIdx, sCurProcIdx, editorLine); - // Navigate and highlight if (sEditor) { - wgtTextAreaGoToLine(sEditor, localLine); + wgtTextAreaGoToLine(sEditor, editorLine); wgtInvalidatePaint(sEditor); } } +// ============================================================ +// buildVmBreakpoints -- convert IDE breakpoints to VM concat line numbers +// ============================================================ +// +// Called after compilation when the source map is fresh. Converts +// each (fileIdx, codeLine) pair to a concatenated source line number +// that matches OP_LINE values in the compiled bytecode. + +static void buildVmBreakpoints(void) { + arrfree(sVmBreakpoints); + sVmBreakpoints = NULL; + + dvxLog("[Debug] Building VM breakpoints: IDE=%d sourceMap=%d", + sBreakpointCount, sProject.sourceMapCount); + + for (int32_t i = 0; i < sBreakpointCount; i++) { + int32_t fileIdx = sBreakpoints[i].fileIdx; + int32_t codeLine = sBreakpoints[i].codeLine; + + // Find this file in the source map + for (int32_t m = 0; m < sProject.sourceMapCount; m++) { + if (sProject.sourceMap[m].fileIdx == fileIdx) { + int32_t base = sProject.sourceMap[m].startLine; + + // .frm files have an injected BEGINFORM line + if (fileIdx >= 0 && fileIdx < sProject.fileCount && + sProject.files[fileIdx].isForm) { + base++; + } + + int32_t vmLine = base + codeLine - 1; + arrput(sVmBreakpoints, vmLine); + + dvxLog(" BP %d: file=%d codeLine=%d startLine=%d isForm=%d -> vmLine=%d", + i, fileIdx, codeLine, sProject.sourceMap[m].startLine, + sProject.files[fileIdx].isForm, vmLine); + break; + } + } + } + + dvxLog(" VM breakpoints built: %d", (int32_t)arrlen(sVmBreakpoints)); +} + + static int cmpStrPtrs(const void *a, const void *b) { const char *sa = *(const char **)a; const char *sb = *(const char **)b; @@ -1161,7 +1448,10 @@ static void compileAndRun(void) { // If this is the active form in the designer, use form->code. if (sDesigner.form && i == sProject.activeFileIdx) { fileSrc = sDesigner.form->code; + dvxLog("[Compile] Form %d: using designer code (code=%s)", + i, fileSrc ? "yes" : "NULL"); } else if (sProject.files[i].buffer) { + dvxLog("[Compile] Form %d: extracting code from buffer", i); // Extract code from the stashed .frm text (after "End\n") const char *buf = sProject.files[i].buffer; const char *endTag = strstr(buf, "\nEnd\n"); @@ -1238,9 +1528,23 @@ static void compileAndRun(void) { } if (!fileSrc) { + dvxLog("[Compile] File %d: no source found, skipping", i); continue; } + { + char preview[81]; + int32_t pl = (int32_t)strlen(fileSrc); + if (pl > 80) { pl = 80; } + memcpy(preview, fileSrc, pl); + preview[pl] = '\0'; + // Replace newlines for readability + for (int32_t j = 0; j < pl; j++) { + if (preview[j] == '\n' || preview[j] == '\r') { preview[j] = '|'; } + } + dvxLog("[Compile] File %d: startLine=%d src=\"%s...\"", i, line, preview); + } + int32_t startLine = line; // Inject BEGINFORM directive for .frm code sections @@ -1431,12 +1735,13 @@ static void debugStartOrResume(int32_t cmd) { case CMD_RUN_TO_CURSOR: if (sEditor) { - basVmRunToCursor(sVm, wgtTextAreaGetCursorLine(sEditor)); + basVmRunToCursor(sVm, localToConcatLine(wgtTextAreaGetCursorLine(sEditor))); } break; } sDbgState = DBG_RUNNING; + sVm->running = true; if (sEditor) { wgtInvalidatePaint(sEditor); @@ -1447,7 +1752,8 @@ static void debugStartOrResume(int32_t cmd) { } if (sDbgState == DBG_IDLE) { - // Not running — compile and start with an immediate break. + // Not running — compile and start in debug mode. + sDbgEnabled = true; sDbgBreakOnStart = true; compileAndRun(); sDbgBreakOnStart = false; @@ -1466,7 +1772,8 @@ static void runModule(BasModuleT *mod) { // Hide designer windows while the program runs. // Keep the code window visible if debugging (breakpoints or step-into). - bool debugging = sDbgBreakOnStart || sBreakpointCount > 0; + bool debugging = sDbgEnabled; + dvxLog("[Run] debugging=%d sDbgEnabled=%d sCodeWin=%p", debugging, sDbgEnabled, (void *)sCodeWin); bool hadFormWin = sFormWin && sFormWin->visible; bool hadToolbox = sToolboxWin && sToolboxWin->visible; bool hadProps = sPropsWin && sPropsWin->visible; @@ -1476,7 +1783,7 @@ static void runModule(BasModuleT *mod) { if (sFormWin) { dvxHideWindow(sAc, sFormWin); } if (sToolboxWin) { dvxHideWindow(sAc, sToolboxWin); } if (sPropsWin) { dvxHideWindow(sAc, sPropsWin); } - if (!debugging && sCodeWin) { dvxHideWindow(sAc, sCodeWin); } + if (sCodeWin) { dvxHideWindow(sAc, sCodeWin); } if (sProjectWin) { dvxHideWindow(sAc, sProjectWin); } // Create VM @@ -1502,6 +1809,8 @@ static void runModule(BasModuleT *mod) { basVmSetPrintCallback(vm, printCallback, NULL); basVmSetInputCallback(vm, inputCallback, NULL); basVmSetDoEventsCallback(vm, doEventsCallback, NULL); + vm->breakpointFn = onBreakpointHit; + vm->breakpointCtx = NULL; // Set SQL callbacks BasSqlCallbacksT sqlCb; @@ -1533,11 +1842,26 @@ static void runModule(BasModuleT *mod) { // 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 + sVm = vm; + sDbgFormRt = formRt; + sDbgModule = mod; + sDbgState = DBG_RUNNING; + + // Set breakpoints BEFORE loading forms so breakpoints in form + // init code (module-level statements inside BEGINFORM) fire. + if (sDbgEnabled) { + buildVmBreakpoints(); + basVmSetBreakpoints(vm, sVmBreakpoints, (int32_t)arrlen(sVmBreakpoints)); + + if (sDbgBreakOnStart) { + basVmStepInto(vm); + } + } + + // Load forms (AFTER breakpoints are set so init code breakpoints fire) loadFrmFiles(formRt); // Auto-show the startup form (or first form if none specified). - // Other forms remain hidden until code calls Show. if (formRt->formCount > 0) { BasFormT *startupForm = &formRt->forms[0]; @@ -1553,19 +1877,6 @@ static void runModule(BasModuleT *mod) { basFormRtShowForm(formRt, startupForm, false); } - sVm = vm; - sDbgFormRt = formRt; - sDbgModule = mod; - sDbgState = DBG_RUNNING; - - // Pass breakpoints to the VM - basVmSetBreakpoints(vm, sBreakpoints, sBreakpointCount); - - // If starting via Step Into, break at the first OP_LINE - if (sDbgBreakOnStart) { - basVmStepInto(vm); - } - // Run in slices of 10000 steps, yielding to DVX between slices basVmSetStepLimit(vm, IDE_STEP_SLICE); @@ -1666,11 +1977,12 @@ static void runModule(BasModuleT *mod) { } } - sVm = NULL; - sDbgFormRt = NULL; - sDbgModule = NULL; - sDbgState = DBG_IDLE; + sVm = NULL; + sDbgFormRt = NULL; + sDbgModule = NULL; + sDbgState = DBG_IDLE; sDbgCurrentLine = -1; + sDbgEnabled = false; // Update output display setOutputText(sOutputBuf); @@ -3317,14 +3629,39 @@ static void handleRunCmd(int32_t cmd) { switch (cmd) { case CMD_RUN: if (sDbgState == DBG_PAUSED) { - // Resume from breakpoint + // Resume from breakpoint — clear debug mode so it runs free sDbgCurrentLine = -1; sDbgState = DBG_RUNNING; + sDbgEnabled = false; + if (sVm) { + basVmSetBreakpoints(sVm, NULL, 0); + sVm->running = true; + } if (sEditor) { wgtInvalidatePaint(sEditor); } setStatus("Running..."); } else { + sDbgEnabled = false; + compileAndRun(); + } + break; + + case CMD_DEBUG: + if (sDbgState == DBG_PAUSED) { + // Already debugging — resume + sDbgCurrentLine = -1; + sDbgState = DBG_RUNNING; + if (sVm) { + sVm->running = true; + } + if (sEditor) { + wgtInvalidatePaint(sEditor); + } + setStatus("Debugging..."); + } else if (sDbgState == DBG_IDLE) { + // Start in debug mode with breakpoints + sDbgEnabled = true; compileAndRun(); } break; @@ -3338,8 +3675,9 @@ static void handleRunCmd(int32_t cmd) { if (sVm) { sVm->running = false; } - sDbgState = DBG_IDLE; + sDbgState = DBG_IDLE; sDbgCurrentLine = -1; + sDbgEnabled = false; if (sEditor) { wgtInvalidatePaint(sEditor); } @@ -5183,7 +5521,31 @@ static void onFormWinClose(WindowT *win) { // ============================================================ static void stashDesignerState(void) { + // If a form is open in the designer, switch to its code view + if (sDesigner.form && sProject.activeFileIdx >= 0 && + sProject.activeFileIdx < sProject.fileCount && + sProject.files[sProject.activeFileIdx].isForm) { + loadFormCodeIntoEditor(); + + if (sCodeWin) { + dvxRaiseWindow(sAc, sCodeWin); + } + + setStatus("Code view."); + return; + } + stashCurrentFile(); + + // Show code window if hidden + if (sCodeWin && !sCodeWin->visible) { + dvxShowWindow(sAc, sCodeWin); + } + + if (sCodeWin) { + dvxRaiseWindow(sAc, sCodeWin); + } + setStatus("Code view."); } @@ -5228,14 +5590,15 @@ static void switchToDesign(void) { WidgetT *root = wgtInitWindow(sAc, sFormWin); WidgetT *contentBox; + // Reuse root VBox directly for VBox layout (matches runtime). + // Only nest for HBox since wgtInitWindow always creates a VBox. if (sDesigner.form && strcasecmp(sDesigner.form->layout, "HBox") == 0) { contentBox = wgtHBox(root); + contentBox->weight = 100; } else { - contentBox = wgtVBox(root); + contentBox = root; } - contentBox->weight = 100; - // Override paint and mouse AFTER wgtInitWindow (which sets widgetOnPaint) sFormWin->onPaint = onFormWinPaint; sFormWin->onMouse = onFormWinMouse; @@ -5284,6 +5647,7 @@ static void switchToDesign(void) { } dvxInvalidateWindow(sAc, sFormWin); + updateProjectMenuState(); setStatus("Design view open."); } @@ -5304,12 +5668,41 @@ static void teardownFormWin(void) { // Toolbar button handlers // ============================================================ -static void onTbOpen(WidgetT *w) { (void)w; loadFile(); } -static void onTbSave(WidgetT *w) { (void)w; saveFile(); } -static void onTbRun(WidgetT *w) { (void)w; compileAndRun(); } -static void onTbStop(WidgetT *w) { (void)w; sStopRequested = true; if (sVm) { sVm->running = false; } setStatus("Program stopped."); } -static void onTbCode(WidgetT *w) { (void)w; stashDesignerState(); } -static void onTbDesign(WidgetT *w) { (void)w; switchToDesign(); } +static void onTbOpen(WidgetT *w) { (void)w; loadFile(); } +static void onTbSave(WidgetT *w) { (void)w; saveFile(); } +static void onTbRun(WidgetT *w) { (void)w; compileAndRun(); } +static void onTbStop(WidgetT *w) { (void)w; sStopRequested = true; if (sVm) { sVm->running = false; } setStatus("Program stopped."); } +static void onTbCode(WidgetT *w) { (void)w; stashDesignerState(); } +static void onTbDesign(WidgetT *w) { (void)w; switchToDesign(); } +static void onBreakpointHit(void *ctx, int32_t line) { + (void)ctx; + dvxLog("[Breakpoint] Hit line %d, sCodeWin=%p visible=%d", + line, (void *)sCodeWin, sCodeWin ? sCodeWin->visible : -1); + sDbgState = DBG_PAUSED; + sDbgCurrentLine = line; + debugNavigateToLine(line); + + // Ensure code window is visible and raised + if (sCodeWin) { + if (!sCodeWin->visible) { + dvxShowWindow(sAc, sCodeWin); + } + dvxRaiseWindow(sAc, sCodeWin); + } + + updateLocalsWindow(); + updateCallStackWindow(); + updateWatchWindow(); + setStatus("Paused."); + dvxLog("[Breakpoint] After navigate: sCodeWin visible=%d sDbgCurrentLine=%d", + sCodeWin ? sCodeWin->visible : -1, sDbgCurrentLine); +} + +static void onTbDebug(WidgetT *w) { (void)w; handleRunCmd(CMD_DEBUG); } +static void onTbStepInto(WidgetT *w) { (void)w; handleRunCmd(CMD_STEP_INTO); } +static void onTbStepOver(WidgetT *w) { (void)w; handleRunCmd(CMD_STEP_OVER); } +static void onTbStepOut(WidgetT *w) { (void)w; handleRunCmd(CMD_STEP_OUT); } +static void onTbRunToCur(WidgetT *w) { (void)w; handleRunCmd(CMD_RUN_TO_CURSOR); } // ============================================================ @@ -5360,7 +5753,7 @@ static void showCodeWindow(void) { sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE); sEditor->weight = 100; wgtTextAreaSetColorize(sEditor, basicColorize, NULL); - wgtTextAreaSetLineDecorator(sEditor, debugLineDecorator, NULL); + wgtTextAreaSetLineDecorator(sEditor, debugLineDecorator, sAc); wgtTextAreaSetGutterClick(sEditor, onGutterClick); wgtTextAreaSetShowLineNumbers(sEditor, true); wgtTextAreaSetAutoIndent(sEditor, true); diff --git a/apps/dvxbasic/runtime/vm.c b/apps/dvxbasic/runtime/vm.c index e9de43e..9437f0a 100644 --- a/apps/dvxbasic/runtime/vm.c +++ b/apps/dvxbasic/runtime/vm.c @@ -62,12 +62,28 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool } if (result == BAS_VM_BREAKPOINT) { - // Pause the VM so the host's debug loop can take over. - // Don't restore savedPc/savedRunning — the VM stays at - // the breakpoint location. The host will resume execution - // which will eventually return here and complete the sub. + // Pause in place: yield to the GUI until the host resumes. + // This ensures the sub runs to completion before returning + // to the caller (critical for form init code, event handlers). vm->running = false; - return true; + + // Notify host to update debug UI (highlight line, locals, etc.) + if (vm->breakpointFn) { + vm->breakpointFn(vm->breakpointCtx, vm->currentLine); + } + + while (!vm->running && vm->doEventsFn) { + if (!vm->doEventsFn(vm->doEventsCtx)) { + // Host is shutting down + vm->pc = savedPc; + vm->running = savedRunning; + return false; + } + } + + // User resumed — continue executing the sub + stepsSinceYield = 0; + continue; } if (result != BAS_VM_OK) { @@ -3334,6 +3350,18 @@ BasVmResultE basVmStep(BasVmT *vm) { uint16_t lineNum = readUint16(vm); vm->currentLine = lineNum; + // Debug trace: log first 20 OP_LINE values when breakpoints active + if (vm->breakpointCount > 0) { + static int32_t sLineLogCount = 0; + + if (sLineLogCount < 20) { + extern void dvxLog(const char *fmt, ...); + dvxLog(" OP_LINE %d (bp[0]=%d, bpCount=%d)", + (int)lineNum, vm->breakpoints[0], vm->breakpointCount); + sLineLogCount++; + } + } + // Step into: break at any OP_LINE if (vm->debugBreak) { vm->debugBreak = false; diff --git a/apps/dvxbasic/runtime/vm.h b/apps/dvxbasic/runtime/vm.h index 8e13274..9f29de0 100644 --- a/apps/dvxbasic/runtime/vm.h +++ b/apps/dvxbasic/runtime/vm.h @@ -74,6 +74,11 @@ typedef bool (*BasInputFnT)(void *ctx, const char *prompt, char *buf, int32_t bu // true to continue execution, false to stop the program. typedef bool (*BasDoEventsFnT)(void *ctx); +// Breakpoint callback: called when a breakpoint fires inside a +// nested sub call (runSubLoop). The host should update debug UI +// (highlight current line, update locals/call stack) and return. +typedef void (*BasBreakpointFnT)(void *ctx, int32_t line); + // ============================================================ // UI callbacks (host-provided, for form/control system) // ============================================================ @@ -352,8 +357,10 @@ typedef struct { void *printCtx; BasInputFnT inputFn; void *inputCtx; - BasDoEventsFnT doEventsFn; - void *doEventsCtx; + BasDoEventsFnT doEventsFn; + void *doEventsCtx; + BasBreakpointFnT breakpointFn; + void *breakpointCtx; // UI callbacks (set by host for form/control support) BasUiCallbacksT ui; diff --git a/apps/dvxbasic/tb_debug.bmp b/apps/dvxbasic/tb_debug.bmp new file mode 100644 index 0000000..95e157c --- /dev/null +++ b/apps/dvxbasic/tb_debug.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4349c82084163e41e49f0282a6c5c851797d0c171b824c0c8dd784102009d415 +size 822 diff --git a/apps/dvxbasic/tb_runtocur.bmp b/apps/dvxbasic/tb_runtocur.bmp new file mode 100644 index 0000000..3ad454f --- /dev/null +++ b/apps/dvxbasic/tb_runtocur.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57dcae0c2fc165f9ea62a2737bc648f1443c06b21152e237b94145b722759b04 +size 822 diff --git a/apps/dvxbasic/tb_stepinto.bmp b/apps/dvxbasic/tb_stepinto.bmp new file mode 100644 index 0000000..1e68a49 --- /dev/null +++ b/apps/dvxbasic/tb_stepinto.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:577a3b3fb061d5e4b0653f3a6f97a83259e3086af5d6924cec40b18892073ac7 +size 822 diff --git a/apps/dvxbasic/tb_stepout.bmp b/apps/dvxbasic/tb_stepout.bmp new file mode 100644 index 0000000..16988d0 --- /dev/null +++ b/apps/dvxbasic/tb_stepout.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b195cf89f15463fd7eb3f5b26e45e837e32cdd2d5a08bc6bfdbe46e9ff196887 +size 822 diff --git a/apps/dvxbasic/tb_stepover.bmp b/apps/dvxbasic/tb_stepover.bmp new file mode 100644 index 0000000..1f6fa38 --- /dev/null +++ b/apps/dvxbasic/tb_stepover.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70b3b96e2136f6f900b9045cf6c8ffd47c9a606efe3b8766bc721471904e76b9 +size 822 diff --git a/apps/progman/progman.c b/apps/progman/progman.c index 9e61bb5..f2817cd 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -201,6 +201,7 @@ static void buildPmWindow(void) { // the next row when the window width is exceeded. WidgetT *scroll = wgtScrollPane(appFrame); scroll->weight = 100; + wgtScrollPaneSetNoBorder(scroll, true); WidgetT *wrap = wgtWrapBox(scroll); wrap->weight = 100; diff --git a/tools/mktbicon.c b/tools/mktbicon.c index 8d7cde7..6ce9c29 100644 --- a/tools/mktbicon.c +++ b/tools/mktbicon.c @@ -173,6 +173,93 @@ static void iconDesign(void) { } +// Debug: bug icon with magnifying glass +static void iconDebug(void) { + clear(192, 192, 192); + // Bug body + rect(5, 4, 6, 8, 60, 160, 60); + outline(5, 4, 6, 8, 0, 100, 0); + // Bug head + rect(6, 2, 4, 3, 60, 160, 60); + outline(6, 2, 4, 3, 0, 100, 0); + // Antennae + px(6, 1, 0, 100, 0); px(5, 0, 0, 100, 0); + px(9, 1, 0, 100, 0); px(10, 0, 0, 100, 0); + // Legs + px(4, 6, 0, 100, 0); px(3, 5, 0, 100, 0); + px(4, 8, 0, 100, 0); px(3, 8, 0, 100, 0); + px(4, 10, 0, 100, 0); px(3, 11, 0, 100, 0); + px(11, 6, 0, 100, 0); px(12, 5, 0, 100, 0); + px(11, 8, 0, 100, 0); px(12, 8, 0, 100, 0); + px(11, 10, 0, 100, 0); px(12, 11, 0, 100, 0); +} + + +// Step Into: arrow pointing down into a line +static void iconStepInto(void) { + clear(192, 192, 192); + // Vertical arrow shaft + vline(8, 2, 8, 0, 0, 180); + // Arrow head + hline(6, 10, 5, 0, 0, 180); + hline(7, 11, 3, 0, 0, 180); + px(8, 12, 0, 0, 180); + // Horizontal line (code line) + hline(3, 14, 10, 80, 80, 80); +} + + +// Step Over: arrow curving over a line +static void iconStepOver(void) { + clear(192, 192, 192); + // Arc over + hline(4, 3, 8, 0, 0, 180); + vline(3, 4, 3, 0, 0, 180); + vline(12, 4, 5, 0, 0, 180); + px(4, 3, 0, 0, 180); + px(11, 3, 0, 0, 180); + // Arrow head pointing down + hline(10, 9, 5, 0, 0, 180); + hline(11, 10, 3, 0, 0, 180); + px(12, 11, 0, 0, 180); + // Code line + hline(3, 14, 10, 80, 80, 80); +} + + +// Step Out: arrow pointing up out of a bracket +static void iconStepOut(void) { + clear(192, 192, 192); + // Vertical arrow shaft going up + vline(8, 2, 8, 0, 0, 180); + // Arrow head pointing up + hline(6, 4, 5, 0, 0, 180); + hline(7, 3, 3, 0, 0, 180); + px(8, 2, 0, 0, 180); + // Bracket (representing the function) + vline(4, 10, 4, 80, 80, 80); + hline(4, 10, 3, 80, 80, 80); + hline(4, 13, 3, 80, 80, 80); + vline(11, 10, 4, 80, 80, 80); + hline(9, 10, 3, 80, 80, 80); + hline(9, 13, 3, 80, 80, 80); +} + + +// Run to Cursor: arrow pointing right to a cursor line +static void iconRunToCursor(void) { + clear(192, 192, 192); + // Horizontal arrow + hline(2, 7, 8, 0, 0, 180); + // Arrow head + vline(10, 5, 5, 0, 0, 180); + vline(11, 6, 3, 0, 0, 180); + px(12, 7, 0, 0, 180); + // Cursor (text cursor line) + vline(14, 3, 10, 200, 0, 0); +} + + static void writeBmp(const char *path) { int32_t rowPad = (4 - (W * 3) % 4) % 4; int32_t rowSize = W * 3 + rowPad; @@ -231,7 +318,12 @@ int main(int argc, char **argv) { else if (strcmp(type, "run") == 0) { iconRun(); } else if (strcmp(type, "stop") == 0) { iconStop(); } else if (strcmp(type, "code") == 0) { iconCode(); } - else if (strcmp(type, "design") == 0) { iconDesign(); } + else if (strcmp(type, "design") == 0) { iconDesign(); } + else if (strcmp(type, "debug") == 0) { iconDebug(); } + else if (strcmp(type, "stepinto") == 0) { iconStepInto(); } + else if (strcmp(type, "stepover") == 0) { iconStepOver(); } + else if (strcmp(type, "stepout") == 0) { iconStepOut(); } + else if (strcmp(type, "runtocur") == 0) { iconRunToCursor(); } else { fprintf(stderr, "Unknown icon type: %s\n", type); return 1; diff --git a/widgets/scrollPane/widgetScrollPane.c b/widgets/scrollPane/widgetScrollPane.c index be0bccb..46a3f4d 100644 --- a/widgets/scrollPane/widgetScrollPane.c +++ b/widgets/scrollPane/widgetScrollPane.c @@ -37,12 +37,18 @@ typedef struct { int32_t scrollPosH; int32_t sbDragOrient; int32_t sbDragOff; + bool noBorder; } ScrollPaneDataT; #define SP_BORDER 2 #define SP_SB_W 14 #define SP_PAD 0 +static int32_t spBorder(const WidgetT *w) { + const ScrollPaneDataT *sp = (const ScrollPaneDataT *)w->data; + return sp->noBorder ? 0 : SP_BORDER; +} + // ============================================================ // Prototypes @@ -203,8 +209,8 @@ static void spCalcNeeds(WidgetT *w, const BitmapFontT *font, int32_t *contentMin *contentMinH = totalMinH; // Available inner area - *innerW = w->w - SP_BORDER * 2; - *innerH = w->h - SP_BORDER * 2; + *innerW = w->w - spBorder(w) * 2; + *innerH = w->h - spBorder(w) * 2; // Determine scrollbar needs (two-pass for mutual dependency) *needVSb = (totalMinH > *innerH); @@ -250,8 +256,8 @@ void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font) { } // The scroll pane's own min size is small -- the whole point is scrolling - w->calcMinW = SP_SB_W * 3 + SP_BORDER * 2; - w->calcMinH = SP_SB_W * 3 + SP_BORDER * 2; + w->calcMinW = SP_SB_W * 3 + spBorder(w) * 2; + w->calcMinH = SP_SB_W * 3 + spBorder(w) * 2; } @@ -313,8 +319,8 @@ void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font) { int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth); int32_t virtualW = DVX_MAX(innerW, contentMinW); int32_t virtualH = DVX_MAX(innerH, contentMinH); - int32_t baseX = w->x + SP_BORDER - sp->scrollPosH; - int32_t baseY = w->y + SP_BORDER - sp->scrollPosV; + int32_t baseX = w->x + spBorder(w) - sp->scrollPosH; + int32_t baseY = w->y + spBorder(w) - sp->scrollPosV; int32_t childW = virtualW - pad * 2; int32_t pos = baseY + pad; @@ -417,8 +423,8 @@ void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font) { virtualW = DVX_MAX(innerW, contentMinW); virtualH = DVX_MAX(innerH, contentMinH); - baseX = w->x + SP_BORDER - sp->scrollPosH; - baseY = w->y + SP_BORDER - sp->scrollPosV; + baseX = w->x + spBorder(w) - sp->scrollPosH; + baseY = w->y + spBorder(w) - sp->scrollPosV; childW = virtualW - pad * 2; if (childW < 0) { @@ -537,6 +543,7 @@ void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod) { // into scroll pane children since their coordinates may be outside // the pane's visible bounds. void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { + const WidgetT *w = hit; // alias for spBorder() ScrollPaneDataT *sp = (ScrollPaneDataT *)hit->data; AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; @@ -567,10 +574,10 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy // Check vertical scrollbar if (needVSb) { - int32_t sbX = hit->x + hit->w - SP_BORDER - SP_SB_W; + int32_t sbX = hit->x + hit->w - spBorder(w) - SP_SB_W; - if (vx >= sbX && vy >= hit->y + SP_BORDER && vy < hit->y + SP_BORDER + innerH) { - int32_t sbY = hit->y + SP_BORDER; + if (vx >= sbX && vy >= hit->y + spBorder(w) && vy < hit->y + spBorder(w) + innerH) { + int32_t sbY = hit->y + spBorder(w); int32_t sbH = innerH; int32_t relY = vy - sbY; int32_t trackLen = sbH - SP_SB_W * 2; @@ -611,10 +618,10 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy // Check horizontal scrollbar if (needHSb) { - int32_t sbY = hit->y + hit->h - SP_BORDER - SP_SB_W; + int32_t sbY = hit->y + hit->h - spBorder(w) - SP_SB_W; - if (vy >= sbY && vx >= hit->x + SP_BORDER && vx < hit->x + SP_BORDER + innerW) { - int32_t sbX = hit->x + SP_BORDER; + if (vy >= sbY && vx >= hit->x + spBorder(w) && vx < hit->x + spBorder(w) + innerW) { + int32_t sbX = hit->x + spBorder(w); int32_t sbW = innerW; int32_t relX = vx - sbX; int32_t trackLen = sbW - SP_SB_W * 2; @@ -655,8 +662,8 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy // Dead corner if (needVSb && needHSb) { - int32_t cornerX = hit->x + hit->w - SP_BORDER - SP_SB_W; - int32_t cornerY = hit->y + hit->h - SP_BORDER - SP_SB_W; + int32_t cornerX = hit->x + hit->w - spBorder(w) - SP_SB_W; + int32_t cornerY = hit->y + hit->h - spBorder(w) - SP_SB_W; if (vx >= cornerX && vy >= cornerY) { return; @@ -721,7 +728,7 @@ static void widgetScrollPaneOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mous int32_t thumbSize; widgetScrollbarThumb(trackLen, contentMinH, innerH, sp->scrollPosV, &thumbPos, &thumbSize); - int32_t sbY = w->y + SP_BORDER; + int32_t sbY = w->y + spBorder(w); int32_t relMouse = mouseY - sbY - SP_SB_W - dragOff; int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; sp->scrollPosV = clampInt(newScroll, 0, maxScroll); @@ -738,7 +745,7 @@ static void widgetScrollPaneOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mous int32_t thumbSize; widgetScrollbarThumb(trackLen, contentMinW, innerW, sp->scrollPosH, &thumbPos, &thumbSize); - int32_t sbX = w->x + SP_BORDER; + int32_t sbX = w->x + spBorder(w); int32_t relMouse = mouseX - sbX - SP_SB_W - dragOff; int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; sp->scrollPosH = clampInt(newScroll, 0, maxScroll); @@ -785,7 +792,7 @@ void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B sp->scrollPosH = clampInt(sp->scrollPosH, 0, maxScrollH); // Sunken border - BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, SP_BORDER); + BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, spBorder(w)); drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); // Clip to content area and paint children @@ -793,10 +800,10 @@ void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B int32_t oldClipY = d->clipY; int32_t oldClipW = d->clipW; int32_t oldClipH = d->clipH; - setClipRect(d, w->x + SP_BORDER, w->y + SP_BORDER, innerW, innerH); + setClipRect(d, w->x + spBorder(w), w->y + spBorder(w), innerW, innerH); // Fill background - rectFill(d, ops, w->x + SP_BORDER, w->y + SP_BORDER, innerW, innerH, bg); + rectFill(d, ops, w->x + spBorder(w), w->y + spBorder(w), innerW, innerH, bg); // Paint children (already positioned by layout with scroll offset) for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { @@ -807,14 +814,14 @@ void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B // Draw scrollbars if (needVSb) { - int32_t sbX = w->x + w->w - SP_BORDER - SP_SB_W; - int32_t sbY = w->y + SP_BORDER; + int32_t sbX = w->x + w->w - spBorder(w) - SP_SB_W; + int32_t sbY = w->y + spBorder(w); drawSPVScrollbar(w, d, ops, colors, sbX, sbY, innerH, contentMinH, innerH); } if (needHSb) { - int32_t sbX = w->x + SP_BORDER; - int32_t sbY = w->y + w->h - SP_BORDER - SP_SB_W; + int32_t sbX = w->x + spBorder(w); + int32_t sbY = w->y + w->h - spBorder(w) - SP_SB_W; drawSPHScrollbar(w, d, ops, colors, sbX, sbY, innerW, contentMinW, innerW); if (needVSb) { @@ -871,6 +878,14 @@ WidgetT *wgtScrollPane(WidgetT *parent) { } +void wgtScrollPaneSetNoBorder(WidgetT *w, bool noBorder) { + VALIDATE_WIDGET_VOID(w, sTypeId); + ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; + sp->noBorder = noBorder; + wgtInvalidate(w); +} + + void wgtScrollPaneScrollToChild(WidgetT *w, const WidgetT *child) { VALIDATE_WIDGET_VOID(w, sTypeId); ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; @@ -887,7 +902,7 @@ void wgtScrollPaneScrollToChild(WidgetT *w, const WidgetT *child) { spCalcNeeds(w, font, &contentMinW, &contentMinH, &innerW, &innerH, &needVSb, &needHSb); // Child's virtual offset within the scrollable content - int32_t childOffY = child->y - w->y - SP_BORDER + sp->scrollPosV; + int32_t childOffY = child->y - w->y - spBorder(w) + sp->scrollPosV; int32_t maxScrollV = contentMinH - innerH; if (maxScrollV < 0) { @@ -912,21 +927,36 @@ void wgtScrollPaneScrollToChild(WidgetT *w, const WidgetT *child) { static const struct { WidgetT *(*create)(WidgetT *parent); void (*scrollToChild)(WidgetT *sp, const WidgetT *child); + void (*setNoBorder)(WidgetT *w, bool noBorder); } sApi = { .create = wgtScrollPane, - .scrollToChild = wgtScrollPaneScrollToChild + .scrollToChild = wgtScrollPaneScrollToChild, + .setNoBorder = wgtScrollPaneSetNoBorder +}; + +static bool spGetNoBorder(const WidgetT *w) { + return ((ScrollPaneDataT *)w->data)->noBorder; +} + +static void spSetNoBorder(WidgetT *w, bool v) { + wgtScrollPaneSetNoBorder(w, v); +} + +static const WgtPropDescT sProps[] = { + { "NoBorder", WGT_IFACE_BOOL, (void *)spGetNoBorder, (void *)spSetNoBorder, NULL }, }; static const WgtIfaceT sIface = { .basName = "ScrollPane", - .props = NULL, - .propCount = 0, + .props = sProps, + .propCount = 1, .methods = NULL, .methodCount = 0, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT, - .isContainer = true + .isContainer = true, + .namePrefix = "Scroll", }; void wgtRegister(void) { diff --git a/widgets/textInput/widgetTextInput.c b/widgets/textInput/widgetTextInput.c index 07c18bf..e3b6da3 100644 --- a/widgets/textInput/widgetTextInput.c +++ b/widgets/textInput/widgetTextInput.c @@ -1823,21 +1823,27 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit if (decBg) { lineBg = decBg; - rectFill(d, ops, textX, drawY, innerW, font->charHeight, lineBg); } + + // Always fill the line background when a decorator is active + // to clear stale highlights from previous paints. + rectFill(d, ops, textX, drawY, innerW, font->charHeight, lineBg); } // Draw line number in gutter if (gutterW > 0) { // Gutter indicator (breakpoint dot) if (gutterColor) { - int32_t dotR = font->charHeight / 4; - int32_t dotX = gutterX + 2 + dotR; - int32_t dotY = drawY + font->charHeight / 2; + // Draw a filled circle using rectFill scanlines. + // Radius 3 gives a clean 7x7 circle at any font size. + int32_t cx = gutterX + font->charWidth / 2 + 1; + int32_t cy = drawY + font->charHeight / 2; + // dy: -3 -2 -1 0 1 2 3 + static const int32_t hw[] = { 1, 2, 3, 3, 3, 2, 1 }; - for (int32_t dy = -dotR; dy <= dotR; dy++) { - int32_t hw = dotR - (dy < 0 ? -dy : dy); - drawHLine(d, ops, dotX - hw, dotY + dy, hw * 2 + 1, gutterColor); + for (int32_t dy = -3; dy <= 3; dy++) { + int32_t w = hw[dy + 3]; + rectFill(d, ops, cx - w, cy + dy, w * 2 + 1, 1, gutterColor); } } diff --git a/widgets/widgetScrollPane.h b/widgets/widgetScrollPane.h index eca3479..7c1144d 100644 --- a/widgets/widgetScrollPane.h +++ b/widgets/widgetScrollPane.h @@ -7,6 +7,7 @@ typedef struct { WidgetT *(*create)(WidgetT *parent); void (*scrollToChild)(WidgetT *sp, const WidgetT *child); + void (*setNoBorder)(WidgetT *w, bool noBorder); } ScrollPaneApiT; static inline const ScrollPaneApiT *dvxScrollPaneApi(void) { @@ -16,6 +17,7 @@ static inline const ScrollPaneApiT *dvxScrollPaneApi(void) { } #define wgtScrollPane(parent) dvxScrollPaneApi()->create(parent) -#define wgtScrollPaneScrollToChild(sp, child) dvxScrollPaneApi()->scrollToChild((sp), (child)) +#define wgtScrollPaneScrollToChild(sp, child) dvxScrollPaneApi()->scrollToChild((sp), (child)) +#define wgtScrollPaneSetNoBorder(w, noBorder) dvxScrollPaneApi()->setNoBorder((w), (noBorder)) #endif // WIDGET_SCROLLPANE_H