Breakpoints seem to be working.

This commit is contained in:
Scott Duensing 2026-04-06 15:36:47 -05:00
parent de7027c44e
commit 85010d17dc
14 changed files with 685 additions and 106 deletions

View file

@ -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

View file

@ -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);

View file

@ -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;

View file

@ -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;

BIN
apps/dvxbasic/tb_debug.bmp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/dvxbasic/tb_runtocur.bmp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/dvxbasic/tb_stepinto.bmp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/dvxbasic/tb_stepout.bmp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/dvxbasic/tb_stepover.bmp (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -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;

View file

@ -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;

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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